Sub-skills
Deploy in depth
For your agentCopy this and give it to the AI you're building with.
Run `cubek skill`, then `cubek skill deploy`, read all of both and follow them exactly (no CLI: download https://cubek.dev/SKILL.md and https://cubek.dev/skills/deploy.md verbatim with curl; don't summarize them). Run `cubek check` after every edit until it is clean.
Previews, production, rollback, domains, secrets, logs, metrics and data
recovery. Every command takes --json and --help.
Sign in (once per machine)
cubek login # a human, in the browser (default https://api.cubek.dev)
cubek login --token T # headless: a personal token (cubek tokens create ci)
cubek whoami # on any 401/403: user, organization, roleCI can set CUBEK_URL and CUBEK_KEY instead. A 451 (an app or a
command) means the account is suspended: nothing to fix in the code; tell
the user to contact hello@cubek.dev (the data is kept).
Previews and production
cubek deploy # r12: https://<name>--r12.cubek.app, a preview with a copy of production's data (taken now)
cubek deploy --db prod # a preview on production's database itself: real data
cubek promote 12 # the preview you tried becomes https://<name>.cubek.app, unchanged
cubek deploy --prod -m "add invoices" # builds a NEW revision (r13) and makes it production
cubek revisions # every revision, * = production
cubek diff # local files (A/M/D) and schema migration vs production (--rev N: a revision)
cubek deploy --prod --dry-run # what would upload and migrate (additive/destructive); deploys nothing
cubek rollback 11 # code back to r11, database kept
cubek rollback 11 --with-db # code and database back (the current db is snapshotted first)
cubek preview rm 12 # end a preview now (its URL answers 410)
cubek preview url 12 # a 1-hour link that opens the private preview (give it to the user)
curl -H "Authorization: Bearer $(cubek tokens print)" https://<name>--r12.cubek.app/api/x- Previews are private: only members of the project's organization open
them (a browser signs in to cubek; scripts send the Bearer header above;
without it: 401, or a redirect to sign in for a browser). The app never
sees that header or cubek's cookie.
deploy --publicmakes one public,cubek preview access publicall of them; production is always public. - A preview lives 7 days from its deploy, then its URL answers 410 (the
revision stays:
promote,rollback); deploy again for a new one. Free keeps 3 per project: a 4th deploy without --prod gets 429 until one ends.deploy --prodmakes no preview. --db copy(default): production's data at deploy time; writes stay in the copy.--db prod: reads and writes go to production, its tasks too (cron never runs on previews); refused unless production's schema already has everything the revision declares (promote schema changes first). Its pages show a "Preview · production data" pill (--no-banner: none).- In every preview
push.sendsends nothing (logged;dryRun: true) unless deployed with--side-effects. - A revision is immutable; production is a pointer:
promote <N>moves it to a revision you already tried;deploy --prodbuilds and uploads the directory again as a new revision (the output names it). A deploy compiles, bundlesclient/with bun and uploads only what changed (it prints the files and bytes, new vs reused). --prodsnapshots the database, applies additive migrations and switches atomically. Destructive schema changes stop with a message:--allow-destructiveafter reading it (database.md).- The project is created on the first deploy, named by cubek.json
name;cubek project createmakes it empty first, to set secrets before that.
Secrets
cubek secrets set STRIPE_KEY sk_live_... # production; read at the next request
cubek secrets list
cubek secrets --dev set STRIPE_KEY sk_test_... # cubek dev: .cubek/secrets.json (keep it out of git)// src/app.tsx
import { app, log, secrets } from "cubek"
app.get("/healthz", async (c) => c.text("ok"))
app.get("/api/status", async (c) => {
const key = secrets.get("STRIPE_KEY")
if (key === null) {
log.error("STRIPE_KEY is not set")
return c.json({ error: "payments are not configured" }, 503)
}
return c.json({ payments: key.startsWith("sk_live_") ? "live" : "test" })
})Never commit secrets or print them in logs; secrets.get returns null
when unset: handle it.
Custom domains
cubek domains add shop.example.com # prints the DNS records to create
cubek domains verify shop.example.com # check now (also automatic)
cubek domains list
cubek domains add example.com && cubek domains add www.example.com
cubek domains redirect www.example.com example.com # 308, path and query kept- DNS: a CNAME to
<name>.cubek.app, or (apex) the A/AAAA records the answer lists. HTTPS certificates are issued once the domain verifies. c.origin()gives the host the request came to: use it for absolute links.c.previewis{ revision, db }in a preview, null in production andcubek dev; tasks get it ast.preview(task.on("x", async (p, t) => ...)): skip real side effects (emails, charges) in previews. Send the production*.cubek.apphost to your domain, never a preview (registered before the routes):
// src/app.tsx
app.use(async (c, next) => {
if (c.preview === null && (c.header("host") ?? "").endsWith(".cubek.app")) return c.redirect(`https://example.com${c.path()}`, 308)
await next()
})Logs, metrics, data
cubek logs # last 100 lines: requests, log.*, tasks, errors
cubek logs --follow --level warn # every node's lines, streamed
cubek logs --route /checkout --since 10m # one route, recent
cubek metrics --window 24h # requests, errors, p50/p95/p99, CPU, DB time, storage
cubek metrics --by-route # per route: requests, 4xx, 5xx, p50/p95/p99, DB time
cubek sql "SELECT status, COUNT(*) AS n FROM orders GROUP BY status"
cubek sql --write "UPDATE orders SET status = 'paid' WHERE id = '01J...'"
cubek db restore --to 2026-09-26T10:15:00Z # the database at that second, onto a preview
cubek db restore --promote # then make it production's
cubek db export --out backup.sqlite # the whole database as one SQLite file (--at <instant>, --snapshot <id>)
cubek db import backup.sqlite --replace # bulk rows back in: SQLite, .csv, .ndjson or a dir (database.md)
cubek sql -f fix.sql --write # a script, one transaction (`-f -`: stdin)
cubek storage export --out files/ # every storage object (rerun to resume)
cubek backup # everything but your code, one .tar.gz (--at <instant>)
cubek project listcubek sqlprints a table (NULL for null) and(N rows), orN rows changed;--json(scripts, agents) →{ "ok": true, "columns": ["status", "n"], "rows": [["paid", 3]] }: rows are arrays in column order (≤ 1,000);--writewithout RETURNING adds"changes": n.--dev: the same on the local dev database.t.number()columns are REAL: they print as floats (3.0);CAST(qty AS INTEGER)for integers.- A 500 answers an empty body; its error is in
cubek logs. log.info("paid", { orderId }): structured data, searchable in the logs.- Restores (
db restore --to,rollback --with-db, snapshots) reach 7 days back on Free (30 on Custom), no further. - Snapshots,
db restoreandrollback --with-dbbring back the database with itsstorageobjects as they were (contents deleted longer ago than the point-in-time window are gone; storage.md), not secrets. If you encrypt data with a key from secrets, keep that key elsewhere too: a lost secret makes restored ciphertext unreadable. cubek sqlstops at 1,000 rows;cubek db exporthas no limit (open the file with any SQLite tool).cubek backuppackages the database, every storage object, the schema, domains, cron schedules and secret NAMES (never values), each file with its sha256 inmanifest.json.cubek restore-backup <file> --project Pputs it into a deployed project (tables replaced, objects uploaded; a schema that does not fit is refused without--force), then lists the secrets to set and the domains to re-attach by hand.
Workflow for agents
cubek checkclean,cubek testgreen, the routes tried oncubek dev.cubek deploy, try the preview URL, thencubek promote <N>(that revision);cubek deploy --proddirectly when no preview is needed.- After production:
cubek logs --level warnand a request to a changed route. - Broken production:
cubek rollback <previous rev>first, fix after.