Introduction
sun is a private cloud control plane for lightweight, isolated virtual machines. It lets you provision secure, memory-safe environments in milliseconds — from a terminal, a mobile phone, or a no-code canvas — without touching a hypervisor or writing a Dockerfile.
You bring the hardware. sun manages the fleet.
What sun is
sun is the layer between your servers and the workloads you want to run on them. It handles:
- Booting microVMs from a minimal kernel + rootfs in < 125 ms
- Routing traffic to containers via a reverse proxy with automatic TLS
- Running build pipelines inside ephemeral microVMs so builds don't pollute each other
- Managing SSH access via certificate-based bastion sessions
- Grouping containers into projects with shared internal networking
- Monitoring infra agents across your fleet in real time
Core concepts
Workspace VM
A workspace VM is an on-demand isolated microVM for interactive use — development, agent execution, running a one-off job, or hosting a long-lived process. It has a shell, a unique VM ID (vm-XXXX), and an optional build step on launch.
Workspace VMs can be persistent (survive stop/start) or ephemeral (destroyed when the session ends).
Deployment
A deployment is the long-running counterpart to a workspace VM. It is the unit of traffic routing — each deployment gets a subdomain, TLS certificate, and sits behind the reverse proxy. Deployments have a status lifecycle: queued → booting → running.
Build
A build compiles source code into a deployable artifact. Builds run inside isolated ephemeral microVMs so they never share state. Builds go through: queued → building → succeeded | failed.
Project
A project groups deployments, storage volumes, and database instances into a shared internal network. Resources inside the same project can reach each other by service name. The no-code canvas (GET/PUT /projects/:id/canvas) provides a visual view of the connections.
Infra Agent
An infra agent is a monitoring daemon running on each physical host. The launcher queries agents to schedule new VMs on nodes with available capacity. The sun capacity CLI command shows live per-agent CPU, memory, and disk utilisation.
Storage Volume
A storage volume is persistent block storage that can be attached to a workspace VM or deployment. Volumes outlive the VMs they are attached to.
How it fits together
┌──────────────────┐
│ sun launcher │ (this API)
└────────┬─────────┘
┌─────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Workspace │ │ Deployment │ │ Build │
│ VM (shell) │ │ (traffic) │ │ (pipeline) │
└──────┬──────┘ └──────┬───────┘ └──────┬───────┘
│ │ │
└────────────────┴─────────────────┘
│
isolated microVM
on your infrastructure
The launcher talks to infra agents on each physical host to decide where to schedule a new VM. Once booted, workspace VMs are reachable via SSH through the bastion, and deployments are reachable via the reverse proxy on their assigned subdomain.
Quick start
# Install the CLI
curl -sSL https://sun.run/install | sh
# Point it at your launcher
sun config set endpoint https://launcher.example.com
sun config set token <your-api-token>
# Launch a workspace VM
sun deploy --image ubuntu-22 --region eu-1
# See what's running
sun deployments
# Check fleet capacity
sun capacity
API Reference
All endpoints are served by the sun launcher. Authenticate with a bearer token in the Authorization header unless noted otherwise.
Authorization: Bearer <token>
Tokens are minted via POST /auth/token/mint after login. Base URL is your launcher instance, e.g. https://launcher.example.com.
User tokens are tenant-scoped: they can only access resources in projects under accounts where the user is a member (account_users). The launcher system token remains operator-scoped.
Quickstart
Pick the first step based on your source type.
Short rule:
- GitHub repo URL - use
POST /deployments/github. - Internal repo ID / already modeled deployment - use
POST /deployments. - Tar / zip upload - use
POST /buildsfirst, thenPOST /builds/:id/source.
| Source type | First call | Next call | Notes |
|---|---|---|---|
| GitHub repo | POST /deployments/github | None | Creates the deployment and queues the build automatically. |
| Repo tar / zip | POST /builds | POST /builds/:id/source | The build must exist first; the upload attaches to that build. |
| Existing deployment rebuild | POST /builds with deployment_id | POST /builds/:id/source | Use this when you already have a deployment and want to rebuild it. |
Rule of thumb: deployment = runtime identity. build = compilation job. For tar/zip uploads, create the build first.
Runtime note for Node/Express apps: the build worker now packages server-side bundles with their dependencies when it detects a server-style package.json. At runtime, the launcher exports PORT from the deployment runtime port, so app code should read process.env.PORT instead of hardcoding a listener port like 5001.
- Save your token and project in environment variables.
- Create deployments/builds using only server-generated IDs.
export LAUNCHER_BASE_URL="https://launcher.haifa.my"
export LAUNCHER_API_KEY="<u1_token>"
export LAUNCHER_PROJECT_ID="<proj-xxxx>"
Verify Access
curl -sS "$LAUNCHER_BASE_URL/projects" \
-H "Authorization: Bearer $LAUNCHER_API_KEY"
Create Deployment (Server Generates dep-xxxx)
DEPLOYMENT_ID=$(
curl -sS -X POST "$LAUNCHER_BASE_URL/deployments" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"region\":\"eu-west-1\",
\"repo_id\":\"https://github.com/<owner>/<repo>\",
\"persistence_mode\":\"ephemeral\"
}" | jq -r '.id'
)
echo "$DEPLOYMENT_ID"
This endpoint is for deployments that already have an internal repo reference or deployment context. If you are starting from a GitHub URL, use POST /deployments/github instead.
Create Deployment From GitHub URL
curl -sS -X POST "$LAUNCHER_BASE_URL/deployments/github" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"repo_url\":\"https://github.com/<owner>/<repo>\",
\"region\":\"eu-west-1\",
\"git_ref\":\"main\",
\"persistence_mode\":\"ephemeral\",
\"env_mode\":\"merge\",
\"env\":{
\"VITE_TRADLY_PUBLISHABLE_KEY\":\"<publishable-key>\",
\"VITE_TRADLY_BASE_URL\":\"https://api.tradly.app\"
}
}"
Use this when the source of truth is a GitHub repository and you want launcher to create the deployment and queue the build automatically.
Example: GitHub deploy with env
curl -sS -X POST "$LAUNCHER_BASE_URL/deployments/github" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"repo_url\":\"https://github.com/<owner>/<repo>\",
\"region\":\"eu-west-1\",
\"git_ref\":\"main\",
\"persistence_mode\":\"ephemeral\",
\"env_mode\":\"merge\",
\"env\":{
\"VITE_TRADLY_PUBLISHABLE_KEY\":\"<publishable-key>\",
\"VITE_TRADLY_BASE_URL\":\"https://api.tradly.app\"
}
}"
This is the recommended shape when you deploy directly from GitHub and need build-time env baked into the frontend bundle.
Deployment Runtime Policy
Runtime policy controls whether a deployment stays hot or cold-starts on request.
Modes:
always_on- keep the runtime runningon_request- cold start on demandon_idle- stop after idle timeouton_inactive- stay running until no external request has been received for seven days, then hibernate and cold-start on the next request
Set always-on:
curl -sS -X PUT "$LAUNCHER_BASE_URL/deployments/$DEPLOYMENT_ID/policy" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d '{
"mode": "always_on",
"auto_start_on_request": true
}'
Hibernate after seven days without traffic:
curl -sS -X PUT "$LAUNCHER_BASE_URL/deployments/$DEPLOYMENT_ID/policy" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d '{
"mode": "on_inactive",
"auto_start_on_request": true
}'
Set cold-start mode:
curl -sS -X PUT "$LAUNCHER_BASE_URL/deployments/$DEPLOYMENT_ID/policy" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d '{
"mode": "on_request",
"auto_start_on_request": true
}'
Trigger Build For Existing Deployment
BUILD_ID=$(
curl -sS -X POST "$LAUNCHER_BASE_URL/builds" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"deployment_id\":\"$DEPLOYMENT_ID\",
\"region\":\"eu-west-1\",
\"env_mode\":\"merge\",
\"env\":{
\"NPM_TOKEN\":\"<github-packages-token>\",
\"WHATSAPP_CHANNEL_LINK\":\"https://whatsapp.com/channel/...\"
}
}" | jq -r '.id'
)
echo "$BUILD_ID"
Use this when you already have a deployment record and want to rebuild it without creating a new deployment first.
Upload Source Bundle (.tar.gz, .tgz, or .zip)
For tar/zip flows: create the build first, then upload the source to /builds/:id/source.
curl -sS -X POST "$LAUNCHER_BASE_URL/builds/$BUILD_ID/source" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/zip" \
-H "x-source-name: local:app.zip" \
--data-binary @app.zip
Use content-type: application/gzip when uploading .tar.gz or .tgz.
Build-First Flow (for tar/zip uploads)
If deployment_id is omitted, launcher can create one server-side for tenant-scoped flow.
curl -sS -X POST "$LAUNCHER_BASE_URL/builds" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"region\":\"eu-west-1\",
\"repo_id\":\"local-upload\",
\"persistence_mode\":\"ephemeral\",
\"env_mode\":\"merge\",
\"env\":{
\"NPM_TOKEN\":\"<github-packages-token>\"
}
}"
Use this flow when you are uploading a repo archive instead of deploying directly from GitHub.
Example: tar/zip build with env
BUILD_ID=$(
curl -sS -X POST "$LAUNCHER_BASE_URL/builds" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/json" \
-d "{
\"project_id\":\"$LAUNCHER_PROJECT_ID\",
\"region\":\"eu-west-1\",
\"repo_id\":\"local-upload\",
\"persistence_mode\":\"ephemeral\",
\"env_mode\":\"merge\",
\"env\":{
\"VITE_TRADLY_PUBLISHABLE_KEY\":\"<publishable-key>\",
\"VITE_TRADLY_BASE_URL\":\"https://api.tradly.app\"
}
}" | jq -r '.id'
)
curl -sS -X POST "$LAUNCHER_BASE_URL/builds/$BUILD_ID/source" \
-H "Authorization: Bearer $LAUNCHER_API_KEY" \
-H "content-type: application/gzip" \
-H "x-source-name: app.tgz" \
--data-binary @app.tgz
This is the recommended shape when clients upload a source archive instead of using GitHub directly.
Check Build Status / Logs
curl -sS "$LAUNCHER_BASE_URL/builds/$BUILD_ID" \
-H "Authorization: Bearer $LAUNCHER_API_KEY"
curl -sS "$LAUNCHER_BASE_URL/builds/$BUILD_ID/logs?limit=200" \
-H "Authorization: Bearer $LAUNCHER_API_KEY"
Rules
- Use project IDs (
proj-xxxx), not project names. - Never invent deployment IDs; always use server-returned
dep-xxxx. - For public URL mode, use public
https://github.com/...repositories. - Token scope is tenant-bound; users only see resources in their account/project scope.
Authentication
POST /auth/login
Exchange credentials for a session.
Request body
| Field | Type | Description |
|---|---|---|
username | string | Account username |
password | string | Account password |
GET /auth/me
Returns the currently authenticated user.
POST /auth/token/mint
Mint a bearer token for API access from your authenticated session.
POST /auth/logout
End the current session.
Workspace VMs
Workspace VMs are on-demand isolated microVMs for interactive development, agent execution, and ephemeral tasks. Each workspace gets its own isolated environment with a shell, optional build pipeline, and an internal network address.
POST /workspaces/empty
Also available as
POST /workspace-vm/create
Create a new workspace VM. The VM boots and is assigned a deployment ID and VM ID.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
region | string | no | Target region for the VM |
project_id | string | no | Assign to a project group for internal networking |
persistence_mode | string | no | persistent or ephemeral |
runtime_port | number | no | Port the runtime listens on inside the VM |
runtime_kind | string | no | Runtime identifier (e.g. shell, node, python) |
bundle_url | string | no | Pre-built artifact URL to load on boot |
vcpu_count | number | no | Number of vCPUs (default: 1) |
mem_size_mib | number | no | Memory in MiB (default: 512) |
idle_timeout_secs | number | no | Auto-stop after N seconds of inactivity |
env_profile | string | no | Named environment profile to apply |
profile_version | string | no | Pin a specific profile version |
bootstrap_script_key | string | no | Key of a bootstrap script to run on first boot |
env_mode | string | no | Env merge mode: merge (default) or replace |
env | object | no | Key/value env map to persist and apply on runtime start |
Response
{
"deployment_id": "dep-3f9a...",
"region": "eu-1",
"status": "booting",
"subdomain": "dep-3f9a",
"fqdn": "dep-3f9a.sun.run",
"runtime_port": 8080,
"container_id": null,
"vm_id": "vm-08a1...",
"runtime_kind": "shell",
"runtime_vendor": null,
"persistence_mode": "ephemeral"
}
GET /workspace-vm/list
List all workspace VMs.
Response
{
"workspaces": [
{
"deployment_id": "dep-3f9a...",
"vm_id": "vm-08a1...",
"status": "running",
"region": "eu-1",
"fqdn": "dep-3f9a.sun.run",
"preview_url": "https://dep-3f9a.sun.run",
"runtime_port": 8080,
"created_at": "2025-03-28T02:00:00Z"
}
]
}
GET /workspace-vm/:id/status
Get the current status of a workspace VM.
Path parameter: id — the deployment_id
POST /workspace-vm/:id/start
Start (or restart) a stopped workspace VM.
POST /workspace-vm/:id/stop
Stop a running workspace VM.
Query parameter: force=true — forcefully terminate without graceful shutdown.
GET /workspace-vm/:id/preview
Returns a preview URL for the workspace VM's exposed port.
POST /workspace-vm/:id/exec
Run a command inside a running workspace VM.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
command | string | yes | Shell command to execute |
cwd | string | no | Working directory |
timeout_ms | number | no | Max execution time in milliseconds |
GET /workspace-vm/:id/shell
WebSocket endpoint — opens an interactive shell session inside the VM. Used by Sun Shell (mobile app) and the web terminal.
ws://launcher.example.com/workspace-vm/:id/shell?token=<api-token>
POST /workspace-vm/:id/build
Trigger a build inside the workspace VM. Initiates source packaging and hands off to the build pipeline.
Deployments
Deployments are the core unit of the sun runtime — a deployed app, service, or workload running inside a microVM.
POST /deployments
Create a new deployment.
Request body
| Field | Type | Description |
|---|---|---|
region | string | Target region |
repo_id | string | GitHub repo ID to deploy |
base_domain | string | Base domain for generated subdomain (example: client-domain.com). Defaults to launcher root domain. In tenant mode, must already be verified and assigned to your project |
persistence_mode | string | persistent or ephemeral |
env_mode | string | Env merge mode: merge (default) or replace |
env | object | Key/value env map to persist and apply on runtime start |
Response
{
"id": "dep-abc123",
"region": "eu-1",
"status": "queued",
"repo_id": "gh-repo-id",
"subdomain": "dep-abc123",
"fqdn": "dep-abc123.sun.run",
"runtime_port": 3000,
"container_id": null,
"persistence_mode": "persistent"
}
GET /deployments
List all deployments.
GET /deployments/:id
Get a single deployment by ID.
DELETE /deployments/:id
Delete a deployment and stop its VM.
POST /deployments/github
Create a deployment from a GitHub repository, triggering a build automatically.
Request body
| Field | Type | Description |
|---|---|---|
repo_url | string | Full GitHub repo URL |
repo | string | Shorthand owner/repo |
repo_id | string | Internal repo ID |
region | string | Target region |
base_domain | string | Base domain for generated subdomain (example: client-domain.com). Defaults to launcher root domain. In tenant mode, must already be verified and assigned to your project |
git_ref | string | Branch, tag, or SHA |
subdir | string | Subdirectory to deploy |
persistence_mode | string | persistent or ephemeral |
auto_sync | boolean | Auto-redeploy on push |
env_mode | string | Env merge mode: merge (default) or replace |
env | object | Key/value env map to persist and apply when deployment is promoted and runtime starts |
Response
{
"deployment_id": "dep-abc123",
"build_id": "bld-xyz789",
"region": "eu-1"
}
Builds
The build pipeline compiles source code into a deployable artifact. Builds run inside ephemeral microVMs.
POST /builds
Enqueue a new build.
Request body
| Field | Type | Description |
|---|---|---|
deployment_id | string | Link to an existing deployment |
region | string | Build region |
repo_id | string | Source repository |
base_domain | string | Base domain for generated subdomain when build creates a new deployment (system-scoped flow) |
persistence_mode | string | persistent or ephemeral |
env_mode | string | Env merge mode: merge (default) or replace |
env | object | Key/value env map to persist and apply to the deployment/build runtime |
Response
{
"id": "bld-xyz789",
"deployment_id": "dep-abc123",
"status": "queued"
}
GET /builds
List builds, optionally filtered.
Query parameters
| Param | Description |
|---|---|
deployment_id | Filter by deployment |
region | Filter by region |
status | queued | building | succeeded | failed |
GET /builds/:id/logs
Fetch build logs.
Query parameter: limit — max number of lines to return.
Response
{
"build_id": "bld-xyz789",
"lines": [
{ "message": "Installing dependencies...", "ts": "2025-03-28T02:01:00Z", "level": "info" }
]
}
POST /builds/:id/retry
Retry a failed build.
GET /builds/:id/artifacts
Get the build output artifacts (bundle URL, static URL).
Response
{
"build_id": "bld-xyz789",
"deployment_id": "dep-abc123",
"status": "succeeded",
"bundle_url": "https://...",
"static_url": "https://...",
"failure_class": null,
"failure_message": null
}
POST /builds/:id/source
Upload source code for a build as .tar.gz, .tgz, or .zip. The launcher normalizes the source bundle into its internal tarball format before dispatching the build. Common GitHub download zips and macOS Finder zips are supported.
Headers
| Header | Description |
|---|---|
content-type | application/zip for .zip; application/gzip for .tar.gz or .tgz |
x-source-name | Optional label for logs/UI, e.g. local:app.zip |
Domains & SSL
POST /deployments/:id/domains
Attach a custom domain to a deployment.
Request body
| Field | Type | Description |
|---|---|---|
domain | string | The custom domain (e.g. app.example.com) |
method | string | Verification method: cname or txt |
domain_role | string | attached (default) or base_domain for tenant-owned base domains |
production | boolean | Optional. Defaults to true for production deployments and false for workspace/preview deployments. |
Rule: verified domains attached to production deployments are treated as production automatically. Workspace and preview deployments stay non-production unless you explicitly override it.
Response
{
"id": "dom-...",
"deployment_id": "dep-abc123",
"domain": "app.example.com",
"domain_role": "attached",
"is_production": true,
"status": "pending_verification",
"verification_method": "cname",
"verification_record_name": "_sun-verify.app.example.com",
"verification_record_value": "sun-abc123xyz",
"cert_status": "pending",
"cert_issued_at": null
}
GET /deployments/:id/domains
List domains attached to a deployment.
POST /domains/:id/verify
Trigger domain verification after the DNS record has been set. Sun checks the record and issues a TLS certificate automatically on success.
Infra Agents
GET /infra/agents
Returns the health and resource usage of all registered infra agents. This is what sun capacity calls.
Response
{
"agents": [
{
"name": "node-eu-1",
"endpoint": "https://agent.example.internal",
"healthy": true,
"capacity": {
"total_vcpu": 8,
"total_mem_mib": 16384,
"cpu_percent": 12.4,
"memory_percent": 38.1,
"disk_percent": 22.7
}
}
]
}
Monitoring
GET /metrics
Current runtime metrics snapshot.
GET /metrics/timeseries
Time-series metrics for charting.
GET /logs
Recent log lines across all deployments.
GET /runtime/slo
Runtime SLO (service level objective) report — uptime, latency, error rates.
Health
GET /healthz
Returns 200 OK when the launcher is up. No authentication required.
Projects & Accounts
Projects group deployments, storage volumes, and database instances into a shared internal network. Accounts are the top-level billing and user boundary.
Tenant access model: Account -> Project -> Resources.
Resources without project_id are hidden from tenant users and are backfilled to a default project on startup when possible.
GET /accounts
List all accounts.
POST /accounts
Create or update an account.
Request body
| Field | Type | Description |
|---|---|---|
id | string | Optional — omit to create new |
name | string | Display name |
GET /accounts/:id/users
List users in an account.
POST /accounts/:id/users
Add a user to an account with a role.
Request body
| Field | Type | Description |
|---|---|---|
user_id | string | User to add |
role | string | Role to assign |
GET /projects
List projects. Optionally filter by account.
Query parameter: account_id
POST /projects
Create or update a project.
Request body
| Field | Type | Description |
|---|---|---|
id | string | Optional — omit to create new |
account_id | string | Owning account |
name | string | Project name |
POST /projects/:id/assign
Assign existing resources (deployments, storage, databases) to a project.
Request body
| Field | Type | Description |
|---|---|---|
deployment_ids | string[] | Deployments to add |
storage_volume_ids | string[] | Storage volumes to add |
db_instance_ids | string[] | Database instances to add |
GET /projects/:id/canvas
Fetch the visual no-code canvas layout for a project — nodes and edges representing containers and their connections.
Response
{
"nodes": [{ "id": "dep-abc123", "type": "deployment", "x": 120, "y": 80 }],
"edges": [{ "from": "dep-abc123", "to": "dep-xyz789", "label": ":5432" }]
}
PUT /projects/:id/canvas
Save the canvas layout.
Storage Volumes
Persistent block storage that can be attached to workspace VMs.
GET /projects/:id/storage
List storage volumes in a project.
POST /projects/:id/storage
Create a storage volume.
Request body
| Field | Type | Description |
|---|---|---|
id | string | Optional — omit to create new |
size_gib | number | Size in GiB |
Response
{
"id": "vol-...",
"project_id": "proj-...",
"deployment_id": null,
"size_gib": 20,
"status": "available",
"created_at": "2025-03-28T02:00:00Z"
}
POST /projects/:id/storage/:volume_id/attach
Attach a volume to a deployment (workspace VM).
Request body
| Field | Type | Description |
|---|---|---|
deployment_id | string | Target deployment to attach to |
Response
{
"volume_id": "vol-...",
"deployment_id": "dep-...",
"mount_path": "/mnt/data"
}
POST /projects/:id/storage/:volume_id/detach
Detach a volume from its current deployment.
SSH Sessions
SSH sessions provide certificate-based access to workspace VMs via the bastion host.
POST /ssh/session
Create an SSH session for a workspace VM. Returns a short-lived signed certificate and connection details.
Request body
| Field | Type | Description |
|---|---|---|
deployment_id | string | Target workspace VM |
client_id | string | Optional client identifier |
principal | string | SSH principal (user) |
public_key | string | Your SSH public key to sign |
ttl_secs | number | Certificate TTL in seconds |
Response
{
"deployment_id": "dep-...",
"vm_id": "vm-...",
"bastion_host": "bastion.sun.run",
"bastion_port": 22,
"bastion_user": "sun",
"principal": "dev",
"client_id": "cli-...",
"ttl_secs": 3600,
"expires_at": "2025-03-28T03:00:00Z",
"ssh_cert": "ssh-rsa-cert-v01@openssh.com ...",
"ssh_public_key": "ssh-ed25519 ...",
"ssh_private_key": null,
"ssh_command": "ssh -i cert.pub -p 22 sun@bastion.sun.run"
}