A Helix project is a set of TypeScript functions plus an optional React SPA, and the same code runs in two places. During development the CLI serves everything from one local server on localhost
. In production each project runs as a single AWS Lambda behindhttps://{project-id}.helix-app.ai, and both runtimes call the same platform services: auth proxy, AI gateway, scheduling, MCP, and managed data stores.
Local development
helix dev starts a Nitro dev server on localhost
- Function routes go to the Nitro function runtime, which maps file paths in
functions/to URLs. - Static assets are served by Vite, integrated as a Nitro plugin.
- Everything else falls back to the SPA’s
index.html.
Because functions and the SPA share an origin, there is no CORS configuration in development. The deployed project keeps the same single-origin layout, so there is none in production either.
The CLI also starts local backing services automatically, no flags needed. Database, key-value store, file storage, scheduler, and log output are always local:
helix dev (localhost:3000)
|
|-- Function routes -> Nitro function runtime
|-- Static assets -> Vite (Nitro plugin)
|-- SPA fallback -> index.html
|
|-- Local Postgres <- ctx.db() (always local)
|-- Local KV, in memory <- ctx.kv() (always local)
|-- Local file system <- ctx.files (always local)
|-- Local scheduler -> runs functions/_scheduled/ on their cron
|
+-- Remote auth service <- ctx.http.authed() (always remote)
The one exception is authentication. ctx.http.authed('alias') always resolves against the remote Helix auth service, even during local development, because credentials are never stored on your machine. To work without a network connection, helix dev --offline returns configurable mock responses instead of calling the auth service.
Production
helix deploy builds the project and ships it to the platform. The build bundles every function into a single Lambda-compatible entry point with an internal router, compiles the SPA with vite build if app/ exists, and generates a manifest describing routes, schedules, and schemas. The deploy then:
- Uploads the function bundle to AWS Lambda, one Lambda per project.
- Uploads SPA assets to the project’s S3 bucket, served through CloudFront.
- Sends the Drizzle schema to the migration API. The CLI never connects to the production database directly.
- Registers schedules and app triggers with the platform.
- Updates API Gateway routing and switches traffic to the new version.
All production traffic arrives at CloudFront, which routes by match priority:
{project-id}.helix-app.ai
|
CloudFront CDN
|
|-- 1. Function route match -> API Gateway -> project Lambda
|-- 2. Static asset match -> S3 (cached at the edge)
+-- 3. SPA fallback -> index.html from S3
Nitro exists only in development; the production runtime is AWS Lambda. Functions scale through Lambda concurrency, scale to zero when idle, and each project’s concurrency cap keeps one project from consuming unbounded resources. Custom domains can be added in the dashboard and serve the same project alongside the UUID subdomain; see the deployment guide.
Platform services
Five services sit behind both runtimes:
| Service | What it does |
|---|---|
| Auth proxy | Resolves auth aliases to credential records, injects credentials server-side, and logs every authenticated call. See the security model. |
| AI gateway | One API for OpenAI, Anthropic, and Google models, with per-request cost tracking and org-level budgets, model rules, and rate limits. |
| Scheduling service | Triggers functions/_scheduled/ functions on their cron schedules in production. A local scheduler does the same during helix dev. |
| MCP service | Exposes the functions listed in your config’s mcp.tools as MCP tools at mcp+https://{project-id}.mcp.helix.ai. |
| Managed data stores | Postgres with pgvector, key-value store, queues, and S3-backed file storage, provisioned per project on deploy. |
The life of a request
Here is what happens when a browser calls GET https://{project-id}.helix-app.ai/api/v1/orders on a deployed project:
- CloudFront receives the request and checks it against the deployed route table.
/api/v1/ordersmatches a function route. - If Helix Identity is enabled for the project, the Helix edge checks the session cookie first and redirects to your IdP when there is no valid session.
- API Gateway forwards the request to the project’s Lambda, where the internal router selects the handler compiled from
functions/api/v1/orders.get.ts. - Middleware runs before the handler: org-level middleware first, if your organization defines any, then the project’s
functions/_middleware.ts. - The runtime validates the merged input (body, params, and query) against the function’s exported Zod schema, then calls the handler with
ctx. - Inside the handler,
ctx.db()queries the project’s managed Postgres, and anyctx.http.authed('alias')call routes through the auth proxy, which injects credentials and forwards the request. - The handler’s return value becomes the response and travels back through API Gateway and CloudFront to the browser.
- Every step is recorded under one execution ID, which you can trace in the dashboard at
https://app.helix.tray.ai.
A request for a static asset such as /assets/main-abc123.js never reaches the Lambda: CloudFront serves it from the S3 cache at step one.