Reference
Everything you can configure in Preview, grounded in the real CLI flags, env templates, and configuration.
dst preview is the supported interface. All subcommands read DESTESI_API_KEY and DESTESI_WORKSPACE from the environment for stateless CI use, falling back to the signed-in CLI session and active workspace.
| Command | Purpose |
|---|---|
dst preview create |
Register a new preview (local directory or GitHub source). |
dst preview upsert |
Idempotent create/redeploy keyed by (workspace, pr_number) — use in CI. |
dst preview wait |
Block until healthy (exit 0) / failed (1) / timeout (2). |
dst preview show |
Show details by id or --by-pr <n>. |
dst preview list |
List previews in the active workspace. |
dst preview logs |
Stream container logs. |
dst preview events |
List recent events for a preview (newest first). |
dst preview trace |
Unified status + events + log-tail view (start here when debugging). |
dst preview redeploy |
Tear down and rebuild in place (keeps the same id). |
dst preview delete |
Delete a preview; the controller cleans up the pod and forked branch. |
dst preview create
Section titled “dst preview create”Register a preview. Use --from-dir for a local directory or --repo for a GitHub source.
dst preview create \ --from-dir ./app \ --name local-test \ --from-remote conn_abc \ --env 'DATABASE_URL=${postgres.dsn}' \ --env-secret NEXTAUTH_SECRET=$(openssl rand -hex 32) \ --public-service app --public-port 3000 \ --ttl 24hKey flags:
--from-dir <path>— tar a local directory containing a compose file and upload it.--repo <owner/name>— pull a GitHub repository. Requires--third-party-app <id>, which takes your workspace’s Connect GitHub connection id. Preview fetches the actual token from Connect by workspace at fetch time, so the id names which connection you mean rather than carrying any credential itself.--branch <name>/--commit <sha>— branch and commit to pin the preview to.--from-remote <name|id>— Snap remote to fork the database from, by name or id.--from-snap <id>— fork from a specific Snap branch (power users; prefer--from-remote).--env NAME=value/--env-secret NAME=value— repeatable. Values may contain${postgres.*}references.--env-secretis encrypted at rest and redacted in the dashboard and API responses.--public-service <name>/--public-port <n>— which compose service and port is exposed. Auto-detected from the compose file when omitted.--ttl <duration>— auto-teardown after this duration. Default24h.
dst preview upsert
Section titled “dst preview upsert”Same source/env flags as create, plus pull-request metadata. Keyed by (workspace, pr_number), so calling it on every synchronize event redeploys the same preview instead of accumulating new ones — this is the command to use in CI.
dst preview upsert \ --repo owner/repo \ --branch feat/x --commit "$SHA" \ --third-party-app "$DESTESI_GITHUB_APP" \ --pr-number 42 \ --pr-title "Add export button" \ --pr-url https://github.com/owner/repo/pull/42 \ -o idFlags beyond create:
--pr-number <n>— required; the idempotency key.--pr-title <s>/--pr-url <s>— used as display metadata.--pr-body <s>/--pr-body-file <path>— PR body (use the file form for multi-line content).--dev-mode— require adocker-compose.dev.ymlin the repo and run the app with hot reload.
dst preview wait
Section titled “dst preview wait”Blocks until the preview reaches a terminal state. The exit code is the outcome — designed for CI gating.
dst preview wait prev_abc --timeout 10m# → https://prev-abc.your-workspace.preview.destesi.ioThe public URL is derived from the preview’s id and workspace (https://<preview-id>.<workspace>.preview.destesi.io, with underscores in the id rendered as hyphens); locally it’s http://localhost:<nodeport>.
- Exit 0 — healthy. Stdout is the public URL.
- Exit 1 — failed. Stderr carries the last error.
- Exit 2 —
--timeoutelapsed while still building. The preview may still reach healthy later. - Exit 3 — the preview was deleted while waiting.
Flags: --timeout <duration> (default 10m), --interval <duration> (poll cadence, default 3s).
dst preview show / list
Section titled “dst preview show / list”dst preview show prev_abc -o url # → the public URLdst preview show --by-pr 42 -o id # look up by PR numberdst preview list --status healthy # filter by statusshow accepts a preview id positionally or --by-pr <n>. Output format is -o json|id|url|status. --workspace-id <id> scopes a --by-pr lookup to a specific workspace (defaults to active).
dst preview logs / events / trace
Section titled “dst preview logs / events / trace”dst preview logs prev_abc --container service:app --tail 200 -fdst preview events prev_abc --limit 50dst preview trace prev_abc # status + events + log tail in one viewlogs flags:
--container <source>— one ofall(default),runner,dind,destesi-overlay, orservice:<name>.--tail <n>— lines to print before streaming (default200;0for the full buffer up to the server cap).--follow/-f— follow new output. Defaults to true on a TTY, false otherwise.
trace is the fastest way to see what a preview is doing: it prints status, recent events, and a log tail in one shot. Add -f to keep streaming.
dst preview redeploy / delete
Section titled “dst preview redeploy / delete”dst preview redeploy prev_abc # same id, fresh pod + branch forkdst preview delete --by-pr 42 # delete; controller cleans up pod + branchdelete accepts an id positionally or --by-pr <n>.
Environment templates
Section titled “Environment templates”Preview resolves ${namespace.field} references server-side against the forked Snap branch’s endpoints, just before the app boots. Declared values come from your CLI flags, a committed .destesi.yml, or the dashboard env editor.
Available references
Section titled “Available references”| Reference | Resolves to |
|---|---|
${postgres.dsn} |
Full connection string (scheme + user + password + host + port + database). |
${postgres.host} |
Branch host. In-cluster Service address for the running app. |
${postgres.port} |
Branch port. |
${postgres.user} |
Branch user. |
${postgres.password} |
Branch password. |
${postgres.database} |
Branch database name. |
${postgres.sslmode} |
sslmode. |
${s3.*}, ${preview.*} |
Reserved; currently unresolved (future release). |
- Template syntax is
${namespace.field}— dotted names, not underscores. - Literals and multiple references compose:
${postgres.user}:${postgres.password}@${postgres.host}. - Resolved values are not rescanned — there is no recursive templating.
- An unknown namespace or field fails the preview with an
EnvErrorinstead of producing an empty string. - Mark a value secret (
--env-secret, orsecret: truein.destesi.yml) to redact it from the dashboard and API responses. - Declared env wins: the runner generates a
docker-compose.override.ymlthat injects every declared variable into every service, so you don’t have to thread${VAR}through each service’senvironment:block. - The connection string the app receives is rewritten from the host-facing form to the in-cluster Service address automatically, so a containerized app always gets a reachable value.
Declaring env in .destesi.yml
Section titled “Declaring env in .destesi.yml”Commit the env list to your repo so an env change ships and reviews in the same diff. The Destesi GitHub App reads .destesi.yml from the pull request’s head commit:
version: 1
preview: enabled: true env: - name: DATABASE_URL value: ${postgres.dsn} - name: APP_ENV value: preview - name: STRIPE_SECRET_KEY value: sk_test_xxx secret: true| File state | Effect on the preview’s env |
|---|---|
| absent | Dashboard env survives. |
present, enabled: false |
Parsed but ignored. Dashboard env survives. |
present, enabled: true, non-empty env |
Dashboard env is overwritten with the file’s env. |
present, enabled: true, empty env |
Dashboard env is cleared. |
| present but malformed | The build fails loud. Fix the file before the next push. |
Variable names must match [A-Z_][A-Z0-9_]*, with no duplicates. The file is read from the PR branch — consistent with Preview’s “what the PR proposes is what deploys” model.
The compose file
Section titled “The compose file”Preview runs your docker-compose stack inside each preview pod. It selects the first file it finds, in this precedence order:
docker-compose.destesi.yml— preview-specific; wins whenever present.docker-compose.dev.yml— bind-mounted source for hot reload.docker-compose.yml— fallback.
Local docker compose up keeps using your own files unchanged; Preview only reads .destesi.yml files when present.
Writing a preview-specific compose file
Section titled “Writing a preview-specific compose file”Because Preview provides the database via a Snap branch, a preview-specific file should strip the local database so your app uses the branch instead:
Keep: your app service(s), bind mounts for hot reload, the app’s own healthcheck (the runner uses it to decide readiness), non-database sidecars (caches, queues), and ${DATABASE_URL} interpolation.
Strip: database services and their named volumes, depends_on: entries pointing at them, healthchecks that waited on them, and any hardcoded DATABASE_URL (replace with ${DATABASE_URL}) or NEXTAUTH_URL (the overlay sets it per-preview).
services: app: image: node:22-alpine working_dir: /app command: sh -c "npm ci && npx prisma db push --skip-generate --accept-data-loss && exec npm run dev" ports: - "8080:3000" environment: DATABASE_URL: ${DATABASE_URL} # injected by Preview from the Snap branch NEXTAUTH_SECRET: ${NEXTAUTH_SECRET:-dev-secret-change-me} volumes: - ./:/app - /app/node_modules healthcheck: test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/api/health"] interval: 5s timeout: 3s retries: 40OAuth passthrough
Section titled “OAuth passthrough”OAuth providers require a pre-registered, exact-match redirect_uri, but previews run on ephemeral hosts. The overlay rewrites outbound redirects to known authorize endpoints so a single pre-registered callback serves every preview URL.
| Provider | Authorize host & path |
|---|---|
accounts.google.com/o/oauth2/v2/auth |
|
| GitHub | github.com/login/oauth/authorize |
| Microsoft | login.microsoftonline.com/common/oauth2/v2.0/authorize |
| Slack | slack.com/oauth/v2/authorize |
The overlay wraps the app’s original state in a signed envelope on the outbound authorize redirect. The redirect_uri itself points at the central callback because the runner sets the app’s AUTH_REDIRECT_PROXY_URL to the Destesi /v1/oauth base (Auth.js v5 appends /callback/<provider>). After the provider authenticates, the central service unwraps the envelope and returns the browser to the preview’s real /api/auth/callback/<provider>. Other providers pass through unchanged.
For Auth.js / NextAuth apps: set trustHost: true and do not set NEXTAUTH_URL — hardcoding it pins one host and breaks previews. Passthrough is active only when the overlay’s OAuth state secret is configured.
The overlay
Section titled “The overlay”Injected at /.destesi/* in front of every preview. No install — it’s part of every preview.
| Surface | Shortcut | What it does |
|---|---|---|
| Change-summary modal | — | First-load dialog (per commit): PR title, AI summary, files changed. |
| Preview agent | Cmd+Shift+A |
Reads, edits, git commits and git pushes back to the PR branch. |
| Pod logs | Cmd+Shift+L |
Live tail of the runner and compose stack. |
| Inspect element | Cmd+Shift+I |
Pick a DOM node to give the agent as context. |
| Network | Cmd+Shift+N |
Captured fetch/XHR from the app frame with status and timing. |
Preview-agent tools: list_files, read_file (up to 50 KB), edit_file (unique-match string replacement), git_diff, git_commit, git_push.
GitHub Actions workflow
Section titled “GitHub Actions workflow”A preview per pull request, driven from your repo’s CI. The GitHub connection lives in Connect; CI authenticates with a preview-runner key.
name: Preview environment
on: pull_request: types: [opened, synchronize, reopened, closed]
concurrency: group: preview-${{ github.event.pull_request.number }} cancel-in-progress: true
jobs: preview: if: github.event.action != 'closed' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dst run: curl -fsSL https://get.destesi.io/install.sh | sh - name: Upsert preview env: DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }} DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }} PR_BODY: ${{ github.event.pull_request.body }} run: | printf '%s' "$PR_BODY" > pr-body.md dst preview upsert \ --repo ${{ github.repository }} \ --branch ${{ github.head_ref }} \ --commit ${{ github.event.pull_request.head.sha }} \ --third-party-app ${{ vars.DESTESI_GITHUB_APP }} \ --pr-number ${{ github.event.pull_request.number }} \ --pr-title "${{ github.event.pull_request.title }}" \ --pr-body-file pr-body.md \ --pr-url ${{ github.event.pull_request.html_url }} \ -o id > preview-id.txt dst preview wait "$(cat preview-id.txt)" --timeout 10m
- name: Comment on PR if: success() env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }} DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }} run: | URL=$(dst preview show --by-pr ${{ github.event.pull_request.number }} -o url) gh pr comment ${{ github.event.pull_request.number }} --body "Preview ready: $URL"
teardown: if: github.event.action == 'closed' runs-on: ubuntu-latest steps: - name: Install dst run: curl -fsSL https://get.destesi.io/install.sh | sh - name: Delete preview env: DESTESI_API_KEY: ${{ secrets.DESTESI_API_KEY }} DESTESI_WORKSPACE: ${{ vars.DESTESI_WORKSPACE }} run: | dst preview delete --by-pr ${{ github.event.pull_request.number }}- Store
DESTESI_API_KEYas a repository secret (mint it withdst api-key create --preview-runner). - Store
DESTESI_WORKSPACEand your GitHub app id as repository variables. upsertis keyed by(workspace, pr_number), and theconcurrencyblock cancels in-flight runs for the same PR, so repeatedsynchronizeevents redeploy in place rather than racing or accumulating previews.
CLI environment variables
Section titled “CLI environment variables”Read by dst for stateless / CI use:
| Variable | Purpose |
|---|---|
DESTESI_API_KEY |
API key. A preview-runner key is recommended for CI. |
DESTESI_WORKSPACE |
Workspace id or name to operate in (overrides the active workspace). |
Controller configuration
Section titled “Controller configuration”These configure the Preview controller deployment itself (self-hosted operators only; not needed to use a managed preview). Source of truth: apps/preview/api/internal/preview/preview.go (ConfigFromEnv).
| Variable | Default | Purpose |
|---|---|---|
DESTESI_API_URL |
http://localhost:9090 |
Destesi API base URL the controller polls. |
CONTROLLER_SECRET |
— | Shared secret for the /v1/controller/previews* routes. Unset on the API side → those routes 404. |
PREVIEW_POLL_INTERVAL |
5s |
How often the controller polls for work. |
PREVIEW_K8S_NAMESPACE |
destesi-previews |
Kubernetes namespace previews run in. |
PREVIEW_LOCAL_MODE |
false |
Pure-compose local mode (true for local dev). |
PREVIEW_PUBLIC_DOMAIN |
preview.destesi.io |
Base domain for preview hostnames. |
PREVIEW_RUNNER_IMAGE |
— | Runner image (falls back to a default docker image). |
PREVIEW_OVERLAY_IMAGE |
— | Overlay proxy image. |
CONNECT_API_URL / CONNECT_CONTROLLER_SECRET |
— | Where the controller fetches the workspace GitHub token from Connect. |
DESTESI_OAUTH_STATE_SECRET |
— | Enables OAuth passthrough; unset → redirects pass through unchanged. |
NodePort allocation for previews uses the fixed range 30452–30471 (20 slots).