Hosted projects for agents
Checkout, development, and persistent previews for agents.
People's guides cover the same ground step by step: Hosted apps explains how the repository, previews, staging, and the live app fit together; Configure a hosted app, Store app data, Stage and share, and Build in Playground hold the details. A project's live app can also serve at a custom domain: see Custom domains.
Source and release workflow
For Domino-hosted repositories, use main for the team's accepted source and separate work branches for local agents and Playground. Staging records the successfully deployed commit; a branch name does not select an environment or prove what is running. Existing apps with their own hosting keep their established repository and release workflow.
Follow this procedure before editing or resuming work, and again before shared staging:
- Inspect the starting point. Read the repository's instructions, inspect the working tree and current branch, and fetch the configured remote. Compare the checkout with remote
mainand the current staging commit reported bydomino builds --json. A Playground checkout or restored backup can be stale. If staged work is missing frommain, reconcile that divergence before treatingmainas the team baseline; preserve both histories and involve the owner when the intended result is unclear. - Work on a branch. Start new work from fetched remote
main. For existing work, preserve uncommitted edits and branch commits before incorporating newermainchanges. Resolve conflicts explicitly. Never reset away work or force-push a shared branch to make it match another checkout. - Save and preview. Commit and push your work branch to save source. Use
domino devfor a running preview. A Playground checkpoint saves its personal branch; it does not integrate that work intomain. Development currently synchronizes behavior into the shared test environment, so branches do not isolate quest definitions or test data. Coordinate changes that affect other developers' previews. - Integrate for shared staging. Incorporate the latest
main, run the app's checks, and verify the requested participant flow. Merge through the repository's review process and push the integrated result tomain. Respect branch protections. If another developer advancesmainbefore submission, reconcile and validate the newer result. Stage the exact pushed, integrated SHA withdomino stage --commit <full-sha> --json; do not replace shared staging directly from an unmerged work branch. - Verify the deployment. Wait for a successful build, report its commit and returned staging URL, and verify the requested flow there. A failed build leaves the previous staging version active even though
mainmay have advanced. Usedomino buildsto distinguish accepted source from the version actually running.
These are agent workflow rules. The current CLI and backend accept arbitrary pushed commits; domino stage does not commit, push, merge, synchronize Playground, or enforce this branch policy. Perform the Git steps explicitly. A push to main or a successful stage does not publish live.
Move to a newer starter
To rebuild an existing hosted project on the latest version of a starter, run domino starters export <template> --directory <dir> --json from the checkout, so it uses the checkout's project scope, with <dir> outside it, such as ../next-starter. It writes the current starter, as domino create would seed a new repository today, into a new or empty directory (a directory with files in it fails with LOCAL_CONFLICT), and reports template, directory, sourceCommit, and the number of files. The files include .domino/starter.json but no project name or brief, and nothing is installed. Only the current version of each starter can be exported; it needs read in the project scope.
Integrate the export on a work branch from fetched remote main: bring in the starter's files, carry over the project's own quests, CAMPAIGN.md brief and design, and keep the exported .domino/starter.json so domino doctor reports the new starter-origin. Install with the version package.json pins, run domino doctor, domino check and the app's tests, then integrate and stage following the source and release workflow.
Take the app live
The live app is the project's public app at https://<project-slug>.domino.page, on live data. Only an approved promotion changes it.
- Set live's environment bindings. App secrets, integrations and participant sign-in are configured per environment and never copied. Set live's secrets with
printf %s "$VALUE" | domino secrets set NAME --environment live --json(a CLI session may do this in live), or ask the owner to use Console Settings → App secrets with Live selected. - Promote. Stage the integrated commit, then run
domino promote --environment test --json. The promotion carries the build serving staging (build) with test's definitions;binding-missingwarnings name what live still lacks, andgroup-createdwarnings the empty groups approval creates in live. - Hand approval to a person. Someone signed in to Console with publish permission approves it in Live → Deployments. An agent cannot approve.
- Verify. After approval, Domino deploys the same build again with live's own key, secrets, stores and schedule signing key, then switches the live app, typically within a couple of minutes, and the address follows within about a minute. Until then the previous live build keeps serving. Before the first such promotion, the address answers 404 "Nothing is live here yet". To check from code,
management.liveApp.get()(readin either environment) returns{ url, state, build, requested, failure }, wherestateisnone,deploying,liveorfailed; Console shows the same as the Live app card in Deployments.
A failed live deployment leaves the previous build serving; the next promotion shows the build as changed again, and approving it retries. A rollback promotion (management.promotions.rollback(id)) carries the earlier build and deploys it the same way. Only a promotion changes the live app; live refuses domino deploy --environment live with promotion-required.
The live app is public: no staging sign-in, banner or noindex. Participant sign-in from its origin selects the project's live environment (live members and live sign-in settings); staging and previews stay on test. Routing works as on staging, minus the staging access path. A slug change moves the live address within about a minute, with no redirect from the old one; deleting the project takes it offline, and restoring brings it back.
To tell test from live in app code, call getHostEnvironment() from @domino-sdk/relay/browser: it resolves to { environment: "test" | "live", kind: "staging" | "preview" | "live" } from the host's /__domino/environment, or to null where the host does not answer (a local dev server, server rendering). A Worker's server code reads DOMINO_ENVIRONMENT. Never decide from hostname patterns, and never from a build-time variable: live serves the same build as staging, and builds see only CI=true. For printed or published links, such as QR codes, use the live address or a custom domain, never staging's.
Custom domains
A custom domain such as quests.example.com serves the project's live app; staging keeps <project-slug>--staging.domino.page. It is a live environment binding, never promoted, and domino domains works the same whichever environment is selected.
- Add.
domino domains add quests.example.com --json(ormanagement.domains.add(hostname),POST /management/v1/domains). Writes need an organization admin signed in withdomino login; API keys get 403, and a person without admin gets 403permission-required. Reading needsread. Root domains (example.com), wildcards, IPs and Domino's own domains are refused withinvalid-hostname: attach a subdomain and tell the owner to redirect the root at their DNS provider. One domain per project, 10 per organization (limit-reached). Where billing enforcement is on, the project needs Growth or above, unless support raised the organization's domain limits: otherwise 403plan-requiredwithfeature: "custom-domain"and the project'splan. Report that the project needs a bigger plan; don't retry. A smaller plan later keeps the domain. - DNS. The result's
recordsare aCNAMEfrom the hostname tocname.domino.pageand aTXTat_domino.<hostname>withdomino-verify=<token>, unique to this attachment. Only the domain's owner can add them; give them both records verbatim. CAA records, if any, must allowletsencrypt.organdpki.goog. - Check.
domino domains get quests.example.com --json(orrecheckto check now, at most every 10 seconds; 429rate-limitedsooner). Adding, re-checking and removing share a budget of 30 per organization per hour, alsorate-limited; poll withget, neverrecheck, because Domino checks on its own.stateisawaiting-dns(TXT not found),provisioning(proven; Cloudflare validates and issues the certificate, and HTTPS fails until it is done),active(serving),attention(issue.messagesays what to fix, then re-check) orsuspended(contact Domino support). Each record'sfoundis what public DNS returned (nullbefore Domino looked,[]for none) andmatchescompares it;cloudflareholds Cloudflare's statuses and errors verbatim. A proxied or flattened CNAME can showmatches: falsewhile the domain works. Times are ISO strings (createdAt,checkedAt,nextCheckAt,cloudflare.certificateExpiresAt), each with an epoch-milliseconds sibling named…Ms; a certificate expiry Cloudflare sent in a form that doesn't parse has a nullcertificateExpiresAtMs. Setup that does not reachactivein 7 days goes toattention.
While a domain is active, GET and HEAD on <project-slug>.domino.page answer 302 to it, and participant sign-in from its origin selects the live environment; members signed in at the old address sign in again. Before the first live build it answers "Nothing is live here yet". Another project that adds the same hostname and publishes its own TXT record takes it over; the first project's domain goes to attention. domino domains remove quests.example.com --yes stops serving within about a minute; re-attaching needs a new TXT record. A suspended domain cannot be removed (domain-suspended). A deployment without custom domains answers 503 custom-domains-unavailable: report that custom domains aren't enabled yet rather than retrying.
Repository access
Each hosted project has one private repository behind Domino's Git gateway, https://git.domino.run/git/<repository-id>.git. It serves only Git fetch and push over HTTPS and accepts a Domino bearer token, or the same token as the Basic-auth password. Fetching and pushing both need publish in the project's test environment; a personal API key or an organization automation key must itself carry publish, and an automation key must reach the project. domino checkout configures the remote and installs domino credential-helper, which answers only for that remote and uses RELAY_MANAGEMENT_TOKEN or the saved login. Do not paste tokens into remote URLs. A 409 "Repository is not ready" means provisioning has not finished.
Set up and develop the app
Before the first implementation, follow the Domino skill's intake, discovery, and confirmation procedure. Ask for missing brand and campaign basics and wait for answers before research or design. Then prepare a concrete proposal and obtain the owner's confirmation. Reuse an already confirmed brief in later sessions.
If Console already created the project and repository, run domino checkout <project> with its ID or the slug domino whoami --json lists; a slug two organizations share needs --organization. Without --json, checkout prints Git's fetch progress on stderr. A fetch that moves under 1 KB/s for 60 seconds fails with CHECKOUT_GIT_FAILED (details.phase fetch); retry checkout once. Use domino create <name> only when the requested hosted project does not exist. Both require authentication. Use domino whoami --json to inspect accessible projects when the target is unclear. Work in the checkout these commands make. A starter copied any other way keeps the placeholder organization local in relay.json, and commands that reach the project refuse it with SCOPE_PLACEHOLDER until you run domino checkout or pass --organization and --project.
For a failure after remote project creation, inspect the existing project's state before repeating creation. Keep partially created local directories intact while recovering. Checkout accepts --directory for a fresh destination.
Enter the checked-out directory before installing guidance or dependencies. Use checkout's packageManager.installCommand, which selects the exact version declared in package.json and preserves the lockfile. For pnpm@10.34.5, this is npx --yes pnpm@10.34.5 install --frozen-lockfile. Wait for installation to succeed before running doctor; a missing SDK during installation is not a new fault. If guidance is already installed, do not reinstall it as a remedy for checkout or authentication failures. The starter pins the SDK and CLI to one published npm release in package.json; upgrade them with the package manager. Read CAMPAIGN.md for the owner's brief.
Every starter uses Vite, React, TanStack Router with routes declared in src/router.tsx, and Tailwind CSS v4, with an app-owned Hono Worker in server/: server/index.ts answers Domino's participant proxy, then the app's /api routes in server/api.ts, then browser assets. A /api route that acts for the participant gets their member ID from signedInMember(c.req.raw, c.env, { apiUrl: config.apiUrl }) from @domino-sdk/relay/portal-proxy, which asks Domino with the request's cookies; never trust a member ID the browser sends. The browser calls /api through the typed client in src/api.ts; the build fails if browser code imports server/, @domino-sdk/relay/server or @domino-sdk/relay-cli. .domino/starter.json records which starter the project began from (template); its AGENTS.md maps the source and describes what that starter is for. The loyalty starter keeps its campaign, members' group and reward items in program.ts and its offers in offers/: a quest's groupMembership reward adds members to the group that the members-only offer's inGroup requirement names, and each offer card explains a lock from the offer's missing. The campaign starter's look is neutral on purpose (greyscale color tokens in src/styles.css, no logo or accent) and it ships sample quests and copy that introduce Domino; replace them with the owner's campaign. Deleting a sample quest's file does not unpublish it, and domino stage and domino deploy warn quests-not-in-bundle naming each one still published: archive each retired quest with domino quests archive <key> --reason "…" --json (for example show-your-setup), then deploy. An active sample quest keeps its interaction, such as photo or staff, in supportedInteractions, so a deployment that drops that interaction fails until the quest is archived. domino deploy reports each release's lifecycle. Build the interface on the @domino-sdk/relay/react hooks (RelayProvider, useSession, useExperience, useQuest with questActionState for whether to show the input, useSignIn, useAccounts, useLeaderboard, useReferralSummary, useInvitation, usePointLedgers, useTieredRewards, useBalances) and keep their recovery behavior; for a read no hook covers, use useRelayQuery so it shares the cache and invalidation (see React hooks). A participant's own points need no server route: the classic starter's header reads them with useBalances(), which reads again after a quest completes. Read hooks keep themselves current, so add no timers or refresh effects of your own: RelayProvider follows the signed-in member's activity feed and reads again only what each event changes, and the referral starter's summary comes from useReferralSummary() alone, which reads again every minute while the page is visible and whenever the member completes something. The starters' buttons disable on isPending; sign-out uses useSignOut().mutate(), which never rejects and reports a failure in error. Browser code imports response types such as QuestView from @domino-sdk/relay and avatar helpers from @domino-sdk/relay/avatar; keep Zod schemas (@domino-sdk/relay/schema) and builders (@domino-sdk/relay/authoring) in quests and server/, out of the browser bundle. Starters name Domino resources by authoring key, but reads return stored IDs: the referral starter resolves its ledger keys to IDs once with usePointLedgers and compares IDs everywhere (see resource references), and pages its activity feed with referrals.history({ cursor }) until nextCursor is null. Starter tests use IDs that differ from keys so a key comparison fails.
To test the app's screens without hosted data, route its /relay/v1/ requests in Playwright, or pass fetch to the client in a component test, to createFakeRelay from @domino-sdk/relay/testing, seeded with the member, quests and points the test needs (see test the UI).
The starter's relay.json declares discovery patterns for quests/**/*.quest.ts and quests/**/*.quest-type.ts. Add a matching default-exported file to include it in builds and development sync. Installed snippets use these paths. Use explicit entries only for named exports or per-resource overrides.
Run domino dev to start the app with hosted test data and real sign-in. Share its HTTPS preview URL. The CLI prepares the tunnel connector automatically. The URL goes offline when development stops; use domino stage for a preview that stays online. Development uses the hosted test environment and real providers.
Real sign-in offers the OAuth providers and email code Domino configures for the platform. Wallet sign-in is the project's own setting, per environment and off by default: enable the wallet families in Console Settings → Sign-in, or with domino api PUT /participant-sign-in body.json (publish permission). Enable it in the test environment before building a wallet-first app, and in live before taking it live. When a source file of the app signs participants in with a wallet (relay.auth.wallet.signIn, useSignIn().withWallet, useSignInWallets, Cosmos chains passed to useSignInOptions, a wallet option picked with find, or wallet options filtered without OAuth, email or Telegram ones) and test has no wallet family enabled, domino stage warns on stderr, adds a wallet-sign-in-off entry, with the families the app names and the enabling command, to its JSON warnings, and still stages; domino doctor --remote reports the same as its wallet-sign-in check. Run the command it names, then verify the wallet sign-in in the staged app. See wallet sign-in.
Participant profiles (names and avatars on leaderboards and profile pages) are private by default too. Read the settings with domino api GET /profile-settings, and change them with domino api POST /profile-settings body.json holding visibility, defaults (the ordered accounts that fill in names and pictures, such as ["x", "discord"]; [] for generated), the read revision as expectedRevision, a reason and an actionId (publish permission; live refuses it from every credential, Console included, with promotion-required). domino profiles settings and domino profiles configure FILE do the same. With the defaults every leaderboard row shows a generated name such as Participant 1a2b3c4d: when the project has leaderboards (declared in relay.json or published in test) and test's profiles are private or have no default account, domino stage warns on stderr, adds a generated-leaderboard-names entry with the configuring command to its JSON warnings, and still stages; domino doctor --remote reports the same as its leaderboard-names check. Ask the owner before showing names, since it is their call; show member profiles has their Console steps. See profiles.
Use domino check and the app's build script to verify source. domino check reports source problems Relay would refuse at deployment, such as two quests declaring one budget key differently, an author budget on a quest without a tracker trigger, or an offer, badge or other data definition that fails its schema (DEFINITION_INVALID, naming the file and field). It also warns, without failing, about a leaderboard whose live ties differs from its settle.ties (LEADERBOARD_TIES_DIFFER, severity warning): its standings show one order and its prizes pay another; and about a quest on a YouTube or TikTok tracker that runs on every observation (TRIGGER_REFIRES_ON_REFRESH), which each refresh evaluates again for every video. Authoring lists every warning domino check reports. Exercise the requested participant flow in the running app before staging: a preview link signs the domino dev preview in as a test member (check participant flows, "Before staging").
For leaderboards, register authored modules in the manifest's leaderboards list. domino check bundles them even when the project has no quests. domino dev synchronizes their definitions with the other project resources. Verify signed-in rank reads through the participant proxy; a successful build alone does not establish access or scoring behavior.
When the task includes sharing the team's staging preview, follow the source and release workflow to integrate and push the intended changes to main first. Stage the exact integrated commit with domino stage --commit <full-sha> --json. Git pushes alone save source; they do not activate a preview or publish live. To take the staged app live, follow Take the app live.
If stage submission loses its response, or answers 502 or 504 "Could not start cloud build", repeat the printed commit and request ID: the build is reserved under that ID and may already have started. Use domino builds --json and domino logs <build-id> --json to recover progress after disconnecting. A failed build leaves the previous preview active. Report the URL returned for the successful build rather than constructing one. Staging requires sign-in: Console members with project access in Test sign in through Console, and anyone with publish can set a staging password for other reviewers, in Console Settings → Staging access or with domino api PUT /staging/access access.json --environment test, where access.json holds {"mode":"password","password":"<8-128 characters>"} (a file keeps the password out of shell history; {"mode":"console"} returns to Console sign-in only, and domino api GET /staging/access --environment test reads the current mode). Ask the owner before setting or changing the password: it replaces the current access mode and signs out everyone who entered the old one. There is no public staging link; share the staging URL with the password, and take the app live to show it to members. To fetch staging from a script, domino api POST /staging/link body.json with { "path": "/" } returns a one-minute URL; opening it answers 303 with the session cookie to send on later requests. To use the app as a test member there, see check participant flows on staging.
Cloud staging supports static apps and compiled Cloudflare Worker apps, including SSR and server routes. Configure app.kind="worker", directory for private modules, main relative to that directory, compatibilityDate, optional compatibilityFlags, and optional assetsDirectory for browser assets. Keep devScript, checkScript, and buildScript. TanStack Start with Cloudflare Vite typically uses dist/server, index.js, dist/client, and nodejs_compat. The dev server must honor DOMINO_APP_PORT and DOMINO_PREVIEW_ORIGIN; with Vite and the Cloudflare Vite plugin, const domino = dominoHosting({ command }) from @domino-sdk/relay/vite supplies both: pass domino.cloudflare to the app's own cloudflare() call (the Worker in Vite's server environment, and domino dev's variables forwarded to the local Worker only when serving) and use domino.server as Vite's server (port from DOMINO_APP_PORT, default 5173; the preview host and HTTPS hot reload when DOMINO_PREVIEW_ORIGIN is set). The build must also emit the private .domino-build quest/catalog artifacts: end the build script with domino build --hosting-artifacts, which writes quests.json and catalog.json to .domino-build/ beside relay.json. The catalog artifact lists relay.json reference images by hash and path; the builder stages those files from the repository, so write domino build --json output through unchanged and commit the images. Quests and quest types import the installed SDK's runtime instead of bundling the SDK; the catalog artifact carries that runtime once in runtimes, so keep it in the output. domino build --hosting-artifacts covers up to 1,000 quests. The builder compresses the quest/catalog artifacts separately from the modules and assets: they may reach 64 MB of JSON and 10 MB compressed, while modules and assets share a 20 MB limit once base64-encoded (limits). Set app.routing="transparent" to preserve methods, cookies, and app authorization. Staging reserves the sign-in path /__domino/staging-access, the test toolbar's /__domino/toolbar.js and /__domino/toolbar/ paths, and /__domino/environment; it adds the toolbar script to every HTML response and removes its own staging session cookie, and otherwise forwards requests and responses unchanged. Every domino.page address also answers /.well-known/security.txt itself with Domino's abuse contact, so the app never receives that path there; a custom domain forwards it. The starter explicitly proxies /relay/* in its own server, used in both development and staging; apps can override or remove that handler. Use proxyParticipant(request, { baseUrl, fetch }) to integrate Domino-managed sign-in explicitly. The API validates the registered app origin, session, and CSRF headers. DOMINO_PARTICIPANT_API supplies the selected upstream during connected development. runWorkerFirst: true lets your Worker handle even requests matching browser assets through its ASSETS binding. Non-secret string vars are supported, and app secrets supply credentials; KV and SQL stores declared in app.storage receive native bindings. Arbitrary Wrangler bindings are not imported. Domino binds each Worker build's own management key and scope: DOMINO_MANAGEMENT_KEY (secret, dhk_…), DOMINO_API_URL, DOMINO_ORGANIZATION, DOMINO_PROJECT, and DOMINO_ENVIRONMENT (test on previews and staging, live on the live app); these names are reserved. In server code, call hostedManagementClient(env) from @domino-sdk/relay/server (Worker-safe) for point writes (points.record), custom tracker ingestion (trackers.ingest), and reads such as leaderboards, referrals, balances, and members. The key has only read, points, and ingest in the environment the app serves (test for staging and development, live for the live app, which writes real members' points); ingest reaches custom tracker records only, and the key cannot enroll members. It cannot publish or qualify referrals. An external score, such as a wallet balance, gets its own ledger, updated with absolute values: see Rank an external score. Never expose it to browser code. domino dev exports the same five variables with a 12-hour key to the dev server process; the app's vite.config.ts must forward them to the local Worker in the cloudflare() plugin's development vars, which dominoHosting() does. Activating a newer build revokes older builds' keys in that environment; staging and live keys rotate separately. New staging builds use the same project test data as development and real participant sign-in. Omitted routing or app.routing="legacy" preserves existing gateway behavior, including implicit /relay/ routes. To migrate, add the app-owned proxy, explicitly select transparent routing, restart and test the preview with the updated CLI, then commit and stage. Routing is recorded per build and preview session; old records and old CLI requests remain legacy. Test data does not imply that external provider operations are simulated.
For referral programs, register authored modules in referrals. domino check and domino dev include them even without quests. Preserve invitation codes across the app's sign-in redirect and verify acceptance through the configured participant proxy before any quest completes. See referrals.
Quests declare integrations: [discord]; Relay derives the participant identities required by each quest. The campaign starter renders account controls from the quest view’s connections field. The Discord membership snippet needs a project Discord integration configured in Console. Sign in with Discord (or link its identity) and check membership before staging.. See connected accounts.
Keep the starter's explicit Start a new submission action for a server-retired saved submission. It discards the local record only after participant review; it does not send a replacement claim. Ordinary interrupted submissions still resume their original operation. See submission recovery.
For streak quests, upgrade the starter's SDK before publication. The starter shows quest.streak.current against its goal and formats nextBoundary in the project timezone (quest.streak.timeZone). It distinguishes progressed attempts from completed milestones, keeps automatic quests free of claim/retry buttons, and reports budget-exhausted milestones without pretending a reward was delivered. Preserve these behaviors when adapting the participant interface. See calendar-day streaks.
App secrets
Store credentials your server code reads, such as APP_API_KEY, as app secrets. Each environment has its own: test's reach staged builds, live's the live app, and a promotion never copies them. Never put them in relay.json vars, source, or browser code. Every domino secrets command uses test unless you pass --environment live; it ignores RELAY_ENVIRONMENT and relay.json's environment.
- Declare. List every secret the server code reads in
relay.jsonapp.secrets, keyed by name:"APP_API_KEY": { "description": "What it opens, format, and how callers send it" }.requireddefaults to true; setfalsefor optional ones. Adescriptionhas 1 to 500 characters, and a build declares at most 50 secrets. A declaration carries no value, a name cannot be both avarsentry and a declared secret, and static apps cannot declare any. When you add a secret to server code, declare it in the same change. - Set.
printf %s "$VALUE" | domino secrets set NAME --jsoncreates or replaces one. The CLI reads the value from stdin or a hidden terminal prompt, never from arguments, and never prints it. An invalid name or value exits 2 withINVALID_ARGUMENT, and a missing value with no terminal to prompt in exits 2 withINPUT_REQUIRED, before anything is sent. A value starting withdi_is checked as an app login key first: one that is unknown, expired or revoked, or belongs to another project or environment, exits 2 withINVALID_ARGUMENTand nothing is stored; a stored key is named in the result'sidentityIntegration. Generate a value in the pipe (openssl rand -hex 32 | domino secrets set APP_API_KEY --json) when the owner does not need to know it; otherwise ask the owner to run the command, and keep the value out of chat and logs. - Check what is missing.
domino stagewarns on stderr about each required declared secret that is not set, with thedomino secrets setcommand to fix it, and addsmissingSecretsto its JSON result. It never blocks: the build stages, and routes that need the secret fail until it is set and staged again. Report missing secrets to the owner rather than treating the stage as complete. Optional missing secrets get one stderr line. - Inspect and remove.
domino secrets list --jsonreturnssecrets(names withupdatedAtandupdatedBy),declared(the active build's declarations merged with this checkout'srelay.json, whose descriptions win), andmissing(declared but not set); no command or route returns a value.domino secrets delete NAME --jsonremoves one. A value a quest's code reads, such as a shared scan code, is a secret setting instead. - Apply. Only Worker builds (
app.kind="worker") deployed after a change receive it, as a secret binding that replaces an appvarsentry of the same name. In test, rundomino stageagain; earlier builds keep their values. In live, the next build promoted to live receives it; the live app keeps its previous value until then. Static apps receive none. - Develop locally.
domino devdoes not receive hosted secrets. Supply local values through the framework, such as a git-ignored.dev.varsfor the Cloudflare Vite plugin. - Rules. Names match
^[A-Z_][A-Z0-9_]{0,63}$, excludingDOMINO_names andASSETS. Values are 1 byte to 5 KiB; each project environment holds at most 50. Setting and deleting needpublishand listing needsreadin the environment (the CLI has the signed-in person's permissions; editors and organization admins have both). A CLI session may set and delete secrets in live; see errors for the other live writes it may make. Operators can also manage them in Console Settings → App secrets. The management client exposessecrets.list(),secrets.set(name, value), andsecrets.delete(name).
App storage
Worker apps declare logical stores in relay.json under app.storage: "kv": { "CACHE": { "description": "Rendered page cache" } } and "sql": { "DB": { "description": "Campaign data", "migrations": "migrations" } }. Domino provisions resources per project and environment: test's on staging builds, live's when a build that declares them is promoted to live (migrations run against live's databases before the live app switches). Data never copies between environments. Never put Cloudflare IDs in this declaration. env.CACHE is a native KVNamespace and env.DB a native D1Database. There is no SDK wrapper or static-app storage. Requires SDK and CLI 1.2.0 or later.
- Types. Add
@cloudflare/workers-typesas a dev dependency andimport type { D1Database, KVNamespace } from "@cloudflare/workers-types/index.ts"inserver/env.ts. Do not add the package to tsconfigtypes: its globals conflict with the DOM library, and the check script fails staging. - Names and limits. Use the app-secret name rules, unique across vars, declared secrets, KV and SQL. A build declares at most 5 KV stores and 5 SQL stores, and a project holds at most 10 of each, counting stores kept after their declaration was removed; the secret limit is under App secrets. A store
descriptionhas 1 to 500 characters. Reuse a name rather than inventing new ones. Stored secrets cannot collide with a store. SQL alone accepts an optional repository-relative migrations directory, without hidden segments, parent traversal, ornode_modules. - Local development. Mirror the names in root
wrangler.json/wrangler.jsonckv_namespacesandd1_databases, with the samemigrations_dir; local resource IDs are placeholders and local data stays separate.domino devanddomino stagewarn about mismatched names or migration directories without failing.domino devdoes not apply migrations: runwrangler d1 migrations apply DB --localafter adding a file. - Migrations. Commit
.sqlfiles directly in the directory; nested layouts such as0000_x/migration.sqlfail the build. Every build ships every file, so the limits (100 files per store, 90,000 UTF-8 bytes each, 1,000,000 bytes per build) cover the whole history; squash files that every database has applied when approaching them. The trusted builder applies missing filenames in Wrangler's numeric-prefix order before Worker upload and activation. Migrations are forward-only and must remain compatible with the currently serving build. Each file and its tracking insert run as one request, so a failing file rolls back alone; earlier files stay applied. A failure fails the build without retrying, keeps the previous build active, and includes D1's error message. Correct the file and stage again. See storage migration recovery. - Inspect.
domino storage list --jsonorGET /management/v1/storagelists kind, name, declaration status and creation time (null until provisioned), never provider IDs. Requiresread; declarations come from the environment's serving build.domino storageuses test unless you pass--environment live. - Browse. Read-only, with
publish:domino storage tables DB,domino storage rows DB TABLE [--cursor C](50 rows; long text{ text, truncated: true }, BLOBs{ blob: bytes }),domino storage cell DB TABLE ROW COLUMN [--cursor C | --all](one value in full, 100,000 characters per page; ROW is the rows page'soffsetplus the row's index on it;type,length,text,nextCursor; a BLOB hastext: null),domino storage keys CACHE [--prefix P] [--cursor C](100 keys),domino storage get CACHE KEY(size and a 2,000-character text preview;textis null for binary). Routes:GET /storage/sql/DB/tables,/rows?table=&cursor=,/cell?table=&row=&column=&cursor=,/storage/kv/CACHE/keys?prefix=&cursor=,/value?key=. Rows, cell and keys pages carrynextCursor; pass it back ascursoruntil it is null. Reads count toward D1 usage. Apart from clearing test data (below), nothing writes: seed with migrations; there is no export. KV values stay cut at 2,000 characters. Console shows the same under Settings → Storage, where people withpublishcan also delete undeclared stores. - Clear test data. Test only, with
publish, on declared stores too:domino storage clear sql DB --table T [--table T2]or--all-tables [--keep T]deletes the tables' rows in one D1 transaction (foreign keys deferred to its end), keeping columns, indexes, triggers, views,d1_migrations,sqlite_*and_cf_*(virtual tables such as FTS5 clear through themselves, never their shadow tables);domino storage clear kv NAME --prefix Por--all-keysdeletes keys, 10,000 per call, and the CLI repeats until the result'scompleteis true.--dry-runonly counts (KV counts up to 100,000,complete: falsebeyond). Without--yesit asks in a terminal and refuses unattended or with--json. Routes:POST /storage/sql/DB/clearwith{ tables }or{ allTables: true, keepTables? },POST /storage/kv/NAME/clearwith{ prefix }or{ all: true }, each with optionaldryRun; SDKstorage.clear(kind, name, input). Answers{ environment, kind, store, dryRun, tables: [{ name, rows }] }or{ …, prefix, keys, complete }. Live: 422test-environment-required; reserved or empty table lists and a missing prefix: 400; an unknown store or table, including a--keepone: 404, nothing cleared. Each clear writes oneapp-storage-clearedaudit event; dry runs none. Test member reset, purge and erase never touch app storage, so use this to reuse app-side records such as ticket mappings. - Delete.
domino storage delete sql OLD_DB --yes(orDELETE /storage/sql/OLD_DB,publish) deletes a store and its data once the serving build no longer declares it; otherwise 409. A CLI session cannot delete in live: the owner uses Console or an API key there (what each credential may do in live). Use it to get back under the 10-per-kind project limit. - Retention. Removing a declaration unbinds the next build but keeps data. Re-adding the same kind and name reuses it; a rename creates another store. Transfers retain resources. Project purge deletes all retained stores.
Recover interrupted creation
domino create waits for a slow repository setup: when a step times out, loses its connection or gets a 502, 503 or 504, it resumes the same saved operation for up to 5 minutes and then prints the normal result. Without --json it says once on stderr that it is still setting up the project. If it still fails, the repository setup is refused, or checkout is interrupted, keep the operation ID from its error. Run domino operations resume ID --json with the same RELAY_CONFIG_DIR to finish the saved project and destination. domino operations list --json finds the ID after a process restart. Do not start another creation to recover the first. The journal keeps the original API scope and actor; sign in to that API as the same actor. See CLI automation for the error and recovery contract.
Scheduled jobs
Declare app.schedules: [{ name, path, every: "15m", description }] in a Worker app, or replace every with a five-field numeric UTC cron. At most ten jobs, at least five minutes between minute slots; interval units are m/h/d, up to 365d. Static apps and development previews do not run schedules. Stage to activate declarations in test; promote that build to run them in live, where they sign with a different DOMINO_SCHEDULE_KEY and keep their own runs and pause state.
Verify every job route with verifyScheduledRequest(request, env.DOMINO_SCHEDULE_KEY) from @domino-sdk/relay/server before doing work. The per-build signing key is separate from the hosted management key. Use the verified id for idempotent external writes or hostedManagementClient(env).trackers.ingest(REFERENCE, { actionId: run.id, records }). Await all work; do not use waitUntil. See scheduled custom tracker ingestion.
Answer with return scheduleResult({ outcome, summary?, metrics?, diagnostics? }) so the run records what happened: outcome is succeeded, partial or failed (the last two fail the run even on 2xx); metrics holds up to 50 named numbers; diagnostics up to 50 { level: "error" | "warning" | "info", message, subject?, code? } lines. Relay reads any application/json answer up to 16,384 bytes at any status; an invalid or oversized one leaves the status alone and sets error to Result ignored: …. Anyone with read in that environment sees results: IDs only, no secrets or personal data. Without a result a 204 is succeeded. Result fields.
Budget each run within 30 seconds: ingests into one tracker run one at a time on the server, so send one trackers.ingest per tracker per run with up to 100 records rather than one per member; pass { timeout } from the time left; retry a timed-out ingest with the same actionId; stop early with a partial result. Budgeting.
domino schedules list --json shows active-build declarations, last run time/status/duration/error/result and next run. domino schedules runs NAME --json returns { runs }, the latest 20 runs newest first with each result; Relay keeps no older runs. domino schedules run NAME queues one manual run. pause, archive and resume control the project's schedules across builds. Add --environment live for the live app's jobs; without it the commands use test. Reading needs read and controls need publish in the environment; a CLI session cannot run or control live jobs, so live controls need Console or an API key. The SDK has management.schedules.list, .runs, .run, .setState and .recover. Domino does not expose a Worker's runtime logs; put what an operator needs in the result.
Each tick gets one attempt, a 30-second timeout and no automatic retries. Missed ticks are skipped. Calls are serial within a project, and activation waits for an in-flight dispatch. An uncertain run blocks that name even after replacement, and its error says what Relay saw; confirm the remote job stopped, then domino schedules recover NAME --yes. A build with no Worker fails the run instead of blocking. Recovery skips that tick. Deletion stops jobs and purge deletes receipts. See schedule dispatch guarantees for what counts as failed or uncertain, and the full guide.