This file defines the baseline engineering conventions for agents working in this repository.
- Never use em-dashes (
—) in any output: code, comments, docs, commit messages, PR descriptions, or drafted emails. Use a comma, a full stop, a colon, or parentheses instead, whichever fits the sentence. - Talk in summary mode. Keep chat responses short and to the point. No filler, no restating the question, no unnecessary elaboration.
- Use plain language (ISO 24495-1:2023) for anything presented to the user: chat responses, docs, commit messages, PR descriptions, and drafted emails. Write so the intended reader can find what they need, understand it the first time, and use it: short sentences, everyday words over jargon, active voice, and one idea per sentence. This applies only to user-facing text, not internal reasoning or code identifiers.
Read docs/project-context.md before making any changes.
It contains the product vision, tech stack, and data model decisions.
Read docs/security.md before any change that touches authentication, roles,
RLS policies, or data access. This includes adding a table, a server function
that reads or writes data, or a new role. It explains how identity flows from browser to
database, the three security layers, and a per-table RLS coverage matrix. RLS is
the security boundary; app-layer assert* checks are convenience only. Keep the
matrix and "Known gaps" section in docs/security.md in sync with any policy change.
Auth emails (invite, magic link, password reset) failing? Read the
troubleshooting section in docs/security.md before touching SES, SMTP credentials,
or sender domains. A stale SUPABASE_SECRET_KEY presents as an email-sending
failure, and debugging the email provider is a dead end.
- PREFER functional approach over object oriented and imperative
- Functions shouldn't be doing too much - try and ensure they do one thing
- Path alias
@/*maps to./src/*. Prefer it over relative imports - Auth is handled by Supabase Auth; use
getSession/ensureSessionfromsrc/lib/auth/auth.functions.ts(seedocs/security.mdfor the full model) - Server-side Supabase access:
createSupabaseServerClient()fromsrc/lib/supabaseServer.ts(used inside.server.tshelpers) - Browser-side Supabase access:
supabasesingleton fromsrc/lib/supabase.ts - Server-side code layout (TanStack Start file-naming convention):
src/lib/<entity>/<entity>.functions.ts:createServerFnwrappers, safe to import from routes and componentssrc/lib/<entity>/<entity>.server.ts: server-only helpers (Supabase queries, admin checks, mappers); never import these from client codesrc/lib/<entity>/schema.ts: Zod schemas and types; client-safe- Do not define
createServerFncalls inside route files; always extract to<entity>.functions.ts - Test files for
.server.tshelpers must use plain.test.tssuffix (not.server.test.ts) to avoid the TanStack import-protection plugin stubbing them out in jsdom
- Environment variables are documented in
.env.example: never put env var lists in docs or README files
We are migrating server modules away from a monolithic .server.ts toward a three-layer split.
orderRequests is the reference implementation; apply this pattern to a module when you touch it
(one module per PR). The split exists so business logic is unit-testable in isolation and portable
to a future standalone API that mobile + web both call.
src/lib/<entity>/<entity>.repository.ts: the only place that talks to Supabase. Exports a<Entity>Repositorytype plus acreate<Entity>Repository()factory. Each method builds a query, executes it, and returns aResultof raw rows. No business rules here. Not unit-tested (covered by e2e).src/lib/<entity>/<entity>.service.ts: all business logic: mappers, role-based decisions, fan-out, authorization orchestration. Pure / Supabase-free. It receives its collaborators via adepsargument:{ repo, session, authorize, notify }. This is the layer we unit-test: pass a hand-built fakerepo(avi.fn()per method) and assert on outcomes. May import server-only modules as types only (import type), never at runtime.src/lib/<entity>/<entity>.functions.ts: transport adapter. Constructs the realdeps(real repository,fetchSessionOrThrow, anauthorizeclosure, realnotify) and passes them to the service. KeepcreateServerFnnames/signatures stable so routes/components/e2e are unaffected.
Rules:
- DI is by argument, not module mocking: services take
deps; tests build fakes. Do notvi.mockthe Supabase client. - Repositories return the existing
Result<T>(ok/errfrom@/lib/result). Services map repo errors to domain outcomes, also returningResult. - Transactions: supabase-js cannot run a multi-statement transaction across two
.from()calls. Atomicity requires a Postgres function called via.rpc(). Model any multi-write as a single repository method (e.g.createOrderWithItems) so converting it to an atomic RPC later is a one-method change. See theTODO(atomicity)marker inorderRequests.repository.ts. - Future API extraction: services are transport-agnostic by design. When we stand up a dedicated
API (for mobile), it calls the same
<entity>.service.tsmodules: no logic is rewritten.
Requires Docker running.
supabase start # starts Postgres, PostgREST, Studio, Auth locally
supabase db reset # applies migrations + seed.sql
vp dev # starts the app on http://localhost:3344Copy .env.example to .env.local and fill in the values. After supabase start, paste the printed anon key into .env.local.
Supabase Studio is at http://localhost:54323. Use it to inspect auth users and data.
After every
supabase db reset: Kong loses its upstream connection to the auth container because the DB container restarts and gets a new IP. Always run:docker restart supabase_kong_orderflowWait ~4 seconds before using the app or making auth requests.
Email/password login requires
enable_signup = truein[auth.email]ofconfig.toml. In CLI v2.98+, this also controlsGOTRUE_EXTERNAL_EMAIL_ENABLED: if it'sfalse, all email logins fail withemail_provider_disabled. Changes toconfig.tomlrequiresupabase stop && supabase startto take effect.
supabase start # must be running
vp dev # must be running in another terminal
vp run test:featuresSMOKE_SITE_URL=https://bwow.app SMOKE_TEST_EMAIL=... SMOKE_TEST_PASSWORD=... vp run test:smokeUse vp as the default interface for local tooling.
vp dev- local development unless a script-specific setup is requiredvp check- lint + format + type checksvp test- test executionvp fmt- formatting codevp lint- formatting codevp build- production buildsvp run <script>- when you need a custompackage.jsonscript
Avoid bypassing Vite+ wrappers for core workflows.
vp exec- Execute a command from localnode_modules/.binvp dlx- Execute a package binary without installing it as a dependencyvp cache- Manage the task cache
Vite+ automatically detects and wraps the underlying package manager through packageManager in package.json and lockfiles.
vp add- Add packages to dependenciesvp remove(rm,un,uninstall) - Remove packages from dependenciesvp update(up) - Update packages to latest versionsvp dedupe- Deduplicate dependenciesvp outdated- Check for outdated packagesvp list(ls) - List installed packagesvp why(explain) - Show why a package is installedvp info(view,show) - View package information from the registryvp link(ln) / unlink - Manage local package linksvp pm- Forward a command to the package manager
- Keep code DRY; extract repeated logic into named functions/constants
- Prefer to use Result over throwing errors
- Prefer small, composable functions over large inline blocks
- Prefer
switchover longif/elsechains when branching on a single discriminator - Avoid magic numbers/strings; introduce well-named constants
- Use camelCase naming for constants: never SCREAMING_CASE
- Group related constants into a single
as constobject rather than many individualconstdeclarations - Avoid casting with TypeScript
as: never useas unknown asor escape hatches; they hide runtime errors and defeat the type system. Fix the root type instead (e.g. usefrom:onuseRouteContext, add a proper type guard withObject.hasOwn, or export the correct type from the source). The only acceptableasusages are narrowing a known-safe union (e.g.value as ConcreteTypeafter anifcheck) or satisfying an external library whose types are wrong. - Object constants should end with
Valuesuffix (for example:defaultConfigValue,sidebarConfigValue) - Place shared constants in a
constants.tsfile co-located with the module that owns them; only extract tosrc/lib/constants.tsif used across multiple domains - Avoid unnecessary comments; add short comments only for non-obvious intent
- Keep modules/components reasonably small; split when complexity grows
- Non-component TypeScript module file names should be camelCase (for example:
authService.ts) - React component file names should be PascalCase (for example:
OrderList.tsx) - Exception: files generated by shadcn (typically under
src/components/ui/and any shadcn-scaffolded hooks) retain their original kebab-case names. Do not rename them - Co-locate types with components/modules when practical
- Do not visually verify UI changes (no browser/agent-browser); the maintainer verifies these manually. Unit tests, linting, and the other quality gates passing is sufficient to consider UI work done.
- Use React + TypeScript with explicit prop types in the same file
- Do not use inline object types in component parameters; declare a
type/interfacefirst - Keep constants outside component bodies unless they must capture runtime state
- Prefer Tailwind utility classes over inline
styleprops - Name event handlers with
handleprefix (for example:handleSubmit) - Use accessible, semantic HTML and keyboard-friendly interactions by default
Routes are glue: they own navigation and server function calls only. Everything else belongs in the component:
| Belongs in component | Belongs in route |
|---|---|
UI state (submitting, error) |
useNavigate() calls |
| Input mapping (form fields → server payload) | createServerFn calls |
| Conditional rendering, form logic | Loader data fetching |
onSubmit prop receives the mapped payload |
Passes loader data + callbacks as props |
The onSubmit callback a route passes to a component should accept an already-mapped, submission-ready payload, not raw form state. This keeps the component self-contained and independently testable: render it with props, interact with it, assert on what onSubmit was called with.
- Centralize environment variable parsing/validation through Zod-based config modules
- Keep data access logic grouped by domain; avoid scattering query logic through UI layers
- Use explicit mapping/transform steps where boundaries between external/internal shapes exist
- Environment variables are documented in
.env.exampleonly: never list env vars in docs, README, or CLAUDE.md files
Vitest runs through Vite+ (vp test). The runner is wired up with jsdom, @testing-library/react, and @testing-library/jest-dom matchers. describe/it/test/expect are globals: do not import them.
- Server functions in
src/lib/<entity>/<entity>.server.ts: test these directly by mockingcreateSupabaseServerClient. Every exported helper should have coverage. Name test files<entity>.test.ts(not<entity>.server.test.ts, see layout note above). - Business logic in
src/lib/(validators, mappers, utilities) - Composed React components that wire shadcn primitives together, handle forms, conditional rendering, or display derived data
- Custom hooks with non-trivial behavior
Do not unit-test shadcn primitives directly, trivial pass-through components, or framework code (TanStack Router/Query/Form internals).
- Co-locate tests next to source:
OrderList.tsx→OrderList.test.tsx - Use
*.test.tsfor logic,*.test.tsxfor components - File naming follows the source file: PascalCase for component tests, camelCase for module tests
Build domain-model test data with the fixture factories in src/test/fixtures/<entity>Fixtures.ts
(imported via @/test/fixtures/...) instead of hand-writing full object literals. Each entity exposes
a make<Entity>(overrides: Partial<T> = {}): T that fills sensible defaults; pass only the fields a
test actually asserts on via overrides so the test still reads self-contained (DAMP). Entities with a
snake_case repository-row variant also expose a make<Entity>Row for service-layer repo mocks. Add a
new factory here when you introduce an entity that more than one test needs to construct.
Per Software Engineering at Google: test code should contain no logic. A test that needs reasoning to verify is a test that itself needs tests. Prefer obvious, repetitive, linear tests over clever ones.
- No conditionals, loops, or
try/catchin tests. If a branch matters, write two tests. - No computed expected values. Hard-code expected output as a literal: don't recompute it from the input in the test.
- Inline test data. Avoid shared fixtures or factory helpers unless the duplication is genuinely painful; descriptiveness beats deduplication ("Descriptive And Meaningful Phrases" over Don't Repeat Yourself).
- One behavior per test. A failing test name should pinpoint the broken behavior without reading the body.
- Arrange / Act / Assert structure, with a blank line between phases when it aids readability.
- No abstraction over the system under test. Call the real function or render the real component directly; avoid wrapper helpers that hide what's being tested.
A good test reads top-to-bottom like a story; a reader should not have to scroll to a helper to know what's going on.
- Query by accessible role, label, or text, not by
data-testidor CSS classes. UsegetByRole,getByLabelText,getByText. - Use
userEvent(notfireEvent) for interactions when we add it. - Use
findBy*for async appearance; do not chainwaitForaroundgetBy*. - Assert on rendered output the user sees, not on internal component state.
Coverage runs via vp test --coverage (v8 provider). The following are excluded from coverage and should not be unit-tested directly:
src/components/ui/**: shadcn primitivessrc/routeTree.gen.ts: generatedsrc/router.tsx: app-wiring entrypointsrc/integrations/**: third-party integration gluesrc/lib/database.types.ts: generated Supabase types
If a folder is fundamentally not worth testing, exclude it in vite.config.ts rather than writing throwaway tests to lift a number.
Keep components free of plumbing. Components should accept callbacks (props) for side effects and not import useRouter, the Supabase client, or other infrastructure directly. The route or parent owns navigation, data fetching, and auth; the component renders UI and invokes the callbacks it was given. This makes tests trivial: pass a vi.fn() and assert on what the component called.
- Prefer real implementations over mocks. Mock only at integration boundaries (Supabase client, network, time, the router).
- Mocks reset automatically between tests:
clearMocks: trueis set globally invite.config.ts. Do not callvi.clearAllMocks()inbeforeEach. - For module mocks, declare
vi.mock(...)at the top of the file, then usevi.mocked(importedFn)inside each test to configure return values. This gives you full type safety onmockResolvedValue/mockReturnValuewithout casts. - Hoist shared setup (
userEvent.setup(), common renders) intobeforeEachrather than repeating it in each test. - For Supabase access, mock the function in
src/lib/queries/*that the component calls: don't mock the Supabase client directly.
Complete the following when completing a set of changes:
vp checkvp testif any source or test files changedvp buildfor larger or riskier changes- Run the
/caveman-reviewskill and address all valid suggestions - Open a PR, then confirm CI checks pass on it
Pre-commit hooks are configured through Vite+ staged checks; ensure any auto-fixes are included in commits.
- Check for current stable package versions before adding dependencies
- Avoid deprecated packages/APIs when alternatives exist
- Always pin dependencies to exact versions: no
^or~prefixes inpackage.json
- Using package managers directly for routine workflows: prefer
vp - Assuming built-in command behavior follows scripts:
vp devruns Vite+, not script aliases - For custom script behavior: use
vp run <script> - Bypassing checks before handoff: always run quality gates from this file
- Run
vp installafter pulling remote changes and before starting work. - Run
vp checkandvp testbefore final handoff. - Run
vp buildwhen changes affect build/runtime boundaries. - Prefer alias imports (
@/*) over long relative paths. - Keep environment variables validated through Zod config.
Prefer to use Typescript and Bun for scripting over Python and other shells.