Skip to main content

Getting Started

This setup runs the React application, Docusaurus site, and local Supabase stack. Gemini, Resend, LiveKit, R2, and Sentry are optional for most UI work, but features that call them will degrade or fail until their secrets are configured.

Prerequisites

  • Node.js 20 or later
  • npm 10 for frontend lockfile generation and validation
  • Docker Desktop or another Docker-compatible daemon
  • Supabase CLI, normally invoked through npx supabase
  • Git
  • pnpm only if developing the separate agent/ service
  • HawkScan CLI only for the local DAST workflow

Check the toolchain:

node --version
npx -y npm@10 --version
docker version
npx supabase --version

Install Dependencies

Frontend

cd frontend
npm ci

npm ci consumes the committed lock without changing it. Do not use a plain npm 11 install to update frontend dependencies.

Documentation

cd docs
npm ci

LiveKit agent

Only required for group-voice development:

cd agent
pnpm install
cp .env.example .env

Dependency-lock discipline

Cloudflare Pages runs npm 10 on Linux. npm 11 can create a lock that omits optional peer transitive dependencies required by npm 10, including the @emnapi/* packages used by Tailwind's WASM fallback.

Whenever frontend/package.json dependencies change, run from frontend/:

npx -y npm@10 install --package-lock-only --no-audit --no-fund
npx -y npm@10 ci --dry-run --no-audit --no-fund

Then verify package-lock.json contains:

"node_modules/@emnapi/core"

Commit package.json and package-lock.json together. A failed npm-10 dry run means the lock is not safe to commit.

Start Local Supabase

From the repository root:

npx supabase start
npx supabase db reset
npx supabase status

db reset recreates the local database, applies all ordered migrations, and runs supabase/seed.sql. It is destructive to the local Supabase database.

supabase/config.toml is Git-ignored in this repository, so a fresh clone does not inherit one developer's ports or Auth control-plane settings. Always use the values printed by supabase status.

The local DAST scripts and the team-local profile audited with this guide expect this remapped range:

ServiceURL
API gatewayhttp://127.0.0.1:54421
Postgrespostgresql://postgres:postgres@127.0.0.1:54422/postgres
Studiohttp://127.0.0.1:54423
Inbucket email UIhttp://127.0.0.1:54424

If your local config uses different ports, update the local-only tooling rather than assuming the table overrides supabase status.

Local Auth behaviour

The team-local profile audited with this guide enables:

  • email OTP signup and sign-in
  • six-digit codes with a 3600-second expiry
  • two email sends per hour
  • 30 sign-in/signup requests per five minutes
  • the public.hook_before_user_created signup gate

No inactivity timeout is enabled in that local profile. Inbucket captures local Auth email instead of delivering it externally. Because config.toml is not tracked, recreate and verify these settings on a new workstation before using them as test assumptions.

Configure the Frontend

Create frontend/.env.local:

VITE_SUPABASE_URL=http://127.0.0.1:54421
VITE_SUPABASE_ANON_KEY=<anon key from supabase status>
VITE_APP_ENV=development

# Optional browser telemetry
VITE_SENTRY_DSN=

# Node-side E2E setup only. Never add a VITE_ prefix.
SUPABASE_SERVICE_ROLE_KEY=<local service-role key>
E2E_ALLOW_PROD=false

Only VITE_* values are bundled into browser JavaScript. The service-role key is permitted only for Node-side test/setup code and must never have a VITE_ prefix.

Serve Edge Functions Locally

Create a gitignored supabase/functions/.env for optional provider secrets:

GEMINI_API_KEY=
APP_URL=http://localhost:5173
RESEND_API_KEY=
LIVEKIT_URL=
LIVEKIT_API_KEY=
LIVEKIT_API_SECRET=
R2_ACCOUNT_ID=
R2_ACCESS_KEY_ID=
R2_SECRET_ACCESS_KEY=
R2_BUCKET=
GEMINI_LIVE_MODEL=
GEMINI_LIVE_VOICE=
GEMINI_TTS_MODEL=

Then serve the checked-in functions:

npx supabase functions serve --no-verify-jwt

All functions verify identity in application code. Public functions and WebSocket/MCP discovery also depend on bypassing gateway JWT validation.

Provider-free development remains useful:

  • CRUD, RLS, invitations, tasks, and most UI work use local Supabase.
  • AI functions return configuration/provider errors without a Gemini key.
  • branded mail returns a successful response with sent:false when Resend is unavailable; link-bearing mail also declines to send without APP_URL.
  • R2 retention reports unconfigured while browser-side extraction can still run.
  • group voice returns 503 without LiveKit secrets.

Run the Frontend

cd frontend
npm run dev

The default Vite URL is http://localhost:5173.

Frontend scripts

CommandPurpose
npm run devVite development server
npm run buildtsc -b then production Vite build
npm run lintESLint
npm testVitest once
npm run test:watchVitest watch mode
npm run test:coverageVitest coverage
npm run test:e2ePlaywright suite
npm run test:e2e:uiPlaywright UI
npm run test:e2e:headedHeaded Playwright
npm run test:e2e:reportOpen the saved HTML report
npm run i18n:auditLocale-key and interpolation-token parity

There is no standalone frontend npm run typecheck; npm run build performs the TypeScript project build.

Run the Documentation

cd docs
npm run start

Before submitting docs:

npm run typecheck
npm run build

The docs package requires Node 20 or later.

Run the LiveKit Agent

The agent is a persistent service, not an Edge Function:

cd agent
pnpm dev

Its .env needs:

  • LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET
  • GOOGLE_API_KEY or GEMINI_API_KEY
  • SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY for session-mode maturity and transcript persistence
  • optional GEMINI_LIVE_MODEL and GEMINI_LIVE_VOICE

Without Supabase service credentials the worker can support lab behaviour but cannot safely participate in production-style session-* rooms.

See Realtime and Voice.

Create a Local User

Account creation is invite gated. The authoritative sequence is:

  1. Open /login?mode=signup.
  2. Enter an email and a valid invite code, or use the exact email on an unexpired company invitation.
  3. The public invite-codes preflight returns a deliberately non-enumerating result and creates a short reservation when appropriate.
  4. signInWithOtp(..., { shouldCreateUser: true }) requests the OTP.
  5. The database before_user_created hook validates and atomically consumes the code, or recognizes the pending invitation.
  6. Read the six-digit code in local Inbucket.
  7. Verify the code.
  8. Create a company, which calls create_company_with_admin() to insert the company and creator's company_admin membership in one transaction; or accept the invitation through accept_invitation().

Normal sign-in calls OTP with shouldCreateUser:false, preventing an unknown email from silently creating an account.

Local system-admin access

For development only, update the local public.users row in Studio:

UPDATE public.users
SET is_system_admin = true
WHERE email = 'your-local-email@example.com';

Refresh the application. Never use a client-side flag or JWT metadata alone to grant platform administration.

Database Changes

Create a migration:

npx supabase migration new descriptive_name

Edit the generated SQL, then rebuild locally:

npx supabase db reset

Inspect the diff and test RLS with at least:

  • unauthenticated caller
  • ordinary member in tenant A
  • member in tenant B
  • company manager
  • company admin/consultant
  • system admin
  • service-role pipeline

Linking a project does not apply migrations. Remote application is an explicit operation:

npx supabase link --project-ref bxmwvqnilignrxkitaae
npx supabase db push

Do not run a remote push merely to verify local SQL. See Deployment.

Edge Function Changes

  1. Update supabase/functions/<slug>/index.ts first.
  2. Add or reuse helpers under _shared/.
  3. Test locally with functions serve --no-verify-jwt.
  4. Verify auth, cross-tenant IDs, malformed bodies, rate/budget behaviour and provider failure.
  5. Deploy from the repository source with:
npx supabase functions deploy <name> \
--project-ref bxmwvqnilignrxkitaae \
--no-verify-jwt

Do not upload function source only through a dashboard and do not use a deploy mechanism that cannot set --no-verify-jwt.

Provider Secrets

Set hosted function secrets explicitly:

npx supabase secrets set \
GEMINI_API_KEY=<key> \
APP_URL=https://app.example.com \
RESEND_API_KEY=<key> \
--project-ref bxmwvqnilignrxkitaae

Set LiveKit and R2 groups only when those features are enabled:

npx supabase secrets set \
LIVEKIT_URL=<wss-url> \
LIVEKIT_API_KEY=<key> \
LIVEKIT_API_SECRET=<secret> \
--project-ref bxmwvqnilignrxkitaae

npx supabase secrets set \
R2_ACCOUNT_ID=<id> \
R2_ACCESS_KEY_ID=<key> \
R2_SECRET_ACCESS_KEY=<secret> \
R2_BUCKET=<bucket> \
--project-ref bxmwvqnilignrxkitaae

APP_URL is required for link-bearing application mail. It must be a trusted absolute HTTP(S) origin.

Verification Before Handoff

For a typical frontend change:

cd frontend
npm run lint
npm test
npm run build
npm run i18n:audit

Run relevant Playwright projects for user-visible flows. After a code change, follow the repository HawkScan loop unless the task explicitly excludes it. Documentation-only edits require the docs typecheck/build, not an application DAST scan.

See Testing and Accessibility.

Troubleshooting

OTP never appears

  • Open http://127.0.0.1:54424.
  • Confirm the email limit has not been exhausted.
  • Confirm signup mode used a valid invite path.
  • Inspect Auth and Postgres logs for a hook_before_user_created rejection.

Unknown email cannot sign in

This is expected: normal sign-in sets shouldCreateUser:false. Use the gated signup route for a new account.

npm ci reports missing @emnapi/*

Regenerate and verify the frontend lock with npm 10 using the exact commands in Dependency-lock discipline.

Function returns 401 with a valid-looking token

  • Ensure the function was served/deployed with --no-verify-jwt.
  • Confirm the request carries the current access token.
  • For WebSockets, confirm the URL builder added the token and did not log it.
  • Use auth.getUser(token), not decoded claims alone, to validate identity.

CORS blocks tracing headers

All function CORS headers must allow:

authorization, x-client-info, apikey, content-type, baggage, sentry-trace

Local schema looks old

Run npx supabase db reset; starting containers alone does not replay newly added migrations into an existing local volume.