Edge Functions
supabase/functions/ contains 26 checked-in function source directories.
This is a source inventory, not a claim about any hosted project's deployment
state.
Every function is configured with verify_jwt = false because:
- the gateway verifier rejects the project's ES256 Auth tokens,
- browser WebSockets require custom query-token validation,
- MCP discovery/challenge endpoints need custom unauthenticated responses, and
- all functions implement their own identity/public-access decision.
Authenticated functions call auth.getUser(token). Gateway bypass is not an
authorization bypass.
Shared Modules
Important modules in supabase/functions/_shared/ are:
| Module | Responsibility |
|---|---|
http.ts | CORS, OPTIONS, JSON, service client, bearer verification, trusted origin/IP |
authz.ts | Active company membership and admin/consultant equivalence |
budgets.ts | Feature-gated monthly budget check and fixed-window rate helper |
models.ts | Canonical text, Live, TTS model IDs and voice defaults |
locale.ts | BCP-47 normalization, user locale and prompt language |
json.ts | Resilient JSON extraction from grounded/model prose |
maturity-signals.ts | Parse/apply maturity signals |
maturity-persist.ts | Optimistic profile-version persistence |
v2-prompt.ts | Advisor prompt composition |
scope.ts | UK-readiness scope guardrail |
tasks.ts | AI task generation and deduplication |
live-usage.ts | Realtime token estimates/accounting |
email-templates.ts / locale assets | Branded localized email |
Canonical CORS
All browser-callable functions use:
export const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'authorization, x-client-info, apikey, content-type, baggage, sentry-trace',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
};
Do not remove baggage or sentry-trace; browser tracing depends on them.
The project contract intentionally uses *.
Canonical authenticated HTTP shape
const pre = handleOptions(req);
if (pre) return pre;
if (req.method !== 'POST') return json({ error: 'method_not_allowed' }, 405);
const supabase = serviceClient();
const auth = await requireUser(req, supabase);
if (auth.response) return auth.response;
const { user } = auth;
// Bind requested resource to user here.
serviceClient() bypasses RLS. Identity verification must be followed by
resource-specific authorization.
Checked-in Function Reference
| Function | Method | Primary contract | Auth and scope |
|---|---|---|---|
aida | POST | model contents + allowlisted tool declarations → text or tool calls | User; aida_enabled unless system admin; company access and budget/rate gate |
aida-live | WS | live audio/control frames | Query-token user; Aida flag; company budget |
chat | POST | {session_id,message,hidden?,attachment?} → advisor response, maturity/task changes | User; session/company membership; budget and rate |
chat-live | WS | session token/ID/resume handle; PCM/control frames | Query-token user; session/company membership; budget |
chat-multi | POST | {session_id} requests the next AI turn | Active participant and company member; turn claim; budget |
competitor-analysis | POST | company/profile or manual context, phase/options → grounded analysis | Company member; budget and rate |
delete-account | POST | authenticated self-delete | User may delete self only; transactional public-data cleanup then Auth delete |
demo | POST | seed, teardown, sweep actions | User; operations confined to caller-owned __demo-* workspace |
documents | POST | status, upload_url, confirm_upload, download_url, delete_object | User; active company membership; uploader/admin action checks |
early-access | POST | email/locale/source → registration | Public, trusted-IP rate limit, validated input |
gemini-tts | POST | text, voice, language → 24 kHz PCM16 payload | User; budget/rate; usage recorded |
generate-summary | POST | {session_id} → persisted structured summary | Session/company member; single-winner claim; budget/rate |
invite-codes | POST | action-routed preflight/list/create/revoke | Preflight public and rate-limited; management system-admin only |
lead-hunt | POST | grounded prospect criteria → lead set | User; feature flag, budget/rate; system-admin flag bypass |
livekit-session-control | POST | leave/remove, session and optional user | Participant may leave self; owner/admin removes others; DB + media plane |
livekit-token | POST | {room} → short-lived room token and URL | User; strict room-name/session/participant checks and budget |
mcp | HTTP | Streamable HTTP MCP discovery and JSON-RPC tools | Public discovery/challenge; OAuth bearer for tools; tenant/role scope |
notify-feedback | POST | feedback ID → admin notifications | Authenticated submitter/resource checks; trusted APP_URL |
notify-session-add | POST | session/user → participant notification | Authenticated owner/admin and real target relationship |
prompt-test | POST | advisor-test or refine prompt mode | System admin; usage recorded |
send-invite | POST | {invitation_id} → localized invitation email | Company admin/consultant; rows and URL loaded server-side |
shared-session | GET | share token → allowlisted read-only session | Public capability; trusted-IP rate limit; expiry/revocation checks |
structure-feedback | POST | raw feedback → normalized fields | User; budget gate; usage recorded |
synthesise-company-info | POST | company ID + extracted text → merged facts | Company admin/consultant; budget/rate; history/accounting |
tasks | POST | list, update, dismiss | User; active company membership and allowlisted update fields |
update-company-facts | POST | update/reset facts/learnings | Company admin/consultant; history row; pipeline-safe service write |
A local, Git-ignored supabase/config.toml may include declarations for future
names without source directories. Treat the 26 checked-in directories above as
authoritative; do not document local placeholders as implemented.
The chat Function
Request Format
The browser supplies:
{
session_id: string;
message: string;
hidden?: boolean;
attachment?: {
filename: string;
text: string;
truncated?: boolean;
};
}
The function rejects malformed/oversized input before calling Gemini. It loads
the session itself and verifies active access to session.company_id; a
client-supplied company ID is not authoritative.
Prompt composition
The v2 prompt combines:
- active
prompt_versionsv2 prompt - UK-readiness scope
- up to the bounded recent transcript
- current company maturity state
- facts and learnings
- outstanding tasks
- focus dimension
- relevant partner/platform capability context
- user locale/language directive
- expected structured response contract
Response Format
The current response shape includes:
{
response: string;
reasoning?: string;
experience_version: 'v2';
token_usage: {
input: number;
output: number;
remaining_input: number | null;
};
metadata: {
processing_time_ms?: number;
maturity_signals?: unknown[];
company_facts?: unknown;
};
maturity_update?: unknown;
maturity_transitions?: unknown[];
tasks?: {
completed?: unknown[];
added?: unknown[];
};
}
Legacy readiness_update and remaining_output_budget examples are not the
current contract.
Processing Steps
- Method/preflight validation.
- Bearer identity verification.
- Session and active company authorization.
- Monthly input/output budget gate.
- Per-user chat rate limit.
- Context and prompt load.
- Gemini call using
GEMINI_TEXT_MODEL. - Structured response parsing.
- Message and token audit persistence.
- Atomic monthly usage roll-up.
- Optimistic maturity persistence with retry.
- Company-fact merge/history and task generation.
- Controlled response.
Error responses
Known classes include:
| Status | Meaning |
|---|---|
| 400 | Invalid body/input |
| 401 | Missing/invalid identity |
| 403 | Cross-tenant or disallowed access |
| 404 | Session unavailable |
| 405 | Wrong method |
| 409 | Inactive/conflicting state |
| 429 | Rate or token budget exceeded |
| 500 | Internal/configuration failure with generic client copy |
| 502/503 | Provider or optional integration unavailable |
Raw exception details stay in server logs.
The generate-summary Function
The function claims the session before provider work. It reads the transcript, current and starting maturity states, tasks and company context, then generates:
- narrative
- constraints
- role-based next steps
- warning flags
- dimensions advanced
- recommended tasks
It writes summary, completion status, maturity snapshot, tasks, token audit, and monthly aggregate. Session completion columns are database-guarded against ordinary client writes.
The tasks Function
Actions:
| Action | Input | Behaviour |
|---|---|---|
list | optional company/status/role/dimension/include-dismissed | Active memberships only |
update | task ID and allowlisted status/priority/notes | Validates membership and values; stamps completion |
dismiss | task ID | Soft deletes through status='dismissed' |
It does not call Gemini or generate tasks. Task generation is an internal step of chat/summary pipelines.
The send-invite Function
The request contains only invitation_id. The function loads and validates:
- invitation
- recipient/role/status
- company
- inviter
- caller's current company-admin/consultant access
- locale
- trusted application origin
The accept link is built from APP_URL. If APP_URL or RESEND_API_KEY is
missing, the function declines delivery rather than accepting request-supplied
link data.
The shared-session Function
This is a public capability endpoint:
- token must map to an unrevoked, unexpired share,
- response fields are allowlisted,
- no service-role row dump is returned,
- trusted-IP rate limiting bounds token guessing,
- CORS and security response behaviour remain generic.
Do not replace it with a public table SELECT.
The chat-live Function
chat-live is a WebSocket relay to Gemini Live. It:
- validates the URL token before upgrade,
- authorizes the session/company,
- checks budget,
- injects prompt/history/locale,
- relays audio and control messages,
- supports Gemini resumption handles,
- persists user/assistant transcript turns,
- runs text-model maturity extraction after assistant turns,
- records realtime usage.
voice_enabled hides/disables the frontend control but is not currently read by
this function.
The prompt-test Function
Modes:
- advisor test against a draft prompt and isolated conversation
- AI-assisted prompt refinement
The function is system-admin only. It does not persist a user conversation, but
it does insert token audit usage and call record_token_usage.
The mcp Function (remote MCP server)
The mcp function remains the checked-in Streamable HTTP resource server. It
serves public discovery/challenge metadata, validates Supabase OAuth tokens
containing a client identity for tool calls, scopes reads to active memberships,
and restricts company-profile writes to admin/consultant access.
Detailed tools, scopes, consent, discovery and troubleshooting are in MCP and OAuth.
The documents Function
Actions:
| Action | Result |
|---|---|
status | Whether all R2 secrets are configured |
upload_url | Five-minute presigned PUT for one pending document |
confirm_upload | HEAD object, enforce measured quotas, atomic commit |
download_url | Five-minute presigned GET for a stored company document |
delete_object | Admin/consultant signed delete |
Controls include active membership, uploader/admin checks, company retention opt-in, deterministic company-prefixed keys, path validation, type/MIME allowlists, 15 MiB file cap, 500-document cap, and 2 GiB company cap.
See Company Knowledge and Documents.
Guided-demo functions
The gemini-tts Function
Returns base64 PCM16 at 24 kHz for allowed voices/languages. It checks user budget/rate and records usage. The client wraps PCM in WAV or feeds its audio pipeline.
The demo Function
Actions are confined to the authenticated user's deterministic demo ownership namespace. Seed operations use service privileges because maturity fields are pipeline-owned; ownership and company access are checked before every privileged path. Teardown and sweep call SECURITY DEFINER cleanup RPCs.
AI, Budget, and Rate Controls
The shared budget gate is used by:
aida
aida-live
chat
chat-live
chat-multi
competitor-analysis
generate-summary
gemini-tts
lead-hunt
livekit-token
structure-feedback
synthesise-company-info
prompt-test is system-admin-only and records usage but does not use the
ordinary tenant budget gate.
Budget enforcement and accounting are separate: every spend must still record usage after it passes a pre-call gate. See AI and Token Accounting.
Gemini Model Configuration
Canonical IDs live in _shared/models.ts:
GEMINI_TEXT_MODEL = 'gemini-3.1-flash-lite'
GEMINI_LIVE_MODEL =
Deno.env.get('GEMINI_LIVE_MODEL') || 'gemini-3.1-flash-live-preview'
GEMINI_LIVE_VOICE_DEFAULT =
Deno.env.get('GEMINI_LIVE_VOICE') || 'Kore'
GEMINI_TTS_MODEL =
Deno.env.get('GEMINI_TTS_MODEL') || 'gemini-3.1-flash-tts-preview'
Text functions use:
import { GoogleGenAI } from "https://esm.sh/@google/genai@2.8.0";
Do not introduce the retired @google/generative-ai client. Import canonical
model constants rather than copying IDs into each function.
Deployment
Deploy via CLI
Repository source is authoritative. For every function:
npx supabase functions deploy <name> \
--project-ref bxmwvqnilignrxkitaae \
--no-verify-jwt
Do not deploy through the Supabase MCP tool or a dashboard-only upload. Those paths cannot guarantee the required flag/source synchronization.
Secrets
Common:
GEMINI_API_KEY
APP_URL
RESEND_API_KEY
Optional LiveKit:
LIVEKIT_URL
LIVEKIT_API_KEY
LIVEKIT_API_SECRET
Optional R2:
R2_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_BUCKET
Supabase provides SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY to functions.
Adding a Function
- Create
supabase/functions/<slug>/index.ts. - Add
[functions.<slug>] verify_jwt = falseto config. - Reuse shared HTTP/authz/locale/model helpers.
- Define method, body size, response and error contract.
- Verify identity or explicitly design/rate-limit a public capability.
- Bind every tenant/resource identifier.
- Add feature, rate, budget and idempotency controls as needed.
- Insert token audit and call
record_token_usagefor every Gemini spend. - Add local tests/DAST seed coverage.
- Deploy with project ref and
--no-verify-jwt. - Update this reference.
Troubleshooting
Function works locally but receives hosted gateway 401
Redeploy with --no-verify-jwt.
Browser preflight rejects tracing
Use the exact shared CORS headers including baggage and sentry-trace.
Cross-tenant UUID returns data
Treat as a security defect. Identity verification alone is insufficient when using the service role; add a current company/session/document binding.
AI succeeds but budgets do not change
Confirm both the token_usage insert and record_token_usage RPC execute on
all success/partial-success paths.
Model returns prose around JSON
Use _shared/json.ts extraction and bounded retry/failure behaviour. Never
eval model output or accept a structurally invalid partial result.