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 aswww.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.siteis 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:
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-9and-, 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 containinglogin,signin,account,verify,secure,bank,paypalorwallet. - On Free, creating a site needs a confirmed email address (403
email_unverifiedotherwise). --spamakes 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:
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-waitskips the wait. --yesskips 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.
# .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-stagingin the test environment, deployed with the test secret key from another branch or workflow. - The CLI too.
dabadata sites deploy ./dist --site my-shop --yestakes the secret key asDABADATA_TOKENand 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:
{
"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:
| Step | Passes when |
|---|---|
dns | The hostname resolves to public addresses. |
tls | The HTTPS connection succeeds with a valid certificate. |
route | The 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. |
deploy | The answer names the live deploy (the X-Dabadata-Deploy header every site file carries); otherwise the hostname still serves an earlier deploy. |
certificate | Your own domains only: the domain and its certificate are active at the edge provider. |
The site's health holds the result:
"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 bypropagatedBy(2 minutes after the publish).unreachable: after 2 minutes some hostname still does not. Each host's failing step has adetailsaying 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 (theirdeploystep isok: null). A site is onlyreachablewhen the deploy is proven.- A step with
ok: nullwas 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:
| Request | Serves |
|---|---|
/ | index.html |
/about/ | about/index.html |
/about | about, else about.html, else a 301 to /about/ when about/index.html exists |
| anything else not found | a 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/- ispublic, 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.jpgorfavicon.svg, ispublic, 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-Typeis the type the file was uploaded with (the CLI derives it from the extension; text types getcharset=utf-8), with anETag,X-Dabadata-Deploy(the deploy it came from),X-Content-Type-Options: nosniffandReferrer-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:
dabadata sites rollback dep_3f2a9c1e7b5d --site my-shopThe 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_xxxxxxxxxxxxfor live (:testfor 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.
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- Add the domain in the console or with the CLI. It answers the DNS records to create.
- At your DNS host, point each hostname at
sites.dabadata.sitewith aCNAME. A bare domain (example.com, withoutwww.) cannot carry a plain CNAME at most DNS hosts: use their CNAME flattening,ALIASorANAMErecord. If yours has none, serve the site onwww.and let the bare domain redirect to it. - 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).
| Status | Means |
|---|---|
pending | Waiting for DNS and the certificate. |
active | The certificate is issued and the domain serves the site's live deploy. Its https:// origin is added to the environment's allowed origins. |
failed | Still pending after 7 days. error says why (often a missing or wrong DNS record); fix it and check again. |
disabled | The 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.
<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
| Free | Starter | Pro | Scale | Enterprise | |
|---|---|---|---|---|---|
| Sites, workspace-wide | 1 | 3 | 10 | unlimited | unlimited |
| Custom domains, workspace-wide | 0 | 1 | 3 | 5 | 10 |
| Size of one deploy | 100 MB | 1 GB | 1 GB | 2 GB | 2 GB |
| Files in one deploy | 1,000 | 10,000 | 20,000 | 20,000 | 20,000 |
| "Hosted on dabadata" badge | yes | no | no | no | no |
- 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 getscustom domains start with Starter. A deploy that is too big gets 403plan_limitwithlimit: "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.
| Route | Does |
|---|---|
GET /v1/project/sites | The environment's sites with their domains |
GET /v1/project/sites/available?name= | Whether a name is free |
POST /v1/project/sites | Create a site: { name, spa? } |
PATCH /v1/project/sites/:id | Change spa |
DELETE /v1/project/sites/:id | Delete the site, its domains and deploys |
POST /v1/project/sites/:id/move | Move the site to the project's other environment: { to: "live" | "test" } (console session only) |
POST /v1/project/sites/:id/deploys | Start 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/deploys | The deploys, newest first |
GET /v1/project/sites/:id/deploys/:dep | One 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/health | Check now whether the public hostnames serve the live deploy |
POST /v1/project/sites/:id/domains | Connect a domain: { hostname, redirectFrom? } |
POST /v1/project/sites/:id/domains/:dom/check | Check a pending domain now |
DELETE /v1/project/sites/:id/domains/:dom | Remove a domain |
A deploy by hand is three steps, which is what dabadata sites deploy does:
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.