The Facet AI generation gateway. A thin Elysia service whose entire job is to call the Synapse AI router safely, so natural-language prompts become parametric CAD code without ever exposing our model keys or letting anyone run up a bill against them.
Facet itself stays a static SPA for local modeling. This gateway owns the one thing the browser must never hold: the server-side Synapse key. It builds a grounded prompt from the Facet authoring API, calls Synapse's Anthropic-compatible POST /v1/messages, extracts the fenced TypeScript block, and returns it with a usage estimate. Every layer of the money-safety model lives here.
| Method | Path | Purpose |
|---|---|---|
POST |
/generate |
Turn a prompt into Facet TypeScript. Managed (our key) or BYOK (caller's key). |
GET |
/health |
Liveness. |
GET |
/ready |
Readiness (DB connectivity, used from Phase C on). |
Request:
The managed path (no byokKey) requires a signed-in Omni user: send the user's access token as Authorization: Bearer <token> (the same Better Auth / Gatekeeper token the rest of the fleet uses). A missing or invalid token returns 401; a user who has spent all their monthly credits returns 402. BYOK requests need no auth and are not credit-metered.
Response:
{
"code": "const part = cube(20).subtract(cylinder(3, 24));\nreturn part;",
"usage": { "credits": 2, "costCents": 1.4 }
}creditsisceil(costCents)(1 credit ~= $0.01 of model spend).- The
previousfield drives the client-side repair loop: on a kernel throw the app re-calls/generatewith the failing code plus the error so the model can fix it.
Defense-in-depth. Each layer is independent, so a bug in one does not defeat the others:
- Server-only key.
SYNAPSE_API_KEYlives only here, neverVITE_-prefixed, never shipped to the browser, never written to a response body or a log. - Synapse-key hard budget. The provisioned key carries a hard monthly ceiling set in Synapse (Phase D), bounding worst-case total loss.
- Global daily spend breaker. Aggregate managed spend is tracked per UTC day;
/generatereturns503onceDAILY_SPEND_CEILING_CENTSis hit. - Auth-gated managed path. The managed path requires a valid signed-in Omni identity (bearer token verified via Gatekeeper); no anonymous access to paid inference,
401otherwise. - Per-user hard-capped credits. Managed usage is metered per user per UTC month against
FREE_MONTHLY_CREDITSwith no overage;/generatereturns402once the cap is reached. Usage is persisted in thecredit_ledgertable, so the cap holds across instances and restarts. - Per-request caps.
MAX_PROMPT_CHARS,MAX_OUTPUT_TOKENS, a request timeout, andMAX_REPAIR_ATTEMPTS, all enforced server-side. - Per-IP rate limit. Sliding-window limiter on
/generate;429on exceed. - BYOK escape hatch. A caller may send their own key per-request over TLS. It is never persisted, bills to them, and bypasses auth, the managed breaker, and the credit meter entirely.
The per-user credit meter is DB-backed. The global daily breaker and per-IP rate limiter stay in-memory: the Synapse-key hard budget plus the per-user cap already bound worst-case loss, so moving them to a shared store is deferred (see the
TODOs insrc/lib/cost/breaker.tsandrateLimit.ts). The per-user cap is read fromFREE_MONTHLY_CREDITSfor now and will be sourced from Aether entitlements in Phase D.
The service boots with nothing set. Every variable is optional; the managed path is the only feature that needs configuration, and it degrades to a clean 503 (with a startup warning) when its key is absent.
| Variable | Default | Purpose |
|---|---|---|
SYNAPSE_BASE_URL |
http://localhost:6000 |
Synapse AI router base URL. |
SYNAPSE_API_KEY |
(unset) | Server-side Synapse key. Unset disables the managed path (BYOK still works). Never shipped to the browser. |
SYNAPSE_API_URL |
(unset) | Synapse provisioning API (api.synapse, not the gateway). With SYNAPSE_GATEWAY_SECRET, enables per-user managed keys. |
SYNAPSE_GATEWAY_SECRET |
(unset) | Gateway secret for the internal provisioning call. With SYNAPSE_API_URL, enables per-user managed keys (additive; the credit ledger still hard-caps). |
FACET_AI_MODEL |
claude-sonnet-5-5 |
Claude model id passed through to Synapse. |
MAX_PROMPT_CHARS |
2000 |
Maximum accepted prompt length. |
MAX_OUTPUT_TOKENS |
2048 |
Output token ceiling per call. |
AI_REQUEST_TIMEOUT_MS |
30000 |
Synapse request timeout. |
MAX_REPAIR_ATTEMPTS |
2 |
Repair-attempt ceiling per generation. |
DAILY_SPEND_CEILING_CENTS |
500 |
Global managed spend breaker, per UTC day. |
FREE_MONTHLY_CREDITS |
100 |
Per-user hard cap on managed credits per UTC month. |
RATE_LIMIT_WINDOW_MS |
60000 |
Per-IP rate-limit window. |
RATE_LIMIT_MAX |
20 |
Max requests per IP per window. |
AUTH_BASE_URL |
https://localhost:8000 |
Gatekeeper base URL used to verify the managed path's bearer token. |
DATABASE_URL |
(unset) | Postgres connection for the credit meter. The managed path needs it; BYOK does not. |
The managed path depends on SYNAPSE_API_KEY, AUTH_BASE_URL, and DATABASE_URL; BYOK needs none of them. GraphQL and Vortex variables remain scaffolding. See .env.local.template for the full list.
cp .env.local.template .env.local # optional, the service boots with no env set
bun install
bun devThe service boots with no env configured: if SYNAPSE_API_KEY is unset it logs a warning and the managed path returns a clean 503, while BYOK requests still work. See .env.local.template for required vs optional variables.
bun test # no database required for Phase A
bun run check # Biome lint + format
bunx tsc --noEmit # type check
bun run knip # dead code + unused deps
bun run build # bundleThe code in this repository is licensed under Apache 2.0, © Omni LLC. See LICENSE.md for more information.
{ "prompt": "a 20mm cube with a 6mm hole through it", "byokKey": "sk-ant-...", // optional, bring-your-own-key "previous": { // optional, repair turn "code": "<prior code>", "error": "<kernel error text>" } }