Cirrus Host documentation
Ship Workers to the edge in seconds. The helix CLI bundles your code locally with esbuild and uploads it to Cirrus Host; this guide walks through the CLI, the REST API (80+ endpoints with granular API key scopes), and worker configuration.
Quickstart
Ship your first worker in four commands.
npm i -g @cirrustech/helix
helix login
helix login opens a device-code flow against Cirrus Host and stores credentials in ~/.config/helix/config.json.
helix init my-project/hello
cd my-project/hello
This creates a helix.json, a src/worker.ts entry, and a tsconfig.json.
helix deploy
Your worker is bundled with esbuild and uploaded. The first deploy provisions a hostname like hello.my-project.helx.dev with SSL automatically.
helix logs --tail
Each request to your worker streams in real time, including console.log output and uncaught exceptions.
Concepts
Cirrus Host runs your code on a globally-distributed V8 isolate runtime — your Worker boots in under a millisecond in 300+ cities. Key resources:
- Organizations — top-level accounts. Every user gets a personal org; team orgs have role-based members (owner, admin, billing, developer, viewer).
- Projects — logical groupings of related deployments within an org. A project has a slug (e.g.
my-project) used in URLs and a billing account for charges. - Deployments — individual workers or containers. Each deployment has a name (e.g.
hello) and runs at<deployment>.<project>.helx.devby default. Custom domains attach via CNAME. - Versions — every
helix deploycreates a new version. The latest is live; older versions can be rolled back to via the panel or API. - Resources — project-scoped databases (D1/SQLite), object storage buckets (R2), and key-value stores. Attached to deployments via bindings.
- Billing accounts — org-level containers for plans, payment methods, and usage tracking. Projects are assigned to a billing account. Billing accounts can be shared across orgs.
helix db create / helix bucket create / helix kv create, then declare each one as a binding in helix.json for the deployments that need access.Helix CLI commands
| Command | What it does |
|---|---|
| helix login | Device-code OAuth flow |
| helix logout | Forget local credentials |
| helix whoami | Print the current user |
| helix init <project>/<name> | Scaffold a new project and deployment |
| helix deploy | Build and ship the current project |
| helix list | List deployments in the current project |
| helix logs --tail | Stream live logs from a deployment |
| helix rollback <version> | Roll back to a previous version |
| helix open | Open the deployment URL in the browser |
| helix delete <name> | Permanently remove a deployment |
| helix env set KEY=val | Set a plain-text env var |
| helix env list | List env vars |
| helix secrets set KEY | Securely store a secret (prompted) |
| helix secrets list | List secret names (values not shown) |
| helix domain add example.com | Attach a custom domain |
| helix domain list | List custom domains |
| helix domain verify example.com | Re-check SSL/verification |
| helix project create <name> | Create a new project |
| helix db create <name> | Provision a dedicated SQL database |
| helix db ls | List databases in the current project |
| helix bucket create <name> | Provision a dedicated object-storage bucket |
| helix bucket ls | List buckets in the current project |
| helix kv create <name> | Provision a dedicated key-value store |
| helix kv ls | List KV stores in the current project |
| helix bindings add --db <name> --as ENV | Attach a resource to this deployment |
| helix usage | Show period requests + CPU usage |
helix deploy
Bundles main from helix.json with esbuild (modules + tree-shaking, target ES2022, with nodejs_compat), then uploads as a multipart form with the metadata declared in your config. Re-deploying the same (project, name) bumps the version and replaces the previous bundle in place.
helix logs
By default helix logs shows the last 50 lines retained on the platform. --tail opens a WebSocket and streams in real time. Logs are produced by every invocation event sent to the platform tail consumer; no observability config is required on your side.
env & secrets
Plain env vars are visible in the dashboard and applied as plain_text bindings on the next deploy. Secrets are AES-GCM encrypted at rest and applied as secret_text bindings — their values are never logged or returned by the API.
Custom domains
Run helix domain add example.com, then point a CNAME for example.com at cname.cirrus-host.com. SSL is provisioned automatically in under 60 seconds. Once active, the deployment is reachable at the custom hostname in addition to its default URL.
Resources
Workers don't get any storage by default. To use a database, bucket, or key-value store, you provision it once with the CLI, then declare it as a binding in helix.json. Resources live at the project level and can be attached to any deployment in that project.
| Resource | Create with | Backed by |
|---|---|---|
| SQL database | helix db create <name> | SQLite database (10 GB) |
| Object storage | helix bucket create <name> | S3-compatible bucket |
| Key-value store | helix kv create <name> | Globally-replicated KV with TTL |
helix.json
The single config file at the root of every project. Only name, project, and main are required.
{
"name": "hello",
"project": "my-project",
"main": "src/worker.ts",
"compatibility_date": "2025-04-01",
"compatibility_flags": ["nodejs_compat"],
"vars": { "STAGE": "prod" },
"bindings": [
{ "type": "kv", "name": "MY_CACHE", "resource": "cache" },
{ "type": "bucket", "name": "IMAGES", "resource": "images" },
{ "type": "db", "name": "PRIMARY_DB", "resource": "primary" }
],
"assets": { "directory": "./public" },
"routes": ["api.example.com/*"]
}
Bindings
Bindings in helix.json attach a project resource onto this deployment under a chosen variable name. With helix deploy the resource must already exist (see Resources); the one-click deploy button can create it on the fly. Either way, the binding declares it visible at env.<NAME> for this deployment.
| Type | Attaches | Available as |
|---|---|---|
db | A SQL database | env.<NAME>.prepare(sql).bind(...).all() — full D1 API |
bucket | An object bucket | env.<NAME>.put(key, val) / .get(key) / .list() — R2 API |
kv | A KV store | env.<NAME>.put(key, val) / .get(key) — KV API |
Example flow:
helix db create primary
# adds an entry to your project's resources
# in helix.json:
"bindings": [
{ "type": "db", "name": "DB", "resource": "primary" }
]
# in your worker:
await env.DB.exec('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, email TEXT)');
await env.DB.prepare('INSERT INTO users (email) VALUES (?)').bind('a@b.com').run();
const { results } = await env.DB.prepare('SELECT * FROM users').all();
Static assets
Add "assets": { "directory": "./public" } to helix.json. The CLI bundles every file under ./public and serves them from env.ASSETS at runtime.
One-click deploy button
Add a "Deploy to Cirrus Host" button to your project's README so visitors can fork-and-deploy in a single click.
template block to your helix.json{
"name": "url-shortener",
"kind": "worker",
"main": "src/worker.ts",
"bindings": [
{ "type": "db", "name": "DB", "resource": "links" }
],
"template": {
"name": "URL Shortener",
"description": "Tiny URL shortener with admin UI.",
"bundle": "dist/worker.js",
"env": [
{ "name": "ADMIN_PASSWORD", "description": "Password for /admin", "required": true, "generator": "secret" },
{ "name": "DEFAULT_REDIRECT", "description": "Where unknown short links go", "default": "https://example.com" }
]
}
}
bindings you declare are picked up by the deploy form (workers only). For each one the visitor can create a new resource — pre-filled with the resource name from your helix.json, or a generated name if that's taken — or attach an existing resource from their project. Resource names are unique per project and type, so the platform reuses a matching resource rather than creating a duplicate. The visitor doesn't need to run helix db create first.https://cirrus-host.com/deploy?url=https://github.com/<owner>/<repo>
Add a button to your README:
[](https://cirrus-host.com/deploy?url=https://github.com/owner/repo)
https://github.com/owner/repo/tree/feature/path-to-app.Authentication
Every endpoint used by the panel is also available over the REST API. Authenticate with an API key created from the dashboard or via the API keys endpoint:
Authorization: Bearer helx_...
Base URL: https://cirrus-host.com/api/v1
All requests and responses use JSON (Content-Type: application/json) unless noted otherwise. Deployment uploads use multipart/form-data.
Session cookie authentication (from the browser) also works and grants full access.
API key scopes
API keys support granular scopes that control which resources and actions the key may access. Each scope follows the pattern resource:action. The wildcard scope * grants unrestricted access (backwards-compatible with older keys).
When creating an API key, specify the scopes array. If omitted, the key receives ["*"]. Invalid scope names are rejected with a 400 error.
If a request requires a scope the API key does not carry, the API returns:
{ "error": "insufficient scope: requires 'deployments:write'", "required_scope": "deployments:write" }
Available scopes
Account
Read or modify your own profile and notifications.
account:read— View your profile, email, and avataraccount:write— Update your profile settingsnotifications:read— View notificationsnotifications:write— Mark notifications as read
Projects
List, create, update, and delete projects.
projects:read— List and view projectsprojects:write— Create, rename, and delete projects
Deployments
View and manage deployments, including containers.
deployments:read— View deployment details, versions, hostnamesdeployments:write— Deploy, rollback, and delete deploymentscontainers:read— View container machines and statuscontainers:write— Deploy, restart, and scale containers
Configuration
Manage env vars, secrets, bindings, and custom domains.
env:read/env:write— Environment variablessecrets:read/secrets:write— Secrets (encrypted)domains:read/domains:write— Custom domainsbindings:read/bindings:write— Resource bindings
Resources
Provision and manage databases, buckets, and KV stores.
resources:read— List and view resourcesresources:write— Create, delete, and query resources
Observability
Read logs, metrics, and usage data.
logs:read— Stream and read deployment logsmetrics:read— View request/CPU metricsusage:read— View billing period usage
Collaboration
Manage project members, org members, and invites.
members:read/members:write— Project & org membersorgs:read/orgs:write— Organizations
Billing
View and manage billing accounts, plans, and payment methods.
billing:read— View billing accounts & plansbilling:write— Manage billing, change plans
Authentication
Manage API keys and sessions.
api_keys:read/api_keys:write— API keyssessions:read/sessions:write— Sessions
Admin
Platform administration (requires admin role).
admin:read— View admin dashboardadmin:write— Manage users, deployments, branding
Project restrictions
API keys can optionally be restricted to specific projects by passing allowed_projects (an array of project slugs) when creating the key. This is orthogonal to scopes — a key can have full scopes but only be able to act on certain projects.
If a request targets a project the key is not authorized for, the API returns:
{ "error": "api key not authorized for project 'my-project'" }
Keys with allowed_projects: null (the default) may access all projects the user has membership in.
Account
Get the authenticated user's profile, unread notification count, and pending invite count.
account:readResponse:
{
"user": { "id": "...", "email": "...", "name": "...", "username": "...", "avatar": "...", "balance_cents": 0 },
"unread_notifications": 3,
"pending_invites": 1
}Dashboard overview with project/deployment counts, 24h request stats, recent activity, and timeseries data.
projects:readResponse:
{
"projects": 5, "deployments": 12,
"requests24h": 48201, "errors24h": 7,
"activity": [ ... ],
"timeseries": [ { "t": "2026-05-07T00:00:00Z", "requests": 1200, "errors": 2 } ]
}Projects
List projects the user has membership in. Filter by org with ?org=slug.
projects:read| Parameter | In | Description |
|---|---|---|
| org | query | Optional org slug filter |
Create a new project.
projects:write| Parameter | In | Description |
|---|---|---|
| slug | body | URL-safe slug, lowercase alphanumeric + hyphens (max 32 chars) |
| name | body | Display name |
| description | body | Optional description |
| org_slug | body | Optional org (defaults to personal org) |
| billing_account_id | body | Optional billing account (defaults to org's default BA) |
Get project details with deployments, membership info, and app base domain.
projects:readRename a project. The slug is immutable.
projects:write{ "name": "New Name" }Delete a project. All deployments must be deleted first. Owner only.
projects:writeReassign the project to a different billing account.
billing:write{ "billing_account_id": "ba_..." }Deployments
Get deployment details with version history, env vars, secrets metadata, hostnames, and bindings.
deployments:readResponse:
{
"project": { ... },
"deployment": {
"id": "...", "name": "hello", "status": "active",
"version": 3, "url": "hello.my-project.helx.dev",
"versions": [ { "version": 3, "message": "fix routing" }, ... ],
"env_vars": [ { "name": "STAGE", "value": "prod" } ],
"secrets": [ { "id": "...", "name": "API_KEY", "hint": "sk_l..." } ],
"hostnames": [ { "hostname": "api.example.com", "ssl_status": "active" } ],
"bindings": [ { "type": "db", "name": "DB", "resource_name": "primary" } ]
},
"membership": { "role": "owner", "permissions": [] }
}Deploy a worker. Body is multipart/form-data with a bundle file and metadata JSON. The CLI wraps this for you.
deployments:writeResponse:
{ "deployment_id": "...", "version": 4, "url": "hello.my-project.example.com" }metadata JSON can include: name, message, deployment_id, compatibility_date, compatibility_flags, and bindings (array of {type, name, resource}).Permanently remove a deployment. The script is removed from the edge runtime.
deployments:writeRestore a previous version as the active one.
deployments:writeResponse:
{ "ok": true, "version": 2 }Containers
Container deployments run OCI images instead of Worker scripts. The same project and billing model applies.
Deploy a container from an OCI image.
containers:write| Parameter | In | Description |
|---|---|---|
| name | body | Deployment name |
| image | body | OCI image reference (e.g. ghcr.io/you/app:latest) |
| region | body | Optional region hint |
| size | body | Optional size preset (e.g. shared-cpu-1x@1024mb) |
| replicas | body | Optional replica count |
| port | body | Optional container port |
| env | body | Optional environment variables object |
Response:
{
"deployment_id": "...", "kind": "container",
"url": "myapp.project.helx.dev",
"machines": [ { "id": "...", "state": "running", "region": "ord" } ]
}List machines for a container deployment.
containers:readRestart all machines in a container deployment.
containers:writeScale a container deployment.
containers:write{ "replicas": 3 }Cron jobs
Attach recurring scheduled triggers to any worker deployment. The platform evaluates cron expressions every minute (UTC) and dispatches a POST request to your worker's fetch handler with the header X-Cron-Trigger: <job-name>.
X-Cron-Trigger header to distinguish scheduled invocations from regular traffic:export default {
async fetch(request) {
const cron = request.headers.get('X-Cron-Trigger');
if (cron) {
// Handle scheduled job by name
if (cron === 'cleanup') await doCleanup();
return new Response('ok');
}
// Normal request handling
return new Response('Hello!');
}
}List all cron jobs for a deployment.
cron:readResponse:
[{
"id": "...", "name": "cleanup", "schedule": "0 3 * * *",
"description": "Nightly data cleanup", "enabled": 1,
"last_run_at": "2025-05-07T03:00:00.000Z", "last_status": "success"
}]Create a new cron job.
cron:write| Parameter | In | Description |
|---|---|---|
| name | body | Job name (lowercase, alphanumeric, hyphens, underscores) |
| schedule | body | Cron expression (5-field: min hour dom mon dow) |
| description | body | Optional description |
{ "name": "cleanup", "schedule": "0 3 * * *", "description": "Nightly data cleanup" }Update a cron job schedule, description, or enabled state.
cron:write{ "schedule": "0 6 * * *", "enabled": false }Delete a cron job.
cron:writeList recent execution history for a cron job (last 50 runs).
cron:readResponse:
[{
"id": "...", "status": "success", "duration_ms": 42,
"detail": null, "started_at": "2025-05-07T03:00:00.000Z"
}]Environment variables
Set or update a plain-text env var. Applied on the next deploy.
env:write{ "name": "STAGE", "value": "prod" }^[A-Z][A-Z0-9_]{0,63}$.Remove an env var.
env:writeSecrets
Set or update a secret. Encrypted with AES-GCM at rest; decrypted only when applied to the worker as a secret_text binding.
secrets:write{ "name": "STRIPE_KEY", "value": "sk_live_..." }Remove a secret.
secrets:writeCustom domains
List all custom domains across projects the user is a member of.
domains:readAttach a custom hostname. Returns the CNAME target to point at.
domains:write{ "hostname": "api.example.com" }${cnameTarget}. SSL provisions automatically.Re-check SSL provisioning and DNS for a custom hostname.
domains:writeRemove a custom hostname.
domains:writeResources
List all resources in a project with binding counts.
resources:readResponse:
[
{ "id": "...", "type": "database", "name": "primary", "cf_resource_id": "...",
"created_at": "2026-05-01T...", "binding_count": 2 },
{ "id": "...", "type": "bucket", "name": "uploads", "binding_count": 1 }
]Provision a new resource.
resources:write| Parameter | In | Description |
|---|---|---|
| type | body | database, bucket, or kv (aliases: d1, r2, kv_namespace) |
| name | body | Resource name (lowercase, project-scoped) |
| location_hint | body | Optional location hint |
Get resource details with bindings and type-specific stats (size, object count, key count).
resources:readDelete a resource. Pass ?force=true to delete even if bindings exist.
resources:writeResource explorer
Query databases, browse buckets, and manage KV stores directly from the API.
Database
Execute a SQL query against a database resource.
resources:write{ "sql": "SELECT * FROM users WHERE id = ?", "params": [42] }"raw": true to execute multi-statement SQL (e.g. migrations).List user tables in a database.
resources:readBucket (Object Storage)
List objects in a bucket.
resources:read| Parameter | In | Description |
|---|---|---|
| prefix | query | Filter by key prefix |
| delimiter | query | Group by delimiter (e.g. / for folder-like listing) |
| limit | query | Max results (default 100, max 1000) |
| cursor | query | Pagination cursor |
Upload an object. Pass the key in the X-Key header and the file as the request body.
resources:writeDownload an object. Pass the key as ?key=....
resources:readDelete an object.
resources:write{ "key": "path/to/file.png" }KV Store
List keys in a KV store.
resources:read| Parameter | In | Description |
|---|---|---|
| prefix | query | Filter by key prefix |
| limit | query | Max results (default 100, max 1000) |
| cursor | query | Pagination cursor |
Read a KV value. Returns text or base64-encoded binary.
resources:read| Parameter | In | Description |
|---|---|---|
| key | query | The key to read |
Write a KV value.
resources:write{ "key": "session:abc", "value": "{...}", "expiration_ttl": 3600 }Delete a KV key.
resources:write{ "key": "session:abc" }Bindings
List resource bindings on a deployment.
bindings:readAttach a project resource to a deployment.
bindings:write{ "resource_id": "res_...", "name": "MY_DB" }UPPER_SNAKE_CASE. The resource must belong to the same project. The binding is applied immediately via a redeployment.Detach a resource binding by name. Triggers a redeployment.
bindings:writeMetrics
Per-deployment metrics: hourly request/error counts, p50/p99 CPU, status-code breakdown.
metrics:read| Parameter | In | Description |
|---|---|---|
| since | query | 24h (default), 7d, or 30d |
Live logs
WebSocket upgrade for real-time logs. Each frame is one JSON-encoded log line.
logs:readResponse:
{ "t": "2026-05-07T01:03:32.896Z", "level": "info", "msg": "GET /api/users → ok (2.4ms cpu)" }console.log, uncaught exceptions, and request summaries all stream automatically.Usage
Account-wide usage for the current 30-day billing period.
usage:readResponse:
{
"plan": { "name": "Pro", "included_requests": 10000000, ... },
"period_requests": 1284502, "period_cpu_ms": 483920,
"included_requests": 10000000, "included_cpu_ms": 5000000,
"daily": [ { "date": "2026-05-01", "requests": 41203, "errors": 12, "cpu_ms": 14820 } ],
"invoices": [ ... ]
}Members & invites
Project members
List project members, pending invites, and the permissions catalog.
members:readInvite a user to a project by email or username.
members:write| Parameter | In | Description |
|---|---|---|
| identifier | body | Email or username |
| role | body | admin, developer, viewer, or custom |
| permissions | body | Array of permissions (for custom role) |
Cancel a pending invite.
members:writeUpdate a member's role or permissions.
members:write{ "role": "admin", "permissions": ["deployments:write", "env:read"] }Remove a member. Anyone can remove themselves (leave).
members:writeTransfer project ownership to another member. Current owner becomes admin.
members:write{ "user_id": "..." }Personal invites
List project invites addressed to you.
members:readAccept a project invite.
members:writeDecline a project invite.
members:writeOrganizations
Endpoints are at /api/v1/orgs.
List organizations the user belongs to.
orgs:readCreate a team organization.
orgs:write{ "name": "My Team", "slug": "my-team", "description": "..." }Get org details with your role.
orgs:readUpdate org name or description. Admin required.
orgs:write{ "name": "New Name", "description": "..." }Delete a team org. All projects must be removed first. Owner only.
orgs:writeOrg members
List org members.
members:readInvite a user to an org by email or username.
members:write| Parameter | In | Description |
|---|---|---|
| email / username | body | The person to invite |
| role | body | admin, billing, developer, or viewer |
Update a member's role.
members:write{ "role": "admin" }Remove a member from the org.
members:writeOrg invites
Preview an org invite by token.
orgs:read| Parameter | In | Description |
|---|---|---|
| token | query | Invite token |
Accept an org invite.
orgs:write{ "token": "..." }Decline an org invite.
orgs:write{ "token": "..." }Org logo
Upload an org logo. Send as multipart/form-data or raw image bytes. Max 2 MB. Accepted types: SVG, PNG, JPEG, WebP, GIF.
orgs:writeRemove the org logo.
orgs:writeBilling accounts
Endpoints are at /api/v1/billing-accounts. Billing accounts belong to orgs and aggregate usage + charges for all projects assigned to them.
List billing accounts available to an org.
billing:read| Parameter | In | Description |
|---|---|---|
| for_org | query | Required org slug |
Create a billing account in an org. Requires admin role.
billing:write{ "org_slug": "my-team", "name": "Production", "description": "..." }Get billing account details with usage and access info.
billing:readUpdate billing account name, description, or spending limits.
billing:write{ "name": "...", "hard_limit_cents": 10000, "soft_limit_cents": 5000 }Delete a billing account.
billing:writeSet as the org's default billing account.
billing:writeList visible plans for this billing account's reseller.
billing:readChange the billing account's plan.
billing:write{ "plan_id": "..." }Per-project usage breakdown for the current billing period.
billing:readBilling account sharing
List orgs and users this billing account is shared with.
billing:readShare a billing account with another org or user.
billing:write{ "org_slug": "partner-team", "permission": "use" }use (assign projects to it) or manage (full control). Identify the target with org_slug, username, or email.Revoke a billing account share.
billing:writePayment methods
User-level (legacy)
Get the Stripe publishable key for client-side card collection.
billing:readList saved payment methods.
billing:readCreate a Stripe SetupIntent for adding a new card.
billing:writeConfirm and save a payment method after Stripe Elements completes.
billing:write{ "payment_method_id": "pm_..." }Set a payment method as default.
billing:writeRemove a payment method.
billing:writeBilling-account-level
List payment methods on a billing account.
billing:readCreate a SetupIntent for adding a card to a billing account.
billing:writeSave a payment method to a billing account.
billing:write{ "payment_method_id": "pm_...", "make_default": true }Set a card as default for a billing account.
billing:writeRemove a card from a billing account.
billing:writeAPI keys
List your API keys with scopes, project restrictions, and expiration.
api_keys:readResponse:
[{
"id": "...", "name": "CI Deploy Key", "key_prefix": "helx_abc",
"scopes": ["deployments:write", "projects:read"],
"allowed_projects": ["my-api"],
"expires_at": "2026-08-01T00:00:00Z", "created_at": "..."
}]Create a new API key with granular scopes and optional project restrictions.
api_keys:write| Parameter | In | Description |
|---|---|---|
| name | body | Display name (max 100 chars) |
| scopes | body | Array of scopes (default: ["*"]). See scopes reference. |
| allowed_projects | body | Optional array of project slugs to restrict access to |
| expires_in_days | body | Optional expiration in days from now |
{
"name": "CI Deploy Key",
"scopes": ["deployments:write", "projects:read"],
"allowed_projects": ["my-api", "my-frontend"],
"expires_in_days": 90
}Response:
{
"id": "...", "secret": "helx_abc123...", "prefix": "helx_abc",
"name": "CI Deploy Key",
"scopes": ["deployments:write", "projects:read"],
"allowed_projects": ["my-api", "my-frontend"],
"expires_at": "2026-08-05T..."
}secret is only returned once at creation time. Store it securely.List all available scopes with descriptions and groupings. Useful for building scope selection UIs.
api_keys:readResponse:
{
"scopes": ["account:read", "account:write", ...],
"groups": { "account": { "label": "Account", "scopes": [...] }, ... },
"descriptions": { "account:read": "View your profile, email, and avatar", ... }
}Revoke an API key.
api_keys:writeSessions
List active browser sessions and CLI tokens. The current session is marked.
sessions:readResponse:
[
{ "id": "...", "kind": "web", "label": "Browser", "ip": "...", "current": true, "expires_at": "..." },
{ "id": "...", "kind": "cli", "label": "Helix CLI", "last_used_at": "..." }
]Revoke a session. Pass {"kind":"cli"} in the body for CLI tokens.
sessions:writeNotifications
List your notifications (last 50).
notifications:readMark a notification as read.
notifications:writeMark all notifications as read.
notifications:writePlans
List visible plans for the current reseller. Useful for plan selection UIs.
billing:readAdmin
Admin endpoints require the super_admin or reseller_admin role plus the admin:read or admin:write scope. All endpoints are at /api/v1/admin.
Platform overview: user/deployment counts, 24h requests, timeseries, recent activity.
admin:readSearch users by email or name.
admin:read| Parameter | In | Description |
|---|---|---|
| q | query | Search query |
Impersonate a user. Sets a session cookie.
admin:writeSuspend a user account.
admin:writeReactivate a suspended user.
admin:writeSearch deployments by name, project, or owner email.
admin:read| Parameter | In | Description |
|---|---|---|
| q | query | Search query |
Suspend a deployment.
admin:writeResume a suspended deployment.
admin:writeList all plans with subscriber counts.
admin:readCreate a new plan.
admin:writeUpdate a plan.
admin:writeDelete a plan (fails if subscribers exist).
admin:writeSearch billing accounts.
admin:readOverride a billing account's plan.
admin:write{ "plan_id": "..." }Get the current reseller's branding configuration.
admin:readUpdate reseller branding (name, colors, links, etc).
admin:writeUpload reseller logo. Max 2 MB.
admin:writeUpload reseller favicon.
admin:writeRemove reseller logo.
admin:writeRemove reseller favicon.
admin:writeSystem health: D1 row count, active deployments.
admin:readList all resellers (super_admin only).
admin:readProvision a new reseller (super_admin only).
admin:writeBackfill wildcard DNS records for all projects.
admin:writeCreate a Stripe webhook endpoint for this panel.
admin:writeError codes
The API returns standard HTTP codes with a JSON body of the form { "error": "..." }.
| Status | Meaning |
|---|---|
| 400 | Validation failed (missing/invalid field) |
| 401 | Missing or invalid Bearer token |
| 402 | Plan limit reached (deployment count, custom domains, requests) |
| 403 | Forbidden — insufficient scope, project restriction, or role |
| 404 | Resource not found |
| 409 | Conflict (e.g. slug already taken, already a member) |
| 410 | Gone (e.g. expired invite) |
| 413 | Payload too large (e.g. logo upload > 2 MB) |
| 415 | Unsupported media type (e.g. invalid image type) |
| 426 | Upgrade required (WebSocket endpoint called without upgrade) |
| 500 | Internal error — safe to retry with backoff |
| 503 | Service unavailable (e.g. containers not enabled) |
When a scope is missing, the response also includes a required_scope field:
{ "error": "insufficient scope: requires 'deployments:write'", "required_scope": "deployments:write" }
Rate limits
The API does not impose hard rate limits today, but abusive patterns (more than ~100 requests/second per key) may be throttled. Deploy endpoints perform real work against the edge runtime and should not be called in tight loops.
Code examples
List your projects
curl https://cirrus-host.com/api/v1/projects \
-H "Authorization: Bearer $HELIX_TOKEN"
Create a project
curl -X POST https://cirrus-host.com/api/v1/projects \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"slug":"my-api","name":"My API","description":"Backend services"}'
Deploy a worker
# Build first
esbuild src/worker.ts --bundle --format=esm --outfile=dist/worker.js
# Upload
curl -X POST https://cirrus-host.com/api/v1/projects/my-api/deployments \
-H "Authorization: Bearer $HELIX_TOKEN" \
-F "bundle=@dist/worker.js" \
-F 'metadata={"name":"api","message":"v1.2.0","compatibility_date":"2025-04-01"}'
Set an environment variable
curl -X POST https://cirrus-host.com/api/v1/deployments/$DEP_ID/env \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"DATABASE_URL","value":"postgres://..."}'
Create a scoped API key
curl -X POST https://cirrus-host.com/api/v1/api-keys \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "CI Deploy",
"scopes": ["deployments:write", "projects:read"],
"allowed_projects": ["my-api"],
"expires_in_days": 90
}'
Provision a database and bind it
# Create the database
curl -X POST https://cirrus-host.com/api/v1/projects/my-api/resources \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"database","name":"primary"}'
# Bind it to a deployment
curl -X POST https://cirrus-host.com/api/v1/deployments/$DEP_ID/bindings \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"resource_id":"RESOURCE_ID","name":"DB"}'
Query a database
curl -X POST https://cirrus-host.com/api/v1/resources/$RES_ID/database/query \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sql":"SELECT * FROM users WHERE email = ?","params":["alice@example.com"]}'
Get deployment metrics
curl "https://cirrus-host.com/api/v1/deployments/$DEP_ID/metrics?since=7d" \
-H "Authorization: Bearer $HELIX_TOKEN"
Stream logs (WebSocket)
websocat "https://cirrus-host.com/api/v1/deployments/$DEP_ID/tail" \
-H "Authorization: Bearer $HELIX_TOKEN"
Add a custom domain
curl -X POST https://cirrus-host.com/api/v1/deployments/$DEP_ID/domains \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"hostname":"api.example.com"}'
Helper function
const API = "https://cirrus-host.com/api/v1";
const TOKEN = process.env.HELIX_TOKEN;
async function api(path, opts = {}) {
const res = await fetch(API + path, {
...opts,
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
...opts.headers,
},
body: opts.body ? JSON.stringify(opts.body) : undefined,
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
return res.json();
}
List projects
const projects = await api("/projects");
console.log(projects);
Create a project and deploy
// Create project
const project = await api("/projects", {
method: "POST",
body: { slug: "my-api", name: "My API" },
});
// Deploy (multipart)
const form = new FormData();
form.append("bundle", new Blob([bundleCode], { type: "application/javascript" }));
form.append("metadata", JSON.stringify({
name: "api",
message: "initial deploy",
compatibility_date: "2025-04-01",
}));
const deploy = await fetch(`${API}/projects/my-api/deployments`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}` },
body: form,
}).then(r => r.json());
console.log("Live at:", deploy.url);
Provision resources and add bindings
// Create a database
const db = await api("/projects/my-api/resources", {
method: "POST",
body: { type: "database", name: "primary" },
});
// Create a KV store
const kv = await api("/projects/my-api/resources", {
method: "POST",
body: { type: "kv", name: "cache" },
});
// Bind both to a deployment
await api(`/deployments/${deployId}/bindings`, {
method: "POST",
body: { resource_id: db.id, name: "DB" },
});
await api(`/deployments/${deployId}/bindings`, {
method: "POST",
body: { resource_id: kv.id, name: "CACHE" },
});
Create a scoped API key
const key = await api("/api-keys", {
method: "POST",
body: {
name: "CI Deploy",
scopes: ["deployments:write", "projects:read", "env:write"],
allowed_projects: ["my-api"],
expires_in_days: 90,
},
});
console.log("Secret (store this!):", key.secret);
Monitor metrics
const metrics = await api(`/deployments/${depId}/metrics?since=24h`);
console.log("Requests:", metrics.total_requests);
console.log("p99 CPU:", metrics.p99_cpu_ms, "ms");
Helper function
import os, requests
API = "https://cirrus-host.com/api/v1"
TOKEN = os.environ["HELIX_TOKEN"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
def api(method, path, **kwargs):
r = requests.request(method, API + path, headers=HEADERS, **kwargs)
r.raise_for_status()
return r.json()
List projects
projects = api("GET", "/projects")
for p in projects:
print(f"{p['slug']} — {p['name']} ({p['deployment_count']} deployments)")
Create a project and deploy
# Create project
project = api("POST", "/projects", json={"slug": "my-api", "name": "My API"})
# Deploy
with open("dist/worker.js", "rb") as f:
r = requests.post(
f"{API}/projects/my-api/deployments",
headers={"Authorization": f"Bearer {TOKEN}"},
files={"bundle": ("worker.js", f)},
data={"metadata": '{"name":"api","message":"v1.0"}'},
)
deploy = r.json()
print("Live at:", deploy["url"])
Provision and bind a database
# Create database
db = api("POST", "/projects/my-api/resources", json={"type": "database", "name": "primary"})
# Bind to deployment
api("POST", f"/deployments/{dep_id}/bindings", json={"resource_id": db["id"], "name": "DB"})
# Query it
result = api("POST", f"/resources/{db['id']}/database/query",
json={"sql": "SELECT COUNT(*) as n FROM users"})
print("Users:", result["result"])
Create a scoped API key
key = api("POST", "/api-keys", json={
"name": "CI Deploy",
"scopes": ["deployments:write", "projects:read"],
"allowed_projects": ["my-api"],
"expires_in_days": 90,
})
print("Secret:", key["secret"])
Usage monitoring
usage = api("GET", "/usage")
print(f"Requests: {usage['period_requests']:,} / {usage['included_requests']:,}")
print(f"CPU: {usage['period_cpu_ms']:,}ms / {usage['included_cpu_ms']:,}ms")
for day in usage["daily"][-7:]:
print(f" {day['date']}: {day['requests']:,} req, {day['errors']} err")
Helper
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
var (
apiBase = "https://cirrus-host.com/api/v1"
token = os.Getenv("HELIX_TOKEN")
)
func api(method, path string, body any) (map[string]any, error) {
var r io.Reader
if body != nil {
b, _ := json.Marshal(body)
r = bytes.NewReader(b)
}
req, _ := http.NewRequest(method, apiBase+path, r)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]any
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
List projects
projects, _ := api("GET", "/projects", nil)
fmt.Println(projects)
Create a scoped API key
key, _ := api("POST", "/api-keys", map[string]any{
"name": "CI Deploy",
"scopes": []string{"deployments:write", "projects:read"},
"allowed_projects": []string{"my-api"},
"expires_in_days": 90,
})
fmt.Println("Secret:", key["secret"])
Guides
Building a full-stack app with a database
This guide walks through creating a project, provisioning a database, deploying a worker, and binding them together.
# 1. Create the project
helix init my-project/api
cd my-project/api
# 2. Provision a database
helix db create primary
# 3. Add the binding to helix.json
# "bindings": [{ "type": "db", "name": "DB", "resource": "primary" }]
# 4. Write your worker
cat <<'EOF' > src/worker.ts
export default {
async fetch(request: Request, env: any) {
// Initialize tables on first request
await env.DB.exec(`
CREATE TABLE IF NOT EXISTS visits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT, visited_at TEXT DEFAULT (datetime('now'))
)
`);
const url = new URL(request.url);
await env.DB.prepare("INSERT INTO visits (path) VALUES (?)").bind(url.pathname).run();
const { results } = await env.DB.prepare(
"SELECT path, COUNT(*) as count FROM visits GROUP BY path ORDER BY count DESC LIMIT 10"
).all();
return Response.json({ top_pages: results });
}
};
EOF
# 5. Deploy
helix deploy
Building an app with object storage
# Provision a bucket
helix bucket create uploads
# helix.json bindings:
# { "type": "bucket", "name": "UPLOADS", "resource": "uploads" }
# In your worker:
export default {
async fetch(request: Request, env: any) {
const url = new URL(request.url);
if (request.method === "PUT") {
const key = url.pathname.slice(1);
await env.UPLOADS.put(key, request.body, {
httpMetadata: { contentType: request.headers.get("content-type") || "application/octet-stream" },
});
return Response.json({ ok: true, key });
}
if (request.method === "GET") {
const key = url.pathname.slice(1);
const obj = await env.UPLOADS.get(key);
if (!obj) return new Response("Not found", { status: 404 });
return new Response(obj.body, {
headers: { "Content-Type": obj.httpMetadata?.contentType || "application/octet-stream" },
});
}
return new Response("Method not allowed", { status: 405 });
}
};
Building an app with KV caching
# Provision a KV store
helix kv create cache
# helix.json bindings:
# { "type": "kv", "name": "CACHE", "resource": "cache" }
# In your worker:
export default {
async fetch(request: Request, env: any) {
const url = new URL(request.url);
const cacheKey = `page:${url.pathname}`;
// Check cache first
const cached = await env.CACHE.get(cacheKey);
if (cached) {
return new Response(cached, {
headers: { "Content-Type": "text/html", "X-Cache": "HIT" },
});
}
// Generate response
const html = `<h1>Hello from ${url.pathname}</h1><p>Generated at ${new Date().toISOString()}</p>`;
// Cache for 5 minutes
await env.CACHE.put(cacheKey, html, { expirationTtl: 300 });
return new Response(html, {
headers: { "Content-Type": "text/html", "X-Cache": "MISS" },
});
}
};
CI/CD with GitHub Actions
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run build
- name: Deploy to Cirrus Host
env:
HELIX_TOKEN: ${{ secrets.HELIX_TOKEN }}
run: |
curl -X POST https://cirrus-host.com/api/v1/projects/my-api/deployments \
-H "Authorization: Bearer $HELIX_TOKEN" \
-F "bundle=@dist/worker.js" \
-F 'metadata={"name":"api","message":"'"$GITHUB_SHA"'"}'
deployments:write and projects:read, restricted to your project slug. This way a leaked CI secret can only deploy to that one project.
curl -X POST https://cirrus-host.com/api/v1/api-keys \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"GitHub Actions","scopes":["deployments:write","projects:read"],"allowed_projects":["my-api"]}'
Deploying a container
# Deploy a Ghost CMS container
curl -X POST https://cirrus-host.com/api/v1/projects/my-blog/deployments/container \
-H "Authorization: Bearer $HELIX_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "ghost",
"image": "ghost:5",
"port": 2368,
"size": "shared-cpu-1x@1024mb",
"env": {
"url": "https://ghost.my-blog.helx.dev",
"database__client": "sqlite3"
}
}'