Skip to content

Ask AI

Ask anything about Destesi — setup, products, APIs.

Powered by Claude. Answers may be wrong — always verify against the docs.

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.

Register a preview. Use --from-dir for a local directory or --repo for a GitHub source.

Terminal window
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 24h

Key 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-secret is 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. Default 24h.

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.

Terminal window
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 id

Flags 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 a docker-compose.dev.yml in the repo and run the app with hot reload.

Blocks until the preview reaches a terminal state. The exit code is the outcome — designed for CI gating.

Terminal window
dst preview wait prev_abc --timeout 10m
# → https://prev-abc.your-workspace.preview.destesi.io

The 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--timeout elapsed 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).

Terminal window
dst preview show prev_abc -o url # → the public URL
dst preview show --by-pr 42 -o id # look up by PR number
dst preview list --status healthy # filter by status

show 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).

Terminal window
dst preview logs prev_abc --container service:app --tail 200 -f
dst preview events prev_abc --limit 50
dst preview trace prev_abc # status + events + log tail in one view

logs flags:

  • --container <source> — one of all (default), runner, dind, destesi-overlay, or service:<name>.
  • --tail <n> — lines to print before streaming (default 200; 0 for 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.

Terminal window
dst preview redeploy prev_abc # same id, fresh pod + branch fork
dst preview delete --by-pr 42 # delete; controller cleans up pod + branch

delete accepts an id positionally or --by-pr <n>.

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.

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 EnvError instead of producing an empty string.
  • Mark a value secret (--env-secret, or secret: true in .destesi.yml) to redact it from the dashboard and API responses.
  • Declared env wins: the runner generates a docker-compose.override.yml that injects every declared variable into every service, so you don’t have to thread ${VAR} through each service’s environment: 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.

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.

Preview runs your docker-compose stack inside each preview pod. It selects the first file it finds, in this precedence order:

  1. docker-compose.destesi.yml — preview-specific; wins whenever present.
  2. docker-compose.dev.yml — bind-mounted source for hot reload.
  3. docker-compose.yml — fallback.

Local docker compose up keeps using your own files unchanged; Preview only reads .destesi.yml files when present.

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: 40

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
Google 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.

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.

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_KEY as a repository secret (mint it with dst api-key create --preview-runner).
  • Store DESTESI_WORKSPACE and your GitHub app id as repository variables.
  • upsert is keyed by (workspace, pr_number), and the concurrency block cancels in-flight runs for the same PR, so repeated synchronize events redeploy in place rather than racing or accumulating previews.

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).

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).