Skip to content

A CLAUDE.md for your fork

Put this at the root of your fork as CLAUDE.md, AGENTS.md, or whatever your assistant reads. Edit the first section to describe your store; leave the rest. The invariants are copied from the invariants page, which is the source; if the two ever differ, that page wins.

# <Your store>
A store built on Beluga: React 19 storefront and admin, Express 5 API,
SQLite or Postgres, Stripe Checkout. Documentation: https://belugajs.com —
the whole set is one file at https://belugajs.com/docs/all.md.
Our changes from upstream live in: <list the files or directories>.
## Setup
Node 22 (`nvm use`). `npm install`, `npm run setup`, `npm run dev:all`.
Storefront on :5173, API on :4000. `stripe listen --forward-to
localhost:4000/api/webhooks/stripe`, put the whsec in .env, restart the API.
## The invariants
A change that breaks one is wrong even if every test passes.
1. Money is integer cents, everywhere. No floats, no `* 100`. See shared/money.ts.
2. Money never comes from the request. Prices and totals are read from the
database on every path; a request carries ids and quantities only.
3. The webhook is the only thing that marks an order paid. The success
redirect proves nothing. Never add a payment state transition elsewhere.
4. Webhook handling is idempotent: events are deduplicated by id, and a
failed handler releases the dedup record so the retry is processed.
5. Every admin route is behind requireAdmin + verifyCsrf, applied once to the
whole router in server/routes/admin.ts. Never mount an admin endpoint
outside it. Register every new route in server/security.test.ts.
6. Every schema change lands in both dialects: db/schema.sqlite.ts and
db/schema.pg.ts together, then `npm run db:generate`, commit both migrations.
7. Products reach Stripe only via explicit publish
(POST /api/admin/products/:id/publish). Never write to Stripe on save.
8. Stripe Prices are immutable. Changing an amount creates a new Price and
archives the old one. Do not mutate.
## Conventions
- Validation lives in shared/ as zod schemas (shared/api.ts for inputs),
never inline in a route.
- Routes stay thin: parse, call a repository function in db/*-repository.ts,
respond. Errors use httpError(status, message) with a user-facing message.
- Client mutations go through csrfPost/csrfPut/csrfDelete in src/lib/api.ts,
wrapped in a hook in src/admin/queries.ts that invalidates the store key.
- Storefront components read colours and radius from --beluga-* tokens.
- Digital lines are excluded from shipping, never given a weight of zero.
- Outbound webhooks are verified against the raw body, never a re-serialised one.
- Comments explain why, not what.
## Before saying a change is done
npm run typecheck && npm run lint && npm test
`npm test -- --reporter=verbose | grep "repository on postgres"` — Postgres
skips silently if embedded-postgres cannot start, so a green run without that
line did not test both dialects.
A fresh checkout has no store: `npm run db:migrate && npm run db:seed` before
`npm run test:e2e`. `.env` is irrelevant to e2e.
## Files that hold a rule (read the invariant before editing the logic)
src/pages/CartPage.tsx, src/pages/ConfirmPage.tsx,
src/components/product/ProductDetails.tsx, server/routes/checkout.ts,
server/routes/webhook.ts, anything under db/.
## Branching
Branch from main for every task. Never commit onto a merged branch.
  • The link to all.md lets an assistant with web access read the real docs instead of guessing from general ecommerce knowledge.
  • The invariants are the things that produce wrong-but-passing code.
  • The Postgres line is there because it is the single most common way a “tests pass” claim is untrue in this codebase.
  • The list of rule-holding files is the short form of The seams.