Operations Runbooks
These runbooks turn the deployment architecture into repeatable procedures. They deliberately separate repository state from hosted state: a migration, function, flag, or secret in source is not evidence that the corresponding remote control plane has been changed.
The production Supabase project reference used by the repository is:
bxmwvqnilignrxkitaae
Confirm the target account, project, environment, and release authority before every remote command. The examples below show the required shape; they do not authorize a production change.
Non-Negotiable Rules
- Edit Edge Function source in
supabase/functions/<slug>/index.tsbefore deployment. - Deploy every Edge Function through the Supabase CLI with both the explicit
project reference and
--no-verify-jwt. - Never deploy a dashboard-only function variant or use a deployment path that cannot pass the required flag.
- Prefer forward corrective migrations; never reset a remote database.
- Keep the service-role key out of the browser, repository, logs, screenshots, and documentation.
- Derive email links from the server-side
APP_URL, never a request field. - Run HawkScan only against local or deliberately isolated synthetic data, never production.
- Record the exact source commit and rollback target before a release.
Runbook: Prepare a Release
1. Establish Scope
Record:
- commit and branch
- target environment and Supabase project reference
- migrations added
- Edge Function directories changed
- frontend, docs, agent, and provider changes
- secrets or feature flags required
- user-visible and data-compatibility impact
- previous known-good versions
Review the diff for accidental .env, build, scan-output, or generated
credential files.
2. Validate Locally
Frontend:
cd frontend
npm run lint
npm test
npm run i18n:audit
npm run build
Run the relevant Playwright projects after checking their target and test tenant. See Testing and Accessibility.
Documentation:
cd docs
npm run typecheck
npm run build
Agent, when changed:
cd agent
pnpm install
pnpm build
Database, when changed:
npx supabase start
npx supabase db reset
A local supabase/config.toml may define ports, Auth hooks, and function
settings, but that file is ignored by Git in this repository. Do not treat one
developer's local config as a production declaration. Verify hosted Auth and
function settings separately.
3. Apply Security Validation
For application code changes, run the local HawkScan remediation loop in
LOCAL_DAST_RUNBOOK.md:
bash scripts/seed-local-scan.sh
bash scripts/scan-local.sh
The scanner targets the local Supabase API at http://localhost:54421 by
default and uses synthetic tenant data. Review stackhawk.yml and the target
environment immediately before the run. Exit code 42 means findings exceed
the configured threshold; do not misreport it as a scanner crash.
The local runbook documents a temporary ?no-dts import workaround. Reverse
only that exact mechanical change afterward; do not discard unrelated working
tree changes.
Documentation-only changes do not require DAST.
Runbook: Apply Database Migrations
Preconditions
- every schema change is a new file under
supabase/migrations/ - the full local reset succeeds against PostgreSQL 17
- RLS, grants, triggers, and SECURITY DEFINER search paths were reviewed
- destructive/data-rewriting statements have an owner-approved backup and recovery plan
- old and new frontend/function versions remain compatible for the rollout window
Compare Remote State
npx supabase link --project-ref bxmwvqnilignrxkitaae
npx supabase migration list
Linking does not apply changes. Compare local and remote lists and investigate unexpected remote-only or missing versions before continuing.
Apply
npx supabase db push
Read the pending list and confirmation carefully. Do not use db reset or an
equivalent destructive command against a hosted project.
Verify
At minimum:
- inspect migration status again
- exercise an ordinary member and an admin/consultant path
- perform a wrong-tenant negative request
- verify affected RLS policies and RPC execute grants
- verify relevant triggers and Realtime publication
- test the prior application version if the rollout is not atomic
- check database and Auth logs for new denials or exceptions
For Auth-hook migrations, also verify the hosted Before User Created setting:
pg-functions://postgres/public/hook_before_user_created
The SQL function existing does not prove that the hosted Auth control plane invokes it.
Correct a Migration
If an applied migration is wrong:
- stop dependent application rollout
- preserve evidence and assess data already changed
- write a new forward migration
- validate from a clean local reset and an upgrade-shaped database
- apply with the same review
Do not edit or delete an already shared migration to make history look clean.
Runbook: Deploy an Edge Function
Preconditions
- repository source contains the intended implementation
- the directory actually exists; local config may contain future placeholders
- methods, body limits, CORS, auth, role, and tenant checks were tested
- AI spend records both
token_usageandrecord_token_usage - required feature flag, rate limit, budget, and idempotency controls exist
- secrets are available in the target environment
Deploy
npx supabase functions deploy <name> \
--project-ref bxmwvqnilignrxkitaae \
--no-verify-jwt
The --no-verify-jwt flag is required for every function. Navaid validates
bearer tokens inside functions with supabase.auth.getUser(token) because the
gateway verifier rejects the project's ES256 tokens.
Verify
Run a bounded smoke matrix:
| Request | Expected result |
|---|---|
OPTIONS | success with canonical CORS headers |
| wrong method | 405 where the endpoint is method-specific |
| no/invalid bearer | generic 401 on protected endpoints |
| wrong tenant/role | generic 403 or 404 |
| malformed/oversized input | controlled 400/413 |
| valid request | expected data/provider path |
| rate/budget denial | controlled 429/402-style application response as implemented |
Confirm Access-Control-Allow-Headers includes:
authorization, x-client-info, apikey, content-type, baggage, sentry-trace
For Gemini functions, compare per-request and monthly usage after the smoke
turn. For link-bearing email functions, unset/missing APP_URL must decline to
send. For WebSockets, test upgrade auth, normal close, abrupt disconnect,
reconnect, transcript persistence, and usage metering.
Function Rollback
Checkout or otherwise stage the prior known-good repository source, then
redeploy that directory with the same project reference and
--no-verify-jwt. Verify the version by behaviour and logs. Never reconstruct a
lost production function from a dashboard editor.
Runbook: Configure Function Secrets
Inventory by capability:
| Capability | Secrets |
|---|---|
| Gemini | GEMINI_API_KEY |
| Live model/voice override | GEMINI_LIVE_MODEL, GEMINI_LIVE_VOICE |
| TTS model override | GEMINI_TTS_MODEL |
| Mail links | APP_URL |
| Resend | RESEND_API_KEY |
| LiveKit | LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET |
| R2 | R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET |
Supabase injects its URL and service-role secret into hosted functions. Never
mirror the service role into a VITE_ variable.
Set values through the approved secret-management path. If using the CLI:
npx supabase secrets set NAME=<value> \
--project-ref bxmwvqnilignrxkitaae
Avoid literal values in shared shell history. Record secret name, owner, rotation date, consuming workloads, and validation result without recording the value.
After rotation, restart or redeploy every long-running consumer and verify both success and old-key rejection.
Runbook: Release the Frontend
Build Contract
Cloudflare Pages uses:
| Setting | Value |
|---|---|
| Root | frontend |
| Node | 20 or later |
| Install | npm 10 npm ci |
| Build | npm run build |
| Output | frontend/dist |
When dependencies changed:
cd frontend
npx -y npm@10 install --package-lock-only --no-audit --no-fund
npx -y npm@10 ci --dry-run --no-audit --no-fund
Confirm package-lock.json contains node_modules/@emnapi/core and commit
package.json and the lock together.
Pre-Publish Inspection
- required browser variables use only public anon/DSN values
- no service-role or provider secret is in
dist _headersand_redirectsare present- no
.mapfiles will be published version.jsonexists and is not assigned immutable caching- direct client-route navigation falls back to the SPA
Post-Publish Smoke
- load marketing and login from a fresh browser
- navigate directly to an authenticated lazy route
- sign in through the expected invite-gated flow
- confirm Supabase REST, function, Realtime, and LiveKit origins are allowed by CSP only as needed
- perform a controlled AI turn and verify accounting
- open an old tab and verify the update prompt or one-shot chunk recovery
- send sanitized Sentry test telemetry and confirm its release
- verify no source map URL is public
Rollback
Promote the previous known-good Cloudflare Pages build. Additive schema changes should keep that client functional. If not, decide on a forward compatibility fix rather than attempting to remove live data blindly.
Runbook: Release the Documentation
cd docs
npm ci
npm run typecheck
npm run build
Publish docs/build through the documentation host configured outside this
repository. This repository currently has no checked-in GitHub Actions
deployment workflow, so do not assume a merge performs the publish.
Verify:
- developer and user navigation
- internal links and code blocks
- Navaid logo, favicon, typography, and palette
- light/dark contrast and mobile menu
- canonical URL/base URL
- no application or provider secret appears in generated HTML
Documentation branding is static and does not read tenant theme_config.
Runbook: Release the LiveKit Agent
The agent/ process is a persistent Node worker, not an Edge Function.
cd agent
pnpm install
pnpm build
pnpm start
The host needs:
LIVEKIT_URL
LIVEKIT_API_KEY
LIVEKIT_API_SECRET
GOOGLE_API_KEY or GEMINI_API_KEY
SUPABASE_URL
SUPABASE_SERVICE_ROLE_KEY
Optional GEMINI_LIVE_MODEL and GEMINI_LIVE_VOICE override the current
defaults.
Acceptance:
- two authorized users and the agent join one test room
- the worker receives the intended
session-*context - each transcript is attributed to the correct app user
- nonparticipant or removed-user speech is refused
- transcript and maturity side effects persist
- usage is recorded periodically and at close
- leave/remove disconnects the media participant
The worker's tighter named dispatch to only session-* rooms remains future
hardening. Do not mark the agent production-ready based on a successful build
alone; its README explicitly requires live validation.
Runbook: Enable Optional Providers
Cloudflare R2 Documents
- create a private bucket
- issue credentials restricted to that bucket
- set all four R2 secrets; partial configuration remains unavailable
- allow only required app origins and
PUT,GET,HEADin bucket CORS - enable storage for an isolated test company
- test sign, direct upload,
HEADconfirmation, quota, download, expiry, cross-tenant denial, and deletion
Never make the bucket public. Signed URLs are short-lived capabilities.
Resend and Trusted Links
- verify sender domain and identity
- set
RESEND_API_KEY - set
APP_URLto the exact trusted application origin - test each locale with a controlled recipient
- confirm the request body cannot substitute a host
- confirm missing
APP_URLsends no link-bearing email
Auth OTP SMTP is a separate hosted Auth configuration even if it uses the same provider account.
MCP OAuth
Local supabase/config.toml, when present, has OAuth server functionality
disabled by default. For a hosted environment:
- enable the Supabase OAuth server deliberately
- set the consent path to
/oauth/consent - decide whether dynamic registration is allowed
- confirm redirect allowlists
- enable
mcp_enabled - exercise discovery, challenge, consent, token issuance, tenant selection, read tools, and authorized writes
- deny a token without the required
client_id/scope context
See MCP and OAuth for the protocol and permission model.
Runbook: Change or Roll Back a Gemini Model
Current defaults are:
Text: gemini-3.1-flash-lite
Live: gemini-3.1-flash-live-preview
TTS: gemini-3.1-flash-tts-preview
Text functions import a source constant. Changing the text model requires a reviewed source change and redeployment of every affected function. Live and TTS paths support environment overrides:
GEMINI_LIVE_MODEL
GEMINI_TTS_MODEL
GEMINI_LIVE_VOICE changes the voice, not the model.
Before changing a model:
- verify official availability and region/project access
- compare request/response and usage metadata contracts
- test JSON extraction, tools, audio modality, and context limits as relevant
- run deterministic tests plus one isolated provider smoke
- confirm pricing/budget assumptions
- record the old value for rollback
Rollback an override to the prior known-good value and reconnect/restart live consumers. Roll back a source constant by redeploying the prior repository version with the mandatory function flags. Never hide a model outage by disabling token accounting or tenant authorization.
Runbook: Feature-Flag Incident
- identify whether the flag is enforced in the browser, server, or both
- use the System Admin path to change only an allowlisted key
- verify the persisted
system_configshape - test an already-open client and a fresh client
- verify direct API calls are denied if the feature requires server enforcement
- record the change and restoration condition
voice_enabled is currently a browser control; chat-live does not read it.
Do not rely on that flag as an emergency server kill switch. Server-enforced
examples include aida_enabled, mcp_enabled, and lead-hunt/budget controls as
described in Administration and Feature Flags.
daily_token_budget and alert_config are stored by the Admin UI but have no
current runtime daily enforcer or alert sender. Monthly budget controls remain
the effective AI spend gate.
Runbook: Investigate an AI Spend Anomaly
- stop or disable the affected server-enforced feature where a genuine kill switch exists
- identify model, function, user, company, and time bucket without exporting confidential prompts
- compare
token_usagewithtoken_usage_dailyandtoken_usage_monthly - inspect
record_token_usagefailures in function/agent logs - check retry, WebSocket close, and partial-success paths
- verify user and company monthly limits
- reconcile provider-side usage
- fix every missing accounting path and backfill only through a reviewed, auditable procedure
An insert into token_usage alone does not update the monthly gate. See
AI and Token Accounting.
Runbook: Incident Triage and Recovery
First 15 Minutes
- name an incident owner and recorder
- identify affected capability, tenant scope, start time, and current release
- preserve logs and status without copying sensitive payloads
- determine whether confidentiality, integrity, availability, or spend is at risk
- apply the smallest reversible mitigation
- identify and protect the rollback target
Use Observability and Reliability to locate browser, function, database, agent, and provider signals.
Recovery Principles
- frontend: promote a previous build
- functions: redeploy previous repository source with
--no-verify-jwt - database: use a forward corrective migration
- agent: deploy/restart a previous worker image/version
- provider override: restore a known-good value, then restart consumers
- compromised secret: rotate; never restore the compromised value
- R2 or Realtime drift: reconcile durable database state before deleting data
After recovery, verify authorization, tenant isolation, accounting, audit/ history side effects, and the original user path. Monitor long enough to cover delayed jobs, cached clients, expiring tokens, and reconnects.
Runbook: Suspected Secret Exposure
- classify the value and all workloads that consume it
- revoke/rotate it in the provider control plane
- update every function, worker, build environment, and local operator store
- redeploy/restart consumers
- prove the old value is rejected
- review access logs for the exposure window
- remove it from current files and coordinate any history rewrite
- document impact and prevention without reproducing the secret
For the known historical Supabase service-role exposure, follow
docs/SECURITY_KEY_ROTATION_RUNBOOK.md. Rotation is the load-bearing response;
history rewriting is coordinated cleanup and force-pushing it is destructive.
Release Record Template
Release:
Commit:
Environment/project:
Owner:
Approved scope:
Migrations:
Functions and mandatory flags:
Frontend/docs/agent versions:
Provider or secret changes:
Feature-flag changes:
Validation performed:
Known limitations:
Rollback targets:
Post-release result:
Keep this record in the approved operational system, not in a public document when it contains internal environment details.
Final Checklist
- exact source commit and target verified
- local validation matched the changed boundaries
- migration list reviewed and forward recovery prepared
- each changed function deployed from repository source with
--no-verify-jwt - hosted Auth hook/OAuth settings verified where relevant
- browser build contains no secrets or public source maps
- one authorized and one wrong-tenant request tested
- AI spend and aggregates reconcile
- optional LiveKit, R2, Resend, or MCP path validated if changed
- Sentry/log signals are present and sanitized
- old clients and rollback targets remain usable
- repository and operational records agree
For component-specific details, continue with Deployment, Security and Privacy, and Database Schema.