Yummy Future · production safety audit · 2026-10-09

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.

How this was checked: live reads of GCP, Supabase and both servers, plus three read-only code reviews (the website repo, the release path, and this Mac's automations). Nothing was changed.

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.

93×3

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.

63×2

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.

63×2

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.

42×2

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.

42×2

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.

42×2

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".

33×1

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.

31×3

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.

31×3

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.

22×1

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

Phase 1 · guardrails for sessions

this week · ~1 day of work · free

Phase 2 · a real test kitchen

next week · ~$10/month

Phase 3 · tidy the servers

later · free

What I need from you

  1. Phase 0 now? It's insurance only, ~$2/month, with no downtime and nothing breaks. I'd do it today.
  2. Phase 1 means more stops. Sessions that touch live data will pause until an unlock is set. That's slower for some tasks. Okay?
  3. Staging database at ~$10/month?
  4. 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