SupaNet
Building on SupaNet

Deploy

Run SupaNet locally and ship it.

SupaNet is a standard Vite + React app with Supabase Edge Functions. If you have deployed a Supabase project before, none of this will surprise you.

Local development

npm install        # install deps
npm run dev        # Vite dev server on http://localhost:5173
npm run build      # typecheck (tsc -b) + vite build
npm run lint       # eslint
npm run typecheck  # tsc -b --noEmit
npm 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

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.

Environment and secrets

Two kinds of configuration matter, and keeping them separate is the whole game.

Front end (build-time, public):

VariableNotes
VITE_SUPABASE_URLInlined into the bundle
VITE_SUPABASE_ANON_KEYPublic by design - RLS protects the data

These are read at build time, so they must exist before npm run build.

Edge function secrets (server-side only):

VariableNotes
OPENROUTER_API_KEYRequired. Set with supabase secrets set ..., never commit it
OPENROUTER_MODEL / OPENROUTER_EFFORTOptional overrides
OPENROUTER_SITE_URL / OPENROUTER_APP_NAMEOptional ranking headers

Never put a secret in the front end. Only the anon key belongs there. The OpenRouter key is an edge-function secret and must stay server-side.

Database

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.

How new migrations go live

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):

SecretWhat
SUPABASE_ACCESS_TOKENA Supabase personal access token (Dashboard → Account → Access Tokens) — the same one the functions workflow uses.
SUPABASE_DB_PASSWORDYour 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 variable SUPABASE_PROJECT_REF.

Adding a migration

# 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.

Fresh project notes

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.

Auth redirects

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.

Automatic deployments

Pushing to main updates everything automatically:

  • 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.

Workflow

The workflow is trunk-based: run npm run build, commit, and push to main; hosting and migrations pick it up.

Releasing to tenants (hosted SaaS)

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.

Production serve

npm run start  # serve dist/ with SPA fallback

On this page