npm install # install depsnpm run dev # Vite dev server on http://localhost:5173npm run build # typecheck (tsc -b) + vite buildnpm run lint # eslintnpm run typecheck # tsc -b --noEmitnpm test # vitest (frontend unit tests)npm run test:deno # deno test (edge-function tests)npm run gen:types # regenerate database types from the linked project
Always run npm run build before committing UI or logic changes - it typechecks
the whole app. Also run npm test (and npm run test:deno if you changed
edge functions) and npm run test:smoke to verify the authenticated app renders.
Add or update tests when you add or change logic like parsing, validation, or
calculations. Tests for frontend logic live next to the code as
src/**/*.test.ts(x); edge-function tests live in supabase/functions/tests/.
The smoke test (npm run test:smoke) is the only check that renders the authenticated
app in a real browser. Lint and typecheck are static; the unit suite mounts one component
at a time; and /login is the only route that renders without a session. So a crash in
the app shell (Layout or a global provider) will pass all those checks but fail at
runtime.
The test loads three real routes (/home, /todos, /artifacts) in headless Chrome
against a stubbed Supabase, with a fake session injected. It fails if a page throws an
uncaught error or if the #root div is empty. It deliberately asserts nothing about
features — it just answers "did the app come up?" — so it stays fast and never flakes
into being ignored.
The test finds a browser that's already installed (via PLAYWRIGHT_CHROMIUM_PATH env
override, or the Claude Code image's /opt/pw-browsers/chromium, or the runner's
Chrome/Chromium) so CI never downloads a browser.
A hook or provider that allocates a globally-keyed resource (a realtime channel topic,
a storage key, a singleton) must be safe to mount more than once. The unit suites cannot
see that, so the smoke test catches it.
The schema lives in supabase/migrations as sequentially-numbered SQL files
(0001_init.sql, 0002_skills.sql, …). They are the single source of truth: a
fresh project becomes a working backend with one supabase db push, and from
then on you never apply migrations by hand.
A GitHub Action (.github/workflows/deploy-migrations.yml) runs supabase db push
whenever a file under supabase/migrations/ lands on main. The CLI only applies
what's pending (the remote tracks applied versions), so merging a PR that adds a new
migration file applies exactly that file — no manual step, safe to re-run.
Automation cron scheduling: After pushing migrations, the workflow automatically
schedules (or reschedules) the background dispatcher and scheduler cron jobs. These
jobs tick once per minute and power listeners, scheduled agents, and webhooks. The
workflow fetches the service role key from the Supabase Management API and calls the
setup_automation_cron() RPC with your project's URL. This is idempotent and safe to
re-run on every deploy. If you set up SupaNet by hand without using the workflow, you
can schedule cron manually from the app: on the Listeners page, if you see an
"Automations aren't running" banner, click Schedule it now — it calls the same RPC.
To set this up, add two repository secrets (Settings → Secrets and variables →
Actions):
Secret
What
SUPABASE_ACCESS_TOKEN
A Supabase personal access token (Dashboard → Account → Access Tokens) — the same one the functions workflow uses.
SUPABASE_DB_PASSWORD
Your project's database password (Dashboard → Project Settings → Database). db push connects straight to Postgres, so the token alone isn't enough.
The project ref defaults in the workflow and is overridable with a repository
variableSUPABASE_PROJECT_REF.
# 1. Create the next sequential file (keep numbers unique and contiguous).# Write it idempotently where practical (create … if not exists, drop … if exists).$EDITOR supabase/migrations/0040_my_change.sql# 2. (Optional) try it locally / against your linked project before merging.supabase db push# 3. Refresh the typed client and open a PR.npm run gen:types
Merging the PR to main triggers the Action, which applies it to the live database.
One rule: every migration filename must have a unique numeric prefix. Two
files sharing a number (e.g. two 0032_*.sql) collide — db push derives the
version from the prefix and will refuse the push. Always use the next free number.
A test (src/lib/migrations.test.ts) runs in CI to catch duplicate or gapped
prefixes before they reach main, preventing silent migration failures. However,
collisions can still slip through a rebase: if your 0086_my_change.sql lands while
someone else's 0086_other_change.sql is already on main, both will be in CI's
single run and the test won't catch it. Re-check the next free number right
before pushing, not just when you create the file.
Changing a function signature: Postgres rejects create or replace function
when the returns or OUT signature changes (error SQLSTATE 42P13), which aborts
the whole db push. When a migration alters an existing function's signature
(e.g. adding a column to a returns table (...)), prepend a DROP:
drop function if exists public.my_function(arg1_type, arg2_type);create or replace function public.my_function(arg1_type, arg2_type)returns table (...) as $$...
A migration that hits this error never records as applied, leaving its objects
missing in production (e.g. a later promote_to_admin in the same file is never
created). Because it failed to apply, editing the file in place to add the DROP is
the correct, safe fix — reapply and it will succeed.
On a brand-new project the storage schema can lag a few seconds. If applying
the full migration fails on storage.buckets, apply the core tables first, then
the storage section.
If you change the schema locally: refresh the typed client with
npm run gen:types and re-check the Supabase security advisors.
Confirmation and magic-link emails use the Supabase project's Site URL plus
the Redirect URLs allowlist. Set these to your deployed origin, or links will
point at localhost.
Tests: a GitHub Action (.github/workflows/test.yml) runs lint, build,
frontend tests, edge-function tests, and the smoke test on every PR and every
push to main. The feature bot's PRs get the same checks via
.github/workflows/claude-feature.yml (GitHub doesn't fire normal PR workflows
for bot PRs, so the feature workflow runs them inline).
Frontend: Railway rebuilds the app from the latest commit.
Edge functions: a GitHub Action redeploys any function that changed.
Database migrations: a separate Action applies any new migration files (see
Database above).
After the one-time setup of secrets and environment variables, you never run
supabase commands by hand.
If you're operating SupaNet as a hosted SaaS with multiple tenant deployments, use the release-to-tenants agent skill to orchestrate the release: a step-by-step runbook that handles preflight checks, opening the main → release PR, merging it, watching the fan-out workflow apply migrations and redeploy edge functions to every live tenant, and verifying the rollout.
The skill needs only gh — no local Supabase credentials. Tell an agent "do a release", "ship to the tenants", or "roll out to all customers" and it will follow the runbook end-to-end.
See Control plane for the architecture of the fan-out workflow.