Deploying Runtype Apps

Runtype Apps host static web apps at https://{slug}-{shortId}.runtype.run. The app is plain static files; everything dynamic (AI chat, flows, agents) goes through Runtype APIs using a client token that Runtype auto-provisions for the app’s origin. This guide covers the manifest format, bundle rules, the full deploy API, and the CLI and MCP deploy paths.

Runtype Apps is in early access and rolling out in stages. You can deploy with the REST API documented here, the Runtype CLI (runtype apps), the MCP tools, or the dashboard Apps page. The serving edge ships separately, so a deployed app may not serve traffic yet in every environment.

How deployment works

Deploys follow an upload-then-activate model:

  1. POST /v1/apps creates the app and assigns its permanent hostname.
  2. POST /v1/apps/:id/versions uploads a zip bundle as a new version. Uploading does not change what is served.
  3. POST /v1/apps/:id/activate flips the active-version pointer. Rollback is the same call with an older version id.

Because activation is a pointer flip, deploys and rollbacks take effect immediately on the API side.

Permissions

API keys need the apps scopes:

PermissionGrants
APPS:READList apps, read an app, list versions
APPS:WRITECreate, update, and delete apps; upload and activate versions. Implies read
APPS:*Full access to apps

See Authentication for the complete permissions model.

The manifest: runtype.app.json

Every bundle must include a runtype.app.json file at its root. It is validated at upload, and unknown keys are rejected (a typo fails loudly instead of being silently ignored).

runtype.app.json
1{
2 "name": "Retro Board",
3 "description": "A team retro board with an AI summarizer",
4 "capabilities": {
5 "flows": ["flow_01h2x..."],
6 "agents": ["agent_01h2x..."]
7 },
8 "data": [
9 { "namespace": "retro_card", "access": "read-write" },
10 { "namespace": "retro_summary", "access": "read" }
11 ],
12 "auth": "none"
13}
FieldRequiredDescription
nameYesDisplay name, up to 255 characters
descriptionNoUp to 2,000 characters
capabilities.flowsNoFlow ids the app’s frontend can dispatch, up to 20. Each must exist and belong to you
capabilities.agentsNoAgent ids the app’s frontend can execute, up to 20. Each must exist and belong to you
dataNoUp to 20 record namespace grants. Each entry is an object with namespace and access (read or read-write). Namespaces are lowercase slugs (letters, digits, underscores; starting with a letter; up to 64 characters)
authNoMust be "none" (the default). "optional" and "required" are reserved for Log in with Runtype; uploads using them are rejected with 422

On activation, the app’s auto-provisioned client token is synced to capabilities.flows and capabilities.agents, so those become the only flows and agents the deployed frontend can call. The data grants declare the record namespaces the app intends to use; the app data API that consumes them is coming in a follow-up.

Bundle rules

A version is a zip archive of your built app:

  • index.html and runtype.app.json must be at the bundle root. A single top-level wrapper directory (the zip -r bundle.zip dist/ shape) is detected and stripped automatically, so dist/index.html also satisfies the rule.
  • At most 500 files per version.
  • Bundle size is capped by plan (see Plan limits); 50 MB is a hard ceiling on every plan.
  • Dotfiles (any path segment starting with ., including .well-known) are never stored or served.
  • Content types are inferred from file extensions at upload; unknown extensions are served as application/octet-stream.
  • Entries with path traversal, absolute paths, or control characters are rejected.

Slug and hostname rules

The hostname is {slug}-{shortId}, where the short id is a random suffix Runtype generates. Slugs must be:

  • Lowercase letters, digits, and hyphens
  • Starting with a letter (digit-prefixed hostnames are reserved for sandbox previews)
  • No leading or trailing hyphen
  • Up to 40 characters

The hostname is permanent for the life of the app. Custom domains are not available in early access.

Step 1: Create the app

TypeScript
1const res = await fetch('https://api.runtype.com/v1/apps', {
2 method: 'POST',
3 headers: {
4 Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
5 'Content-Type': 'application/json',
6 },
7 body: JSON.stringify({
8 slug: 'retro-board',
9 name: 'Retro Board',
10 visibility: 'unlisted',
11 }),
12})
13const app = await res.json()
14console.log(app.url) // https://retro-board-x7k2p9.runtype.run
cURL
$curl -X POST https://api.runtype.com/v1/apps \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "slug": "retro-board", "name": "Retro Board", "visibility": "unlisted" }'

The response includes id, hostname, url, visibility, status, and activeVersionId (null until the first activation). visibility defaults to unlisted; creating a public app requires a plan with public visibility and returns 402 otherwise.

Step 2: Upload a version

Send the zip as the raw request body with Content-Type: application/zip:

TypeScript
1import { readFile } from 'node:fs/promises'
2
3const zip = await readFile('bundle.zip')
4const res = await fetch(`https://api.runtype.com/v1/apps/${appId}/versions`, {
5 method: 'POST',
6 headers: {
7 Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
8 'Content-Type': 'application/zip',
9 },
10 body: zip,
11})
12const version = await res.json()
13console.log(version.versionNumber, version.status) // e.g. 3 "uploaded"
cURL
$curl -X POST https://api.runtype.com/v1/apps/APP_ID/versions \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/zip" \
> --data-binary @bundle.zip

