Production Blast Radius
Can product iteration hurt the live site, customer data, or money? Yes. Right now we test recipes in the same kitchen, with the same pots, during dinner service.
The core problem: there is only one copy of everything. One database, one set of keys, and one website server. Every Claude session on the Mac can use all of it, and nothing prompts before it acts. The website release is well guarded. Almost nothing else is.
What "production" is today
Website server
yummy-future.com on one small VM (1 GB RAM). It also hosts the investor deck, the preview backend and four timers.
The VM named "test"
Actually live: admin.yummy-future.com (Directus) plus the café services: orders, payments, users, menus, Redis.
One Supabase database
yummy-future-website is the only project. It holds leads, orders, bookings, Directus content and facts. There is no dev copy.
Outside services
Stripe (live), Slack, Resend email, PostHog analytics, Airtable. Each has one account and one project.
Who can touch it
Every Claude session: 103 keys, no permission prompts. 6 Mac background jobs, 23 scheduled Claude tasks, the GitLab runner, and the deck repo.
Ranked risks
Score = damage (1 annoying · 2 customers notice · 3 data or money lost) × how easily it happens (1 needs a mistake plus bad luck · 2 one wrong command · 3 already happening or routine). Ties go to the higher damage.
Every session holds every key and asks no one
Any session can fetch the live Stripe key, the database master key and the Slack bot token, then use them in one command. Sessions run with permission prompts switched off. The only hard stops are the website release gate, a Cloudflare gate and a QuickBooks deny list. Nothing stops a database write, a Slack post, a Stripe refund or an ssh into a server.
if it goes wrongReal refunds, deleted rows, messages to customers or staff channels.
Evidence: yfkey list shows 103 keys with no test/live split. ~/.claude/settings.json defaultMode is bypassPermissions. No hook covers supabase, Directus, Slack, Stripe or ssh.
One database, and the undo button is a full day wide
Tests, scripts, Directus admin and the live site all share one database. Schema changes are run by hand straight against it. Backups are daily and kept 7 days, with no point-in-time restore. Undoing one bad change means rolling everything back up to 24 hours, which also loses that day's real leads and orders.
if it goes wrongA bad migration or DELETE takes the site's forms, shop and admin down together.
Evidence: supabase projects list returns 1 project. The repo is linked to it. Backups show 8 daily and PITR off. Some live columns (e.g. leads.is_test) exist only in ad-hoc SQL, so prod can't be rebuilt from the repo.
Scripts that write to production unless told not to
A session that runs a script "to check something" can change live data. The shop test inserts a real order, lowers real stock, then writes the old stock number back. A real sale in that window would be overwritten. 15 Directus scripts default to the live admin and write unless given --plan. The PostHog warehouse setup creates a database role unless given --dry-run.
Evidence: scripts/shop-guest-e2e.mjs:160-172, ops/directus/fix-refund-notice.mjs:24-33 (+14 more), scripts/posthog-warehouse-setup.mjs:63. A live Directus token sits in ~/.yf-secrets/directus.env.prod.
The release gate is easy to get around
The gate catches the normal release command. Wrapping it (bash -c, an env prefix, a comment containing "--dry-run") or ssh-ing straight to the server slips past. The "Jack approved" text is typed by the session itself. After release, the smoke test only checks that bad input gets rejected. It never submits a form or takes a payment. Rollback restores files, but not database changes, server settings or the deck's web config.
Evidence: release_gate.py:56-61,94, ship.sh:166, smoke.mjs:38-57, deploy.sh:559-583. There is no staging site; preview is a backend-only container on the same VM.
Background jobs run whatever lands on main
Several Mac jobs pull the latest code from git and run it unattended, holding live keys. Examples: the daily Stripe pull (live key), the deck CRM sync (writes Directus every 3 h), job-health (posts to Slack) and nightly journeys (on a stale branch, last run 30 red). Also 23 scheduled Claude tasks, two of which write to live Directus, and a GitLab runner that executes pipelines as Jack on this Mac. One bad merge runs everywhere by itself.
Evidence: ~/Library/LaunchAgents (com.yummy.finance.stripepull, com.yummyfuture.deck-crm-sync, com.yummy.jobhealth.*, com.yummy.website.ujjourneys), ~/.claude/scheduled-tasks, gitlab-runner shell executor.
The website shares one tiny server with other jobs
The investor deck (a different repo) writes into the same web server config and reloads it. A bad deck config can take yummy-future.com down. The VM has 1 GB of RAM, containers have no memory caps, release backups sit on the same disk, and the disk itself has never been snapshotted. Nothing pages anyone if the site goes down; the only watcher is a daily check.
Evidence: deck push.py:512-551 reloads the shared Caddy. No mem_limit in either compose file. gcloud compute snapshots list has no website-vm entry. README:46 says "no uptime probe".
The VM named "test" is live and over-powered
It runs the admin panel and the café services. Its machine account has Editor on the whole Google project, so a bad command or a hacked container there could delete the website server too. No VM has delete protection. Redis has no password and any VM on the network can reach it. One café service is already broken: the cache synchronizer has restarted 14,795 times because it can't reach Redis.
Evidence: gcloud projects get-iam-policy (compute SA = roles/editor), deletionProtection=False on all 3 VMs, redis-cli config get requirepass is empty, docker inspect RestartCount 14795.
Test traffic pollutes the numbers we decide with
The site's analytics script has the live PostHog key built in and does not skip localhost. Several browser tests load real pages with it on, so test visits land in the visitor counts and replays. Every event is also copied into the live database. Test leads still go into the real Airtable, just ticked "Is Test".
Evidence: site/js/observe-beacon.js:340,436-446 (no localhost check), site/tests/homepage-sd-hero-test.js:77-85, client-event.mjs:96-128,260, lead-submit.mjs:697.
Half-finished work rides along in a release
A release ships all of main, and merging to main is free. A half-built page under site/ goes live and into the sitemap. The release list shows commit titles, not what changed, and calls server config, database and package changes "internal". 7 of the last 20 main commits were direct pushes with no merge.
Evidence: ship-queue.mjs:74-76,95-101, deploy.sh:259-262, gen-sitemap.mjs:15-16.
The "this is a test" label is only honoured by the lead form
Lead forms send test entries to a test channel. The shop payment webhook, the fulfilment email and the booking emails ignore the label. A test order with a real-looking email would email that person and post to #shop-fulfillment.
Evidence: shop-payment-webhook.mjs:236, shop-fulfillment.mjs:304, booking-approve.mjs:264. These are dormant on the Mac (no keys in the shell) until someone loads live env.
How to shrink it
The idea: a wall between the test kitchen and the dining room. Make the safe path the default and the live path a deliberate, one-time unlock. Cheapest and most protective first.
Phase 0 · insurance
today · ~1 h · ~$2/month · no downtime- Turn on delete protection for all 3 VMsfreeNobody can delete a server by accident, including a bad command run on the "test" VM.
- Daily disk snapshots of website-vm and test, keep 7~$1.5/moIf a server dies or gets wrecked, rebuild it from yesterday's copy in minutes. Today website-vm has no copy at all.
- Nightly database export to the cold bucket, keep 30 days<$0.10/moRestore one table instead of rolling the whole database back a day. Also covers more than 7 days.
- Free uptime check on yummy-future.com and adminfreeAn email within 5 minutes when either goes down, instead of finding out from a customer.
Phase 1 · guardrails for sessions
this week · ~1 day of work · free- A "live write" gate, like the Cloudflare onefreeBlocks database writes and schema changes, Directus/Slack/Stripe writes and ssh to the two live servers, unless a one-time unlock is set. It also closes the release gate's wrapping tricks.
- Live keys locked by defaultfreeKey fetches for live-class keys (Stripe live, database master, Slack bot, Directus admin) need the same one-time unlock. Test keys stay open.
- Dangerous scripts flip to "preview only" unless told otherwisefreeShop test, the 15 Directus scripts and the warehouse setup print what they would do. They change nothing without --write.
- Analytics ignores local and test browsersfreeVisitor numbers and replays only count real visitors.
- Shop and booking honour the test labelfreeA test order can never email a real person or post to a real channel.
Phase 2 · a real test kitchen
next week · ~$10/month- A second Supabase project for staging~$10/moTests, scripts, preview and nightly journeys use it by default. Schema changes run there first, then on live through the release, with an export taken right before.
- After every release, one real tagged form submission end to endfreeProves forms save and send their notifications. Today's check only proves the site loads. A failure rolls back automatically.
Phase 3 · tidy the servers
later · free- Shrink the "test" VM's powersfreeIts account keeps only what it uses (the bucket and logs) and loses Editor on the whole project. Redis gets a password and the broken synchronizer gets fixed.
- Deck stops reloading the website's web serverfreeCheck the deck config before reload, or give the deck its own server block that can't break the site. Cap container memory.
- Releases show what actually changedfreeList changed pages and flag server, database and package changes as "needs a look", not "internal". New pages stay hidden until switched on.
What I need from you
- Phase 0 now? It's insurance only, ~$2/month, with no downtime and nothing breaks. I'd do it today.
- Phase 1 means more stops. Sessions that touch live data will pause until an unlock is set. That's slower for some tasks. Okay?
- Staging database at ~$10/month?
- Are the café services on the "test" VM serving a real store right now? (orders, payments, users) If yes, I fix the broken synchronizer before anything else there.
Not covered by this audit
- Other repos (finance, wefunder-pitch, china-hiring, investor-update) were only scanned for which ones reach the live database and Directus. Most do. Their scripts weren't reviewed one by one.
- Port reachability from the internet couldn't be tested from this Mac (a local proxy answers every port). The Google firewall only opens 80/443 to the "test" VM, plus ssh and remote desktop to all VMs.
- Whether the nightly journeys use live keys was not confirmed. The test harness strips Slack/email/Airtable keys and blocks outside network calls.