Skip to content

Commit b2296a3

Browse files
committed
docs: correct env/deploy model and sync admin write-protection in CLAUDE.md
Runtime server secrets (incl. ADMIN_SECRET) are read from the Cloudflare Worker's own Variables and Secrets at runtime — that's the authoritative production source; NEXT_PUBLIC_* are inlined at build time (local .env.local or CI secrets). Document both deploy paths (manual wrangler + GitHub Actions on push to main) instead of implying GitHub secrets hold runtime values. Also sync the write-protection prose to the merged UI: adminFetch opens a styled unlock dialog that verifies via POST /api/admin/verify, plus a nav lock with login/logout. https://claude.ai/code/session_01UcazDxsJCyZoTop3psfTY2
1 parent 66ee7ea commit b2296a3

1 file changed

Lines changed: 13 additions & 6 deletions

File tree

CLAUDE.md

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,22 +47,29 @@ There is no test framework. `npx tsc --noEmit` is the only automated correctness
4747
| `GOOGLE_MAPS_API_KEY` | **API routes only** — Google Places API (New), proxied via `/api/places/*` |
4848
| `ADMIN_SECRET` | **API routes only** — shared secret gating writes (see "Write protection" below) |
4949

50-
`SUPABASE_SERVICE_ROLE_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_MAPS_API_KEY`, and `ADMIN_SECRET` must never reach the client — the Places key is used only server-side in `lib/places.ts`, and the browser hits `/api/places/{autocomplete,details}` instead. Local dev reads from `.env.local`; production deploys via `.github/workflows/deploy.yml` inject the build environment from GitHub Actions secrets (`NEXT_PUBLIC_*` get inlined into the bundle by Next.js at build time, so they don't need a `[vars]` block in `wrangler.toml`). **Add `GOOGLE_MAPS_API_KEY` as a GitHub Actions secret and pass it through in `deploy.yml`**, otherwise the deployed Worker's Places routes return 503. **`ADMIN_SECRET` is wired the same way — set it in `.env.local` for dev and as a GitHub Actions secret (already passed through in `deploy.yml`); if it's missing, every guarded route fails closed with 503, so the curator can't edit either.** `.env.local.example` also lists `TELEGRAM_BOT_TOKEN` / `TELEGRAM_WEBHOOK_URL` but no Telegram routes currently exist in the codebase — treat those as dormant.
50+
`SUPABASE_SERVICE_ROLE_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_MAPS_API_KEY`, and `ADMIN_SECRET` must never reach the client — the Places key is used only server-side in `lib/places.ts`, and the browser hits `/api/places/{autocomplete,details}` instead.
51+
52+
**Where each variable lives.** Local dev reads everything from `.env.local`. In production the two kinds of variable live in different places:
53+
54+
- **`NEXT_PUBLIC_*`** are inlined into the JS bundle by Next.js **at build time**, so they must be present wherever `cf:build` runs — in `.env.local` for a local build, and as GitHub Actions secrets for the CI workflow. They are not runtime Worker vars and don't need a `[vars]` block in `wrangler.toml`.
55+
- **Server-only secrets** (`SUPABASE_SERVICE_ROLE_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_MAPS_API_KEY`, `ADMIN_SECRET`) are read via `process.env` **at runtime** by the open-next adapter, sourced from the **Cloudflare Worker's own Variables and Secrets** (dashboard → Workers → `food-picker` → Settings → Variables and Secrets, as encrypted **Secrets**, or `wrangler secret put`). That Worker config is the authoritative production source — set them there. If `GOOGLE_MAPS_API_KEY` is missing the Places routes return 503; if `ADMIN_SECRET` is missing every guarded route fails closed with 503, so the curator can't edit either.
56+
57+
Two things can deploy the Worker, both running `wrangler deploy`: a **manual** deploy (`npm run cf:deploy`, or `bash scripts/deploy.sh`) and the **GitHub Actions** workflow `.github/workflows/deploy.yml`, which fires on every push to `main`. `.env.local.example` also lists `TELEGRAM_BOT_TOKEN` / `TELEGRAM_WEBHOOK_URL` but no Telegram routes currently exist in the codebase — treat those as dormant.
5158

5259
### Write protection (`ADMIN_SECRET`)
5360

54-
The DB is single-curator: only the creator writes, friends only read and ask for recommendations. All API routes use the service-role client, which bypasses Supabase RLS, so the gate lives in the route handlers, not the database. `requireAdmin()` (`lib/admin-auth.ts`) compares an `x-admin-secret` header against `ADMIN_SECRET` and is called at the top of every **mutating or cost-incurring** route: `POST /api/restaurants`, `PATCH`/`DELETE /api/restaurants/[id]`, `generate-tags`, `generate-summary`, and `places/{autocomplete,details}`. Reads (`GET`) and the friend-facing `/api/recommend` + `/api/recommend/feedback` stay open. On the client, `adminFetch()` (`lib/admin-client.ts`) attaches the header from `localStorage`, prompting the curator once on a 401 (passive callers like address autocomplete pass `prompt: false` so typing is never interrupted). This is a shared password, not per-user auth — it stops anonymous scripts from wiping or spamming the DB; it does not authenticate individuals.
61+
The DB is single-curator: only the creator writes, friends only read and ask for recommendations. All API routes use the service-role client, which bypasses Supabase RLS, so the gate lives in the route handlers, not the database. `requireAdmin()` (`lib/admin-auth.ts`) compares an `x-admin-secret` header against `ADMIN_SECRET` and is called at the top of every **mutating or cost-incurring** route: `POST /api/restaurants`, `PATCH`/`DELETE /api/restaurants/[id]`, `generate-tags`, `generate-summary`, and `places/{autocomplete,details}`. Reads (`GET`) and the friend-facing `/api/recommend` + `/api/recommend/feedback` stay open. On the client, `adminFetch()` (`lib/admin-client.ts`) attaches the header from `localStorage`; when the secret is missing or rejected it opens a styled unlock dialog (`components/AdminUnlockDialog.tsx`) that verifies the input against `POST /api/admin/verify` before storing it, so a wrong password shows an inline error instead of silently failing (passive callers like address autocomplete pass `prompt: false` so typing is never interrupted). A lock control in the nav (`components/AdminNavLock.tsx`) reflects locked/unlocked state via `useSyncExternalStore` and offers proactive login/logout — logout clears the stored secret on that device. This is a shared password, not per-user auth — it stops anonymous scripts from wiping or spamming the DB; it does not authenticate individuals.
5562

56-
### Cloudflare deploy prerequisites
63+
### Cloudflare deploy auth
5764

58-
The deploy workflow needs two more GitHub Actions secrets:
65+
Both deploy paths run `wrangler deploy`, so wrangler needs Cloudflare auth — `wrangler login` (browser OAuth) for a local deploy, or these two for the GitHub Actions workflow (read from GitHub Actions secrets):
5966

60-
| Secret | Notes |
67+
| Variable | Notes |
6168
|---|---|
6269
| `CLOUDFLARE_API_TOKEN` | Create via the dashboard preset **"Edit Cloudflare Workers"** — not a hand-rolled token with only `Workers Scripts: Edit`. The asset-upload-session endpoint requires the full preset's scopes (Workers Scripts/Routes/KV/R2, Account Settings, User Details, Memberships). |
6370
| `CLOUDFLARE_ACCOUNT_ID` | The account that owns the `food-picker` Worker. |
6471

65-
If deploy fails with `entitlements.not_available [code: 10007]` on the `assets-upload-session` call, the cause is one of: token missing scopes, account hasn't accepted the current Workers TOS in the dashboard, or wrong `CLOUDFLARE_ACCOUNT_ID`. The "Cloudflare auth diagnostics" step in `deploy.yml` runs `wrangler whoami` before deploy — check its output (token permissions and account list) in the failed Actions run first.
72+
These authenticate the deploy only — the app's **runtime** secrets are not set here; they live on the Worker itself (see Environment variables above). If deploy fails with `entitlements.not_available [code: 10007]` on the `assets-upload-session` call, the cause is one of: token missing scopes, account hasn't accepted the current Workers TOS in the dashboard, or wrong `CLOUDFLARE_ACCOUNT_ID`. Run `wrangler whoami` to check token permissions and the account list first.
6673

6774
## Architecture — things that need >1 file to understand
6875

0 commit comments

Comments
 (0)