dabadataDocs
Concepts / Sites

Concepts

Sites

dabadata Sites host your app's frontend next to its data. A site is a folder of static files - a plain HTML page, or the build output of Vite, React, Vue, Svelte or Solid - served from the edge at <name>.dabadata.site on every plan, and at your own domain from Starter. Deploy it with one command or let your AI agent put it live; the page talks to your project with its publishable key, and its address is already an allowed origin.

What a site is

  • A site belongs to one environment of a project, live or test, and counts toward its workspace's plan. Deleting the project deletes its sites, their domains and their files.
  • Every site gets https://<name>.dabadata.site. From Starter you can connect your own domain, such as www.example.com (Your own domain).
  • The files are served by a worker at the edge, straight from object storage; your API server is not involved, so a busy site never slows your API down. Serving is free and does not count toward egress or requests (fair use: a site whose traffic is abusive may be suspended).
  • dabadata.site is a domain of its own, apart from the service's domain, and on the Public Suffix List: one site cannot set cookies for another site, the console or the API.

Create a site

In the console, open the project's Sites page and create one there; it says as you type whether the name is free. From a terminal, with the dabadata CLI signed in and pointed at the environment:

bash
dabadata use prj_xxxxxxxxxxxx:live     # the environment the site belongs to
dabadata sites create my-shop --spa    # https://my-shop.dabadata.site
dabadata sites list
  • Names are 3 to 40 characters of a-z, 0-9 and -, not starting or ending with -, without --. A name is the site's address and cannot change later (delete the site and create another). A deleted site's name is free again after 30 days.
  • Some names are reserved (409 name_taken): www, api, app, admin, console, docs, mail, status, sites, dabadata, and any name containing login, signin, account, verify, secure, bank, paypal or wallet.
  • On Free, creating a site needs a confirmed email address (403 email_unverified otherwise).
  • --spa makes it a single-page app; you can switch it later.

A new site answers "nothing deployed yet" until its first deploy.

Deploy with the CLI

Build your app, then deploy the output folder:

bash
npm run build
dabadata sites deploy ./dist --site my-shop
# uploads the folder, publishes it, checks that https://my-shop.dabadata.site serves it
# Live: https://my-shop.dabadata.site (deploy dep_..., 12 files, 240 KB)
# Reachable: checked from our server at 10:00:03 UTC; every location has it by 10:02:00 UTC.
#   ok   my-shop.dabadata.site
  • A deploy is the whole site: the folder as it is now. Nothing is merged with the deploy before, so a file you deleted locally is gone from the site too.
  • Dot files and dot folders are skipped, except .well-known/.
  • The files go straight to object storage, 8 at a time, each with a short-lived signed URL; then the deploy is published in one step.
  • After publishing, the CLI waits (up to a minute) for the readiness check and prints what each public hostname serves. --no-wait skips the wait.
  • --yes skips the confirmation, for scripts and CI.

Deploying needs the environment's secret key or a console sign-in with the developer role or above; viewers can look but not deploy. The JavaScript client has no site calls, since deploys hold the secret key.

Deploy from CI

A CI job deploys with the environment's secret key, kept as a CI secret. deploy-site.mjs does the same steps as dabadata sites deploy over the HTTP API: it uploads the folder, publishes it, waits for the readiness check, and lists every file that did not upload, with the storage's answer. It needs Node 18 or later (or Bun, or Deno) and nothing else. Commit a copy to your repository, for example as scripts/deploy-site.mjs, so the code that holds your key is code you have read.

yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
      - run: npm run build
      - run: node scripts/deploy-site.mjs ./dist my-shop
        env:
          DABADATA_SECRET_KEY: ${{ secrets.DABADATA_SECRET_KEY }}
  • Exit code. 0 when the deploy is live, 1 when nothing was published (a failed upload leaves the live site as it was), 2 for a usage error. The readiness result is printed; the deploy is live either way.
  • Which key. Use the key of the environment the site belongs to: the live key for the production site. The key can do everything in its environment, so keep it in the CI's secrets only. If it leaks, revoke it at once.
  • Staging and previews. There are no preview addresses per branch yet. Use a second site for staging, for example my-shop-staging in the test environment, deployed with the test secret key from another branch or workflow.
  • The CLI too. dabadata sites deploy ./dist --site my-shop --yes takes the secret key as DABADATA_TOKEN and works out the environment from it.
  • Old deploys. The newest 10 deploys and the live one are kept for rollback; older ones are deleted by the nightly job.

Deploy from an AI agent

An agent connected to the project's MCP server with the secret key can build a page and put it live in one call. site_deploy takes the files themselves, uploads them on the server and publishes:

