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:
- The worker + self-hosted n8n isolate the runtime — an experiment can't take down the rep-facing worker or the production n8n.
- 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, ormarketing_ops); treat the livefcr_operationstables 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 writingfcr_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_operationsvia 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.