Team Environments — building on the data lake

When a team (e.g. marketing) wants to build and experiment on FCR's data lake without branch-fighting on the core dashboard, give them their own sandbox: their own repo, their own Cloudflare Worker, their own (self-hosted) n8n, writing to their own BigQuery dataset — while reusing the dashboard's data layer and sharing credentials. This is the end-to-end pattern. Runnable starter: examples/marketing-worker/.


The shape (and the one idea behind it)

A team gets an isolated place to work, but they read everything, share credentials, and reuse the dashboard's endpoints. The isolation is about runtime + accidents, not trust — these are people you trust, who also work on the core dashboard. We isolate so a buggy experiment can't (a) take down the rep-facing stack or the production automation, or (b) corrupt the data reps see live. Everything else, share.

Layer Isolated or shared How
Source (git) isolated their own repo, not FCR4IE/DASHBOARD
Worker runtime isolated their own Cloudflare Worker (own deploy / CPU / cron / logs)
n8n runtime isolated self-hosted n8n Community, not the shared company cloud tenant
Credentials / secrets shared copy the existing values in (small, trusted team)
Data — reads shared read anything (fcr_operations, KI, …) via service binding or the BQ cred
Data — writes isolated write to their own dataset; fcr_operations is read-only to experiments
Dashboard endpoints shared service binding (worker) / public API + key (n8n)

The two blast-radius truths to remember:

  1. The worker + self-hosted n8n isolate the runtime — an experiment can't take down the rep-facing worker or the production n8n.
  2. BigQuery is the shared thing — isolation does not extend to data. So the "write to your own dataset" rule below is the real guardrail.

1. Source — their own repo

A new repo (e.g. FCR4IE/marketing-worker), not the main DASHBOARD repo. The whole point is to avoid branch-fights and let them move independently; putting their worker in the main repo pulls them back into shared branches/PRs on the dashboard's terms. Because they consume the dashboard via the service binding / API (not by importing its source), a separate repo costs almost nothing. The examples/marketing-worker/ folder here stays as the template; the live one lives in their repo.

(Counter-case: a monorepo gives one-place visibility and easy code promotion, at the cost of branch contention. For a "don't fight about branches" goal, separate repo wins.)

2. The worker

A separate Cloudflare Worker in the FCR Media account (0481b38f…) — service bindings + shared KV/Vectorize/R2 are account-scoped; cross-account isn't supported. It service-binds to fcr-dashboard-api and reuses its composed endpoints rather than duplicating the data layer.

name = "fcr-marketing-api"
main = "src/index.js"

# Call the dashboard worker directly — no public hop, no second copy of the BQ creds.
[[services]]
binding = "DASHBOARD"
service = "fcr-dashboard-api"

# Optional: share READ resources by ID (same account)
# [[kv_namespaces]]
# binding = "CACHE"
# id = "7115c43af9284b5bbfc96993e16b8ca2"   # FCR prod CACHE
# [[vectorize]]
# binding = "DOCS_VECTORIZE"
# index_name = "fcr-internal-docs"
// env.DASHBOARD routes to fcr-dashboard-api; host is ignored, path matters.
// The dashboard still enforces x-api-key, so pass it from a secret.
async function dashboard(path, init = {}) {
  return env.DASHBOARD.fetch(`https://dashboard${path}`, {
    ...init,
    headers: { ...(init.headers || {}), "x-api-key": env.DASHBOARD_API_KEY },
  });
}
const res = await dashboard(`/dashboard-check-account?subscriberId=${sid}`);

The producer (fcr-dashboard-api) needs no changes — service-binding requests hit its normal fetch, so its x-api-key auth still applies.

Deploy — their own, and trivial: just wrangler deploy from their repo (or a 5-line script). They do not use scripts/deploy-worker.sh — that's hardwired to the dashboard's worker/ plus the heavy gate (clean-tree + no-undef + two accounts + tags), which is overkill for a sandbox. Add a gate only if their worker ever becomes something reps depend on.

3. Their n8n — self-hosted Community

