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
pnpmonly if developing the separateagent/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:
| Service | URL |
|---|---|
| API gateway | http://127.0.0.1:54421 |
| Postgres | postgresql://postgres:postgres@127.0.0.1:54422/postgres |
| Studio | http://127.0.0.1:54423 |
| Inbucket email UI | http://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_createdsignup 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:falsewhen Resend is unavailable; link-bearing mail also declines to send withoutAPP_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
| Command | Purpose |
|---|---|
npm run dev | Vite development server |
npm run build | tsc -b then production Vite build |
npm run lint | ESLint |
npm test | Vitest once |
npm run test:watch | Vitest watch mode |
npm run test:coverage | Vitest coverage |
npm run test:e2e | Playwright suite |
npm run test:e2e:ui | Playwright UI |
npm run test:e2e:headed | Headed Playwright |
npm run test:e2e:report | Open the saved HTML report |
npm run i18n:audit | Locale-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_SECRETGOOGLE_API_KEYorGEMINI_API_KEYSUPABASE_URLandSUPABASE_SERVICE_ROLE_KEYfor session-mode maturity and transcript persistence- optional
GEMINI_LIVE_MODELandGEMINI_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:
- Open
/login?mode=signup. - Enter an email and a valid invite code, or use the exact email on an unexpired company invitation.
- The public
invite-codespreflight returns a deliberately non-enumerating result and creates a short reservation when appropriate. signInWithOtp(..., { shouldCreateUser: true })requests the OTP.- The database
before_user_createdhook validates and atomically consumes the code, or recognizes the pending invitation. - Read the six-digit code in local Inbucket.
- Verify the code.
- Create a company, which calls
create_company_with_admin()to insert the company and creator'scompany_adminmembership in one transaction; or accept the invitation throughaccept_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
- Update
supabase/functions/<slug>/index.tsfirst. - Add or reuse helpers under
_shared/. - Test locally with
functions serve --no-verify-jwt. - Verify auth, cross-tenant IDs, malformed bodies, rate/budget behaviour and provider failure.
- 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_createdrejection.
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.