json
{
  "site": "my-shop",
  "files": [
    { "path": "index.html", "content": "<!doctype html><html>...</html>" },
    { "path": "app.js", "content": "import { createClient } from ..." },
    { "path": "logo.png", "content": "iVBORw0KGgo...", "encoding": "base64" }
  ]
}

encoding is "utf-8" (the default) or "base64" for images and other binary files. One call carries at most 10 MB; a bigger site is deployed with the CLI. site_deploy and site_rollback wait up to 30 seconds for the readiness check and say whether the site's hostnames serve the deploy. The other tools are sites_list, site_create, site_domain_add and site_domain_check (MCP tools).

Readiness: is it really live?

Publishing answers as soon as the deploy is live in the database. The edge locations pick a publish up within about 30 seconds, so for a few seconds some visitors can still get the previous deploy, or "No site here" on a first deploy. Readiness checks that the public hostnames actually serve the live deploy, so a script or an agent knows when to send people to the site.

Publishing, a rollback and a domain turning active start a check. It asks each public hostname - <name>.dabadata.site and every active domain - for one file of the deploy (index.html, or its first file), and checks four steps:

StepPasses when
dnsThe hostname resolves to public addresses.
tlsThe HTTPS connection succeeds with a valid certificate.
routeThe site answers 200. A 404 means the edge has no route for the hostname yet; 503 means the site is paused or over its plan; 451 means it was taken down.
deployThe answer names the live deploy (the X-Dabadata-Deploy header every site file carries); otherwise the hostname still serves an earlier deploy.
certificateYour own domains only: the domain and its certificate are active at the edge provider.

The site's health holds the result:

json
"health": {
  "deploy": "dep_3f2a9c1e7b5d",
  "status": "reachable",
  "startedAt": "2026-10-18T10:00:00.000Z",
  "checkedAt": "2026-10-18T10:00:03.000Z",
  "reachableAt": "2026-10-18T10:00:03.000Z",
  "propagatedBy": "2026-10-18T10:02:00.000Z",
  "hosts": [
    { "hostname": "my-shop.dabadata.site", "ok": true,
      "dns": { "ok": true, "detail": "104.16.1.1" }, "tls": { "ok": true, "detail": null },
      "route": { "ok": true, "detail": null }, "deploy": { "ok": true, "detail": null } }
  ]
}
  • pending: checking. The check runs at once, then every 5 seconds while a hostname is not ok, for at most 2 minutes.
  • reachable: every hostname serves the live deploy. The check runs from our server, so it proves the location nearest to it; every location has the deploy by propagatedBy (2 minutes after the publish).
  • unreachable: after 2 minutes some hostname still does not. Each host's failing step has a detail saying why, such as a DNS record that points elsewhere or a certificate that is not issued yet.
  • unverified: after 2 minutes every hostname answers, but for some of them it could not be told which deploy they serve (their deploy step is ok: null). A site is only reachable when the deploy is proven.
  • A step with ok: null was not reached because an earlier step failed, or could not tell.
  • What a visitor's own network does (a company filter, a DNS block) cannot be seen from our server.

To wait in a script, add ?wait= (seconds, at most 60) to publish or rollback: the answer comes once the check leaves pending, with site.health. To check again later, use the console's Check now on the Sites page or POST /v1/project/sites/:id/health (once every 10 seconds per site; sooner answers 429 with retryAfter).

How paths map to files

File paths in a deploy are relative (index.html, assets/app.3f2a.js): no leading /, no . or .. segment, no \, at most 512 bytes. A request's path is URL-decoded and looked up like this:

RequestServes
/index.html
/about/about/index.html
/aboutabout, else about.html, else a 301 to /about/ when about/index.html exists
anything else not founda single-page app's index.html, else your 404.html with status 404, else a plain 404 page

Sites answer GET and HEAD only (anything else is a 405), and always over HTTPS: http:// redirects.

Single-page apps and 404.html

An app with client-side routing (React Router, Vue Router, Solid Router) has one index.html for every address. Mark the site spa - --spa when you create it, the switch on the console's Sites page, or PATCH with { "spa": true } - and a path that matches no file, and whose last segment has no extension, gets index.html with status 200. So /orders/42 loads the app, while a missing /assets/app.js is still a real 404 instead of HTML pretending to be JavaScript.

A site that is not a single-page app can ship a 404.html at its root; it is served with status 404 for any path that matches no file.

