# Hosted apps (/docs/concepts/hosting)



Domino can host the app your members use, such as your quest page or rewards hub, so you can launch without servers of your own. While you build, your team shares a private **staging** app that runs on test data. When you're ready, a [promotion](/docs/concepts/promotion) puts the same app **live** at `<your-project>.domino.page`, or at your own address such as `quests.example.com`.

Hosting is optional: if you already have a website or app, it can keep its own code, login and hosting and add Domino to it. See [connect your app](/docs/start/existing-app).

```mermaid
flowchart LR
    Repo["Hosted repository"] --> Dev["domino dev or Playground preview"]
    Repo --> Stage["domino stage"]
    Stage --> Build["Isolated build"]
    Build --> Staging["Staging app on test data"]
    Staging --> Promote["Promotion approved in Console"]
    Promote --> Live["Live app on live data"]
```

## What Domino hosts [#what-domino-hosts]

Your app goes through a few stages, each at its own address:

| Piece             | Where it runs                                                                            | Address                                          | Stays online                                      |
| ----------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------ | ------------------------------------------------- |
| **Repository**    | A private Git repository behind Domino's Git gateway                                     | `https://git.domino.run/git/<repository-id>.git` | Always                                            |
| **Preview**       | Your device with `domino dev`, or Console's [Playground](/docs/build/playground) sandbox | `https://<project-slug>--<hash>.domino.page`     | Only while development runs                       |
| **Staging**       | Domino's builder and Cloudflare Workers                                                  | `https://<project-slug>--staging.domino.page`    | Until the next successful build replaces it       |
| **Live**          | Domino's builder and Cloudflare Workers                                                  | `https://<project-slug>.domino.page`             | Until an approved promotion carries another build |
| **Custom domain** | The live app, at a domain you control                                                    | `https://quests.example.com`                     | While its CNAME stays in place                    |

