The system at a glance
Three planes — clients, the broker (on GCP), and the knowledge base (vault → site + Notion). Everything reaches the box over outbound channels, so the firewall stays shut.
flowchart LR
subgraph Clients
CA["claude.ai (cloud)"]
CC["Claude Code (your Mac)"]
end
EDGE["Cloudflare edge"]
subgraph GCP["GCP e2-small · London"]
FW{{"deny-all-ingress firewall"}}
CFD["cloudflared"] --> BRK["Firney broker :8002 (Docker)"]
TS["tailscaled"]
end
NOTION[("Notion workspace")]
MAC["Your Mac (admin)"]
CA -- "HTTPS → /proxy/notion_api" --> EDGE
EDGE -- "outbound tunnel" --> CFD
CC -. "header-auth" .-> EDGE
BRK -- "OAuth (stored token)" --> NOTION
MAC -- "Tailscale mesh (SSH/Ansible)" --> TSComponents
| Piece | What it is | Where it runs |
|---|---|---|
| Firney MCP broker | OAuth token broker + reverse proxy for remote MCP; exposes Notion tools | GCP box, Docker, port 8002 |
| Tailscale | WireGuard mesh for all admin/SSH/Ansible — no public SSH | GCP box + your Mac |
| Cloudflare-Tunnel | Outbound tunnel publishing the broker at bobsmcp.uk — no open ports |
GCP box (cloudflared) |
| Notion connector | The broker's stored Notion OAuth token; proxies MCP calls to Notion | broker → Notion API |
| The wiki vault | Git-markdown knowledge base — the source of truth | your Mac / BobMck/Obsidian-Docs |
| The static site | build_site.py renders the vault → styled HTML |
Cloudflare edge, docs.bobsmcp.uk |
| Cloudflare Access | Login gate (email one-time-PIN) in front of the docs site | Cloudflare edge |
Security model
The guiding rule: the GCP box has zero public inbound ports. Reachability is always outbound-initiated.
- Firewall — a Terraform-managed
deny-all-ingressrule at priority 100 overrides GCP's default permissive rules. The public IP is kept for egress only. - Admin — only over the Tailscale mesh (
ssh ubuntu@gcp-mcp-broker). Tailscale SSH was disabled in favour of plainsshd+ key so automation works. - Public broker traffic — arrives via the Cloudflare Tunnel (outbound), never an open port. Gated by the broker's inbound OAuth 2.1 with an exact-match redirect-URI allow-list.
- Docs site — static files on Cloudflare's edge (no server), gated by Cloudflare Access; the public
*.workers.devURL is disabled so the Access-gated domain is the only way in. - Secrets — never committed:
terraform.tfvars(Tailscale key), broker.env, and the broker key (${NOTION_BROKER_KEY}env var) are all git-ignored.
Access & authentication
Every surface is authenticated, each in the way that fits it:
| Surface | Address | Auth | Who gets in |
|---|---|---|---|
| Broker (from claude.ai) | bobsmcp.uk/proxy/notion_api |
OAuth 2.1 (dynamic client registration) | allow-listed redirect URIs |
| Broker (from Claude Code) | same | header X-Broker-Key + X-App-Id |
holder of the br_ key |
| Admin / SSH / Ansible | gcp-mcp-broker (100.x) |
Tailscale (WireGuard) | your tailnet |
| Docs website | docs.bobsmcp.uk |
Cloudflare Access (email OTP) | allow-listed email |
| Notion mirror | your Notion workspace | Notion sign-in | workspace members |
Hosting
- GCP —
e2-small, Ubuntu 22.04,europe-west2(London). Runs the broker,cloudflared,tailscaled. - Cloudflare edge — terminates the tunnel (
bobsmcp.uk) and serves the static site (docs.bobsmcp.uk) from Workers static assets. TLS + CDN handled for free. - Notion — managed SaaS; the read-mirror.
- Your Mac — Claude Code compiles the wiki; Obsidian reads it.
- Domains —
bobsmcp.ukis a dedicated domain on Cloudflare Registrar;robmck.ukstays untouched in AWS Route 53.
Deployment
Two independent pipelines — the broker (rarely) and the site (on every push).
flowchart TB
subgraph BrokerPipe["Broker — on changes"]
TF["terraform apply"] --> VM["GCP instance + deny-ingress firewall"]
VM --> BOOT["startup script joins Tailscale"]
BOOT --> ANS["ansible-playbook install.yml"]
ANS --> RUN["broker + cloudflared running"]
end
subgraph SitePipe["Site — every commit"]
PUSH["git push"] --> CFBUILD["Cloudflare build: build_site.py"]
CFBUILD --> DEP["deploy → docs.bobsmcp.uk (Access)"]
endGit strategy
~/Terraformis a plain folder, not a repo. Each project subfolder is its own git repo.- The vault repo
BobMck/Obsidian-Docsis the single source of truth. Markdown is canonical; the HTML site and the Notion mirror are derived outputs. - Generated artifacts are never committed —
public/(built site) and.venvare git-ignored; the site is rebuilt fresh in CI on every push. - Secrets are never committed —
terraform.tfvars,.env, vault.env, and the broker key live outside git (env vars / gitignored files). - Push is the trigger —
git push→ Cloudflare builds and deploys the site automatically.
The knowledge base
A hybrid LLM-Wiki (Model-Context-Protocol-adjacent in spirit): compile once at ingest, store the artifact, never re-derive.
flowchart LR
RAW["sources/ (raw, human)"] --> ING["/wiki-ingest"]
ING --> WIKI["wiki/*.md (compiled)"]
WIKI --> GIT["git: BobMck/Obsidian-Docs"]
GIT --> SITE["docs.bobsmcp.uk"]
WIKI -- "/wiki-publish" --> NOTION[("Notion mirror")]
OBS["Obsidian (read)"] --- WIKI- Layers —
sources/(raw inputs you own),wiki/(model-generated),CLAUDE.md(the schema: templates, conventions, lint rules). - Taxonomy —
study/(cloud · hashicorp · ai-ml · ansible · linux · claude-course),concepts/(cross-cutting reference),projects/(per-project README/HLD/LLD/ADR/runbook). - Skills —
/wiki-ingest(source → notes),/wiki-query(answer from the wiki with citations),/wiki-lint(health-check),/wiki-publish(push to Notion). - Two derived views — this website (rich, rendered Mermaid, login-gated) and the Notion mirror (read anywhere).
Day-to-day
- Drop a source in
sources/or run/wiki-ingest <source>→ it files notes into the rightstudy/<area>(or a project), updates the index + log. git push→ the site rebuilds and deploys todocs.bobsmcp.ukautomatically./wiki-publishto refresh the Notion mirror;/wiki-lintfor a health report;/wiki-queryto answer from your own notes.
Cost
A deliberately light box (the heavy compile runs on your Mac). London on-demand pricing:
| Line item | Detail | Per month |
|---|---|---|
| Compute | e2-small, 2 vCPU, 2 GB |
~$13.50 |
| Boot disk | 20 GB standard PD | ~$0.80 |
| External IP | egress only, no inbound | ~$3.00 |
| Egress | light API proxying | <$1.00 |
| Domain | bobsmcp.uk (~£1/yr) |
~$0.10 |
| Tailscale · Cloudflare Tunnel · Pages · Access | personal / free tiers | Free |
| Total | all-in | ≈ $17/mo |
New GCP accounts get $300 of credit (~18 months at this rate). It is not the Always-Free tier — that needs an e2-micro in a US region; this runs e2-small in London by choice (latency + RAM headroom).
Related
wiki/Project-Overview.md · git is the source of truth