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-tolocalhost: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"` — Postgresskips silently if embedded-postgres cannot start, so a green run without thatline 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.Why each section is there
Section titled “Why each section is there”- The link to
all.mdlets 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.