The company cloud n8n is a shared runtime: a heavy or looping experimental flow competes with the production ETL / sync / InSites workflows for task-runner slots, and we've already hit task-runner saturation on that tenant. So give the team a self-hosted n8n Community instance (free; cost is a container/VM) — their own isolated runtime.

  • It isolates compute, not data. The box still talks to the same BigQuery, so the own-dataset write rule (§4) applies just as hard — arguably more, since there's now zero friction to fire a big job.
  • Keep production automation on the managed cloud n8n. Treat the Community box as a sandbox; don't let critical scheduled jobs drift onto something you must patch and back up yourself.
  • Secure the box — it holds real credentials. A self-hosted n8n on the open internet with the BQ service account + HubSpot creds is the one genuine risk here regardless of trust. Put it behind a login + HTTPS, ideally Cloudflare Access or a tunnel, never a bare public port.
  • Promotion path: export the workflow JSON → import into the company tenant; keep the two on similar n8n versions so nodes don't break on the way over.

4. Credentials — share, with one habit

Small, trusted team → reuse the existing secrets (the BQ service account, the dashboard API key, HubSpot, …). Copy the values into their worker secrets and their n8n credential store. (Cloudflare secrets are per-worker, so it's a copy of the value, not a live link — fine.)

The one habit to keep — accidents, not trust:

  • Reads: share everything, read anything.
  • Writes: experiments write to their own dataset (a shared sandbox, or marketing_ops); treat the live fcr_operations tables as read-only.

Why: those tables are what the advisor, HubSpot card, and dashboard read in real time, so a stray DML or a runaway batch loop would show reps wrong data — not just break a sandbox. Most of fcr_operations self-heals on the next sync (active_clients rebuilds daily), but the Vectorize indexes and hand-built tables don't.

Optional future hardening (not needed now): if you ever want that enforced rather than by convention, mint a service account with bigquery.jobUser (project) + dataViewer on fcr_operations + dataEditor on their dataset only. Zero flexibility lost — reads stay wide open. Don't bother until an accident or a headcount makes it worth it.

⚠️ One sharp edge: handing a team the dashboard API key gives them dashboard-bq-execute, which runs arbitrary SQL with the powerful dashboard service account — a back door to writing fcr_operations. With a trusted team that's acceptable; just know it's there, and lean on the own-dataset habit.

5. Their own BigQuery

Their writes land in their own dataset (e.g. marketing_ops or a shared sandbox) — created once. Tracking tables and experiment outputs live there. Design tracking tables as idempotency ledgers: one row per item with a unique key + created_at/sent_at + status, so a re-run never double-processes.

Worked example — Graham's Listing-Manager reactivation flow

Goal: take open Listing-Manager partials, check if they're on file, and email each a completed listing + a short-form LRC for their keywords.

Step Where it runs Reuses
Input: open LM partials confirm the feed first — BQ table / LM export / webhook
On-file check his worker (service binding) dashboard-check-account / -check-keywords (subscriber-id bridges absorb name drift)
Complete listing + short LRC keywords his worker dashboard-discovery-suggest (keywords/satellites) + dashboard-serp-grid (quick rank)
Render short-form text + send email n8n Community (email node / ESP)
Track what was sent + when his dataset, e.g. marketing_ops.lm_partial_sends idempotency ledger — no double-sends

Placement: the heavy part (the partials sweep, the LRC fan-out, the throttled send loop) lives in his worker (isolated, own cron); n8n does the light glue and the email node. Tracking writes go to his dataset, never fcr_operations.

Compliance: outreach email to partials needs consent + unsubscribe (GDPR) and SPF/DKIM on the sending domain — settle before the first send.

When to just add to the core instead

For a tightly-coupled feature the rep-facing surfaces consume directly (a new advisor tool, an account-panel endpoint), add a route to fcr-dashboard-api — you avoid a second deploy target and shared-code drift. Reach for a team environment when the work is team-owned, experimental, or operationally heavy.

Promotion path (experiment → product)

  • Worker code: move it into the main repo's worker/ as a route, or keep it as its own worker if it stays team-owned.
  • n8n: export the JSON from Community → import to the company cloud tenant.
  • Data: promote a sandbox table into fcr_operations via the normal sync / scheduled-query path (reviewed) — not by pointing experiments at prod.

Starter: examples/marketing-worker/. Related: fcr-dashboard-architecture-overview.md, external-apis.md, bigquery-and-sync.md, commit-and-deploy.md.

FCR Dashboard documentation · generated from docs/ · keep counts verified, not guessed.

Ask the docsRAG over this site
Ask anything about the FCR Dashboard platform — architecture, BigQuery, the worker routes, billing rules, the LRC stack, scoring… Answers are grounded in this documentation, with source links.
How does the deal-brief refresh work? Which routes are Worker vs n8n? How is account health scored?