Caching and headers

  • HTML is sent with Cache-Control: no-cache, so a new deploy shows at the next page load.
  • A fingerprinted file - one whose name carries its build hash, such as assets/main-pvlvU-M5.js (Vite), app.3f2a9c1e.js (webpack) or anything under _next/static/ - is public, max-age=31536000, immutable: new content gets a new name, so a browser keeps it for a year.
  • Every other file, such as gallery/photo-1.jpg or favicon.svg, is public, no-cache: the browser asks each time and gets a short 304 when nothing changed, so a new deploy or a rollback shows at once even for files that keep their name.
  • The edge cache is keyed by deploy, so publishing never serves a stale file from the edge. A visitor's browser may keep an asset up to an hour; build tools that put a hash in asset names (Vite, Next's export, Astro) avoid that entirely.
  • Content-Type is the type the file was uploaded with (the CLI derives it from the extension; text types get charset=utf-8), with an ETag, X-Dabadata-Deploy (the deploy it came from), X-Content-Type-Options: nosniff and Referrer-Policy: strict-origin-when-cross-origin. Sites never set cookies.

Rollback

Every deploy is kept as it was: the newest 10 and the live one. Putting an older one back is instant, with no upload:

bash
dabadata sites rollback dep_3f2a9c1e7b5d --site my-shop

The console's Sites page lists the deploys with their size, who made them and when, and rolls back from there too. Open a deploy's row to see its files (path, type and size); on the live deploy each path opens on the site. Older deploys, and uploads never published within 24 hours, are removed by the nightly job. Kept deploys count toward the workspace's file storage.

Move a site between test and live

A site built on test can move to live when it is ready, and back: on the console's Sites page press Move to live (or Move to test). The site keeps its name, its deploys and files, its domains and its readiness; nothing is uploaded, and its address keeps serving the same deploy without a gap. Its origins leave the old environment's allowed origins and join the new one's.

  • Later deploys go to the new environment: dabadata use prj_xxxxxxxxxxxx for live (:test for test), or that environment's secret key.
  • If the page calls dabadata, point it at the new environment's publishable key: the data stays where it was.
  • A move needs a console session of a developer or up. A secret key reaches only its own environment, so it cannot move a site (403 forbidden). The site counts toward the same workspace, so the plan is not checked again.

Your own domain

From Starter, connect a domain you own to a site. One domain is one hostname plus, if you like, its redirect partner: www.example.com with example.com redirecting to it (or the other way round) counts as one.

bash
dabadata sites domain add www.example.com --site my-shop --redirect-from example.com
# Add at your DNS host:
#   CNAME  www.example.com  ->  sites.dabadata.site
#   CNAME  example.com      ->  sites.dabadata.site   (needs CNAME flattening, ALIAS or ANAME)
dabadata sites domain check www.example.com
  1. Add the domain in the console or with the CLI. It answers the DNS records to create.
  2. At your DNS host, point each hostname at sites.dabadata.site with a CNAME. A bare domain (example.com, without www.) cannot carry a plain CNAME at most DNS hosts: use their CNAME flattening, ALIAS or ANAME record. If yours has none, serve the site on www. and let the bare domain redirect to it.
  3. Wait. The HTTPS certificate is issued by itself once DNS points the right way, usually within minutes. The server checks pending domains every 5 minutes; checking in the console (or domain check) asks at once.

--redirect-from must be the same hostname with or without www.; requests to it get a 301 to the same path on the main hostname. A hostname under dabadata.site or the service's own domain, or one already connected to any site, is refused (409 domain_taken).

StatusMeans
pendingWaiting for DNS and the certificate.
activeThe certificate is issued and the domain serves the site's live deploy. Its https:// origin is added to the environment's allowed origins.
failedStill pending after 7 days. error says why (often a missing or wrong DNS record); fix it and check again.
disabledThe workspace has more domains than its plan allows (after a move to a smaller plan or an unpaid invoice). It comes back when the plan allows it again.

Removing a domain (dabadata sites domain remove www.example.com) stops serving it at once.

Calling your API from the site

A site is a frontend like any other: it calls https://dabadata.com with the environment's publishable key and the signed-in user's token, under your rules. Creating a site adds https://<name>.dabadata.site to the environment's allowed origins, and a domain turning active adds its origin too, so there is nothing to configure; deleting either removes it. An environment with no allowed origins listed already accepts every origin and is left as it is.

html
<script type="module">
  import { createClient } from "https://dabadata.com/client/dist/dabadata.js";
  // A site on the live environment uses the live publishable key.
  const db = createClient({ url: "https://dabadata.com", key: "pk_live_EXAMPLEexample0000000000" });
  db.subscribe("products", {}, { onChange: (c) => render(c) });