* **One repository per project.** Creating a project from a [starter](/docs/start/new-app) gives it a working app to start from.
* **Previews are for working.** A preview shows the app while your developer or Playground works on it, and goes offline when they stop. See [Develop and share a preview](/docs/build/development).
* **Staging is for sharing.** Each time your developer stages a version, the staging address serves it, for your team to try. See [Stage and share](/docs/build/staging).
* **Live is for your members.** Approving a [promotion](/docs/concepts/promotion) that carries a version puts it live. See [How test and live hosting relate](#how-test-and-live-hosting-relate).
* **A custom domain serves live under your name.** See [Custom domains](#custom-domains).

> [!DEV]
> `domino checkout` clones the repository and installs a credential helper, so Git uses your Domino sign-in. Fetching and pushing need `publish` in the project's test environment. Each `domino stage` builds an exact pushed commit; when it succeeds, the staging address serves it.

### Changing the address [#changing-the-address]

The project name is only a label; renaming it doesn't change any address. The addresses come from the project's **slug**, which organization admins can change from Console's danger zone. See [project settings](/docs/use/projects#rename-a-project-or-change-its-slug).

* **Staging moves without a rebuild.** Domino registers the new address before switching. If that fails, the old address keeps working and you can retry.
* **Live moves within about a minute.** The old live address stops serving, with no redirect, so links containing the old slug stop working and members sign in again at the new address.
* **Your custom domain is unaffected.** A [custom domain](#custom-domains) belongs to the project, not the slug.
* **Old slugs stay yours.** Previous and prepared slugs stay reserved to the project until it is purged. Secrets and hosted storage keep their permanent internal identity.

## Static sites and Worker apps [#static-sites-and-worker-apps]

An app is either a plain website, or one with its own server code that can, for example, award points when something happens on your site.

> [!DEV]
> The `app` section of `relay.json` says which kind of app the project is. Both kinds use the same repository, previews, staging, and live app.
>
> | Kind | What Domino serves | Server code |
> | --- | --- | --- |
> | `static` | Built files, with a single-page-app fallback to `index.html` | None |
> | `worker` | A compiled Cloudflare Worker, with optional public files beside it | Your Worker handles requests |
>
> Every starter is a Worker app. [Configure a hosted app](/docs/build/hosted-app) lists every setting.

## What a Worker app receives [#what-a-worker-app-receives]

An app with server code gets what it needs to talk to Domino and keep its own data, with separate keys and storage for test and live, and no passwords stored in the code.

Staged and live Worker apps get these through their environment. Each environment supplies its own values:

| Feature        | What it gives your server code                                                                                                                                  | Guide                                                                                      |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Member sign-in | A `/relay/*` proxy to Domino's participant API, mounted by your app                                                                                             | [Add Domino participant sign-in](/docs/build/hosted-app#add-domino-participant-sign-in)    |
| Management key | `DOMINO_*` bindings for awarding points and reading project data, scoped to the environment the app serves                                                      | [Call the management API](/docs/build/hosted-app#call-the-management-api-from-your-server) |
| App secrets    | Credentials such as API keys, set per environment in Console or the CLI, never in the repository. Console can save a new app login key here when it creates one | [Set app secrets](/docs/build/hosted-app#set-app-secrets)                                  |
| Schedules      | Signed calls to routes on the serving build, in test for staging and in live for the live app                                                                   | [Run scheduled jobs](/docs/build/schedules)                                                |
| Storage        | Cloudflare KV namespaces and D1 SQL databases, separate for test and live                                                                                       | [Store app data](/docs/build/storage)                                                      |
| Variables      | Non-secret strings committed in `relay.json`                                                                                                                    | [Configure Worker variables](/docs/build/hosted-app#configure-worker-variables)            |

## Scheduled jobs [#scheduled-jobs]

An app can run jobs on a schedule, such as a nightly sync of your shop's orders.

> [!DEV]
> A Worker app declares schedules in `app.schedules`. Relay invokes their routes on the serving build in each environment, using a signing key the app verifies; the live app's key differs from staging's. An interval or UTC cron expression controls when each job becomes due. Jobs have bounded execution and one attempt per tick; an uncertain result blocks future calls until an operator confirms recovery. A job can answer with a result (an outcome, a summary, counts and diagnostics) that Relay records with the run, and each schedule keeps its latest 20 runs. Pausing or archiving an environment's schedules stops new calls there and survives build replacement. In-flight dispatch completes before activation switches builds. Deletion stops all schedules. See [Run scheduled jobs](/docs/build/schedules).

## How test and live hosting relate [#how-test-and-live-hosting-relate]

Previews and staging run in the project's **test** environment, on test data with real member sign-in. The live app runs in **live**, on live data. A version reaches live only through a promotion that a person approves in Console.

1. **Stage a build.** Your developer puts a version on staging, with test's key, secrets, stores, and schedules.
2. **Promote.** A promotion carries the version serving staging. Its preview warns about app secrets set in test and missing in live.
3. **Approve.** Once approval commits, Domino deploys the same version again with live's own key, secrets, stores, and schedule signing key. Live stores that do not exist yet are created, and SQL migrations run against live's databases before the switch. Then the live app serves the new version, typically a couple of minutes after approval. Until then, the version live already serves keeps serving.

> [!DEV]
> `domino stage` stages a build; `domino promote` prepares the promotion.

* **See where it stands.** The **Live app** card in **Deployments**, shown in both environments, gives the address, `<project-slug>.domino.page` (it redirects to the [custom domain](#custom-domains) while one is active), and its state: **Nothing live yet**, **Deploying**, **Live**, or **Deployment failed** with the reason. Choose **Open live app** to open it.
* **Public.** The live app has no staging sign-in, no staging banner, and search engines may index it.
* **Member sign-in selects live.** Sign-in from the live app's address uses the project's live members and live sign-in settings. Staging and previews stay on test.
* **Your app can ask which one it is.** Every hosted address tells the app whether it serves test or live; see [Tell test from live](/docs/build/hosted-app#tell-test-from-live).
* **Nothing live yet.** Before the first approved promotion that carries a build, and for a deleted project, `<project-slug>.domino.page` answers 404 with a page titled **Nothing is live here yet**. The page does not say whether a project exists.
* **Offline without a plan.** Where plans apply, a project whose live trial and its 7-day grace ended without a plan goes offline: `<project-slug>.domino.page` and its custom domain answer 503 with a page titled **This app is offline**, and participant requests to live are refused with `live-offline`. A plan brings the same build back within about a minute. See [Going live needs a plan](/docs/concepts/promotion#going-live-needs-a-plan).
* **Failed deployments keep the previous build.** If the live deployment fails, or does not finish within an hour, the previous live build keeps serving. The next promotion shows the app build as changed again; approve it to retry.
* **Rollback.** A rollback promotion carries the earlier promotion's build. Approving it deploys that build to live the same way.

> [!DEV]
> `management.liveApp.get()` answers the same as the **Live app** card, with `read` permission in either environment. The live app has no `noindex`. It has the same routing as staging: a `transparent` app receives requests unchanged; a `legacy` app sends `/relay/*` to the participant API and accepts only `GET` and `HEAD` for everything else, with `Cookie`, `Authorization`, and response `Set-Cookie` removed. On every `domino.page` address Domino answers `/.well-known/security.txt` itself, with its abuse contact; a custom domain passes that path to the app.

> [!NOTE] Serving an earlier build
> To serve an earlier build in live, approve a rollback promotion.

Your app and its quests travel together through a promotion, which carries both test's definitions and the version serving staging. Staging a version also publishes that commit's quests and catalog to test. Live refuses code sent to it directly (`domino deploy --environment live`), so a promotion is the only way either reaches live. See [Publish behavior](/docs/build/publishing#publish-behavior-to-live).

## Custom domains [#custom-domains]

A custom domain, such as `quests.example.com`, puts your live app at an address you own. You add it in Console, then whoever manages your domain adds two DNS records. It is a setting of the live environment: it serves only the live app, a promotion never copies or changes it, and staging keeps `<project-slug>--staging.domino.page`. Set it up in Console **Settings → Domains**; [Use your own domain](/docs/build/hosted-app#use-your-own-domain) has the steps.

> [!DEV]
> Add a domain with `domino domains add` or `management.domains.add()`.

```mermaid
flowchart LR
    Add["Add the domain"] --> Waiting["Waiting for DNS"]
    Waiting -->|"TXT record found"| Setup["Setting up HTTPS"]
    Waiting -->|"Not found in 7 days"| Attention["Needs attention"]
    Setup -->|"Certificate issued"| Active["Active"]
    Setup -->|"Not active in 7 days"| Attention
    Active -->|"CNAME moved or certificate failed"| Attention
    Attention -->|"Fix, then re-check"| Setup
```

| State                | What it means                                                                                                                           |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Waiting for DNS**  | Domino has not found the TXT record yet.                                                                                                |
| **Setting up HTTPS** | Domino found the TXT record, and Cloudflare is validating the domain and issuing its certificate. HTTPS to the domain fails until then. |
| **Active**           | The domain serves the live app.                                                                                                         |
| **Needs attention**  | You or Domino must act. The domain shows what to fix.                                                                                   |
| **Suspended**        | Domino suspended the domain. Contact Domino support.                                                                                    |

* **Two DNS records.** A `CNAME` from your hostname to `cname.domino.page`, which sends its traffic to Domino, and a `TXT` record at `_domino.<hostname>` with a value unique to this attachment, which proves you control the domain's DNS today. Domino does nothing at Cloudflare until it finds the TXT record, so typing someone else's domain takes nothing from them.
* **What Domino saw, not a summary.** The domain shows each record next to what public DNS returned, Cloudflare's hostname and certificate statuses and its validation errors word for word, when Domino last checked and checks next, and when the certificate expires. Domino checks often while a domain is set up and daily once it is active.
* **Subdomains only.** Root domains such as `example.com` cannot be attached. Attach `www.example.com` or another subdomain, and redirect the root to it at your DNS provider. Wildcards, IP addresses and Domino's own domains are refused too.
* **Growth and up.** Where plans apply, adding a custom domain needs the project on Growth, Scale or Enterprise; on Free or Launch it fails with `plan-required`. A project that moves to a smaller plan through Domino billing keeps its domains until the period it paid for ends; when the smaller plan starts, its domains are detached and the app is served on its `domino.page` address again. A plan Domino support set keeps the domains. Organizations whose domain limits Domino support raised can add domains on any plan. When the project's plan doesn't include a domain, Console's **Domains** settings say so and link to **Settings → Plan & usage**.
* **One per project.** By default a project has one custom domain, and an organization at most 10. Contact Domino support to raise either limit.
* **The domino.page address redirects.** While the domain is active, page loads (`GET` and `HEAD`) on `<project-slug>.domino.page` answer with a temporary redirect to it, keeping the path and query. Members signed in at the old address sign in again at the new one. Other requests keep working at both addresses.
* **Before the first live build,** the domain answers as `<project-slug>.domino.page` does: **Nothing is live here yet**.
* **Moving a site.** HTTPS fails between pointing the CNAME at `cname.domino.page` and the certificate's issue, usually a few minutes. If the domain has CAA records, they must allow `letsencrypt.org` and `pki.goog`.
* **Moving the domain to another project.** Add it to the other project and publish that attachment's TXT record. Once Domino finds it, the domain stops serving the first project, then serves the new one within a couple of minutes; the first project's domain shows **Needs attention**.
* **Removing it** stops serving within about a minute. Attaching it again, to any project, needs a new TXT record. Subdomains of one domain, such as `a.example.com` and `b.example.com` in two organizations, can set cookies for each other; that is the domain owner's to manage.

## Who can open it [#who-can-open-it]

Staging is never public, and neither is its participant API: calls that name the staging address answer only for browsers signed in to staging, through the staging address, and anything else is refused with `staging-access-required`. By default, visitors sign in with Domino Console and need access to the project in test. A person with `publish` can switch [staging access](/docs/build/staging#control-who-can-open-staging) to a password for reviewers outside Console. A preview is reachable by anyone with its address while it runs; member sign-in still controls what members can do. The live app is public.

## Traffic from one address [#traffic-from-one-address]

A conference venue, school or office often puts every visitor behind one shared internet address. Domino sets no limit on how many requests a hosted app takes from one address, on staging, live or a custom domain, so a crowd on one Wi-Fi network is served like a crowd on many. Sign-in is separate: it limits attempts per address to slow down abuse, so a crowd signing in at the same moment on one network can reach those limits. Member sign-in has its own limits (see [Add Domino participant sign-in](/docs/build/hosted-app#add-domino-participant-sign-in)), and a [staging password](/docs/build/staging#control-who-can-open-staging) takes 10 attempts per address in 5 minutes.

> [!DEV]
> Hosted addresses run behind Cloudflare, whose automatic protection against attack-scale traffic still applies. Staging also checks each request's access with Domino; when that check takes longer than 10 seconds, staging answers `503` with `Retry-After` instead of keeping the request waiting. Live serves from its own copy of the address map and has no such check.

## When a project is deleted or moved [#when-a-project-is-deleted-or-moved]

| Action                           | Effect on hosting                                                                                                                                                                                       |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Delete the project               | The staging address stops serving, and the live address and the custom domain stop serving within about a minute. Domino support can restore the project within 30 days; restoring it brings both back. |
| Purge after 30 days              | Domino deletes the builds, the live app and its address, the custom domain, the staging hostname, the repository, app storage, and Playground data.                                                     |
| Transfer to another organization | The slug, repository, staging and live addresses, custom domain, secrets, and stores move with the project. The new organization's admins manage the domain, and its DNS records keep working.          |

See [Delete or move a project](/docs/use/projects#delete-or-move-a-project).

## Where next [#where-next]

- [Create a new app](/docs/start/new-app) — start from a starter and get a hosted app and staging address.
- [Build in Playground](/docs/build/playground) — work with an agent in Console.
- [Configure a hosted app](/docs/build/hosted-app) — every `relay.json` app setting and server feature.
- [Stage and share](/docs/build/staging) — build a pushed commit and share staging.
- [Promotion](/docs/concepts/promotion) — how a build and test's definitions reach live.
