Architecture Overview
Plain-English walkthrough of the FCR Dashboard platform: what it is, the pieces it’s built from, how they fit together, and why it’s fast. Pair this with
fcr-dashboard-architecture-deck.md(the slide storyboard) to build a presentation, andhubspot-data-architecture.mdfor the data-layer detail.Written 2026-05-20; figures verified live 2026-06-09.
1. What it is
Section titled “1. What it is”The FCR Dashboard is an internal sales & account-management intelligence platform. It pulls FCR’s data — CRM, billing, HubSpot deals, Google Business Profiles, ads, search, keyword intelligence, census/area data — into one place and puts an AI advisor on top of it, so a rep or AM can ask a plain-English question about any account or prospect and get a complete, sourced answer in seconds.
It shows up in three places a rep already works:
- a web dashboard (portfolio, accounts, maps, reports),
- the HubSpot deal sidebar (the Prospect Advisor card),
- Roam chat (the same advisor in the team’s chat tool), and
- Claude Code skills (
/check-account,/prospect, … — for power users in Claude Code / Claude for Work).
All four are powered by the same brain.
2. The four surfaces (what the user touches)
Section titled “2. The four surfaces (what the user touches)”| Surface | What it is | Runs on |
|---|---|---|
| Web UI | React dashboard — portfolio, account lookups, area maps, reports | Cloudflare Pages (fcr-dashboard-ui.pages.dev) |
| HubSpot card | ”Prospect Advisor” chat in the deal sidebar | HubSpot’s UI-extension sandbox |
| Roam advisor | Same advisor, in Roam chat | Roam, via webhook |
| Claude Code skills | Slash-command workflows (/check-account, /prospect, /deep-dive, /portfolio, …) | Claude Code / Claude for Work (CLI / desktop agent) — not the web UI |
Every one of them is a client — they all call the same back-end Worker API.
Claude Code skills (a note on what these are)
Section titled “Claude Code skills (a note on what these are)”The skills are an FCR-specific layer that runs only inside Claude Code /
Claude for Work — they are not part of the web app, the worker, or the
HubSpot card. Each is a SKILL.md (in .claude/skills/ and .agents/skills/)
that tells the agent how to orchestrate the Worker’s endpoints for a task, e.g.:
- Account / portfolio:
/check-account,/portfolio,/deep-dive,/am-review,/deals,/tickets,/revenue-bridge - Prospecting & maps:
/prospect,/area-map,/insites,/proposal,/seo-gap-audit - Performance & data:
/ads-report,/ads-stats,/analytics,/search-console,/gbp-check,/gmb-report,/ahrefs,/keyword-gaps,/category-keywords,/merchant-centre,/call-tracking,/ads-aimax-audit - Ops:
/deploy-worker,/bq-query
They’re how a power user drives the platform from the terminal/agent. Same data, same Worker — a different (and the most flexible) front door. They’re versioned in the repo so the agent’s behaviour is reproducible.
3. The brain: one Worker at the edge
Section titled “3. The brain: one Worker at the edge”All the logic lives in a single Cloudflare Worker called fcr-dashboard-api,
served on the branded custom domain api.fcrinsights.ie (also still reachable at
fcr-dashboard-api.fcrmedia.workers.dev).
A Cloudflare Worker is serverless edge compute: there’s no server and no region. Cloudflare runs the code in a lightweight V8 isolate per request, in whichever of its ~330 global data centres is nearest the caller, then tears it down. The same code runs everywhere, on demand. That’s why responses are fast no matter where the rep is.
It’s a monolith: 176 endpoint handlers (account lookup, enrichment, keyword intelligence, the AI advisor, the deal-brief system, etc.) are bundled into one Worker. It doesn’t call other Workers — everything is in the one deployable unit.
The Worker also runs on a schedule (cron triggers) and as a queue consumer, not just on web requests — that’s how background jobs like cache pre-warming and the deal-brief refresh happen.
4. The data layer (what the Worker reads)
Section titled “4. The data layer (what the Worker reads)”The Worker holds bindings to Cloudflare data services and reaches out to external systems. It doesn’t store data itself — it orchestrates.
BigQuery (the warehouse) — the system of record for analytical data:
hubspot_deals— currently-open deals (refreshed daily).hubspot_deals_backcatalogue_18m— 18 months of closed won/lost deals (~10,800), assembled from three rolling 6-month cohorts, plus a frozen year-on-year comparison pair.hubspot_engagements— every note / email / call / meeting (~230k).active_clients, keyword intelligence, GBP performance, census/area data, etc.v_opportunity_tier— the 20-Minute Market (signed off June 2026): one row per (category × locality) saying whether an established website already competes inside the business’s real service radius (THIN / CONTESTED / SATURATED), at what market depth (households per competitor), and therefore which product rung is the honest pitch. Built from drive-time catchments (drive_time_catchments), the 100%-coverage category bridge (category_bridge+canonical_key_class), CSO census overlay (drive_time_catchment_demographics) and an observed-rankings evidence base (serp_grid_results, 109K rows). Consumers must read the VIEW and honour its conventions (never filterminutes=20; missing row = ultra-THIN; respectconfident). Full reference: Opportunity Tiers · skill:/opportunity-tier· rebuild tooling:scripts/opportunity-tier/.
Vectorize (semantic search indexes) — for “find things like this”:
fcr-deal-history— past deals, so the advisor can say “deals like this one won/lost because…”.fcr-site-portfolio— FCR-built sites, for “similar clients in this category”.fcr-company-knowledge— FCR’s own knowledge base.
KV (instant key-value cache) — sub-30ms reads at the edge, e.g. the precomputed deal briefs.
External, live — BigQuery REST, the CRM API, Google (SerpAPI/Places), Pleper (GBP), Ahrefs, Meta. These are the slow sources, which is exactly why we pre-compute (next section).
5. The deal-brief system (why it’s instant)
Section titled “5. The deal-brief system (why it’s instant)”The problem: when a rep opened a deal, the advisor used to fan out 4–5 live calls (CRM, Google Business Profile, website crawl, similar deals, category intelligence) while the rep waited — 15–30+ seconds every time.
The fix: precompute a complete deal brief for every open deal, ahead of time, and cache it. When a rep opens the deal, the advisor reads one cached blob in ~30ms instead of making the rep wait for live lookups.
How it’s kept fresh:
- A background job (cron, every 2 hours during the day) refreshes briefs in small batches via a queue, so the heavy lookups happen when nobody’s waiting.
- Opening a deal self-heals it — if its brief is out of date, opening it triggers a background refresh so it’s current next time.
- The brief carries everything the four canonical rep questions need: “What’s this deal about?”, “Run prospect intel on this category”, “What was last discussed?”, “Draft a proposal note” — all answered from the cached brief with no live wait.
A brief bundles: deal facts, CRM + billing, Google Business Profile (live for prospects we don’t have on file), a website crawl, category keyword intelligence, the local area picture, similar paying clients, similar past deals, and the recent conversation history (real rep contact ranked above automated emails).
6. The AI advisor (the intelligence)
Section titled “6. The AI advisor (the intelligence)”The advisor is Claude (Anthropic) running inside the Worker, given a toolbox of ~73 functions — look up an account, fetch a deal brief, run prospect intelligence, search similar deals, draft an email from approved templates, post a note back to the deal, and so on.
When a rep asks a question, Claude decides which tools to call, the Worker runs them (now mostly instant cache reads), and Claude writes the answer in FCR’s voice. The same advisor serves the HubSpot card and Roam. On the card, the deal on screen gets special treatment: the Worker injects its precomputed brief into Claude’s context before the first model call, so “what’s this deal about”-class questions answer in a single model pass with zero tool calls.
Guardrails are built into its instructions — e.g. it reads the injected brief first, never presents free directory listings as paying clients, and frames a brand-new invisible business as a greenfield opportunity rather than a data gap.
7. End-to-end: what happens when a rep opens a deal
Section titled “7. End-to-end: what happens when a rep opens a deal”1. Rep opens a deal in HubSpot → the Prospect Advisor card loads.2. The card pre-warms: a fresh brief is ensured AND the rep's identity + template libraries are resolved into KV (keyed by their HubSpot email).3. Rep clicks "What's this deal about?" → the turn is enqueued onto fcr-hubspot-chat-queue (worker-native; no n8n hop since 2026-06-10).4. The Worker reads the brief from KV (~30ms) and INJECTS it into Claude's context — one model pass, zero tool calls for the deal on screen.5. Claude writes the answer, streamed back into the card; completion is signalled through the ProgressDO so the card's poll sees it instantly. (No live CRM/Google/website calls in the rep's path — already precomputed.)Behind the scenes, on a schedule, the Worker keeps every open deal’s brief fresh by composing them in the background through a queue.
8. Where the code lives & how it ships
Section titled “8. Where the code lives & how it ships”- Source of truth: the Git repo (
github.com/FCR4IE/DASHBOARD).- Worker:
worker/src/**(entryindex.js,router.js, 126handlers/). - Web UI:
src/**(React/Vite). - HubSpot card:
hubspot-extension/src/**.
- Worker:
- Deploy:
scripts/deploy-worker.shbundlesworker/src/**into one script and ships it to Cloudflare (it lint-gates, refuses a dirty tree, refuses a production deploy from any branch butmain— added 2026-06-10 — and tags every deploy for rollback). The UI builds with Vite and uploads to Pages. The card uploads to HubSpot. - Two Cloudflare accounts: FCR Media (production —
…fcrmedia.workers.dev) and a personal dev account (…cathaldempsey.workers.dev).
On Cloudflare the Worker is stored as one compiled bundle, not individual files — the individual files only exist in the repo. The CF dashboard’s “Workers bound” panel counts other Workers this one calls via service bindings — exactly one:
CREATOR_GEN→creator-image-gen(the advisor’s image-generation gateway on the same account).
9. Runtime, bindings & secrets (reference)
Section titled “9. Runtime, bindings & secrets (reference)”Snapshot of the production Worker (fcr-dashboard-api, FCR account) as
configured 2026-06-10.
Connected bindings (19) — resources the Worker talks to
Section titled “Connected bindings (19) — resources the Worker talks to”| Type | Binding name | Resource | What it’s for |
|---|---|---|---|
| R2 bucket | ADVISOR_CSV | fcr-advisor-csv | CSV attachments the advisor generates |
| R2 bucket | SERP_SNAPSHOTS | fcr-serp-snapshots | durable SERP HTML/PNG snapshots |
| KV | CACHE | CACHE | general cache + deal-brief:<id> + advisor-identity:<email> blobs |
| KV | REPORTS | fcr-REPORTS | Discovery LRC reports + shared artefact sidecars |
| Queue | BRIEF_QUEUE | fcr-deal-brief-queue | deal-brief refresh jobs (producer + consumer) |
| Queue | DEAL_VECTOR_QUEUE | fcr-deal-vector-queue | daily incremental deal-history re-embeds (+ DLQ) |
| Queue | ADVISOR_QUEUE | fcr-advisor-queue | heavy Roam advisor runs handed off the sync path (+ DLQ) |
| Queue | HUBSPOT_CHAT_QUEUE | fcr-hubspot-chat-queue | HubSpot card chat turns (transport since 2026-06-10) |
| Vectorize | VECTORIZE | fcr-site-portfolio | similar FCR sites / clients |
| Vectorize | COMPANY_KNOWLEDGE_VECTORIZE | fcr-company-knowledge | company knowledge-base search |
| Vectorize | DEAL_VECTORIZE | fcr-deal-history | similar past deals |
| Vectorize | DOCS_VECTORIZE | fcr-internal-docs | ”Ask the docs” RAG over this docs site |
| Vectorize | OUTREACH_VECTORIZE | fcr-outreach-templates | marketing-approved outreach template search |
| Vectorize | PROSPECT_VECTORIZE | fcr-prospect-opportunities | prospect-spine opportunity search |
| Durable Object | PROGRESS_DO | ProgressDO | live chat-progress + completion signal + STT hand-off |
| Browser | BROWSER | — | headless Chrome (SERP screenshots / renders) |
| Service | CREATOR_GEN | creator-image-gen | advisor image generation (same-account Worker) |
| Analytics Engine | API_USAGE | fcr_api_usage | worker-originated external API call counter |
| Analytics Engine | CRAWL_METRICS | fcr_crawl_metrics | prospect-crawler telemetry (tick + slow-host rows) |
Secrets & variables
Section titled “Secrets & variables”Plaintext variables (non-sensitive config): BQ_PROJECT_ID,
GOOGLE_ADS_CUSTOMER_ID, GOOGLE_ADS_LOGIN_CUSTOMER_ID, N8N_BASE,
SELF_BASE_URL, DASHBOARD_BASE_URL, BQ_MAX_BYTES_BILLED,
BQ_ALERT_ROAM_ID, CF_ACCOUNT_ID, HUBSPOT_CHAT_TRANSPORT
("queue" | "n8n" — the card-chat transport switch).
Secrets (encrypted; values never in the repo — set via wrangler secret put).
What each unlocks:
| Secret | Unlocks |
|---|---|
API_KEY | the x-api-key all UI/advisor calls authenticate with |
ADMIN_API_KEY | extra gate for /dashboard-bq-admin (DDL/writes) |
ANTHROPIC_API_KEY | Claude — the advisor |
OPENAI_API_KEY | embeddings (vector search) + Whisper STT |
BQ_SERVICE_ACCOUNT_JSON | BigQuery auth |
HUBSPOT_ACCESS_TOKEN | live HubSpot deal/engagement reads |
SERPAPI_KEY | Google Maps / SERP |
PLEPER_API_KEY, PLEPER_API_SIG | live Google Business Profile data |
AHREFS_API_KEY | Ahrefs SEO data |
GOOGLE_ADS_CLIENT_ID/SECRET/DEVELOPER_TOKEN/REFRESH_TOKEN | Google Ads API |
META_ACCESS_TOKEN, META_AD_ACCOUNT_ID | Meta reach estimates |
INSITES_API_KEY_DF, INSITES_API_KEY_LRC, INSITES_PUBLIC_KEY | InSites audits |
N8N_API_KEY | n8n webhooks |
ROAM_BEARER, ROAM_DEV_CHAT_ID, ROAM_DEV_GROUP_ID | Roam advisor posting |
TEAMWORK_DESK_API_KEY, TEAMWORK_DESK_WEBHOOK_SECRET | Teamwork tickets |
TEAMWORK_PROJECTS_API_KEY | Teamwork Projects reads (falls back to the Desk key) |
YEXT_POSTS_API_KEY | Post Studio publish (Yext Posts API) |
AE_READ_TOKEN | querying the Analytics Engine datasets back via the SQL API |
Trigger events
Section titled “Trigger events”| Type | Schedule / source | Job |
|---|---|---|
| Cron | * * * * * (every minute) | Pleper + SerpAPI prospect enrich + crawler tick + HubSpot engagements drain |
| Cron | 30 6 * * * | daily incremental fcr-deal-history vector reindex (changed deals only) |
| Cron | 40 6 * * * | pre-warm AM portfolio caches (before reps start 08:00 local) |
| Cron | 50 6 * * * | pre-warm check-account for open-deal subscribers |
| Cron | 0 8-20/2 * * * | chunked deal-brief refresh (08:00–20:00 every 2h) |
| Cron | 5 8 * * 1 (weekly Mon) | DM advisor-usage + external-API-usage reports to Ro.am |
| Cron | 0 9 * * * | scheduled-jobs health monitor — DMs Ro.am only if a cron/feed is overdue |
| Queue | fcr-deal-brief-queue | queue() consumer composes briefs |
| Queue | fcr-deal-vector-queue (+-dlq) | embeds deal batches; DLQ DMs Ro.am on dead batches |
| Queue | fcr-advisor-queue (+-dlq) | finishes heavy advisor runs off the request path, posts to Roam |
| Queue | fcr-hubspot-chat-queue | runs a HubSpot card chat turn + deposits the result (no DLQ — the card’s poll timeout covers a dropped turn) |
Runtime settings
Section titled “Runtime settings”- Compatibility date: 2024-09-23 · flags:
nodejs_compat - Placement: default (CF picks the edge location per request)
- CPU time limit: raised to 300s via
[limits] cpu_ms = 300000(both accounts) — a backstop against Cloudflare 1102 on heavy single invocations (the advisor loop + inline crawls). Handlers are still I/O-bound — a compose is 14–46s of wall time but only a few hundred ms of CPU (waiting on BQ/SerpAPI/Pleper/KV doesn’t count as CPU) — so the ceiling is rarely approached. - Observability: Workers Logs enabled (codified in
wrangler.toml[observability] enabled = true+[env.fcr.observability]since 2026-05-20), sowrangler tailand the Logs dashboard work on both accounts. Traces remain off (not needed for an I/O-bound monolith). - Two accounts: production = FCR Media (
…fcrmedia.workers.dev), dev = personal (…cathaldempsey.workers.dev). Secrets/bindings exist in both.
10. Glossary
Section titled “10. Glossary”| Term | Plain meaning |
|---|---|
| Worker | Serverless code that runs at Cloudflare’s edge, per request |
| Pages | Cloudflare’s static-site hosting (the web UI) |
| Edge | Cloudflare’s ~330 global data centres; code runs at the nearest one |
| Binding | A connection from the Worker to a data service (KV, Vectorize, Queue…) |
| BigQuery (BQ) | Google’s data warehouse — FCR’s analytical system of record |
| Vectorize | Cloudflare’s semantic-search index (“find things like this”) |
| KV | Cloudflare’s fast key-value cache |
| Queue | Background job pipeline — spreads slow work across many runs |
| Cron trigger | A schedule that runs the Worker automatically |
| Deal brief | A precomputed bundle of everything known about a deal |
| The advisor | Claude + a toolbox, answering rep questions in FCR’s voice |