These rules apply equally to human contributors and AI agents. They exist because each one corresponds to an incident — something that got broken once, took a while to figure out, and is now cheap to avoid by following a small convention.
Do not hand-edit generated artifacts
Anything below is produced by a tool. If the output looks wrong, fix the input and regenerate; don't patch the output. supabase/migrations/*.sql is the one entry with a documented exception — see below.
Default to the generated migration: run pnpm db:diff and commit its output. Hand-writing or hand-editing a file in supabase/migrations/ is allowed only when db:diff can't express the change safely — and only if you mirror the same change into supabase/schemas/ so a follow-up pnpm db:diff produces an empty diff (zero drift). The schema stays the source of truth; the migration must converge to it. Everything else in the list above has no such exception: fix the input and regenerate.
Schemas-first for the database
supabase/schemas/ is the source of truth for the database. supabase/migrations/ is generated history. The full rules and rationale are in supabase/migrations/README.md; the short version is:
- Edit the file under
supabase/schemas/that owns the affected product domain. - If you added a new schema file, register it in
supabase/config.tomlunder[db.migrations].schema_paths. - Run
pnpm db:diff <short_snake_case_name>to generate a timestamped migration. - Inspect the migration. Prefer fixing the schema and rerunning
db:diffover patching the migration by hand. Hand-edit only whendb:diffcan't express the change safely — and then mirror the same change into the schema so a follow-updb:diffproduces an empty diff (zero drift). - Regenerate types with
pnpm db:types:localand commit schema + migration + regenerated types in the same PR.
See Database Workflow for the schema file layout and the standard flow.
Dedupe-first when bumping cross-package dependencies
pnpm hashes peer-dependency variants. If two workspace packages depend on, say, react@19.2.4 and react@19.2.5, pnpm will install two copies — and any library that relies on module-level singletons (Sonner toasts, React contexts, Zustand stores, etc.) will silently break because each package gets a different singleton.
Before bumping a shared dep in one app, check whether any other app or workspace package depends on it:
pnpm why <pkg>If multiple workspace entries pin to different versions, either bump them all in lockstep or align the new app to the existing pin. Don't introduce a fresh version just because the install resolves cleanly — the consequence shows up at runtime as features that visibly don't work and no error in the console.
Sonner is a known landmine. If apps/admin and packages/ui resolve to different sonner copies, the toast() calls in admin point at one singleton and the <Toaster /> mounted in admin (via the ui package) listens to another — toasts silently fail. Keep React and Sonner versions identical across every app that consumes @referenta/ui.
Regenerate derived artifacts after their source changes
| Changed | Regenerate with |
|---|---|
supabase/schemas/*.sql | pnpm db:types:local (regenerates apps/dashboard/database.types.ts). For apps/admin, also run pnpm --filter admin db:types:local. |
apps/cms content schema | pnpm --filter cms strapi generate if you edited content types via the file system; the Strapi UI handles it otherwise. |
| Service-role keys / Supabase project URLs | Rewrite the relevant .env.local. Restart any running dev server — Next.js reads env at process start. |
If the schema change adds or renames columns, type regeneration must happen in the same PR as the schema change. A merged migration with stale types will break type-checking on the next person's branch.
Don't bypass git hooks
lefthook + biome enforce formatting and lint on every commit. If a hook blocks a commit, fix the underlying issue — don't pass --no-verify. The same applies to --no-gpg-sign and amending published commits.
If a hook is wrong (not the code), fix the hook in lefthook.yml and ship it as a separate small PR.
Don't redeclare env vars unnecessarily
pnpm env:pull writes hosted-dev credentials to apps/dashboard/.env. The local Supabase wrapper (supabase/dev.sh) writes the local Supabase + service URLs to apps/dashboard/.env.development.local. Next.js precedence makes .env.development.local win for local dev, so the two coexist cleanly.
If you find yourself adding the same key to multiple .env* files, stop and check which one Next.js actually loads first (precedence: .env.local → .env.development.local → .env.development → .env). Redeclaring a key in a higher-precedence file is a deliberate override, not a default.