Skip to content
- Node 22 required. Node 18 fails in ways that read as âcommand not foundâ.
nvm use.
npm run setup writes .env once and never touches it again. Re-running it
will not fix a value you changed by hand.
SESSION_SECRET is optional in development, and without it a fresh one is
generated per boot, so restarting the API signs you out. Required in production.
- Ports: Vite 5173, API 4000. Avoid 5000 on macOS; AirPlay Receiver binds it.
VITE_BELUGA_API=false renders the bundled fixture with no database. Right for
UI work, confusing if you set it and forget.
- A fresh checkout has no store in it.
npm test seeds per suite; npm run test:e2e drives the real app against data/beluga.sqlite and fails on a
missing heading in a new clone. npm run db:migrate && npm run db:seed once
per checkout.
.env is irrelevant to e2e. Playwright points ENV_FILE at a file that does
not exist, on purpose.
- Postgres skips silently in
db/dialect.test.ts if embedded-postgres
cannot start. Confirm with --reporter=verbose and look for repository on postgres.
- The success redirect proves nothing. Only the webhook marks an order paid.
Anyone testing without
stripe listen concludes checkout is broken.
- Local testing needs
stripe listen --forward-to localhost:4000/api/webhooks/stripe, the printed whsec_⌠in .env, and an
API restart. The restart is the step people skip.
- Stripe Prices are immutable. Changing an amount mints a new Price and
archives the old one. That is why historic orders still resolve.
- Same for
tax_behavior, so changing how a store quotes prices reaches Stripe
only when each product is republished, and nothing republishes itself.
- A product not published to Stripe cannot be bought. Saving does not publish.
- Test and live keys have separate catalogues. Publishing under test keys puts
nothing in the live account.
- SQLite is a file and uploaded images are files. On any platform with an
ephemeral filesystem, both vanish on redeploy. Attach a volume, or move to
Postgres and a bucket. The shape of a deployment.
PUBLIC_URL builds Stripeâs success and cancel URLs and every emailed link.
Wrong value: buyers redirected somewhere wrong after paying.
- SEO head rewriting runs only in the production branch, invisible under
npm run dev. Verify with npm run build && npm start and curl the title.
- A half-configured
ASSETS_S3_* group, or a stray one with no bucket, is
refused at boot. That is the feature; read the message.
- The Stripe webhook endpoint has to be created again for live keys, with a new
secret. Until then the live store records no orders.
- Email is a logged no-op until
SMTP_URL is set. Orders complete, no mail
sent, nothing errors.
- Tax is off by default and under-collection is silent. Three things must be
true in Stripe first, none doable from Beluga.
- Shipping fails silently three ways. A coverage gap ships free; no recorded
weights means everything matches the lightest band; subtotal bounds ignore
digital lines. Shipping.
- Abandoned cart recovery is off by default, and a guestâs cart is never
stored server-side.
- Customer orders link to an account only after email verification. Not an
oversight.
- A partial refund does not restock. Full refunds and cancellations do, once.
role is recorded but gates nothing. Every administrator can do everything.
- Removing a staff member destroys their sessions immediately.
- The webhook signing secret is shown once. Rolling it invalidates the old one
immediately.
- Every schema change lands in both dialects, then
npm run db:generate.
- New routes go in
MUTATIONS or READS in server/security.test.ts. Not
optional.
- Money is integer cents everywhere. CSV columns are
*_cents for the same
reason.
- Storefront search is client-side against the
/api/store snapshot, capped
at 200 products. Past that, swap to GET /api/products?search= behind
src/lib/store-source.ts.
- Image derivative naming lives in
shared/images.ts because both sides use it,
and nothing type-checks that they agree.
- Reserved page slugs (
shop, cart, confirm, product, collection,
about, admin, setup, account) are refused.
variantName and aboutText are deprecated but still present, kept one
release for rollback. Do not build on them.
- Verify outbound webhooks against the raw body. A re-serialised body fails
on the first payload with different key ordering.