The response is the new version: id, versionNumber, bundleHash (SHA-256 of the zip), the parsed manifest, sizeBytes, fileCount, and status: "uploaded".

StatusMeaning
400Invalid bundle (missing index.html or manifest, over the size or file cap, bad zip) or the manifest references flows/agents you do not own
402Apps are not enabled on your plan
422The manifest uses a reserved feature (auth other than "none")
503App storage is not configured in this environment

JSON upload alternative

If sending a binary body is awkward (for example from an agent or a code sandbox), the same endpoint also accepts Content-Type: application/json with file maps instead of a zip. files maps bundle-relative paths to text content; the optional filesBase64 maps paths to base64-encoded binary content (images, fonts). The API zips the file maps server-side and applies the same bundle validation as a raw zip upload:

cURL
$curl -X POST https://api.runtype.com/v1/apps/APP_ID/versions \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "files": {
> "index.html": "<!doctype html>...",
> "runtype.app.json": "{ \"name\": \"Retro Board\" }"
> },
> "filesBase64": {
> "assets/logo.png": "iVBORw0KGgo..."
> }
> }'

Step 3: Activate (deploy or rollback)

TypeScript
1const res = await fetch(`https://api.runtype.com/v1/apps/${appId}/activate`, {
2 method: 'POST',
3 headers: {
4 Authorization: `Bearer ${process.env.RUNTYPE_API_KEY}`,
5 'Content-Type': 'application/json',
6 },
7 body: JSON.stringify({ versionId }),
8})
9const { app, version } = await res.json()
cURL
$curl -X POST https://api.runtype.com/v1/apps/APP_ID/activate \
> -H "Authorization: Bearer YOUR_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "versionId": "apv_01h2x..." }'

Activation flips activeVersionId, syncs the app’s client token to the active manifest’s capabilities, and rewrites edge routing. Exactly one version is active at a time; the previously active version becomes superseded and stays in history.

Rollback is the same endpoint: list versions with GET /v1/apps/:id/versions (newest first) and activate an older one.

Using the CLI

The Runtype CLI (@runtypelabs/cli) wraps the whole workflow in two commands:

$# Create the app
$runtype apps create --slug retro-board --name "Retro Board"
$
$# Zip, upload, and activate a built bundle in one command
$runtype apps deploy ./dist --app APP_ID

runtype apps deploy zips the directory (it must contain index.html and runtype.app.json at its root), uploads it as a new version, and activates it. Pass --no-activate to stage the version without changing what is served.

Rollback is the same two steps as the API:

$runtype apps versions APP_ID
$runtype apps activate APP_ID VERSION_ID

runtype apps list and runtype apps delete APP_ID round out the management commands, and every command accepts --json for structured output.

Using the MCP tools

An agent connected to the Runtype MCP server can deploy end to end: call create_app with a slug and name, then deploy_app_version with the bundle as files (a map of bundle-relative paths to text content) and optional filesBase64 (paths to base64-encoded binary content). The tool zips the files, uploads them as a new version, activates it, and returns the live URL in one call. Pass activate: false to stage a version without changing what is served.

The full tool set also includes list_apps, get_app, update_app, activate_app_version (deploy or rollback), and delete_app.

Managing apps

EndpointPurpose
GET /v1/appsList apps, newest first
GET /v1/apps/:idRead one app, including its URL and active version
PATCH /v1/apps/:idUpdate name, description, visibility, or status. Suspending ("status": "suspended") makes the app’s URL return 410 Gone
DELETE /v1/apps/:idDelete the app, its versions, and its routing. Stored files are cleaned up in the background, and the auto-provisioned client token is deactivated
GET /v1/apps/:id/versionsVersion history

How the deployed app talks to Runtype

Your bundle never hard-codes credentials. At serve time, the edge injects a public boot config into HTML entrypoints:

1window.__RUNTYPE_APP__ = {
2 appId: 'app_01h2x...',
3 versionId: 'apv_01h2x...',
4 apiUrl: 'https://api.runtype.com',
5 clientToken: 'ct_live_...',
6}

The client token is origin-locked to the app’s own hostname and scoped to the manifest’s flows and agents, so the frontend uses it with the client runtime endpoints (POST /v1/client/init, POST /v1/client/chat) the same way any client-token integration does. Rotating the token never requires a redeploy. See Client tokens and domain restrictions.

Plan limits

PlanAppsBundle size limitPublic visibility
Build110 MBNo (unlisted only)
Startup525 MBYes
Growth2050 MBYes
Team5050 MBYes
EnterpriseUnlimited50 MBYes

Requests over a limit return 402 with an actionable message. The 500-file cap and the 50 MB hard ceiling apply on every plan.

Next steps