This is the full developer documentation for Comwit Cloud # Comwit Cloud > Databases, runtime apps, and domains — one platform, three ways to drive it. Ship the whole stack from one place: libSQL databases, isolate-based runtime apps, and the domains and DNS that tie them together — through the web console, the `comwit` CLI, or the platform API. Pick an area below, or jump straight into the quickstart. ## Explore the platform [Section titled “Explore the platform”](#explore-the-platform) Databases Create libSQL databases, connect your app, run SQL, and rotate query tokens. [Database overview →](/databases/overview/) Runtime apps Deploy Next/V8 isolate apps, set environment variables, and attach hostnames. [Apps overview →](/apps/overview/) Domains & DNS Bring a domain you own, manage DNS records, and connect it to an app. [Domains overview →](/domains/overview/) CI/CD Automate deploys from your pipeline with the GitHub Actions workflow. [Deploy from GitHub →](/ci-cd/github-actions/) Platform API A product-shaped HTTP API behind everything — same contract as the console. [API overview →](/api/overview/) CLI Drive databases, apps, and domains from your terminal with `comwit`. [CLI reference →](/reference/cli/) ## Start in 5 minutes [Section titled “Start in 5 minutes”](#start-in-5-minutes) [Quickstart ](/get-started/quickstart/)Create a database, deploy an app, and see it live — end to end. [Install the CLI ](/get-started/install-cli/)Install comwit, sign in, and run your first command. [Core concepts ](/get-started/concepts/)Projects, databases, apps, and domains — how the pieces fit. ## Migrating an existing app? [Section titled “Migrating an existing app?”](#migrating-an-existing-app) Move a **Next.js** or **Cloudflare Workers + D1** app onto Comwit Cloud — these guides are written as runbooks a coding agent can execute end to end. [Migration overview ](/migrate/overview/)What changes, and the four moves every migration makes. [From Next.js (15 & 16) ](/migrate/from-nextjs/)Add @brrrd/adapter, build, and deploy. [From Cloudflare Workers + D1 ](/migrate/from-cloudflare/)Swap the runtime + the D1 → libSQL database move. ## Built for agents [Section titled “Built for agents”](#built-for-agents) This site is meant to be read by coding agents as much as by people. Point yours at the machine-readable mirror: * [`/llms.txt`](/llms.txt) — index of the documentation * [`/llms-full.txt`](/llms-full.txt) — the entire site as a single file, ideal to ingest in one shot # Authentication & scopes > How the Comwit Cloud API authenticates requests with bearer tokens, what scopes do, and how to get a cwt_ personal access token. Every request to the Comwit Cloud API at `https://api.cloud.comwit.io` is authenticated with a **bearer token** sent in the `Authorization` header: ```http Authorization: Bearer ``` If you have never used the API before, the short version is: you get a personal access token (it starts with `cwt_`), and you send it on every call. This page explains the two kinds of tokens, what scopes are, and how to obtain your token with the `comwit` CLI or the console. ## Token classes [Section titled “Token classes”](#token-classes) There are two classes of token. As a user, you only ever use the first one. ### `cwt_` — user personal access token (what you use) [Section titled “cwt\_\ — user personal access token (what you use)”](#cwt_hex--user-personal-access-token-what-you-use) A **personal access token (PAT)** is tied to your user account. It starts with the prefix `cwt_` followed by a hex string. A `cwt_` token is **scoped**: it can only perform the actions its scopes allow, and it can only touch **projects you are a member of**. On every request, the API verifies the token and enforces both scope and project membership. If the token lacks the required scope, or you are not a member of the target project, the request is rejected. ### `PLATFORM_API_SERVER_TOKEN` — internal operator/web token (not for you) [Section titled “PLATFORM\_API\_SERVER\_TOKEN — internal operator/web token (not for you)”](#platform_api_server_token--internal-operatorweb-token-not-for-you) `PLATFORM_API_SERVER_TOKEN` is a shared, global-access token used internally by the Comwit Cloud console (`apps/web`) and by operators. It **bypasses** scope and membership checks, so it is powerful and is **not** issued to CLI or API end users. Caution Never use `PLATFORM_API_SERVER_TOKEN` as a user token. User tokens are the scoped `cwt_` PATs described above. The server token is internal-only. When the console makes a change on a user’s behalf using this internal token, it adds an `X-Comwit-Actor-User-Id` header so that audit logs and console writes are attributed to the real user. Direct `cwt_` callers never send that header — the API uses the user id from the introspected token itself. ## Scopes [Section titled “Scopes”](#scopes) A `cwt_` token carries one or more scopes. Each scope grants a specific class of action: | Scope | Grants | | ----------------- | --------------------------------------------- | | `project:read` | List projects you can access | | `database:read` | List/inspect project databases | | `database:write` | Create databases | | `app:read` | List/inspect apps, builds, env, domains | | `app:write` | Create/delete apps, set env, attach hostnames | | `app:deploy` | Deploy builds, roll back | | `domain:read` | List/inspect project domains and DNS records | | `domain:write` | Onboard domains, manage DNS records | | `domain:purchase` | Registrar purchase | Planned The `domain:purchase` scope (registrar purchase) is planned and not yet live. Do not depend on it for current workflows. Tip Grant only the scopes a token actually needs. A read-only automation, for example, is safer with just `project:read` and `app:read` than with write or deploy scopes. ### Operator-only routes [Section titled “Operator-only routes”](#operator-only-routes) Some routes are reserved for operators and the console. A `cwt_` user token is rejected with `403` on: * `/v1/status` * the non-project `/v1/databases*` family * hidden legacy aliases These routes require the internal operator token, not a user PAT. ## Getting a `cwt_` token [Section titled “Getting a cwt\_ token”](#getting-a-cwt_-token) You can obtain a `cwt_` token two ways: through CLI device login (recommended) or by creating a personal access token in the console. ### Option A — CLI device login (recommended) [Section titled “Option A — CLI device login (recommended)”](#option-a--cli-device-login-recommended) If you have the `comwit` CLI installed, run: ```sh comwit login ``` This starts the **device flow**: 1. The CLI calls `POST /v1/auth/device` and receives a `user_code` and a `verification_uri`. 2. The CLI opens your browser to that URL. While signed in to the console, you approve the displayed code. 3. The CLI polls `POST /v1/auth/device/token` until it receives your `cwt_` token. The token is saved to `~/.config/comwit/config.json`. To pin a default project at the same time: ```sh comwit login --project ``` Note New to the CLI? See [Install the CLI](/get-started/install-cli/) to get the `comwit` binary first. ### Option B — dashboard personal access token [Section titled “Option B — dashboard personal access token”](#option-b--dashboard-personal-access-token) You can also create a scoped `cwt_` token directly in the console at `/tokens`. Once you have it, hand it to the CLI: ```sh comwit login --token cwt_xxx --project ``` ## CLI config and resolution order [Section titled “CLI config and resolution order”](#cli-config-and-resolution-order) The CLI resolves your token, project, and API host in a fixed order. Knowing this order makes it easy to override settings per command or per environment. * **Token / config file** — read from `~/.config/comwit/config.json`, which holds `{ "token", "default_project" }`. Override the path with the `COMWIT_CONFIG` environment variable; otherwise it falls back to `$XDG_CONFIG_HOME/comwit/config.json`. * **Project** — the `--project` flag wins, then the `COMWIT_PROJECT` environment variable, then `default_project` from the config file. * **API host** — defaults to `https://api.cloud.comwit.io`. ## Using a token with raw HTTP [Section titled “Using a token with raw HTTP”](#using-a-token-with-raw-http) The CLI is convenient, but you can call the API directly with any HTTP client. Send your `cwt_` token in the `Authorization` header: List your projects ```sh curl -H "Authorization: Bearer cwt_xxx" \ https://api.cloud.comwit.io/v1/projects ``` ## What rejection looks like [Section titled “What rejection looks like”](#what-rejection-looks-like) A request that asks for an action outside your token’s scopes, or that targets a project you are not a member of, is rejected with HTTP `403`. The operator-only routes listed above also return `403` for `cwt_` tokens. For the full error shape and how to retry safely, see [Errors & idempotency](/api/errors-and-idempotency/). ## Next steps [Section titled “Next steps”](#next-steps) * [API overview](/api/overview/) — base URL, versioning, and request basics. * [Install the CLI](/get-started/install-cli/) — get the `comwit` binary. * [Errors & idempotency](/api/errors-and-idempotency/) — status codes and retries. # Errors, idempotency & limits > How the Comwit Cloud API reports errors, which operations are safe to retry, and what pagination and idempotency support is planned. When you automate against the Comwit Cloud API, two questions come up fast: “what does the API tell me when something goes wrong?” and “is it safe to run this request again?” This page answers both. It covers the error model (HTTP status codes plus problem-detail bodies), which operations you can safely retry today, and what idempotency and pagination support is still on the roadmap. If you have not authenticated yet, start with [Authentication](/api/authentication/). For the big picture of the API, see the [API overview](/api/overview/). ## The error model [Section titled “The error model”](#the-error-model) Every error is an HTTP status code paired with a generated **problem-detail** body. The status code tells you the *class* of the problem; the body gives you details. You almost always branch your automation on the status code. ### Status codes [Section titled “Status codes”](#status-codes) | Status | Meaning | Typical cause | | ------ | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | `400` | Caller mistake — bad input | You sent something the API can’t accept, for example an environment variable with `secret: true` where that flag isn’t allowed | | `403` | Forbidden | Your token is out of scope, you’re not a member of the project, or you hit an operator-only route with a `cwt_` user token | | `404` | Not found | The project, database, app, domain, or record you referenced doesn’t exist (or isn’t visible to you) | | `409` | Conflict | The request collides with existing state, for example a conflicting DNS record | | `501` | Not implemented / feature disabled | A capability the route depends on is turned off on this deployment — for example project domain management, which is disabled by default | | `502` | Upstream control-plane failure | An internal dependency (Louhi or brrrd) failed, or a required control-plane URL is unset, while the API was orchestrating your request | Note A `400` you can fix by changing the request; a `501` means the capability is turned off on this deployment, not something your input controls. A `502` means an upstream control-plane dependency failed and may succeed if you retry once it recovers. (Separately, the unauthenticated `GET /readyz` readiness probe returns `503` when Louhi or brrrd is unreachable — normal API routes don’t return `503`.) ### What error bodies never contain [Section titled “What error bodies never contain”](#what-error-bodies-never-contain) Error bodies are deliberately sanitized. Upstream detail from Louhi and brrrd is logged server-side, not echoed back to you. The following never appear in a public error body: * secret values (such as the contents of secret environment variables), * operator or server tokens, * internal tenant headers, * CloudFront tenant references. So if an upstream system fails, you’ll get a clean `502` rather than a stack trace full of internal identifiers. When you need to debug a `502`, the server-side logs hold the upstream detail — your error body intentionally doesn’t. ## Idempotency: which requests are safe to retry [Section titled “Idempotency: which requests are safe to retry”](#idempotency-which-requests-are-safe-to-retry) The API is **synchronous wherever the upstream control plane is synchronous** — when a call returns, the work is done (or it failed). There’s no generic “submit and poll” model yet (see [Planned](#planned-idempotency-and-pagination) below), so the practical question is: which specific operations can I safely run twice? Today’s retry-safe spots: * **Create a project domain** — if the domain already exists, the create call returns the existing domain instead of failing. * **Create a hosted zone** — uses a deterministic caller reference, so repeating the request doesn’t create a duplicate zone. * **Delete a DNS record** — “already gone” is treated as success, so a repeated delete won’t error out. * **Attach / finalize an app domain** and **delete an app** — these are re-entrant, so running them again converges on the same end state. Tip Build your automation so it can re-run the operations above without special handling — that’s the safest way to recover from a dropped connection or a timeout where you’re unsure whether the first attempt landed. Caution Outside the operations listed above, do not assume a request is automatically idempotent. A generic, opt-in idempotency mechanism is planned but not yet live (see below). Until then, treat any other write as “run once” and check the resource’s current state before retrying. ## Planned: idempotency and pagination [Section titled “Planned: idempotency and pagination”](#planned-idempotency-and-pagination) Planned — not yet live The following are on the roadmap and **not available today**. Don’t build against them yet. * **Generic `Idempotency-Key` header + operation resources** for long-running workflows. This will let you safely retry any write by reusing a key, and track async work through operation resources. * **Pagination** via `page_size` and `page_token` query parameters. Until this ships, **list endpoints return all results in a single response** — they are currently unpaginated. Because lists are unpaginated today, a single list call returns the full set. If you’re scripting against very large accounts, keep an eye out for the pagination parameters above once they go live. ## Operator routes vs. user routes [Section titled “Operator routes vs. user routes”](#operator-routes-vs-user-routes) One common source of `403` errors is hitting a route your token isn’t meant to reach. The API has two tiers: * **User routes** — `cwt_` tokens reach project-scoped routes according to their scopes and your project membership. This is what your CLI and automation use. * **Operator-only routes** — these require the platform’s operator/server token and **reject `cwt_` user tokens with a `403`**. They include the platform status route and the non-project database family, plus some hidden legacy aliases that aren’t part of the public spec. Note If a request that looks valid returns `403`, first confirm you’re calling a user route, not an operator-only one — and that your `cwt_` token has the right scope and project membership. See [Authentication](/api/authentication/) for token classes and scopes. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [API overview](/api/overview/) — base URL, the live OpenAPI spec, and how requests and responses are shaped. * [Authentication](/api/authentication/) — `cwt_` token classes, scopes, and membership. * [Limits](/reference/limits/) — platform limits to design your automation around. # Platform API overview > How the Comwit Cloud platform API is structured — base URL, discovery, health checks, response envelopes, and request bodies. Comwit Cloud has one public HTTP API. The `comwit` CLI and the web console at both call it, and you can call it yourself for automation, CI, or custom tooling. The routes are **product-shaped**: you work with stable `project`, `database`, `app`, and `domain` resources, and the API hides the underlying database (Louhi) and runtime (brrrd) internals from you. This page explains the shape of the API so the rest of the reference makes sense. ## Base URL [Section titled “Base URL”](#base-url) Everything lives under one host: ```text https://api.cloud.comwit.io ``` Project-scoped routes are versioned under `/v1`, for example `/v1/projects`. ## Discovery: the spec is the source of truth [Section titled “Discovery: the spec is the source of truth”](#discovery-the-spec-is-the-source-of-truth) The API ships a generated OpenAPI specification, and that spec — not any prose doc — is the authoritative description of every route, field, and status code. | Endpoint | What it is | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `GET /openapi.json` | The generated OpenAPI spec. It is produced from the typed route contracts, so it always matches the running service. There is no hand-written spec. | | `GET /docs` | An interactive API reference (Scalar) rendered from that same spec — browse routes, schemas, and try requests. | Treat these as ground truth When this site and the spec ever disagree about an exact schema, status code, or field name, believe `/openapi.json` and `/docs`. They are generated directly from the service. The docs here explain *intent* and *behavior* that a schema alone cannot. You can fetch the spec with no auth: Fetch the OpenAPI spec ```sh curl https://api.cloud.comwit.io/openapi.json ``` Then open in a browser for the interactive reference. ## Health checks [Section titled “Health checks”](#health-checks) Two unauthenticated endpoints report service health: | Endpoint | Meaning | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `GET /healthz` | Liveness — is the API process up and serving? | | `GET /readyz` | Readiness — is the API ready to do real work? This also checks that the control-plane dependencies (Louhi and brrrd) are reachable. | Check that the API is ready ```sh curl https://api.cloud.comwit.io/readyz ``` Use `/healthz` for simple “is it alive” probes and `/readyz` when you need to know the platform can actually serve database and runtime requests. ## Authentication [Section titled “Authentication”](#authentication) Most routes require a bearer token. Send it in the `Authorization` header: An authenticated request ```sh curl -H "Authorization: Bearer cwt_xxx" \ https://api.cloud.comwit.io/v1/projects ``` User tokens always start with `cwt_`. See [Authentication](/api/authentication/) for how to get a token and what scopes it carries. ## Response envelopes [Section titled “Response envelopes”](#response-envelopes) Responses wrap their payload in a named envelope rather than returning a raw array or an implementation root object. This keeps responses stable and easy to read. **A single resource** is returned under its singular name: GET a single app ```json { "app": { "...": "..." } } ``` The single-resource keys you’ll see are `database`, `app`, `domain`, and `record`. Some create/mutation responses are flat The envelope rule covers single-resource reads and most responses. A few create/mutation calls instead return a **flat** object plus a status flag rather than a `{ "singular": { … } }` wrapper — for example creating a database returns `database_id`, `database_url`, `created`, and a one-time `database_token` at the top level, and a deploy returns `app_id`, `build_id`, `hosts`, and `uploaded`. **A list** is returned under its plural name: List apps in a project ```json { "apps": [ { "...": "..." } ] } ``` The list keys are `apps`, `builds`, `domains`, `records`, `databases`, and `projects`. **One-time tokens** keep explicit, descriptive names so they’re never confused with a resource — for example `database_token` and `query_token`. These are returned once when you create them; store them right away. ### Public ids are product ids [Section titled “Public ids are product ids”](#public-ids-are-product-ids) The ids you see in responses are product ids — for example an app is identified by `app_id`, not by an internal `service_id`. Product behavior keys off the project id, the resource id, the resource name, and status. Lower-level infrastructure identifiers (such as DNS provider zone or change ids, or registrar operation ids) may show up in operator or debug fields, but you don’t need them for normal use. ## Request bodies [Section titled “Request bodies”](#request-bodies) Almost every request body is JSON, and you should send `Content-Type: application/json`. The one exception is a **deploy upload**. When you deploy an app, the request body is the raw build artifact (`application/octet-stream`, a `.tar.zst` archive), and the deploy options — hosts, environment reference, concurrency — are passed as query parameters instead of in the body. See [Deploy an app](/apps/deploy/) for the full flow. ## Errors and idempotency [Section titled “Errors and idempotency”](#errors-and-idempotency) Errors are returned as standard HTTP status codes with a generated problem-detail JSON body. Upstream detail is logged server-side and stripped out of public responses, so error bodies never leak secret values or internal tokens. Idempotency behavior and the full status-code table are covered on their own page — see [Errors and idempotency](/api/errors-and-idempotency/). ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Authentication](/api/authentication/) — get a `cwt_` token and understand scopes. * [Errors and idempotency](/api/errors-and-idempotency/) — status codes and retry-safety. * [CLI reference](/reference/cli/) — the `comwit` CLI that wraps this API. # Custom hostnames > Attach your own hostname to a Comwit Cloud app, using either manual DNS records or fully automatic DNS for a managed project domain. By default a Comwit Cloud app is reachable at the hostname brrrd assigns it. A **custom hostname** lets you serve that same app from a name you control — for example `app.example.com` or your apex `example.com` — backed by an automatically provisioned CloudFront certificate. Attaching a hostname is a two-step flow: 1. **Attach** the hostname to the app. This always calls brrrd-control first, so brrrd owns the CloudFront certificate and the host-binding lifecycle. The response tells you which DNS records need to exist. 2. **Finalize** the hostname once those DNS records are in place, which completes activation. How you get the DNS records in place depends on which of the two **DNS modes** applies to your hostname. Note This page covers the app-side mechanics of attaching a hostname. For an end-to-end walkthrough of pointing a domain at an app, see [Connect a domain to an app](/domains/connect-domain-to-app/). ## The hostname routes [Section titled “The hostname routes”](#the-hostname-routes) A hostname (custom domain) attached to an app lives under that app’s `domains` collection: ```txt GET /v1/projects/{projectId}/apps/{appId}/domains POST /v1/projects/{projectId}/apps/{appId}/domains POST /v1/projects/{projectId}/apps/{appId}/domains/{domain}/finalize ``` * `GET .../domains` lists the hostnames currently attached to the app. * `POST .../domains` attaches a new hostname (the **attach** step). * `POST .../domains/{domain}/finalize` completes activation (the **finalize** step). Whichever DNS mode you end up in, the attach call is always made first and always goes through brrrd-control before any DNS work happens. ## DNS mode 1: Manual DNS (default) [Section titled “DNS mode 1: Manual DNS (default)”](#dns-mode-1-manual-dns-default) This is the default mode, `dns_mode: "external_records"`. Use it when the hostname’s DNS is hosted somewhere Comwit Cloud does not manage — your registrar, another DNS provider, or a zone you administer yourself. When you attach the hostname, platform-api returns the DNS records you must publish at your own DNS provider: POST .../apps/{appId}/domains response (manual DNS) ```json { "domain": "app.example.com", "dns_mode": "external_records", "dns_records": [ { "record_type": "CNAME", "name": "...", "value": "..." } ] } ``` Then: 1. Publish every record in `dns_records` at your DNS provider exactly as returned. 2. Call the **finalize** route to complete activation: ```txt POST /v1/projects/{projectId}/apps/{appId}/domains/{domain}/finalize ``` Tip The records returned in `dns_records` are what prove ownership and route traffic to the CloudFront-backed app. Activation is not complete until those records exist **and** you have called `finalize`. ## DNS mode 2: Automatic DNS (managed project domain) [Section titled “DNS mode 2: Automatic DNS (managed project domain)”](#dns-mode-2-automatic-dns-managed-project-domain) This mode, `dns_mode: "managed_project_domain"`, is available when the hostname is the **apex or a subdomain of a managed project domain**. (A managed project domain is one whose DNS Comwit Cloud hosts for you in Route 53 — see [Connect a domain to an app](/domains/connect-domain-to-app/).) In this mode you do not publish records yourself. When you attach the hostname, platform-api creates the required records in that domain’s Route 53 zone for you — including any CloudFront `_cf-challenge` TXT records — and returns: POST .../apps/{appId}/domains response (automatic DNS) ```json { "domain": "app.example.com", "dns_mode": "managed_project_domain", "managed_dns": { "project_domain": "example.com", "status": "applied", "records": [] } } ``` These automatically created records are owned by the platform as `platform_app` (with `owner_resource_type=runtime_app` and `owner_resource_id={appId}`), so your own manual record edits in the same zone will not overwrite them. You still complete activation through the **finalize** route once the records are applied. ### Retrying when record application fails [Section titled “Retrying when record application fails”](#retrying-when-record-application-fails) It is possible for the brrrd attach to succeed but the Route 53 record application to fail. When that happens you get a `202` response with `managed_dns.status: "failed"` and a retry message: 202 — managed DNS application failed ```json { "domain": "app.example.com", "dns_mode": "managed_project_domain", "managed_dns": { "project_domain": "example.com", "status": "failed" } } ``` To recover, **repeat the exact same attach request**. The attach is idempotent, so re-sending it retries the Route 53 record application without creating a duplicate or disturbing the existing brrrd binding. Once `managed_dns.status` comes back as `"applied"`, proceed to the finalize route as usual. Note Even in automatic DNS mode, final activation always goes through the finalize route. Attaching (and any retries) gets the records in place; finalize turns the hostname on. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Connect a domain to an app](/domains/connect-domain-to-app/) — the full, newcomer-friendly walkthrough of attaching a domain to a running app, including how project domains become “managed”. # Deploy an app > Create a runtime app in a project, upload a build, and roll back to a previous build with the comwit CLI or the platform API. A **runtime app** is a deployable Next/V8 isolate application that runs on Comwit Cloud. You create an app inside a project, upload a build (a packaged copy of your compiled app), and Comwit makes that build live. If a new build misbehaves, you can roll back to a previous one. This page walks through the whole loop end to end. Note Everything here runs through the `comwit` CLI or the public platform API at `https://api.cloud.comwit.io`. You need a logged-in CLI (see [Install the CLI](/get-started/install-cli/)) and a project to work in. If you are brand new, start with the [Quickstart](/get-started/quickstart/). ## Create an app [Section titled “Create an app”](#create-an-app) An app lives inside a project. Creating one only registers the app — it does not deploy anything yet. Give it a `name` (for example, `web`): Create an app ```sh comwit apps create --project --name web ``` The equivalent API call posts a small JSON body: API ```txt POST /v1/projects/{projectId}/apps { "name": "web" } ``` ## List and inspect apps [Section titled “List and inspect apps”](#list-and-inspect-apps) List the apps in a project, or fetch a single one to see its current state: List apps ```sh comwit apps list --project ``` API ```txt GET /v1/projects/{projectId}/apps { "apps": [...] } GET /v1/projects/{projectId}/apps/{appId} DELETE /v1/projects/{projectId}/apps/{appId} ``` An app resource carries these fields: * `app_id` — the app’s identifier (you pass this as `--app `). * `name` — the human-readable name you chose. * `active_build_id` — the build that is currently live (empty until your first deploy). * `builds` — the list of builds you have uploaded. Note A project with no apps yet returns `200` with `{ "apps": [] }` — an empty list, not an error. Detail and mutation routes (the single-app `GET`/`DELETE`) still return `404` for an app that does not exist. ## Deploy a build [Section titled “Deploy a build”](#deploy-a-build) Deploying uploads a build artifact and activates it. The simplest form points the CLI at your build output directory: Deploy a directory ```sh comwit deploy --project --app --package ./dist ``` The `--package` value can be either: * **A built directory** — the CLI packs it locally with `tar --zstd` into a temporary `.tar.zst` archive before uploading. * **A prebuilt `.tar.zst` file** — upload it as-is. Tip If your local `tar` does not support zstd compression, pack the directory yourself and pass the prebuilt `.tar.zst` file to `--package`. ### Optional deploy flags [Section titled “Optional deploy flags”](#optional-deploy-flags) You can configure hostnames, the runtime environment, and concurrency at deploy time: Deploy with options ```sh comwit deploy \ --project \ --app \ --package ./dist \ --host a.example.com,b.example.com \ --env-ref brrrd/env/app \ --max-concurrent-requests 100 ``` * `--host a.example.com,b.example.com` — bind one or more hostnames on this deploy (comma-separated). * `--env-ref ` — a runtime environment reference. * `--max-concurrent-requests ` — the runtime concurrency cap. ### The deploy API [Section titled “The deploy API”](#the-deploy-api) The CLI calls the deployments route under the hood: API ```txt POST /v1/projects/{projectId}/apps/{appId}/deployments ``` This route does not take a JSON body Almost every platform API route uses a JSON body, but the deployment route is the exception. The **request body is the raw `.tar.zst` artifact** sent as `Content-Type: application/octet-stream`. The hosts, env-ref, and concurrency options are passed as **query parameters**, not in the body. A successful deploy returns: Response ```json { "app_id": "app-xxx", "build_id": "build-yyy", "hosts": [], "uploaded": true } ``` The new `build_id` is the build you just uploaded; it becomes the app’s `active_build_id`. ## List builds and roll back [Section titled “List builds and roll back”](#list-builds-and-roll-back) Every deploy creates a build, and Comwit keeps the history so you can return to a known-good one. List the builds for an app: List builds ```sh comwit apps builds --project --app ``` API ```txt GET /v1/projects/{projectId}/apps/{appId}/builds POST /v1/projects/{projectId}/apps/{appId}/rollbacks ``` A **rollback re-activates a prior build without re-uploading an artifact** — it flips the live build pointer back to a build you already deployed, so it is fast and does not require you to repackage anything. ## Next steps [Section titled “Next steps”](#next-steps) * Set runtime configuration with [environment variables](/apps/environment/). * Attach your own hostnames in [Custom hostnames](/apps/custom-hostnames/). * Wire deploys into your pipeline with [CI/CD](/ci-cd/overview/). # Environment variables > Set, read, and remove plain configuration values for a Comwit Cloud runtime app. Environment variables are key/value settings that your runtime app can read at runtime — things like a feature flag, a log level, or a public base URL. In Comwit Cloud you manage them per app, and they are applied to the builds that run for that app. Plain values only — not for real secrets App environment values are **plain text only**. There is no secret backend enabled for app env today, so do not store passwords, API keys, signing keys, or other sensitive credentials here. See [Keep real secrets out](#keep-real-secrets-out) below before you put anything sensitive in an environment variable. ## List environment variables [Section titled “List environment variables”](#list-environment-variables) Read the current environment for an app. API ```txt GET /v1/projects/{projectId}/apps/{appId}/environment ``` You’ll need your project id (`projectId`) and the app id (`appId`) — see [Deploy an app](/apps/deploy/) and [Apps overview](/apps/overview/) for where those come from. ## Set or update a variable [Section titled “Set or update a variable”](#set-or-update-a-variable) Setting a variable uses the variable’s key as the last path segment, with the value in the request body. API ```txt PUT /v1/projects/{projectId}/apps/{appId}/environment/{key} ``` Request body ```json { "value": "info", "secret": false } ``` The body has two fields: * `value` — the string value to store for this key. * `secret` — must be `false`. Sending `true` is rejected (see below). PUT both creates a new key and updates an existing one — there is no separate “create” versus “update” call. ## Remove a variable [Section titled “Remove a variable”](#remove-a-variable) API ```txt DELETE /v1/projects/{projectId}/apps/{appId}/environment/{key} ``` This removes a single key from the app’s environment. ## Why `secret: true` is rejected [Section titled “Why secret: true is rejected”](#why-secret-true-is-rejected) The `secret` field exists in the request shape, but the **live policy does not use a secret store for app environment values**. If you send `secret: true`, the API responds with: Response (400 problem-detail) ```json { "title": "Bad Request", "status": 400, "detail": "secret app env values are not enabled; store plain env values only" } ``` In other words, every app environment value is stored and served as plain configuration. To set a value successfully, send `"secret": false`. ## Keep real secrets out [Section titled “Keep real secrets out”](#keep-real-secrets-out) Because there is no secret backend for app env, treat everything you put here as **plain, readable configuration**: * Safe to store: log levels, feature flags, public URLs, non-sensitive build settings. * Do **not** store: passwords, API keys, signing secrets, or anything you would not want exposed in plain form. Database connection strings A database connection URL or token is sensitive. Only place a database connection URL or token in app environment with this plain-storage caveat in mind — and ideally wait until a paid secret backend is intentionally enabled before doing so. See [Connect to a database](/databases/create-and-connect/) for how connection details are issued. ## Related [Section titled “Related”](#related) * [Deploy an app](/apps/deploy/) — push a build that picks up these variables. * [Apps overview](/apps/overview/) — how apps, builds, and projects fit together. * [Authentication](/api/authentication/) — the `cwt_` token you use to call these routes. # Runtime apps overview > What a Comwit Cloud runtime app is, how it maps to projects, and where to deploy, configure, and attach hostnames. A **runtime app** is a deployable Next/V8 isolate application that Comwit Cloud runs for you. You write a Next app, deploy a build, and Comwit Cloud serves it on the internet — no servers to provision and no edge platform configuration on your side. Behind the scenes runtime apps are powered by **brrrd**, Comwit Cloud’s runtime/fleet plane. You never talk to brrrd directly, though: everything you do to a runtime app — create it, deploy builds, set environment variables, roll back, attach custom hostnames, delete it — goes through the product API at `https://api.cloud.comwit.io` (or the `comwit` CLI, which calls the same API). ## How a runtime app is identified [Section titled “How a runtime app is identified”](#how-a-runtime-app-is-identified) Runtime apps live inside a **project**, and every app has two stable identifiers you’ll use in CLI commands and API routes: * **`projectId`** — the project the app belongs to. * **`appId`** — the app itself, unique within its project. Note Internally, the public `projectId` maps to a brrrd tenant and the public `appId` maps to a brrrd service id. You don’t need to know or use those internal ids — the product API hides them, and you only ever pass `projectId` and `appId`. The product API is intentionally **product-shaped**, not a thin pass-through to brrrd. For example, listing apps for a brand-new project that has never created an app returns `200` with an empty list rather than an error: List the runtime apps in a project (CLI) ```sh comwit apps list --project ``` The equivalent API route ```txt GET /v1/projects/{projectId}/apps → { "apps": [] } ``` Detail and mutation routes for an app that doesn’t exist still return `404`. ## What an app looks like [Section titled “What an app looks like”](#what-an-app-looks-like) When you create an app you give it a name; the API assigns the `appId`: Create an app (CLI) ```sh comwit apps create --project --name web ``` Create an app (API) ```txt POST /v1/projects/{projectId}/apps { "name": "web" } ``` An app resource carries: * `app_id` — the app’s `appId`. * `name` — the human-friendly name you chose. * `active_build_id` — the build currently being served (if any). * `builds` — the list of builds you’ve deployed. A runtime app starts out empty. It only serves traffic once you deploy a build and that build becomes the active build. ## The lifecycle of a runtime app [Section titled “The lifecycle of a runtime app”](#the-lifecycle-of-a-runtime-app) Working with a runtime app follows a simple loop, and each step has its own detailed page: 1. **Deploy a build.** Package your built Next app and upload it; the new build becomes active. See [Deploy a runtime app](/apps/deploy/). Comwit Cloud keeps your build history, so you can also roll back to a previous build without re-uploading anything. 2. **Configure environment.** Set plain configuration values your app reads at runtime. See [Environment variables](/apps/environment/). 3. **Attach custom hostnames.** Serve the app on your own domain instead of (or in addition to) its default address. See [Custom hostnames](/apps/custom-hostnames/). Tip New to Comwit Cloud? Start with the [Quickstart](/get-started/quickstart/) to install the CLI, sign in, and deploy your first app end to end. The [concepts](/get-started/concepts/) page explains projects, apps, and databases together. ## Environment values are plain, not secret [Section titled “Environment values are plain, not secret”](#environment-values-are-plain-not-secret) App environment variables are **plain configuration only**. The live platform does not store secrets for app env — attempting to mark a value as secret is rejected with a `400` error. Keep real secrets out of app environment until a secret backend is intentionally enabled, and use this for non-sensitive configuration. Full details are on the [Environment variables](/apps/environment/) page. Caution A dedicated secret backend for app environment is **planned, not yet live**. Until it ships, do not put credentials, API keys, or other sensitive values in app environment variables. ## Custom hostnames in brief [Section titled “Custom hostnames in brief”](#custom-hostnames-in-brief) By default your app is reachable at the address Comwit Cloud assigns it. To serve it on a domain you control, you attach a hostname. Comwit Cloud supports two DNS modes: * **Manual DNS** — the API returns the DNS records to publish at your own DNS provider, then you finalize activation. * **Automatic DNS** — when the hostname is the apex or a subdomain of a **managed** project domain, Comwit Cloud creates the required DNS records for you in that domain’s zone. The [Custom hostnames](/apps/custom-hostnames/) page walks through both modes. If you also manage the domain itself in Comwit Cloud, see the [Domains overview](/domains/overview/) and [Connect a domain to an app](/domains/connect-domain-to-app/). ## Deleting an app [Section titled “Deleting an app”](#deleting-an-app) Deleting an app tears it down and reports what was cleaned up — the routing pointer, runtime resources, build artifacts, environment keys, and attached hostnames. If any DNS cleanup still needs to happen at your DNS provider, the response lists it for you. Delete an app (API) ```txt DELETE /v1/projects/{projectId}/apps/{appId} ``` ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Deploy a runtime app](/apps/deploy/) — package, upload, and activate a build, plus rollbacks. * [Environment variables](/apps/environment/) — set and remove plain config values. * [Custom hostnames](/apps/custom-hostnames/) — serve your app on your own domain. * [API authentication](/api/authentication/) — how the `comwit` CLI and API authenticate. # Deploy with GitHub Actions > Automatically deploy your Comwit Cloud app on every push with a complete GitHub Actions workflow. GitHub Actions is GitHub’s built-in automation system: it runs jobs in response to repository events, such as a push to your main branch. This page shows a complete, working setup that builds your app and deploys it to Comwit Cloud automatically every time you push. The deploy itself is done by the `comwit` CLI, the same command-line tool you would run on your own machine. The only difference in CI is that you authenticate with a token stored as a repository secret instead of logging in through a browser. ## What you’ll set up [Section titled “What you’ll set up”](#what-youll-set-up) The flow has three pieces: 1. A `cwt_` personal access token with the `app:deploy` scope, created in the console. 2. That token stored as a GitHub repository **secret**, plus your project and app ids stored as repository **variables**. 3. A workflow file (`.github/workflows/deploy.yml`) that builds the app, installs the CLI, authenticates, and runs `comwit deploy`. Note This page assumes you already have a runtime app. If you don’t, create one first with `comwit apps create --project --name web` (see [Deploy an app](/apps/deploy/)). A “runtime app” is a deployable Next/V8 isolate application powered by Comwit Cloud’s runtime. ## Step 1 — Create a deploy token [Section titled “Step 1 — Create a deploy token”](#step-1--create-a-deploy-token) In CI you can’t open a browser to log in, so you need a token that is created once and stored. Comwit Cloud uses `cwt_` **personal access tokens**: scoped credentials tied to your user that can only do what their scopes allow and only touch projects you’re a member of. 1. Open the console at and go to **API tokens** at `/tokens`. 2. Create a token and give it the **`app:deploy`** scope. That scope grants exactly what a deploy needs: | Scope | Grants | | ------------ | ------------------------ | | `app:deploy` | Deploy builds, roll back | 3. Copy the token value. The plaintext is shown **once** at creation — if you lose it, revoke it and create a new one. Tip Scope the token as narrowly as the job needs. `app:deploy` is enough to ship a build. If the same workflow also needs to create apps or set environment variables, you’d additionally need `app:write` — but for a pure deploy job, keep it to `app:deploy`. You can revoke any token from the same `/tokens` page. Caution Use a `cwt_` user token only. Never put `PLATFORM_API_SERVER_TOKEN` (the internal operator/web token) into a workflow — it bypasses scope and membership checks and is not a user credential. ## Step 2 — Store the token and ids in GitHub [Section titled “Step 2 — Store the token and ids in GitHub”](#step-2--store-the-token-and-ids-in-github) GitHub separates **secrets** (encrypted, never printed in logs) from **variables** (plain config values). Put the token in a secret and the ids in variables. In your repository, go to **Settings → Secrets and variables → Actions**, then: * Under **Secrets**, add `COMWIT_TOKEN` with your `cwt_...` value. * Under **Variables**, add `COMWIT_PROJECT` (your project id) and `COMWIT_APP` (your app id). These names map straight onto how the workflow references them: `${{ secrets.COMWIT_TOKEN }}`, `${{ vars.COMWIT_PROJECT }}`, and `${{ vars.COMWIT_APP }}`. Note The project id is also accepted via the `COMWIT_PROJECT` environment variable by the CLI itself, which is why the workflow below sets it once at the job level. Authentication, however, is always done with `comwit login --token` (or the saved config file) — there is no separate “API key” environment variable. ## Step 3 — Install the CLI in CI [Section titled “Step 3 — Install the CLI in CI”](#step-3--install-the-cli-in-ci) The workflow needs the `comwit` binary on the runner. The repository ships an install script, `scripts/install-comwit.sh`, that you can use locally; it works like this: * It first tries to download a GitHub **release asset** named like `comwit__.tar.gz`. * It supports a few environment overrides: install-comwit.sh environment overrides ```sh COMWIT_VERSION=v0.1.0 scripts/install-comwit.sh COMWIT_INSTALL_DIR=/usr/local/bin scripts/install-comwit.sh COMWIT_INSTALL_REPO=comwit/comwit-cloud scripts/install-comwit.sh ``` * `COMWIT_VERSION` pins the release tag, `COMWIT_INSTALL_DIR` chooses where the binary lands, and `COMWIT_INSTALL_REPO` chooses which GitHub repo to pull the release from. Caution Released binaries are not published yet. Until release assets exist, the installer **falls back to building the CLI from source** out of a checkout and installing it to `~/.local/bin/comwit`. The workflow below reflects that: it checks out the `comwit-cloud` repo and builds the CLI, which works today. Once `comwit__.tar.gz` release assets are published, you can replace the build step with a download of that asset (or run `scripts/install-comwit.sh` with `COMWIT_VERSION`/`COMWIT_INSTALL_REPO`) and drop the source checkout. ## Step 4 — Add the workflow file [Section titled “Step 4 — Add the workflow file”](#step-4--add-the-workflow-file) Create the file below at `.github/workflows/deploy.yml` in your application repository. It runs on every push to `main`, builds your app into `./dist`, installs the CLI, authenticates with your token, and deploys. .github/workflows/deploy.yml ```yaml name: Deploy to Comwit Cloud on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest # Project id is read by the CLI from COMWIT_PROJECT when --project is omitted; # we pass it explicitly below as well. env: COMWIT_PROJECT: ${{ vars.COMWIT_PROJECT }} steps: # 1. Check out YOUR application source. - name: Check out app uses: actions/checkout@v4 # 2. Build your application into ./dist. # Replace this with your real build. The output can be a built # directory (the CLI packs it with `tar --zstd`) or a prebuilt # `.tar.zst` package file. - name: Build app run: | npm ci npm run build # Ensure your build output ends up in ./dist # 3. Install the comwit CLI. # Release binaries are not published yet, so build from source. # (Replace this step with a release-asset download once # comwit__.tar.gz assets exist.) - name: Install comwit CLI run: | git clone --depth 1 https://github.com/comwit/comwit-cloud.git /tmp/comwit-cloud (cd /tmp/comwit-cloud && just cli-build) mkdir -p "$HOME/.local/bin" cp /tmp/comwit-cloud/dist/comwit "$HOME/.local/bin/comwit" echo "$HOME/.local/bin" >> "$GITHUB_PATH" # 4. Authenticate with your cwt_ deploy token. # `comwit login --token` stores the token in the CLI config file # so later commands are authenticated. - name: Authenticate run: | comwit login \ --token ${{ secrets.COMWIT_TOKEN }} \ --project ${{ vars.COMWIT_PROJECT }} # 5. Deploy the build. - name: Deploy run: | comwit deploy \ --project ${{ vars.COMWIT_PROJECT }} \ --app ${{ vars.COMWIT_APP }} \ --package ./dist ``` ### What each command does [Section titled “What each command does”](#what-each-command-does) * **`comwit login --token --project `** stores your `cwt_` token (and a default project) in the CLI config file, `~/.config/comwit/config.json`. After this, the CLI is authenticated for the rest of the job. * **`comwit deploy --project --app --package ./dist`** uploads your build and activates it. `--package` may be a built directory — which the CLI packs into a temporary `.tar.zst` using local `tar --zstd` — or an already-prebuilt `.tar.zst` file. Note If your CI runner’s `tar` does not support zstd, pass a prebuilt `.tar.zst` package file to `--package` instead of a directory. ## Optional deploy flags [Section titled “Optional deploy flags”](#optional-deploy-flags) `comwit deploy` accepts a few extra flags you can add to the deploy step: Deploy step with optional flags ```yaml - name: Deploy run: | comwit deploy \ --project ${{ vars.COMWIT_PROJECT }} \ --app ${{ vars.COMWIT_APP }} \ --package ./dist \ --host app.example.com \ --env-ref brrrd/env/app \ --max-concurrent-requests 100 ``` * `--host` binds one or more hostnames on deploy (comma-separated, e.g. `a.example.com,b.example.com`). See [Custom hostnames](/apps/custom-hostnames/). * `--env-ref` selects a runtime environment reference. * `--max-concurrent-requests` sets the runtime concurrency cap. ## How authentication works in CI [Section titled “How authentication works in CI”](#how-authentication-works-in-ci) It’s worth being precise about what the token can and can’t do, because that’s what keeps the setup safe: * The `cwt_` token authenticates every API call the CLI makes via `Authorization: Bearer `. * It can only perform actions its scopes allow and can only touch projects you’re a member of. A deploy with an out-of-scope token or a project you’re not a member of returns HTTP `403`. * The token is the only secret in the workflow. Keep it in `secrets.COMWIT_TOKEN`, never in a variable, log line, or committed file. For the full token and scope model, see [API authentication](/api/authentication/). For other CI systems, see [Other CI providers](/ci-cd/other-ci/). # Other CI & raw API deploy > Deploy to Comwit Cloud from any CI system without the CLI by POSTing a .tar.zst artifact straight to the deployments API. Not every CI system can install the `comwit` CLI — locked-down runners, minimal container images, or platforms where you would rather not add a tool. In those cases you can deploy by calling the public API directly: build your app into a compressed `.tar.zst` artifact and upload it to the deployments endpoint. This page shows exactly how. If you can install the CLI, [GitHub Actions](/ci-cd/github-actions/) and the CLI flow are simpler. This page is for everything else. ## What a “deploy” actually is [Section titled “What a “deploy” actually is”](#what-a-deploy-actually-is) A deploy uploads one build artifact for an app and (optionally) binds hostnames to it. The artifact is a directory of your built app, packed with `tar` and compressed with zstd into a single `.tar.zst` file. The CLI does this packing for you; here you do it yourself. Before you can deploy you need two ids: * **`projectId`** — the project that owns the app. * **`appId`** — the app you are deploying to. Create one first if you have not: `POST /v1/projects/{projectId}/apps` with body `{ "name": "web" }`. You also need a user API token (`cwt_...`). See [Authentication](/api/authentication/) for how tokens and scopes work, and [API overview](/api/overview/) for base URL and conventions. Caution Never put the platform server/operator token in CI. User tokens start with `cwt_`. The operator token is internal-only and is rejected on these project-scoped routes. ## The deployment endpoint [Section titled “The deployment endpoint”](#the-deployment-endpoint) The deployments route is the one exception to the API’s usual JSON-body rule. The **request body is the raw artifact**, and the deploy options are passed as **query parameters**, not as JSON fields. ```txt POST /v1/projects/{projectId}/apps/{appId}/deployments ``` | Part | Value | | ------------- | --------------------------------- | | Authorization | `Bearer cwt_xxx` | | Content-Type | `application/octet-stream` | | Body | the raw `.tar.zst` artifact bytes | Query parameters (all optional): | Query param | Maps to CLI flag | Purpose | | ------------------------- | --------------------------- | -------------------------------------------------------------------------------- | | `hosts` | `--host` | Comma-separated hostnames to bind on deploy (e.g. `a.example.com,b.example.com`) | | `env_ref` | `--env-ref` | Runtime environment reference | | `max_concurrent_requests` | `--max-concurrent-requests` | Runtime concurrency cap | Note The raw query parameters use underscores (`env_ref`, `max_concurrent_requests`), even though the matching `comwit` CLI flags use hyphens (`--env-ref`, `--max-concurrent-requests`). When you call the API directly, use the underscore form — a mistyped parameter is silently ignored. A successful deploy returns: ```json { "app_id": "app-xxx", "build_id": "build-yyy", "hosts": [], "uploaded": true } ``` ## Build the artifact [Section titled “Build the artifact”](#build-the-artifact) Pack your built output directory (here `./dist`) into a `.tar.zst`: Build the artifact ```sh tar --zstd -cf app.tar.zst -C ./dist . ``` Note `-C ./dist .` packs the *contents* of `dist/` at the root of the archive, which is what the runtime expects. Adjust `./dist` to wherever your framework writes its build output. ## Upload with curl [Section titled “Upload with curl”](#upload-with-curl) Send the file as the raw request body. With curl, `--data-binary` sends the bytes unmodified (do not use `-d`/`--data`, which mangles binary input). Deploy via curl ```sh curl -X POST \ "https://api.cloud.comwit.io/v1/projects/$PROJECT_ID/apps/$APP_ID/deployments" \ -H "Authorization: Bearer $COMWIT_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @app.tar.zst ``` To bind hostnames and set runtime options at deploy time, add them as query parameters. Remember to URL-encode the comma in `hosts` if your shell needs it: Deploy and bind hostnames ```sh curl -X POST \ "https://api.cloud.comwit.io/v1/projects/$PROJECT_ID/apps/$APP_ID/deployments?hosts=a.example.com,b.example.com&max_concurrent_requests=50" \ -H "Authorization: Bearer $COMWIT_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @app.tar.zst ``` In CI, set `PROJECT_ID`, `APP_ID`, and `COMWIT_TOKEN` (your `cwt_` token) as environment variables or secrets, then run the two commands above as a build step. ## After deploying [Section titled “After deploying”](#after-deploying) Uploading a build makes it the active build for the app. To list builds or revert, use the same API your CI can already reach: ```txt GET /v1/projects/{projectId}/apps/{appId}/builds POST /v1/projects/{projectId}/apps/{appId}/rollbacks ``` A rollback re-activates a prior build without re-uploading an artifact — handy as a CI “undo” step that does not need the original files. ## Errors [Section titled “Errors”](#errors) Failures come back as standard HTTP status codes with a problem-detail JSON body. The common classes you will see from a deploy script: | Status | Meaning | | ------ | -------------------------------------------------------------------------------------------------------- | | `400` | Caller mistake (bad input) | | `403` | Out-of-scope token, a project you are not a member of, or an operator-only route hit with a `cwt_` token | | `404` | Missing resource (wrong `projectId` or `appId`) | | `409` | Conflict (e.g. a conflicting DNS record when binding hostnames) | | `502` | Internal control-plane dependency failure — including when the runtime control plane is unconfigured | (A deploy never returns `503`; that status is reserved for the `GET /readyz` health probe.) Upstream detail is logged server-side and sanitized out of public bodies — secret values and internal references never appear in error responses. See [Errors & idempotency](/api/errors-and-idempotency/) for the full treatment. ## Idempotency and retries [Section titled “Idempotency and retries”](#idempotency-and-retries) The deployments call is **synchronous**: the response returns once the upload is accepted. The API is synchronous wherever the upstream control plane is, so there is no separate “operation” resource to poll for a plain deploy today. Planned A generic `Idempotency-Key` header and long-running **operation resources** (so you can submit once and poll for completion) are planned, not yet live. Until then, do not blindly auto-retry a deploy on a network timeout without checking `GET .../builds` first — a retry uploads a fresh build rather than de-duplicating the previous one. Some related flows *are* already re-entrant — for example, attaching an app hostname is idempotent, so repeating a hostname attach request to recover from a partial failure is safe. See [Errors & idempotency](/api/errors-and-idempotency/) for what is retry-safe today. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [API overview](/api/overview/) — base URL, envelopes, and conventions. * [Authentication](/api/authentication/) — creating and scoping `cwt_` tokens. * [Errors & idempotency](/api/errors-and-idempotency/) — status classes and retry behavior in full. # Automate deploys (CI/CD) > Deploy and roll back Comwit Cloud apps from automation using a scoped cwt_ token, with either the comwit CLI or raw HTTP. CI/CD (“continuous integration / continuous delivery”) means letting a pipeline — GitHub Actions, GitLab CI, a build server, or any script — build and ship your app automatically instead of running deploys by hand from your laptop. On Comwit Cloud this works the same way a manual deploy does: your automation authenticates with a token and calls the platform API at `https://api.cloud.comwit.io`, either through the `comwit` CLI or with raw HTTP. This page covers the pieces that are the same no matter which CI system you use: the token, the build artifact, and the two integration styles. For copy-paste workflows, see [GitHub Actions](/ci-cd/github-actions/) and [other CI systems](/ci-cd/other-ci/). ## Use a scoped `cwt_` token [Section titled “Use a scoped cwt\_ token”](#use-a-scoped-cwt_-token) Automation authenticates with a **personal access token** — a `cwt_` token. It is tied to your user, can only act on **projects you are a member of**, and can only do what its **scopes** allow. That makes it safe to drop into a pipeline: even if the secret leaked, it can only do the narrow set of things you granted. For deploy automation you almost always want one of two scopes: | Scope | Grants | | ------------ | --------------------------------------------- | | `app:deploy` | Deploy builds, roll back | | `app:write` | Create/delete apps, set env, attach hostnames | If your pipeline only ships new builds of an app that already exists, `app:deploy` is enough. If the pipeline also creates the app, sets environment variables, or attaches hostnames, add `app:write`. (`app:read` lets a pipeline list apps and inspect builds if you need that for a check.) See [Authentication](/api/authentication/) for the full scope table. Never use the operator token `PLATFORM_API_SERVER_TOKEN` is an internal operator/web token. It bypasses scope and project-membership checks and **must never be used as a CI secret or a user token**. User tokens always start with `cwt_`. If you only have a server token, stop — create a scoped `cwt_` token instead. ### Create the token [Section titled “Create the token”](#create-the-token) Create a scoped `cwt_` token from the console at [`https://cloud.comwit.io/tokens`](https://cloud.comwit.io/tokens) (“API tokens”). Pick the scopes your pipeline needs and nothing more. The plaintext token is shown **once** at creation — copy it then. You can revoke it from the same page at any time. Tip Use a separate token per pipeline (or per repository) with the minimum scopes it needs. Revoking or rotating one then never disturbs the others. ### Store it as a CI secret [Section titled “Store it as a CI secret”](#store-it-as-a-ci-secret) Save the `cwt_` token as an encrypted secret in your CI provider — never commit it to your repository or print it in build logs. Most providers expose secrets to the build as environment variables; a common convention is `COMWIT_TOKEN`. ## The build artifact [Section titled “The build artifact”](#the-build-artifact) A deploy ships one build artifact: your app’s compiled output. Comwit accepts it in two shapes: * a **directory** of build output (a brrrd output directory), or * a prebuilt **`.tar.zst`** package file. When you point the CLI at a directory, it packs the directory into a temporary `.tar.zst` for you using local `tar --zstd`. If your CI image’s `tar` does not support zstd, build the `.tar.zst` yourself (or have your build step produce one) and pass that file instead. Over raw HTTP there is no directory upload: the deployment request body is the raw `application/octet-stream` `.tar.zst` artifact. ## Two integration styles [Section titled “Two integration styles”](#two-integration-styles) ### A. Run the `comwit` CLI in CI [Section titled “A. Run the comwit CLI in CI”](#a-run-the-comwit-cli-in-ci) The friendliest option when you can install a binary in your pipeline. Authenticate non-interactively with `--token` (no browser device flow in CI), then deploy. CI pipeline (comwit CLI) ```sh # Authenticate with the token from your CI secret store. comwit login --token "$COMWIT_TOKEN" --project "$COMWIT_PROJECT" # Build artifact is a directory or a prebuilt .tar.zst. comwit deploy \ --project "$COMWIT_PROJECT" \ --app "$COMWIT_APP" \ --package ./apps/web/dist/brrrd ``` `comwit login --token` stores the token in the CLI config instead of running the interactive device flow, so it works headless. You can also pass the project with the `COMWIT_PROJECT` environment variable instead of `--project`. `comwit deploy` accepts the same optional flags in CI as anywhere else: comwit deploy (optional flags) ```sh comwit deploy \ --project "$COMWIT_PROJECT" \ --app "$COMWIT_APP" \ --package ./dist/brrrd \ --host app.example.com \ --env-ref brrrd/env/app \ --max-concurrent-requests 100 ``` For installing the binary in a pipeline, see [Install the CLI](/get-started/install-cli/). ### B. Call the API over raw HTTP [Section titled “B. Call the API over raw HTTP”](#b-call-the-api-over-raw-http) Use this when installing the CLI is awkward — a minimal container image, a platform where you can only run `curl`, or tooling in another language. Send the token as a bearer header and POST the `.tar.zst` artifact as the raw request body. Hosts, env-ref, and concurrency go on the query string (not in the body). CI pipeline (raw HTTP) ```sh curl -sS -X POST \ -H "Authorization: Bearer $COMWIT_TOKEN" \ -H "Content-Type: application/octet-stream" \ --data-binary @./dist/brrrd.tar.zst \ "https://api.cloud.comwit.io/v1/projects/$COMWIT_PROJECT/apps/$COMWIT_APP/deployments" ``` The deploy endpoint is: ```txt POST /v1/projects/{projectId}/apps/{appId}/deployments ``` A request whose token lacks the required scope, or that targets a project you are not a member of, returns `403`. For the full status-code list and retry behavior, see [Errors and idempotency](/api/errors-and-idempotency/) and the [API overview](/api/overview/). Note Generic `Idempotency-Key` support for long-running workflows is planned, not yet live. Today the API is synchronous where the upstream control plane is synchronous; design your pipeline so a retried deploy is acceptable. ## Next steps [Section titled “Next steps”](#next-steps) * [GitHub Actions](/ci-cd/github-actions/) — a ready-to-use workflow. * [Other CI systems](/ci-cd/other-ci/) — GitLab CI, build servers, and raw-HTTP patterns. * [App deploys](/apps/deploy/) — what a deploy does and how rollbacks work. * [Authentication](/api/authentication/) — token classes and the full scope table. # Create & connect a database > Create a Comwit Cloud database, copy its one-time connection token, and connect your app over libSQL. A Comwit Cloud database is a Turso/libSQL database (powered by Louhi) that lives inside one of your projects. You create and manage it through the product API or the `comwit` CLI, and your app talks to it directly as a plain libSQL endpoint at `https://db.cloud.comwit.io`. This page walks you through creating a database, safely handling the connection token, listing the databases in a project, and pointing your app at the new database. ## Before you start [Section titled “Before you start”](#before-you-start) You need a project to put the database in. Every database belongs to a project, and most commands take a `--project ` (or `{projectId}` in the API). If you have not installed the CLI yet, see [Install the CLI](/get-started/install-cli/). ## Create a database [Section titled “Create a database”](#create-a-database) Pick a `--name` for the database (for example `app-db`). With the CLI: comwit CLI ```sh comwit databases create --project --name app-db ``` The equivalent API call is a `POST` to the project’s databases collection: API ```http POST /v1/projects/{projectId}/databases ``` Request body ```json { "name": "app-db" } ``` A successful create returns the new database and, importantly, a one-time connection token: Response ```json { "database_id": "db-xxxx", "database_url": "https://db.cloud.comwit.io/v1/", "created": true, "database_token": "" } ``` The fields are: * `database_id` — the stable identifier for this database (for example `db-xxxx`). Use it in lifecycle and SQL operations. * `database_url` — the libSQL data endpoint your app connects to (see [Connect your app](#connect-your-app) below). * `created` — `true` when the database was provisioned by this request. * `database_token` — the long-lived connection token your app authenticates with. ### Copy the token now — it is shown only once [Section titled “Copy the token now — it is shown only once”](#copy-the-token-now--it-is-shown-only-once) The connection token is returned exactly once `database_token` is returned **once**, at create time. Comwit Cloud never stores raw tenant tokens, so it cannot show it to you again later. Copy it straight into your app’s secret store now. If you lose it, you can’t look it up — you have to rotate the token to get a fresh one. See [Manage a database](/databases/manage/) for rotation. Tip Keep the token out of source control and out of the browser. For ad-hoc SQL in the console you don’t need this durable token at all — Comwit issues short-lived query tokens for that. See [Run SQL](/databases/run-sql/). ## List the databases in a project [Section titled “List the databases in a project”](#list-the-databases-in-a-project) To see what already exists in a project: comwit CLI ```sh comwit databases list --project ``` API ```http GET /v1/projects/{projectId}/databases ``` The response wraps the databases in a `databases` array: Response ```json { "databases": [ { "database_id": "db-xxxx", "name": "app-db", "status": "...", "created_at": "2026-06-16T12:34:56Z", "database_url": "https://db.cloud.comwit.io/v1/" } ] } ``` Each entry includes `database_id`, `name`, `status`, `created_at`, and `database_url` (omitted when empty). Note that the list does **not** include any token — tokens only ever come back from create and rotate. ## Connect your app [Section titled “Connect your app”](#connect-your-app) Your database is a standard libSQL endpoint, so any libSQL client can connect to it using the URL plus the token. The URL has this shape: Database URL ```txt https://db.cloud.comwit.io/v1/ ``` Use `database_url` (from create or list) as the connection URL and the `database_token` (from create or rotate) as the auth token in your libSQL client. Store both as secrets/environment variables in your app. db.cloud.comwit.io is a data endpoint only `db.cloud.comwit.io` serves database traffic and nothing else. Management, docs, and debug paths are blocked on the public listener. Every lifecycle operation — create, rotate, suspend, resume, delete — goes through the product API (`api.cloud.comwit.io`) or the CLI, never the data host. ## Next steps [Section titled “Next steps”](#next-steps) * [Run SQL](/databases/run-sql/) against your new database from the console or with a query token. * [Manage a database](/databases/manage/) to rotate the token, check status and usage, or delete it. * [Deploy an app](/apps/deploy/) and wire it to this database. # Manage a database > Check status, rotate tokens, suspend, resume, inspect usage, and delete a Comwit Cloud database — plus the guardrails that protect critical tenants. Once your database exists, you manage its whole lifecycle through the Comwit Cloud product API: check whether it’s healthy, rotate the connection token, pause it, bring it back, look at usage, or delete it for good. A Comwit database is a Turso/libSQL database (powered by Louhi) that your app connects to at `https://db.cloud.comwit.io` — but you never run management operations against that data host. Every lifecycle action goes through the product API at `https://api.cloud.comwit.io`. If you haven’t created a database yet, start with [Create and connect a database](/databases/create-and-connect/). To run queries against one, see [Run SQL](/databases/run-sql/). ## The lifecycle operations [Section titled “The lifecycle operations”](#the-lifecycle-operations) Each operation below acts on one specific database. The table shows the API route, what it does, and which authentication scope it needs. | Action | What it does | API | | ----------------------- | --------------------------------------------------- | -------------------------------------------- | | Get status | Read current health and internals | `GET /v1/databases/{database}` | | Rotate connection token | Issue a fresh one-time token; old one stops working | `POST /v1/databases/{database}/token/rotate` | | Issue query token | Mint a short-lived SQL token | `POST /v1/databases/{database}/query-token` | | Suspend | Pause the database | `POST /v1/databases/{database}/suspend` | | Resume | Bring a suspended database back | `POST /v1/databases/{database}/resume` | | Get usage | Read usage metrics | `GET /v1/databases/{database}/usage` | | Delete | Permanently remove the database | `DELETE /v1/databases/{database}` | Product surface vs. operator routes The **project-scoped** path (the routes under `/v1/projects/{projectId}/databases`) is the product surface you use for everyday work in the console and CLI. The flat `/v1/databases/{database}` family shown in the table is **operator-token only** — those routes back the same lifecycle workflow but are not part of the public, user-token product surface. ## Check database status [Section titled “Check database status”](#check-database-status) Read the current state of a database to confirm it’s healthy before you connect or deploy against it. API ```http GET /v1/databases/{database} ``` The status response includes a `status` field plus low-level libSQL/WAL internals — the `runner`, the generation, and frame numbers — exposed for inspection. You’ll usually only care about `status`; the rest is there when you need to debug replication or storage behavior. ## Rotate the connection token [Section titled “Rotate the connection token”](#rotate-the-connection-token) Your long-lived **database token** is the durable secret your app uses to connect. Rotate it when it leaks, when you’ve lost it, or on a routine schedule. API ```http POST /v1/databases/{database}/token/rotate ``` Rotation returns a fresh, one-time `database_token` in the response. Rotation is a hard cutover The moment you rotate, the **previous token stops working**. Anything still using the old token — running apps, scripts, other developers — loses access until you update it with the new value. Copy the new `database_token` into your secret store and roll it out before old clients fail. Comwit never stores raw tenant tokens, so a rotation response is the *only* time you’ll see the new secret. If you lose it, your only recovery is to rotate again. ## Issue a query token [Section titled “Issue a query token”](#issue-a-query-token) For transient SQL access (for example, an ad-hoc query from a browser session) mint a short-lived **query token** instead of exposing your durable database token. API ```http POST /v1/databases/{database}/query-token ``` ```json { "ttl_seconds": 300, "audience": "sql-console", "reason": "ad-hoc query" } ``` `ttl_seconds` is bounded between 1 and 3600. The full query-token flow — including the response shape and how the console SQL editor uses it — is covered in [Run SQL](/databases/run-sql/). ## Suspend and resume [Section titled “Suspend and resume”](#suspend-and-resume) Suspend pauses a database; resume brings it back. Suspend ```http POST /v1/databases/{database}/suspend ``` Resume ```http POST /v1/databases/{database}/resume ``` Use suspend to temporarily stop a database without losing it, then resume when you need it again. ## Inspect usage [Section titled “Inspect usage”](#inspect-usage) Read usage metrics for a database to understand how much it’s being used. API ```http GET /v1/databases/{database}/usage ``` ## Delete a database [Section titled “Delete a database”](#delete-a-database) Deleting permanently removes the database. API ```http DELETE /v1/databases/{database} ``` Deletion is permanent There is no undo. Make sure you have copied out any data you need before you delete. ## Guardrails [Section titled “Guardrails”](#guardrails) A few safety rules protect platform-critical databases and your secrets. ### Protected tenants [Section titled “Protected tenants”](#protected-tenants) Some databases are critical to the platform itself and are protected by the `PLATFORM_API_PROTECTED_TENANTS` setting (which by default includes `louhi-app` and `comwit-db-synthetic`). Against a protected tenant, the following operations are **rejected with `403`**: * Suspend * Resume * Delete * Rotate connection token * Issue query token This is a launch safety guard to keep critical infrastructure from being paused or torn down by accident. It is **not** a substitute for project authorization — your request still has to be authorized for the project regardless. ### Comwit never stores raw tenant tokens [Section titled “Comwit never stores raw tenant tokens”](#comwit-never-stores-raw-tenant-tokens) Comwit does not keep a copy of your raw database token anywhere. The output of **create** and **rotate** is the single source of that secret. Treat it accordingly: copy it into your own secret store immediately, and if you lose it, rotate to get a new one. Tip The internal `PLATFORM_API_SERVER_TOKEN` is an operator credential and is never a user token. Your own API tokens are `cwt_` tokens — see [Authentication](/api/authentication/). ## Related [Section titled “Related”](#related) * [Create and connect a database](/databases/create-and-connect/) * [Run SQL](/databases/run-sql/) * [Databases overview](/databases/overview/) * [API errors and idempotency](/api/errors-and-idempotency/) # Databases overview > What a Comwit Cloud database is — a Turso/libSQL database powered by Louhi — and how its data endpoint and management API fit together. A Comwit Cloud database is a [Turso/libSQL](https://docs.turso.tech/) database powered by Louhi, Comwit’s database server plane. You create and manage it through the product API (or the `comwit` CLI and the web console), and your app connects to it as a plain libSQL endpoint reachable at `https://db.cloud.comwit.io`. If you have used SQLite, a libSQL database will feel familiar: it speaks SQLite SQL, but it runs as a network service you connect to with a URL and a token instead of a local file. ## Two endpoints, two jobs [Section titled “Two endpoints, two jobs”](#two-endpoints-two-jobs) It helps to keep two hosts straight from the start, because they do very different things. | Host | Purpose | | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `https://api.cloud.comwit.io` | The **product API** — create, list, rotate tokens, suspend/resume, view usage, delete. All lifecycle and management. | | `https://db.cloud.comwit.io` | The **data endpoint** — where your app sends SQL. This is the libSQL connection your application uses. | The data host is data only `db.cloud.comwit.io` is a **data endpoint only**. Management, docs, and debug paths are blocked on the public listener — every lifecycle operation (create, list, rotate, suspend, resume, usage, delete) goes through the product API at `api.cloud.comwit.io`, never the data host. Concretely, the public Louhi listener blocks tenant-management, OpenAPI, debug, and metrics paths. Only the per-database SQL paths are public, and those are always token protected. So you cannot manage databases by poking at `db.cloud.comwit.io` — you talk to the product API for that. ## How a database is addressed [Section titled “How a database is addressed”](#how-a-database-is-addressed) When you create a database, the product API returns a connection URL of this shape: ```txt https://db.cloud.comwit.io/v1/ ``` That URL plus a database token is everything your app needs to open a libSQL connection. See [Create and connect](/databases/create-and-connect/) for the full create-then-connect flow. ## Tokens at a glance [Section titled “Tokens at a glance”](#tokens-at-a-glance) Comwit Cloud uses two kinds of database tokens, and the difference matters: * A **database token** (also called a connection token) is the long-lived credential your application uses to connect. It is returned **once** at create time — Comwit never stores raw tenant tokens, so copy it straight into your secret store. If you lose it, [rotate](/databases/manage/) to get a new one (which invalidates the old one). * A **query token** is a short-lived token the web console’s SQL editor uses so a browser session never holds the durable connection token. The console mints these for you — the underlying query-token route is operator-only, so a `cwt_` user token cannot mint query tokens directly. See [Run SQL](/databases/run-sql/). Note These are user-facing tenant tokens, distinct from the internal bearer token the platform uses for its own service-to-service calls. Your tokens are what the create and rotate flows hand back to you. ## What you can do with a database [Section titled “What you can do with a database”](#what-you-can-do-with-a-database) Once a database exists, the product API lets you: * **Connect your app** with the data URL and a database token. * **Run SQL** from the web console (or via a query token) — see [Run SQL](/databases/run-sql/). * **Rotate** the connection token, **suspend** and **resume** the database, check **usage**, and eventually **delete** it — see [Manage databases](/databases/manage/). ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Create and connect](/databases/create-and-connect/) — create your first database and wire it into an app. * [Run SQL](/databases/run-sql/) — query your data from the console or with a short-lived query token. * [Manage databases](/databases/manage/) — list, rotate tokens, suspend/resume, view usage, and delete. Tip New to the platform? Start with [What is Comwit Cloud](/get-started/what-is-comwit-cloud/) and the [Quickstart](/get-started/quickstart/), then come back here once the CLI is installed. # Run SQL & query tokens > Run ad-hoc SQL against a Comwit database from the console or your own tooling using short-lived query tokens. A Comwit database is a Turso/libSQL database you can talk to over SQL. There are two kinds of credential involved: the **durable database token** you keep in your app’s secret store, and a **short-lived query token** used for transient, interactive access. This page explains how to run SQL in both places — and why the difference matters. If you have not created a database yet, start with [Create & connect a database](/databases/create-and-connect/). ## Run SQL in the web console [Section titled “Run SQL in the web console”](#run-sql-in-the-web-console) The fastest way to poke at your data is the SQL editor in the console: ```txt https://cloud.comwit.io/projects/{projectId}/databases/{databaseId}/query ``` Pick your project and database, type a statement, and run it. No credentials to copy or paste — the console handles auth for you. Behind the scenes the console does **not** use your long-lived database token. Instead it mints a **short-lived query token** scoped to that one database and uses it for the editor session. That keeps your durable connection secret out of the browser entirely. Tip Use the console editor for one-off inspection and quick fixes. For application traffic, connect with the durable database token from your app — see [Create & connect a database](/databases/create-and-connect/). ## Why query tokens exist [Section titled “Why query tokens exist”](#why-query-tokens-exist) Your durable database token is the credential your application uses to connect to `https://db.cloud.comwit.io`. It is long-lived and powerful, so it belongs in a secret store, not in a browser tab. A browser session is a different trust environment: tokens there can leak through logs, extensions, or shared screens, and a long-lived secret in a browser is hard to revoke cleanly. The query token solves this — it is **short-lived** and **scoped**, so an interactive session never has to hold the durable secret. When the token expires, access ends on its own. Caution Never embed your durable database token in front-end code or hand it to a browser session. Issue a query token for transient SQL access instead. ## How query tokens are issued [Section titled “How query tokens are issued”](#how-query-tokens-are-issued) The console mints a query token for you automatically — there is nothing to call by hand to use the SQL editor above. The underlying route exists in the platform API, but it lives in the **operator-only** `/v1/databases/*` family and is *not* reachable with a user `cwt_` token (see [Running SQL from your own app](#running-sql-from-your-own-app) below): API: `POST /v1/databases/{database}/query-token` *(operator token only)* Request body ```json { "ttl_seconds": 300, "audience": "comwit-cloud-console", "reason": "ad-hoc query" } ``` | Field | Meaning | | ------------- | ----------------------------------------------------------------------------- | | `ttl_seconds` | How long the token stays valid, in seconds. Bounded **1–3600** (max 1 hour). | | `audience` | A label describing where the token will be used (for example, `sql-console`). | | `reason` | A human-readable note describing why the token was issued. | Response: Response ```json { "database_id": "db-xxxx", "query_token": "", "expires_at": "2026-06-16T12:34:56Z", "ttl_seconds": 300, "scope": "..." } ``` Use the returned `query_token` to run SQL against the data endpoint until `expires_at`. Once it expires, request a new one — query tokens are meant to be cheap and disposable, so prefer minting a fresh, short-lived token over keeping one around. Note `ttl_seconds` is clamped to the **1–3600** range. Keep it as short as your workflow allows: a token good for five minutes is much safer than one good for an hour. ## Running SQL from your own app [Section titled “Running SQL from your own app”](#running-sql-from-your-own-app) The non-project `/v1/databases/*` family — including `query-token`, `token/rotate`, `suspend`, `resume`, and `usage` — is **operator-token only** and rejects user `cwt_` tokens with `403`. In other words, you do not issue query tokens yourself from a `cwt_` token; the console does it for you. To run SQL from your **own application**, connect with the durable database token you received at create (or rotate) time, straight to the data endpoint at `https://db.cloud.comwit.io`. Keep that token in a secret store — never in a browser. See [Create & connect a database](/databases/create-and-connect/) for how to obtain and use it, and [Authentication](/api/authentication/) for how `cwt_` user tokens and scopes work on the rest of the platform API. ## Guardrails [Section titled “Guardrails”](#guardrails) Some platform-critical databases are protected. Issuing a query token (along with suspend, resume, delete, and token rotation) against a protected database is rejected with a `403`. This is a launch safety guard and is not a substitute for your project’s own authorization — see [Manage a database](/databases/manage/) for the full lifecycle. ## Related [Section titled “Related”](#related) * [Create & connect a database](/databases/create-and-connect/) — get a durable token and connect your app. * [Manage a database](/databases/manage/) — rotate tokens, suspend, resume, and check usage. * [Authentication](/api/authentication/) — how user tokens work. # Bring a domain you own > Onboard a domain you already own to Comwit Cloud using delegated DNS, point your registrar's nameservers, and confirm delegation so Comwit can manage its records. A **project domain** is a domain you already own (like `example.com`) that you hand to Comwit Cloud so it can manage the domain’s DNS for you. Comwit creates a dedicated DNS zone for the domain, you point your registrar at it, and from then on Comwit is the source of truth for the domain’s records. This page walks through the **delegated DNS** flow end to end: onboard the domain, set the nameservers at your registrar, and confirm delegation so record management unlocks. How delegation works Comwit creates an AWS Route 53 hosted zone for your domain and gives you its nameservers. You update the nameservers at your *registrar* (the company you bought the domain from) to point at that zone. Once the change propagates, your domain is delegated to Comwit and it can write DNS records on your behalf. Comwit never touches your registrar login — you control that step. ## Step 1 — Onboard the domain [Section titled “Step 1 — Onboard the domain”](#step-1--onboard-the-domain) Start by telling Comwit about a domain you already own. The CLI: Onboard a domain with delegated DNS ```sh comwit domains add --project --domain example.com ``` The equivalent API call posts the domain together with the DNS mode: POST /v1/projects/{projectId}/domains ```http POST /v1/projects/{projectId}/domains ``` ```json { "domain": "example.com", "dns_mode": "route53_delegated" } ``` Comwit creates the hosted zone and responds with the zone’s id, a starting status of `pending_delegation`, and the **nameservers** you need to set at your registrar: Response ```json { "domain": { "domain": "example.com", "dns_mode": "route53_delegated", "status": "pending_delegation", "hosted_zone_id": "Z0123456789ABCDEFGHIJ", "nameservers": ["ns-1.awsdns-00.com", "ns-2.awsdns-00.net"] } } ``` The status `pending_delegation` means the zone exists but your domain is not yet pointing at it, so record management stays locked until you confirm delegation (Step 3). Per-project active-domain limit Each project has a configurable limit on how many **active** project domains it can have. This is a cost and quota guard, and it is checked *before* the hosted zone is created — so if you are at the limit, onboarding is rejected up front. If Comwit creates the zone but then fails to save the domain locally, it rolls the new zone back so you are not left with an orphaned, billable zone. ## Step 2 — Set the nameservers at your registrar [Section titled “Step 2 — Set the nameservers at your registrar”](#step-2--set-the-nameservers-at-your-registrar) Copy the `nameservers` values from the response and update your domain’s **NS records** at your registrar to exactly those values. This is the step that delegates the domain to Comwit. Where to do this depends on your registrar (look for “Nameservers”, “DNS”, or “Domain settings”). Replace any existing nameservers with the ones Comwit returned. Note Nameserver changes can take anywhere from a few minutes to a few hours to propagate across the internet, depending on your registrar and the domain’s previous TTLs. Step 3 lets you check whether propagation has completed. ## Step 3 — Confirm delegation [Section titled “Step 3 — Confirm delegation”](#step-3--confirm-delegation) Once you have set the nameservers, ask Comwit to verify that the domain is actually delegated to its zone: Check delegation ```sh comwit domains check --project --domain example.com ``` The equivalent API call: POST /v1/projects/{projectId}/domains/{domain}/delegation-check ```http POST /v1/projects/{projectId}/domains/{domain}/delegation-check ``` Comwit resolves the domain’s **authoritative nameservers** (the ones the internet actually sees) and compares them to the hosted zone’s nameservers: * **They match** → the domain moves to status `managed` and DNS **record mutations unlock**. You can now create, update, and delete records — see [DNS records](/domains/dns-records/). * **They do not match yet** → the domain stays `pending_delegation`. This is normal right after you change nameservers; just wait for propagation and run the check again. A failed lookup is not an error If the domain does not resolve yet — for example the lookup returns NXDOMAIN, or no authoritative nameservers are found — Comwit reports this as `matched: false`, **not** as a provider error. So you can safely run the check repeatedly while you wait for DNS to propagate. ### If a managed domain stops delegating [Section titled “If a managed domain stops delegating”](#if-a-managed-domain-stops-delegating) Delegation is checked over time, not just once. If a domain that was already `managed` later stops delegating to the Comwit zone (for example someone changes the nameservers back at the registrar), Comwit moves it back to `pending_delegation`: * **Create and update** of records is locked again. * **Deletes still work**, so you are never trapped with records you cannot remove. Re-point the nameservers and run `comwit domains check` again to return the domain to `managed`. ## What’s next [Section titled “What’s next”](#whats-next) * Add and edit DNS records once the domain is `managed`: [Manage DNS records](/domains/dns-records/). * Point an app at the domain with Automatic DNS: [Connect a domain to an app](/domains/connect-domain-to-app/). * Remove a domain you no longer need (and a note on registrar purchase, which is planned): [Remove and purchase domains](/domains/remove-and-purchase/). # Connect a domain to an app > Point a domain at a deployed Comwit Cloud app using either automatic DNS on a managed project domain or manual DNS records you publish yourself. Once you have deployed a runtime app, you can serve it on your own domain (for example `app.example.com` instead of the default app URL). Connecting a domain to an app is a two-part job: Comwit asks the runtime (brrrd) to bind the hostname and provision its certificate, and then the right DNS records have to exist so the public internet can find your app. There are two ways to get those DNS records in place: * **Automatic DNS (recommended)** — the domain is a **managed project domain** whose DNS Comwit already controls, so Comwit writes the records for you. * **Manual DNS** — the domain’s DNS lives somewhere else, so Comwit hands you the records to publish at your provider yourself. Both paths attach the hostname to your app and then **finalize** it. This page walks through each one end to end. Before you start You need a project, a deployed app (`appId`), and a domain. If you have not deployed yet, see [Deploy an app](/apps/deploy/). For background on the hostname modes, see [Custom hostnames](/apps/custom-hostnames/). ## How attaching works [Section titled “How attaching works”](#how-attaching-works) Every hostname you attach goes through the same app **domains** endpoints: App hostname API ```txt GET /v1/projects/{projectId}/apps/{appId}/domains POST /v1/projects/{projectId}/apps/{appId}/domains POST /v1/projects/{projectId}/apps/{appId}/domains/{domain}/finalize ``` Attaching a hostname always calls the runtime control plane first, so brrrd owns the CloudFront certificate and the host-binding lifecycle. The DNS mode you choose in the attach request decides who creates the DNS records. | DNS mode | When to use it | Who writes DNS | | ---------------------------- | ----------------------------------------------------------------------- | ------------------------------ | | `managed_project_domain` | The hostname is the apex or a subdomain of a **managed** project domain | Comwit, into the Route 53 zone | | `external_records` (default) | The domain’s DNS is hosted elsewhere | You, at your DNS provider | *** ## Path 1 — Automatic DNS (recommended) [Section titled “Path 1 — Automatic DNS (recommended)”](#path-1--automatic-dns-recommended) Use this path when the domain is a project domain that Comwit already manages. Comwit writes the required records into the domain’s Route 53 zone for you, including any CloudFront `_cf-challenge` TXT records, so there is nothing to copy and paste at a registrar. ### 1. Make sure the project domain is managed [Section titled “1. Make sure the project domain is managed”](#1-make-sure-the-project-domain-is-managed) Automatic DNS is only available when the hostname is the apex or a subdomain of a project domain whose status is `managed`. If you have not onboarded the domain yet, follow [Bring your own domain](/domains/bring-your-own-domain/) first: add the domain, point your registrar’s nameservers at the Comwit zone, and confirm delegation so the domain becomes `managed`. Tip You can confirm the domain reached `managed` with `comwit domains check`. Until it does, DNS record mutations stay locked and Automatic DNS is not offered. ### 2. Attach the app hostname with `managed_project_domain` [Section titled “2. Attach the app hostname with managed\_project\_domain”](#2-attach-the-app-hostname-with-managed_project_domain) Attach the hostname and ask Comwit to manage its DNS: CLI ```sh comwit deploy --project --app --package ./dist \ --host app.example.com ``` `--host` binds the hostname during a deploy. The attach request itself maps to the app domains route: API ```http POST /v1/projects/{projectId}/apps/{appId}/domains ``` Request body ```json { "domain": "app.example.com", "dns_mode": "managed_project_domain" } ``` On success, Comwit creates the records in the project domain’s Route 53 zone and returns the managed-DNS result: Response ```json { "domain": "app.example.com", "dns_mode": "managed_project_domain", "managed_dns": { "project_domain": "example.com", "status": "applied", "records": [] } } ``` These records are owned by Comwit as `platform_app` records (tagged with `owner_resource_type=runtime_app` and `owner_resource_id={appId}`). You will see them when you list the project domain’s records, but your own manual record edits will not overwrite them — see [Manage DNS records](/domains/dns-records/). If `managed_dns.status` is `failed` If brrrd binds the hostname but Comwit cannot apply the Route 53 records, the attach returns `202` with `managed_dns.status: "failed"` and a retry message. **Repeat the exact same attach request** — it is idempotent, so retrying simply finishes applying the records. ### 3. Finalize activation [Section titled “3. Finalize activation”](#3-finalize-activation) Creating the DNS records is not the last step. Finish activation through the finalize route: API ```http POST /v1/projects/{projectId}/apps/{appId}/domains/app.example.com/finalize ``` Once finalize completes, your app is reachable on `app.example.com`. *** ## Path 2 — Manual DNS [Section titled “Path 2 — Manual DNS”](#path-2--manual-dns) Use this path for a domain whose DNS you keep at another provider (your registrar, Cloudflare, or anywhere else). Comwit cannot write into that zone, so it returns the records you need to publish, and you finalize after they are live. ### 1. Attach the app hostname with `external_records` [Section titled “1. Attach the app hostname with external\_records”](#1-attach-the-app-hostname-with-external_records) `external_records` is the default DNS mode, so attaching a hostname this way needs no extra mode flag: CLI ```sh comwit deploy --project --app --package ./dist \ --host app.example.com ``` API ```http POST /v1/projects/{projectId}/apps/{appId}/domains ``` Request body ```json { "domain": "app.example.com", "dns_mode": "external_records" } ``` platform-api returns the DNS records you must publish at your own provider: Response ```json { "domain": "app.example.com", "dns_mode": "external_records", "dns_records": [ { "record_type": "CNAME", "name": "...", "value": "..." } ] } ``` ### 2. Publish the records at your provider [Section titled “2. Publish the records at your provider”](#2-publish-the-records-at-your-provider) Create each returned record in your DNS provider’s dashboard exactly as given — match the `record_type`, `name`, and `value`. This typically includes a `CNAME` pointing the hostname at the runtime, plus any validation records needed for the certificate. Note DNS changes can take time to propagate. Wait until the records resolve at your provider before moving on to finalize. ### 3. Finalize activation [Section titled “3. Finalize activation”](#3-finalize-activation-1) When the records are live, complete activation: API ```http POST /v1/projects/{projectId}/apps/{appId}/domains/app.example.com/finalize ``` After finalize succeeds, your app serves on `app.example.com`. *** ## Check what is attached [Section titled “Check what is attached”](#check-what-is-attached) List the hostnames currently bound to an app at any time: API ```http GET /v1/projects/{projectId}/apps/{appId}/domains ``` ## Which path should I pick? [Section titled “Which path should I pick?”](#which-path-should-i-pick) * If you want Comwit to handle DNS automatically and you can change your domain’s nameservers, onboard the domain as a project domain and use **Automatic DNS**. Start at [Bring your own domain](/domains/bring-your-own-domain/). * If you need to keep your DNS where it is today, use **Manual DNS** and publish the returned records yourself. ## Related [Section titled “Related”](#related) * [Custom hostnames](/apps/custom-hostnames/) — the hostname modes from the app side. * [Bring your own domain](/domains/bring-your-own-domain/) — onboard a domain so Automatic DNS becomes available. * [Manage DNS records](/domains/dns-records/) — view and edit records in a managed project domain. # Manage DNS records > List, create, update, and delete DNS records on a domain that Comwit Cloud manages. A **DNS record** tells the internet where a name should point — for example, that `www.example.com` should serve from a particular host. Once you have onboarded a domain so Comwit Cloud manages its DNS (see [Bring your own domain](/domains/bring-your-own-domain/)), you can manage that domain’s records with the `comwit` CLI or the platform API. Note Record management only unlocks after your domain reaches the `managed` state. If delegation is still pending — or a previously managed domain stops delegating — **create** and **update** are locked. **Delete** always works, so you are never trapped. See [Bring your own domain](/domains/bring-your-own-domain/) for how delegation is confirmed. ## Supported record types [Section titled “Supported record types”](#supported-record-types) When you create a record you choose its `type`. Comwit Cloud supports: | Type | What it does | | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `A` | Points a name at an IPv4 address. | | `AAAA` | Points a name at an IPv6 address. | | `CNAME` | Aliases one name to another name. | | `TXT` | Stores arbitrary text (verification, SPF, etc.). | | `MX` | Routes mail for the domain. | | `A_ALIAS` | An **apex alias** — lets the bare/root domain (e.g. `example.com`) point at a target the way a `CNAME` would, which a plain `A` record at the apex cannot. | ## List records [Section titled “List records”](#list-records) This shows every record in the domain’s zone, including ones Comwit wrote for you. comwit CLI ```sh comwit domains records list --project --domain example.com ``` API ```txt GET /v1/projects/{projectId}/domains/{domain}/records ``` ### What a record looks like [Section titled “What a record looks like”](#what-a-record-looks-like) Each record carries an id, its name, type, TTL, one or more values, and ownership metadata: ```json { "record": { "id": "dnsrec_...", "name": "www.example.com", "type": "CNAME", "ttl": 300, "values": ["target.example.net"], "owner": "user", "status": "in_sync" } } ``` | Field | Meaning | | ------------ | ----------------------------------------------------------------------------- | | `id` | Stable id (`dnsrec_...`) you pass to update/delete. | | `name` | The DNS name this record applies to. | | `type` | One of the supported types above. | | `ttl` | Time-to-live in seconds. | | `values` | The record’s value(s) — a list, because some types (like `MX`) carry several. | | `owner` | Who controls the record — see below. | | `status` | Sync state of the record, e.g. `in_sync`. | | `last_error` | A sanitized failure message, present only on failed records. | ## Record ownership: yours vs. Comwit’s [Section titled “Record ownership: yours vs. Comwit’s”](#record-ownership-yours-vs-comwits) Every record has an `owner`: * `owner: "user"` — records **you** created and manage. You can freely update or delete these. * `owner: "platform_app"` — records **Comwit wrote for you** when you attached an app hostname using Automatic DNS (these also carry `owner_resource_type: "runtime_app"`). See [Connect a domain to an app](/domains/connect-domain-to-app/). Caution Do not try to overwrite a `platform_app`-owned record. Comwit owns those records for an app hostname, and conflicting user writes are **rejected**. If one of these records fails, its `last_error` field holds a sanitized message explaining why. ## Create a record [Section titled “Create a record”](#create-a-record) comwit CLI ```sh comwit domains records create \ --project \ --domain example.com \ --name www \ --type CNAME \ --value target.example.net \ --ttl 300 ``` API ```txt POST /v1/projects/{projectId}/domains/{domain}/records ``` Tip You can pass `--value` more than once for record types that hold multiple values (such as `MX`). ## Update a record [Section titled “Update a record”](#update-a-record) Updating is **partial**: any field you leave out is loaded from the existing record. Concretely, if you omit `--name`, `--type`, or `--ttl`, the current value is kept. Supplying one or more `--value` flags **replaces** the record’s values. comwit CLI ```sh comwit domains records update \ --project \ --domain example.com \ --record \ --value target2.example.net \ --ttl 300 ``` API ```txt PUT /v1/projects/{projectId}/domains/{domain}/records/{recordId} ``` ## Delete a record [Section titled “Delete a record”](#delete-a-record) comwit CLI ```sh comwit domains records delete \ --project \ --domain example.com \ --record ``` API ```txt DELETE /v1/projects/{projectId}/domains/{domain}/records/{recordId} ``` Note Deletion is **retry-safe**. If the record is already gone on the DNS provider side, Comwit treats that as clean and finishes the local cleanup — so re-running a delete that already succeeded is harmless. ## Required scopes [Section titled “Required scopes”](#required-scopes) When you call these routes with a `cwt_` API token, the token must carry the right scope: * `domain:read` — for **list** / read. * `domain:write` — for **create**, **update**, and **delete**. See [Authentication](/api/authentication/) for how token scopes work. ## Related [Section titled “Related”](#related) * [Bring your own domain](/domains/bring-your-own-domain/) — onboard and delegate a domain first. * [Connect a domain to an app](/domains/connect-domain-to-app/) — attach an app hostname with Automatic DNS. * [Domains overview](/domains/overview/) — how domains fit together in Comwit Cloud. * [CLI reference](/reference/cli/) — full command surface. # Domains & DNS overview > How Comwit Cloud manages DNS for a domain you already own through a delegated Route 53 hosted zone. A **project domain** is a domain you already own (like `example.com`) that you onboard to Comwit Cloud so the platform can manage its DNS for you. Comwit creates a hosted zone for the domain in Amazon Route 53, you point your registrar’s nameservers at that zone, and from then on Comwit becomes the DNS manager — you create and edit DNS records through Comwit instead of at your registrar. This is called the **delegated-DNS** flow: 1. You onboard the domain. Comwit creates a Route 53 hosted zone and returns its nameservers. 2. You update the nameserver (NS) records at your registrar to those values. 3. Comwit confirms the delegation, and DNS record management unlocks. You keep owning the domain Onboarding does **not** transfer your domain to Comwit. You still own it at your registrar — you are only delegating who answers DNS queries for it. You can restore your original nameservers at any time to move DNS management back. ## How DNS work flows through the platform [Section titled “How DNS work flows through the platform”](#how-dns-work-flows-through-the-platform) Every Route 53 action — creating the hosted zone, reading and writing records, checking delegation, and tearing the zone down — goes through the Comwit platform API. The console (`apps/web`) never talks to AWS directly; it calls the platform API, which is the single component that holds AWS credentials and owns all Route 53 mutations. In practice you have two ways to drive these flows: * The **`comwit` CLI** (shown first on each sub-page). * The **HTTP API** at `https://api.cloud.comwit.io` (the equivalent route is shown alongside). ## What you can do today [Section titled “What you can do today”](#what-you-can-do-today) The delegated-DNS flow is live end to end: * **Onboard a domain** you already own and get back the nameservers to set at your registrar. * **Confirm delegation** so Comwit verifies your registrar now points at the Comwit zone. * **Manage DNS records** (`A`, `AAAA`, `CNAME`, `TXT`, `MX`, and apex `A_ALIAS`) once the domain is managed. * **Attach an app** to a managed domain so Comwit writes the right records for a custom app hostname automatically. * **Remove a domain**, which cleans up the records and the hosted zone. ## Domain lifecycle and status [Section titled “Domain lifecycle and status”](#domain-lifecycle-and-status) As you move a domain through onboarding, its `status` reflects where it is: * `pending_delegation` — the hosted zone exists, but your registrar’s nameservers do not yet point at it. Record create/update is locked. * `managed` — delegation is confirmed and DNS record management is unlocked. Delegation can lapse If a domain that was `managed` later stops delegating to Comwit (for example, someone changes the nameservers back at the registrar), it returns to `pending_delegation` and record create/update is locked again. Deletes still work in that state, so you are never trapped. ## Buying new domains (planned) [Section titled “Buying new domains (planned)”](#buying-new-domains-planned) Onboarding a domain you **already own** is the live path. Registrar **purchase** of brand-new domains through Comwit is designed but not yet implemented. Domain purchase is not live The registrar/purchase routes (domain availability, pricing, and domain orders) are planned and not yet shippable. Today, register or buy your domain at any registrar you like, then onboard it here with the delegated-DNS flow. When purchase ships it will return an asynchronous order resource rather than pretending registration is instant. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Bring your own domain](/domains/bring-your-own-domain/) — onboard a domain you own and confirm delegation. * [DNS records](/domains/dns-records/) — list, create, update, and delete the records in your managed zone. * [Connect a domain to an app](/domains/connect-domain-to-app/) — point a custom hostname at a deployed app with Automatic DNS. * [Remove and purchase](/domains/remove-and-purchase/) — take a domain back out of Comwit, plus the planned purchase flow. New to the platform? If you have not deployed anything yet, start with the [Quickstart](/get-started/quickstart/) and the [Apps overview](/apps/overview/) before connecting a custom domain. # Remove a domain & purchase (planned) > Remove a project domain from Comwit Cloud safely, and a preview of the planned registrar-purchase flow. A **project domain** is a domain you connected to Comwit Cloud so the platform can manage its DNS. This page covers two things: how to **remove** a domain you no longer want Comwit to manage, and a preview of the **planned** ability to **purchase** brand-new domains directly through Comwit. If you are new here, start with [Bring your own domain](/domains/bring-your-own-domain/) to see how a domain is connected in the first place, and [DNS records](/domains/dns-records/) for managing the records inside it. ## Remove a domain [Section titled “Remove a domain”](#remove-a-domain) Removing a domain stops Comwit from managing its DNS and tears down the Route 53 hosted zone that was created when you connected it. You can do this from the CLI: Remove a project domain ```sh comwit domains delete --project --domain example.com ``` The equivalent API route: API ```txt DELETE /v1/projects/{projectId}/domains/{domain} ``` When you remove a domain, Comwit: * Deletes the active Route 53 records in the zone — including any **platform-owned** records Comwit wrote for an app hostname (for example, records created by [connecting an app to your domain](/domains/connect-domain-to-app/)). * Deletes the Route 53 **hosted zone** itself. * **Soft-deletes** the project-domain row. The domain is not erased: its audit and operation history stays inspectable, and you can re-add the same domain later if you change your mind. Restore your nameservers first If the domain is already **delegated** to Comwit (you pointed your registrar’s nameservers at the Comwit-created zone), restore your registrar’s **original nameservers before** removing the domain. Once the hosted zone is deleted, the nameservers it provided no longer resolve — so any domain still pointing at them will stop serving DNS. Switching back at your registrar first keeps your domain resolving through the rest of the removal. Note Removal is reversible at the record level: because the project-domain row is only soft-deleted, you can connect the same domain again later with [`comwit domains add`](/domains/bring-your-own-domain/). You will get a **new** hosted zone and a **new** set of nameservers to set at your registrar. ## Purchase a domain [Section titled “Purchase a domain”](#purchase-a-domain) Planned — not yet available Buying a brand-new domain directly through Comwit Cloud is **designed but not yet implemented**. The routes below are part of the planned contract and are not live today. To use a domain right now, register it with any registrar and then [bring your own domain](/domains/bring-your-own-domain/). The plan is to let you search for an available domain, check its price, and order it without leaving Comwit. The designed (but not yet shipped) API routes are: Planned API routes ```txt GET /v1/domain-availability?domain=example.com GET /v1/domain-prices?tld=com POST /v1/projects/{projectId}/domain-orders GET /v1/projects/{projectId}/domain-orders/{orderId} ``` How these are intended to work: * `GET /v1/domain-availability` — check whether a specific domain can be registered. * `GET /v1/domain-prices` — look up pricing for a top-level domain (for example, `com`). * `POST /v1/projects/{projectId}/domain-orders` — place an order to register a domain for a project. * `GET /v1/projects/{projectId}/domain-orders/{orderId}` — check the status of a placed order. These routes will wrap **Route 53 Domains** and return an asynchronous `domain_order` resource rather than pretending registration happens instantly — registrar orders take time to complete, so you will poll the order status route until it settles. ## Related [Section titled “Related”](#related) * [Domains overview](/domains/overview/) * [Bring your own domain](/domains/bring-your-own-domain/) * [DNS records](/domains/dns-records/) * [Connect a domain to an app](/domains/connect-domain-to-app/) # Core concepts > A plain-language glossary of Comwit Cloud — projects, databases, apps, builds, hostnames, domains, tokens, and scopes. Comwit Cloud is a project workspace that wraps two systems behind one product API and console: **databases** (libSQL, reachable at `https://db.cloud.comwit.io`) and **runtime apps** (Next/V8 isolate apps). This page defines the handful of terms you’ll meet everywhere else in these docs, so the rest reads smoothly. You can drive the platform three ways — the web console at `https://cloud.comwit.io`, the `comwit` CLI, and the public platform API at `https://api.cloud.comwit.io`. The CLI and console both call the same platform API, so anything the CLI does, you can do with a raw HTTP call using a token. ## Project [Section titled “Project”](#project) A **project** is the workspace that owns everything else. It groups its databases, apps, and domains, and your access is scoped per project. A project is identified by its `project_id`, and your token grants access to specific projects. Note When you log in with the CLI you can pin a default project with `comwit login --project `, so you don’t have to pass `--project` on every command. See [Install the CLI](/get-started/install-cli/). ## Database [Section titled “Database”](#database) A **database** is a libSQL database (powered by Louhi), reachable at `https://db.cloud.comwit.io`. It is identified by a `database_id` and has a `database_url` you connect to. One-time tokens A database access token is returned **once**, at create or rotate time, and is **never stored by Comwit**. Copy it immediately and keep it safe — if you lose it, you must rotate to get a new one. See [Create and connect a database](/databases/create-and-connect/). ## App [Section titled “App”](#app) An **app** is a deployable runtime application (powered by brrrd), identified by an `app_id`. You create an app, deploy code to it, set environment variables, roll back, and attach hostnames to it. See [Apps overview](/apps/overview/). ## Build [Section titled “Build”](#build) A **build** is the artifact produced by a single deploy of an app, identified by a `build_id`. Each deploy produces a new build, and **the active build serves traffic**. Rolling back means making a previous build the active one again. See [Deploy an app](/apps/deploy/). ## Runtime app hostname [Section titled “Runtime app hostname”](#runtime-app-hostname) A **runtime app hostname** is a hostname attached to one app. There are two flavors, which differ only in who publishes the DNS records: * **Manual DNS** — Comwit returns the DNS records you need, and **you publish them yourself** at whatever DNS provider hosts that hostname. * **Automatic DNS** — Comwit **writes the records for you** into a project domain’s Route 53 zone, so there’s nothing for you to publish. See [Custom hostnames](/apps/custom-hostnames/). ## Project domain [Section titled “Project domain”](#project-domain) A **project domain** is a domain you already own that you’ve onboarded for **delegated DNS**, so Comwit can manage its Route 53 records. Onboarding a domain is what makes Automatic DNS hostnames possible, and it’s how you manage DNS records through Comwit. See [Domains overview](/domains/overview/) and [Bring your own domain](/domains/bring-your-own-domain/). ## Token [Section titled “Token”](#token) A **token** is how you authenticate every request to the platform API. A user token looks like `cwt_` — a personal access token tied to your user. It can only do what its **scopes** allow, and it can only touch **projects you are a member of**. You send it as a bearer token: Authorization header ```sh Authorization: Bearer cwt_xxx ``` Caution The internal `PLATFORM_API_SERVER_TOKEN` is an operator/web token used by the console itself. It is **never** a user CLI token — your token always starts with `cwt_`. See [Authentication](/api/authentication/). Get a token with `comwit login` (device flow) or by creating a personal access token in the console. Full walkthrough in [Install the CLI](/get-started/install-cli/). ## Scopes [Section titled “Scopes”](#scopes) **Scopes** are the permissions attached to a `cwt_` token. They decide which actions the token can perform; a request for an out-of-scope action (or a project you’re not a member of) returns `403`. | Scope | Grants | | ----------------- | --------------------------------------------- | | `project:read` | List projects you can access | | `database:read` | List/inspect project databases | | `database:write` | Create databases | | `app:read` | List/inspect apps, builds, env, domains | | `app:write` | Create/delete apps, set env, attach hostnames | | `app:deploy` | Deploy builds, roll back | | `domain:read` | List/inspect project domains and DNS records | | `domain:write` | Onboard domains, manage DNS records | | `domain:purchase` | Registrar purchase | Planned `domain:purchase` (registrar purchase) is **planned, not yet live**. Delegated DNS for a domain you already own is live; registrar purchase through Comwit is not. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Quickstart](/get-started/quickstart/) — token → database → app, end to end. * [Authentication](/api/authentication/) — how `cwt_` tokens and scopes work in detail. * [Databases overview](/databases/overview/) and [Apps overview](/apps/overview/). # Install the CLI & sign in > Get the comwit CLI onto your machine and sign in with a cwt_ token using device login or a dashboard personal access token. `comwit` is the command-line tool for Comwit Cloud. You use it to create databases, deploy apps, and manage domains from your terminal. Under the hood it only talks to the public platform API at `https://api.cloud.comwit.io` — it never touches your cloud provider, the database server, or the runtime fleet directly. This page gets you from nothing to a signed-in CLI. You will install the binary, sign in to get a token, and (optionally) pin a default project so you can stop typing `--project` on every command. ## Install the CLI [Section titled “Install the CLI”](#install-the-cli) The simplest way to get `comwit` is the install script: Install comwit ```sh scripts/install-comwit.sh ``` This installs the binary to `~/.local/bin/comwit`. Make sure `~/.local/bin` is on your `PATH` so you can run `comwit` from anywhere. How the installer chooses a binary The installer first tries to download a published GitHub release asset named like `comwit__.tar.gz` (for example `comwit_darwin_arm64.tar.gz`). Until releases exist for your platform, it automatically falls back to building the CLI from the current checkout. ### Installer options [Section titled “Installer options”](#installer-options) You can tune the installer with environment variables: Installer environment variables ```sh # Install a specific released version instead of the latest COMWIT_VERSION=v0.1.0 scripts/install-comwit.sh # Install to a different directory (default is ~/.local/bin) COMWIT_INSTALL_DIR=/usr/local/bin scripts/install-comwit.sh # Pull release assets from a specific GitHub repository COMWIT_INSTALL_REPO=comwit/comwit-cloud scripts/install-comwit.sh ``` ### Build it yourself [Section titled “Build it yourself”](#build-it-yourself) If you are working from a checkout of the repository and want to build the CLI directly, use the `just` recipes: Build from source ```sh just cli-test # run the CLI test suite just cli-build # build the binary into ./dist ./dist/comwit version ``` The CLI source lives in `tools/comwit`. ## Sign in [Section titled “Sign in”](#sign-in) Every command authenticates to the platform API with a bearer token. As an end user you always use a **`cwt_` personal access token** — a scoped token tied to your user that can only act on projects you are a member of. There are two ways to get signed in. `PLATFORM_API_SERVER_TOKEN` is not your token `PLATFORM_API_SERVER_TOKEN` is an internal server/operator token used by the console itself. It bypasses scope and membership checks and is **not** a user token. Never put it in your CLI config, scripts, or issue reports. Your token always starts with `cwt_`. ### Option A — device login (recommended) [Section titled “Option A — device login (recommended)”](#option-a--device-login-recommended) Run `comwit login` with no token to start the device authorization flow: Sign in with device login ```sh comwit login ``` What happens: 1. The CLI requests a short code and opens your browser. 2. You approve the code while signed in to the console at . 3. The CLI receives your `cwt_` token and saves it to your config file. That’s it — no copying and pasting tokens. You can pin a default project at the same time: Device login and pin a project ```sh comwit login --project ``` ### Option B — dashboard personal access token [Section titled “Option B — dashboard personal access token”](#option-b--dashboard-personal-access-token) If you’d rather create the token yourself (for example for CI, or to control its scopes), generate one from the console: 1. Go to **API tokens** in the console at . 2. Create a token. The `cwt_` plaintext is shown **once** at creation — copy it then. 3. Hand it to the CLI: Sign in with an existing token ```sh comwit login --token cwt_xxx --project ``` You can revoke a token any time from the same **API tokens** page. Scopes A `cwt_` token only does what its scopes allow (for example `database:read`, `app:deploy`, `domain:write`) and can only touch projects you can access. Actions outside your scope or on a project you are not a member of return `403`. See [API authentication](/api/authentication/) for the full scope table. ## Configuration and environment [Section titled “Configuration and environment”](#configuration-and-environment) The CLI stores your token and default project in a config file: \~/.config/comwit/config.json ```txt { "token": "cwt_...", "default_project": "" } ``` You can influence where the CLI reads its token, which project it targets, and which API it calls with environment variables: | Variable | Purpose | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `COMWIT_CONFIG` | Path to the config file. If unset, the CLI uses `$XDG_CONFIG_HOME/comwit/config.json` when `XDG_CONFIG_HOME` is set, otherwise `~/.config/comwit/config.json`. | | `COMWIT_PROJECT` | Default project for commands that accept `--project`. | The CLI resolves the **project** for a command in this order: 1. The `--project` flag. 2. The `COMWIT_PROJECT` environment variable. 3. The `default_project` saved in your config file. The API host always defaults to `https://api.cloud.comwit.io`. ### Pin a default project [Section titled “Pin a default project”](#pin-a-default-project) If most of your work happens in one project, pin it once so you can drop the `--project` flag from individual commands. The easiest way is during login: Pin a default project at login ```sh comwit login --project ``` You can also pin it for a single shell session with the environment variable: Override the project for one session ```sh export COMWIT_PROJECT= comwit databases list ``` ## Verify it works [Section titled “Verify it works”](#verify-it-works) Once you’re signed in, list the databases in your project to confirm everything is wired up: Smoke test ```sh comwit databases list --project ``` If you pinned a default project, you can drop the flag entirely: ```sh comwit databases list ``` If you get a `403`, the token’s scopes or your project membership don’t cover the action — see [API authentication](/api/authentication/). ## Next steps [Section titled “Next steps”](#next-steps) * [Create your first database](/databases/create-and-connect/) * [Deploy an app](/apps/deploy/) * [Connect a domain](/domains/bring-your-own-domain/) * [CLI reference](/reference/cli/) # Quickstart > Go from nothing to a deployed app with a database and a custom hostname using the comwit CLI. This walkthrough takes you from an empty account to a running app — with its own database and, optionally, a custom domain. Comwit Cloud groups your resources under a **project**: databases, runtime apps, and domains all live inside one. Everything here uses the `comwit` command-line tool (CLI), and every step maps to a public API call you could script instead. The CLI talks to the API host at `https://api.cloud.comwit.io`; the console is at `https://cloud.comwit.io`. Before you start You’ll need the CLI installed and a project to work in. The [Install the CLI](/get-started/install-cli/) guide covers setup; this page assumes you’ve finished it. 1. ### Install and log in [Section titled “Install and log in”](#install-and-log-in) Install the CLI, confirm it runs, then sign in. `comwit login` uses a device flow: it opens your browser and asks you to enter a short code. Install and authenticate ```sh scripts/install-comwit.sh # builds/installs comwit to ~/.local/bin comwit version comwit login # device flow: opens browser, enter the code ``` After login, your token is stored at `~/.config/comwit/config.json`. Pin a default project so you can leave off `--project` in later commands: Pin a default project ```sh comwit login --project # or, with an existing token: comwit login --token cwt_xxx --project ``` User tokens always start with `cwt_`. Don’t have your project id handy? List your projects: Find your project id ```sh comwit projects list ``` See [Authentication](/api/authentication/) for scopes and token details. 2. ### Create a database [Section titled “Create a database”](#create-a-database) Each app usually needs somewhere to store data. Create a database in your project: Create a database ```sh comwit databases create --project --name app-db ``` The output includes a `DATABASE_ID`, a `DATABASE_URL`, and — when one is issued — a one-time `token`. Copy the token now The database token is shown **once** and is never stored by Comwit. Copy it immediately; if you lose it you’ll have to rotate the credential. Your app connects using the URL plus this token. The connection URL points at the database data endpoint: DATABASE\_URL ```txt DATABASE_URL = https://db.cloud.comwit.io/v1/ ``` See [Create & connect a database](/databases/create-and-connect/) for connecting, and [Run SQL](/databases/run-sql/) for querying. 3. ### Create a runtime app [Section titled “Create a runtime app”](#create-a-runtime-app) A **runtime app** is the deploy target that serves your code. Create one: Create an app ```sh comwit apps create --project --name web ``` The output includes an `APP_ID`. Set any environment variables your app needs (plain string values only) — for example, point your app at the database by setting `DATABASE_URL`. Environment is set through the API or the console: Set an environment variable (API) ```http PUT /v1/projects/{projectId}/apps/{appId}/environment/DATABASE_URL ``` See [Configure environment & secrets](/apps/environment/) for the full flow. 4. ### Deploy a build [Section titled “Deploy a build”](#deploy-a-build) Now ship your built code. `--package` accepts a built directory (which the CLI auto-packs to `.tar.zst`) or a prebuilt `.tar.zst` archive. Deploy ```sh comwit deploy --project --app --package ./dist ``` The deploy uploads the artifact and activates a new `build_id`. Useful optional flags: * `--host a.example.com,b.example.com` — attach hostnames during deploy * `--env-ref ` — pin a specific environment reference * `--max-concurrent-requests ` — cap concurrency Review history (and roll back if you need to) with: List builds ```sh comwit apps builds --project --app ``` See [Deploy an app](/apps/deploy/) for deploys, rollbacks, and build history. 5. ### (Optional) Bring a domain and attach a hostname [Section titled “(Optional) Bring a domain and attach a hostname”](#optional-bring-a-domain-and-attach-a-hostname) If you own a domain, you can onboard it for delegated DNS. Add it to your project: Add a domain ```sh comwit domains add --project --domain example.com ``` This returns a set of Route 53 **nameservers**. Update the nameservers at your registrar, then confirm the delegation took effect: Check delegation ```sh comwit domains check --project --domain example.com ``` Once the check reports `matched: true`, the domain is `managed`. You can then attach a hostname to your app. Under a managed project domain you can use **Automatic DNS** (the API sets `dns_mode: "managed_project_domain"`) so Comwit writes the records for you; otherwise you get **Manual DNS** records to publish yourself. See [Bring your own domain](/domains/bring-your-own-domain/) and [Connect a domain to an app](/domains/connect-domain-to-app/). ## What you just used [Section titled “What you just used”](#what-you-just-used) Every CLI command above maps to a public API route, so you can automate the same flow from a script or your CI: | Step | CLI | API route | | ---------------- | ------------------------- | -------------------------------------------------------- | | Log in | `comwit login` | `POST /v1/auth/device`, `.../device/token` | | List projects | `comwit projects list` | `GET /v1/projects` | | Create DB | `comwit databases create` | `POST /v1/projects/{projectId}/databases` | | Create app | `comwit apps create` | `POST /v1/projects/{projectId}/apps` | | Deploy | `comwit deploy` | `POST /v1/projects/{projectId}/apps/{appId}/deployments` | | Add domain | `comwit domains add` | `POST /v1/projects/{projectId}/domains` | | Check delegation | `comwit domains check` | `POST .../domains/{domain}/delegation-check` | Next steps Ready to go deeper? Explore [Databases](/databases/overview/), [Apps](/apps/overview/), and [Domains](/domains/overview/), or learn to drive everything from the [API](/api/overview/). # What is Comwit Cloud? > A newcomer's overview of Comwit Cloud — projects, databases, runtime apps, and domains behind one product API, console, and CLI. Comwit Cloud is a project workspace that wraps two systems behind one product API and console. Instead of stitching together a database service, an app runtime, and a DNS provider yourself, you work with all three from a single place — a web console, a `comwit` CLI, or a public HTTP API — with access scoped to your project. If you have never used Comwit Cloud before, this page explains what it is and how its pieces fit together. Later pages show the exact commands. ## What Comwit Cloud gives you [Section titled “What Comwit Cloud gives you”](#what-comwit-cloud-gives-you) Everything you build lives inside a **project**. A project groups its databases, apps, and domains together, and your access is scoped per project. Within a project you get three capabilities: * **Databases** — Turso/libSQL databases (powered by Louhi), reachable at `https://db.cloud.comwit.io`. You can create a database, connect to it, run SQL, rotate its tokens, and view usage. * **Runtime apps** — Next/V8 isolate apps (powered by brrrd). You can create an app, deploy a build, set environment variables, roll back to a previous build, and attach hostnames. * **Project domains & DNS** — bring a domain you already own under Route 53 delegated DNS, manage its records, and attach hostnames to your runtime apps. Note You don’t talk to Louhi or brrrd directly. Comwit Cloud exposes a single, product-shaped API in front of both, so the underlying systems can evolve without changing how you use the platform. ## How the pieces relate [Section titled “How the pieces relate”](#how-the-pieces-relate) A project is the top-level container. Under it: * A **database** is a libSQL database, identified by a `database_id` and reached through a `database_url`. Its access tokens are returned to you **once**, at create or rotate time, and are never stored by Comwit — so save them when you see them. * An **app** is a deployable runtime application, identified by an `app_id`. Each deploy produces a **build** (a `build_id`), and the active build is the one serving traffic. * A **project domain** is a domain you own that has been onboarded for delegated DNS, so Comwit can manage its Route 53 records on your behalf. * A **runtime app hostname** is a hostname attached to one app. It comes in two flavors: *Manual DNS*, where you publish the returned records yourself, and *Automatic DNS*, where Comwit writes the records into a project domain’s Route 53 zone for you. You authenticate with a **token** — a `cwt_` personal access token that is constrained to specific scopes and to your projects. See [Concepts](/get-started/concepts/) for the full glossary. Caution The internal `PLATFORM_API_SERVER_TOKEN` is an operator/web token used inside the platform. It is never a user CLI token. As a platform user, your tokens always start with `cwt_`. ## Three ways to drive the platform [Section titled “Three ways to drive the platform”](#three-ways-to-drive-the-platform) There are three surfaces, and they all sit in front of the same public platform API. Pick whichever fits the task. | Surface | Host | Best for | | ------------ | ----------------------------- | ---------------------------------------------- | | Web console | `https://cloud.comwit.io` | Interactive use, SQL console, first-time setup | | `comwit` CLI | calls `api.cloud.comwit.io` | Day-to-day dev, deploys, scripting | | Platform API | `https://api.cloud.comwit.io` | Automation, CI, custom integrations | The CLI and console both call the same public **platform API**. Anything the CLI can do, you can also do with a raw HTTP call using a token — so you can start in the console, automate with the CLI, and integrate directly against the API as your needs grow. Tip New to the platform? Start in the **web console** to get oriented, then move to the **CLI** for everyday work once you’re comfortable. ## Good to know up front [Section titled “Good to know up front”](#good-to-know-up-front) A few platform behaviors are worth knowing before you dig in: * App environment variables are **plain values only**. Marking a variable as `secret: true` is rejected, because Secrets Manager is not enabled for app environment variables. * Delegated DNS for a domain you **already own** is live. Buying a domain through a registrar is planned, not yet live. Planned, not yet live Domain **purchase** (registrar ordering) is planned and not available yet. You can onboard and manage a domain you already own today. ## Where to next [Section titled “Where to next”](#where-to-next) * [Quickstart](/get-started/quickstart/) — go from a token to a database, an app, and a domain, end to end. * [Install the CLI](/get-started/install-cli/) — set up the `comwit` CLI for day-to-day work. * [Concepts](/get-started/concepts/) — projects, databases, apps, builds, domains, hostnames, and tokens explained in one place. # From Cloudflare Workers + D1 > Migrate a Next.js app from Cloudflare Workers/Pages + D1 onto the Comwit Cloud brrrd runtime and a Louhi libSQL database — runtime swap, database client swap, and D1 → libSQL data move. This is the full path for a Next.js app running on **Cloudflare Workers/Pages** (typically via OpenNext) with a **D1** database. You will move the runtime to brrrd, the database to Louhi (libSQL), and your data from D1 to Louhi. It is written as a runbook a coding agent can execute. In practice the Cloudflare coupling is small and usually lives in **one file** (your database client). The steps below are the validated path used to migrate real apps onto the platform. ## 1. Switch the runtime to brrrd [Section titled “1. Switch the runtime to brrrd”](#1-switch-the-runtime-to-brrrd) Do the runtime work from the [Next.js guide](/migrate/from-nextjs/) first: * `pnpm add @brrrd/adapter`; set `adapterPath: require.resolve("@brrrd/adapter")` in `next.config`. * **Remove the OpenNext / `@opennextjs/cloudflare` setup** (the init call, `wrangler` deploy wiring, `open-next.config.ts`). * Upgrade to **Next ^16.2 / React 19** if you are not there yet. * Apply the [runtime checklist](/migrate/from-nextjs/#4-apply-the-runtime-checklist) (OG routes → `runtime = "edge"`, middleware matcher with no look-around, etc.). * Build with `next build --webpack` → `dist/brrrd`. ## 2. Swap the database client (D1 → libSQL) [Section titled “2. Swap the database client (D1 → libSQL)”](#2-swap-the-database-client-d1--libsql) Find your D1 client — usually `src/server/db/index.ts` — and replace the Cloudflare/D1 wiring with a libSQL client. Keep the **same exported names** so the rest of your app doesn’t change. Before — D1 on Cloudflare ```ts import { drizzle } from "drizzle-orm/d1"; import { getCloudflareContext } from "@opennextjs/cloudflare"; export const getDb = async () => { const { env } = await getCloudflareContext({ async: true }); return drizzle(env.DB, { schema }); }; ``` After — libSQL on Louhi ```ts import "server-only"; import { type Client as LibsqlClient } from "@libsql/client/web"; import { createClient } from "@tursodatabase/serverless/compat"; import { drizzle } from "drizzle-orm/libsql/web"; import * as schema from "./schema"; export const getDb = async () => { const client = createClient({ url: process.env.DATABASE_URL!, authToken: process.env.DATABASE_AUTH_TOKEN!, }); // Create a fresh client per request: the serverless compat client serializes // work internally, so one long-lived instance can stall unrelated requests. return drizzle({ client: client as unknown as LibsqlClient, schema }); }; ``` Install / remove deps ```sh pnpm add @tursodatabase/serverless @libsql/client drizzle-orm pnpm remove @opennextjs/cloudflare # and any wrangler-only D1 deps ``` Your Drizzle **schema** is portable as-is — only the client changes. ## 3. Create the Louhi database [Section titled “3. Create the Louhi database”](#3-create-the-louhi-database) Create the database ```sh comwit databases create --project --name app-db ``` Copy the one-time `database_token` and note the `database_url` — these become `DATABASE_AUTH_TOKEN` and `DATABASE_URL`. ## 4. Create the schema on Louhi [Section titled “4. Create the schema on Louhi”](#4-create-the-schema-on-louhi) For a **fresh database (no data to move)**, push your final schema directly — this is more reliable than replaying a D1 migration history, because a few D1 migration statements don’t translate to libSQL (see the caveats below): Push the schema ```sh DATABASE_URL=... DATABASE_AUTH_TOKEN=... pnpm drizzle-kit push --force ``` drizzle-kit + Louhi: pipeline version `drizzle-kit` talks to the libSQL HTTP `/v2/pipeline` endpoint, while Louhi exposes `/v3/pipeline`. If `drizzle-kit` can’t connect, add a small fetch-proxy shim that rewrites the path to `/v3/pipeline`, imported by your `drizzle.config.ts` (and inlined into any ad-hoc migration script). This is a one-time wiring detail, not a per-deploy step. ## 5. Move your data (only if you have data to keep) [Section titled “5. Move your data (only if you have data to keep)”](#5-move-your-data-only-if-you-have-data-to-keep) Export from D1, sanitize for libSQL, then import in chunks. Export from D1 ```sh wrangler d1 export --remote --output d1-dump.sql ``` libSQL is stricter than D1, so **strip these from the dump** before importing — they will otherwise be rejected: * `PRAGMA defer_foreign_keys = ...;` * `DELETE FROM sqlite_sequence;` * `DROP INDEX` statements for **UNIQUE / primary-key constraint-backed indexes** (libSQL rejects dropping a constraint’s backing index). Then import. Two Louhi behaviors shape how: * Foreign-key enforcement is **connection state** that may be on, and that state isn’t guaranteed to carry across separate HTTP calls. So wrap the whole load in a single `executeMultiple` that begins with `PRAGMA foreign_keys=OFF;` — it is a safe no-op if FKs were already off, and keeps the disable in the same connection as the inserts. * A single very large `executeMultiple` (\~1.5 MB+) can fail at the edge (observed **502**). **Chunk** the statements (≈100 per batch). import.ts (sketch) ```ts import { createClient } from "@tursodatabase/serverless/compat"; const client = createClient({ url: process.env.DATABASE_URL!, authToken: process.env.DATABASE_AUTH_TOKEN!, }); // quote-aware split of the sanitized dump into statements, then: for (const batch of chunk(statements, 100)) { await client.executeMultiple("PRAGMA foreign_keys=OFF;\n" + batch.join("\n")); } ``` ## 6. Set environment and deploy [Section titled “6. Set environment and deploy”](#6-set-environment-and-deploy) Configure and deploy ```sh comwit apps create --project --name web # Set DATABASE_URL and DATABASE_AUTH_TOKEN (plain values; no secret store yet): # PUT /v1/projects/{projectId}/apps/{appId}/environment/DATABASE_URL { "value": "...", "secret": false } # PUT /v1/projects/{projectId}/apps/{appId}/environment/DATABASE_AUTH_TOKEN { "value": "...", "secret": false } comwit deploy \ --project \ --app \ --package ./dist/brrrd \ --host app.example.com ``` Plain env only App environment values are stored as plain text today (no secret backend). The Louhi token is sensitive — set it with that in mind. See [Environment variables](/apps/environment/). ## 7. Verify [Section titled “7. Verify”](#7-verify) * App renders on its hostname (test pages in a browser — isolate-only runtime errors pass the build). * Database reads/writes work against Louhi. * If you moved data, spot-check row counts and a few records. ## Common Cloudflare-specific gotchas, recapped [Section titled “Common Cloudflare-specific gotchas, recapped”](#common-cloudflare-specific-gotchas-recapped) | Symptom | Cause | Fix | | --------------------------------- | ------------------------------------------------------------------------------------- | --------------------------------------------------------- | | `503 … look-around not supported` | Middleware matcher uses `(?!…)` | Match-all matcher + filter in the function | | OG/icon route 500s | `next/og` pulls native `sharp` | `export const runtime = "edge"` + `images.unoptimized` | | `502` on data import | One oversized `executeMultiple` (\~1.5 MB+) | Chunk to \~100 statements | | Import rejected | D1-only statements (`defer_foreign_keys`, `sqlite_sequence`, constraint index `DROP`) | Strip them from the dump | | FK errors mid-import | FK enforcement may be on; state isn’t guaranteed across calls | One `executeMultiple` starting `PRAGMA foreign_keys=OFF;` | Once it’s live, automate redeploys from [GitHub Actions](/ci-cd/github-actions/). # From Next.js (15 & 16) > Host an existing Next.js app on the Comwit Cloud brrrd runtime using @brrrd/adapter — including the Next 15 → 16 upgrade and the V8-isolate runtime gotchas. This guide takes a standard Next.js app and runs it on the **brrrd** runtime. It is the runtime half of a migration; if your data is in Cloudflare D1, do the [Cloudflare guide](/migrate/from-cloudflare/) instead (it includes this runtime work plus the database move). Next version requirement The `@brrrd/adapter` peer dependency is **Next ^16.2**. If you are on Next 15 or an older 16.x, [upgrade first](#next-15--upgrade-to-16) — it is usually a small change (and a bump to React 19). ## 1. Add the brrrd adapter [Section titled “1. Add the brrrd adapter”](#1-add-the-brrrd-adapter) Install ```sh pnpm add @brrrd/adapter ``` Wire it into your Next config via Next’s `adapterPath`. If your config is ESM (`next.config.ts` / `.mjs`), resolve the adapter with `createRequire`: next.config.ts ```ts import type { NextConfig } from "next"; import { createRequire } from "node:module"; const require = createRequire(import.meta.url); const nextConfig: NextConfig = { adapterPath: require.resolve("@brrrd/adapter"), // If you talk to libSQL over websockets, keep the ws client external: serverExternalPackages: ["@libsql/isomorphic-ws"], }; export default nextConfig; ``` Remove any other hosting adapter (e.g. OpenNext / `@opennextjs/cloudflare` init) — brrrd is now your target. ## 2. Build [Section titled “2. Build”](#2-build) Build for brrrd ```sh next build --webpack ``` The adapter emits a deployable package at **`dist/brrrd`**. That directory is what you hand to `comwit deploy`. ## 3. (If you need a database) create one on Louhi [Section titled “3. (If you need a database) create one on Louhi”](#3-if-you-need-a-database-create-one-on-louhi) Create a Louhi database ```sh comwit databases create --project --name app-db ``` Copy the one-time `database_token` from the output (it is shown once). Your app connects with the returned `database_url` + token — see [Create & connect a database](/databases/create-and-connect/). You will set these as environment variables in step 5. ## 4. Apply the runtime checklist [Section titled “4. Apply the runtime checklist”](#4-apply-the-runtime-checklist) brrrd runs your app in V8 isolates. These are the constraints that actually bite, each with a one-line fix. Skim them now — they are cheaper to fix before the first deploy than to debug after. * **No native `.node` addons.** `next/og`’s `ImageResponse` pulls in `sharp` on the Node runtime, which isolates can’t load. Make OG/icon routes run on the edge runtime (a WASM encoder is used instead): app/opengraph-image.tsx (and icon routes) ```ts export const runtime = "edge"; ``` `@brrrd/adapter` already forces `images.unoptimized` for you (there is no `sharp` in the isolate), so you don’t strictly need to set it — adding `images: { unoptimized: true }` yourself is harmless and just makes the intent explicit. * **Middleware matchers: no regex look-around.** brrrd’s matcher engine rejects negative lookahead like `/((?!api|_next/static).*)` (you’ll see *“look-around not supported”*). Use a match-all matcher and filter inside the function: middleware.ts ```ts export const config = { matcher: "/:path*" }; // …then early-return for paths you want to skip inside middleware(). ``` * **30s request wall-clock.** A single request may run up to \~30s. Move anything longer (heavy jobs, long streams) off the request path. * **`public/` assets are served** by the adapter, with correct MIME types for common file types (images, video, audio, fonts, PDF, CSV, ZIP…) and HTTP Range support, so `