</script>

Never put the secret key in a site: everything in it is public.

On Free: the badge and idle pause

  • Pages of a site on Free carry a small "Hosted on dabadata" link in a corner. Paid plans have none; it goes away as soon as the workspace changes plan.
  • A site sleeps with its environment. When a Free environment is paused after 7 idle days, its sites answer 503 "This site is paused", with a link for the owner to sign in. Visits to the site do not count as activity and do not wake it: free hosting is for apps that use dabadata. Any call with the environment's keys or tokens, or a deploy, wakes the environment and its sites.

A site over its plan's allowance (after a move to a smaller plan or an unpaid invoice) answers 503 "This site is over its plan" until the plan allows it again; a site taken down for abuse answers 451.

Limits

FreeStarterProScaleEnterprise
Sites, workspace-wide1310unlimitedunlimited
Custom domains, workspace-wide013510
Size of one deploy100 MB1 GB1 GB2 GB2 GB
Files in one deploy1,00010,00020,00020,00020,000
"Hosted on dabadata" badgeyesnononono
  • One file of a deploy may be at most the plan's largest file (50 MB on Free). Kept deploys count toward file storage. Serving costs nothing.
  • A site or domain beyond the allowance is refused with 403 { "error": "plan_limit", "limit": "sites" } or "customDomains"; on Free a domain gets custom domains start with Starter. A deploy that is too big gets 403 plan_limit with limit: "siteBytes", "siteFiles" or "maxFileBytes", before anything is uploaded.
  • Moving to a plan with fewer sites or domains than the workspace has is refused with 409 over_limit.

See Plans and limits.

HTTP API

The CLI, the console and the MCP tools use these routes; a script or CI job can too. Who: the environment's secret key, or a console session (viewers read, developers and up write). Details on HTTP API: Sites.

RouteDoes
GET /v1/project/sitesThe environment's sites with their domains
GET /v1/project/sites/available?name=Whether a name is free
POST /v1/project/sitesCreate a site: { name, spa? }
PATCH /v1/project/sites/:idChange spa
DELETE /v1/project/sites/:idDelete the site, its domains and deploys
POST /v1/project/sites/:id/moveMove the site to the project's other environment: { to: "live" | "test" } (console session only)
POST /v1/project/sites/:id/deploysStart a deploy: the file list in, a signed upload URL per file out
POST /v1/project/sites/:id/deploys/:dep/publish?wait=Make an uploaded deploy live; wait (0 to 60 s) waits for the readiness check
GET /v1/project/sites/:id/deploysThe deploys, newest first
GET /v1/project/sites/:id/deploys/:depOne deploy and its files: { deploy, files: [ { path, size, type } ] }
POST /v1/project/sites/:id/rollback?wait=Make an earlier deploy live: { deploy }
POST /v1/project/sites/:id/healthCheck now whether the public hostnames serve the live deploy
POST /v1/project/sites/:id/domainsConnect a domain: { hostname, redirectFrom? }
POST /v1/project/sites/:id/domains/:dom/checkCheck a pending domain now
DELETE /v1/project/sites/:id/domains/:domRemove a domain

A deploy by hand is three steps, which is what dabadata sites deploy does:

bash
curl -X POST https://dabadata.com/v1/project/sites/$SITE_ID/deploys \
  -H "Authorization: Bearer $DABADATA_SECRET_KEY" -H "Content-Type: application/json" \
  -d '{"files":[{"path":"index.html","size":1834,"type":"text/html"}]}'
# -> { "deploy": { "id": "dep_...", "status": "uploading", "files": 1, "bytes": 1834 },
#      "uploads": [ { "path": "index.html", "url": "https://...r2.cloudflarestorage.com/...",
#                     "method": "PUT", "headers": { "Content-Type": "text/html" } } ],
#      "expiresAt": "..." }
curl -X PUT "$UPLOAD_URL" -H "Content-Type: text/html" --data-binary @dist/index.html
curl -X POST "https://dabadata.com/v1/project/sites/$SITE_ID/deploys/$DEPLOY_ID/publish?wait=60" \
  -H "Authorization: Bearer $DABADATA_SECRET_KEY"
# -> { "deploy": { ... "status": "live" }, "site": { ... "health": { "status": "reachable", ... } } }

The upload URLs are valid an hour, with each file's exact size and type signed in. Publishing checks that every file arrived as declared; otherwise it answers 409 { "error": "upload_incomplete", "missing": [ path ], "files": [ { path, size, stored } ] } (stored: the size that arrived, null when nothing did) and you upload those and publish again.

dabadata documentation, written against API contract v2.