Getting started
Pages hosts static sites — anything that builds down to plain files (HTML,
CSS, JS, images, fonts). If your framework produces a dist/ or build/ folder, you can deploy it. There's no runtime and no server
config: you build locally (or in CI), and we serve the files.
The whole flow is three steps:
- Install the CLI and sign in with
one9x login. -
Run
one9x pages release ./dist --site mysite --deploy. - Open the HTTPS URL it prints.
Everything on this page is part of the launch product: signed-in deploys to a
platform subdomain (mysite.one9x.app) over HTTPS, atomic deploys,
staged releases with preview URLs, one-command
rollback, custom domains with managed certificates, and deploy from an AI
agent over MCP. Paid tiers are on the roadmap.
Install & sign in
The CLI is a single static binary — the same one9x binary used across
every one9x product, with Pages as a subcommand group. Install it, then sign in
once to link the CLI to your account.
$ curl -fsSL https://get.one9x.com | shinstalled one9x → /usr/local/bin$ one9x loginopening browser to authorize …✓ signed in as you@example.com $ curl -fsSL https://get.one9x.com | shinstalled one9x → /usr/local/bin$ one9x loginopening browser to authorize …✓ signed in as you@example.com $ irm https://get.one9x.com/install.ps1 | iexinstalling one9x (windows/amd64) …✓ one9x.exe → %LOCALAPPDATA%\Programs\one9x$ one9x login✓ signed in as you@example.com
The macOS and Linux installer is a shell script and drops the binary in /usr/local/bin. The Windows one drops one9x.exe in %LOCALAPPDATA%\Programs\one9x and adds it to your PATH — no admin
rights needed, and it works in the same terminal.
Prefer to place it yourself? The binaries are plain downloads — get.one9x.com/<version>/one9x-windows-amd64.exe (or -arm64), with the current version at get.one9x.com/VERSION. macOS and Linux builds sit alongside them under
the same paths.
one9x login authorizes the CLI against your one9x account and stores a
token locally — there's no API key to copy around, and it renews itself
so you sign in once per machine. Every Pages command runs as you, against
your sites. For CI, cron and agents, mint a machine token with one9x tokens create <name> (or from the dashboard) and set it as ONE9X_TOKEN — it lasts 90 days, extends without changing the secret,
and you can revoke it any time:
$ one9x tokens create "ci"one9x_tok_7Kd… (shown once)$ export ONE9X_TOKEN=one9x_tok_…$ one9x pages release ./dist --site mysite --deploy✓ deployed
Signup is open — you need an account before the CLI can log in.Create one, confirm your email, then run one9x login.
Deploy a directory
Point the CLI at your build output and give the site a name with --site. The site name is a DNS label — lowercase letters, digits and
hyphens, 1–63 characters — and it becomes your subdomain.
$ one9x pages release ./dist --site mysite --deployhashing ./dist … 248 filesuploading … 41 changed, 207 unchanged✓ v8 live in 1.4s→ https://mysite.one9x.app/ The CLI hashes your files locally and uploads only the ones that actually changed since your last release, so the first push is quick and every push after is quicker — an unchanged re-release uploads nothing. The new version goes live in a single atomic flip: visitors are served either the old version or the new one, never a half-uploaded mix. The full mechanics are in How deploys work.
--deploy is what makes it live. Leave it off and you get a staged release with a preview URL instead.
Build your command
Tick what you need and copy the result. Every flag here is one the binary actually takes — the full list is in the CLI reference.
--error takes <code>:<path> and repeats — one row per status.
More flags
What gets uploaded
Everything in the directory except dot-paths — .git, .env and
anything else beginning with a dot stays on your machine. .well-known is the one exception, because it's a reserved public
path: assetlinks.json and apple-app-site-association only work
at that exact URL, so Android App Links and iOS universal links verify.
To hold anything else back, pass --exclude with a glob. It repeats,
and each pattern is matched against the path relative to your directory and against the file name — so '*.map' catches
source maps at any depth, while 'admin/*' stays where you
wrote it. Excluding a directory drops everything under it. Quote the
pattern, or your shell will expand it first.
$ one9x pages release ./dist --site mysite --deploy \ --exclude '*.map' --exclude admin12 path(s) excluded✓ v9 live Releases & previews
Building and going live are two verbs, not one. release uploads a
directory and creates an immutable version; deploy --to decides which
version production serves. Adding --deploy to a release does both in
one step, which is the common case.
Without --deploy the release is staged: it exists, it's
fully uploaded, and it's reachable at its own preview URL — but
production keeps serving whatever it was already on. Check the preview, then
promote it.
$ one9x pages release ./dist --site mysite✓ v9 staged · production still on v8→ https://v9--mysite.one9x.app/$ one9x pages deploy --site mysite --to v9active version v8 → v9✓ Live · nothing re-uploaded
A preview URL is v<n>--<site>.one9x.app — the same content,
the same edge, the same certificate, served from the same immutable files. It
costs nothing extra to keep around: previews share every unchanged file with
production. Preview URLs are public, so treat them as unlisted rather than
private.
Naming a release
Every release gets an automatic per-site number — v1, v2, v3 — and you can add your own label with --tag. Both work as a --to target, as does a version-hash prefix.
$ one9x pages release ./dist --site mysite --tag prod✓ v10 staged · tag prod$ one9x pages deploy --site mysite --to prod✓ Live How long releases are kept
Retention is count-based, not time-based, so how far you can roll back
doesn't depend on how often you ship. The last 5 untagged releases are
kept on the free tier — paid tiers keep more — and older ones age out. Tagged releases never age out
and don't use up one of those slots, but there's a cap of 5 tags per
site — at the cap a tagged release fails with an error rather than quietly
dropping one of your tags. one9x pages releases rm frees a slot.
SPA mode & error pages
Two options cover the common routing cases for static sites.
Single-page apps
Add --spa and any path that doesn't match a file is served /index.html with a 200 — so client-side routers (React Router,
Vue Router) handle deep links instead of returning a 404.
$ one9x pages release ./dist --site myapp --spa --deploy✓ Live · unknown routes → /index.html Custom error pages
Map a status code to a page in your build with --error code:path. The
flag repeats, so you can set as many as you need.
$ one9x pages release ./dist --site mysite --deploy \ --error 404:/404.html \ --error 403:/403.html✓ Live · custom 404, 403 Release history
Every release is recorded for the site, newest first. The STATUS column
says what each one is: live is the release production is serving, staged is one that exists but hasn't been deployed, and kept is a tagged release held back from retention.
$ one9x pages releases list --site mysiteSEQ VERSION TAG WHEN STATUSv9 sha256:5c1904ab 2m ago stagedv8 sha256:9f2a1b3c prod 1h ago livev7 sha256:41de88a0 3h agov6 sha256:aa03f1cc rc1 1d ago kept
A version id is the content hash of that release's manifest. The release
number (v8) and any tag are the friendlier way to refer to it; the hash
is there when you want to be certain which bytes you mean, and a prefix of it
works anywhere a release ref does.
Delete one with one9x pages releases rm v6 --site mysite. The live
release can't be deleted — deploy another one first.
Rolling back
Because every version is an immutable manifest of immutable, content-addressed files, no release ever overwrites another. Rolling back is just re-pointing the site at an earlier one — the files are already there, so it takes effect instantly with nothing to rebuild and nothing to re-upload. Keeping a long history is nearly free: shared files are stored once, so you're never charged twice for the same bytes.
There's no separate rollback command, because rolling back is the same
operation as going live: deploy --to points the site at a release. It
takes a release number, a tag, or a version-hash prefix.
$ one9x pages deploy --site mysite --to v6active version v7 → v6✓ Live · nothing re-uploaded
An empty --to "" means the release before the active one, which
is the one you want when a deploy has just gone wrong and you don't want to
go looking up a number first.
Custom domains
Every site is reachable immediately at its platform subdomain
(mysite.one9x.app) over HTTPS, with no setup. To serve it on your own
domain, link the domain to the site, prove you own it with a DNS record, and
point your DNS at us — we issue and renew the TLS certificate automatically.
domains is a top-level command, not part of pages: a domain
points at a site, whatever product runs it.
$ one9x domains link example.com --site mysite1. TXT _one9x-challenge.example.com -> 9f2a…2. CNAME example.com -> sites.one9x.app$ one9x domains verify example.com --site mysite✓ verified · certificate issued→ https://example.com/ link prints both records with your own token filled in — copy them from
there rather than from this page. Two records, and both are required:
The two DNS records
Ownership. A TXT record at _one9x-challenge.example.com holding the token link gave you.
Only whoever controls the domain's DNS can publish it, which is what makes
it proof — pointing DNS at us shows that someone did, not who.
Routing. A CNAME from your domain to sites.one9x.app.
DNS does not allow a CNAME at the apex (example.com with no
subdomain), so there the record depends on what your DNS provider supports:
| Name | Type | Value |
|---|---|---|
_one9x-challenge | TXT | the token link printed |
www (or any subdomain) | CNAMEpreferred | sites.one9x.app |
@ (apex, ALIAS support) | ALIAS / ANAMEpreferred | sites.one9x.app |
@ (apex, no ALIAS) | Afallback | 167.104.222.9 |
Prefer ALIAS or ANAME at the apex where you can. Cloudflare, Route 53 and DNSimple all support it; GoDaddy does not, which is why the A record exists. The difference matters over time: an ALIAS follows us if our edge address ever changes, while an A record is a number in your zone file that only you can update. We will give notice before ever changing it, but a record we can follow is better than a record you have to remember.
Order does not matter, and neither record expires. Verification is checked
against public resolvers rather than our own, so give a new record a minute to
propagate before running verify — and if it fails, the message names
what we actually saw, which is usually enough to spot a typo or a proxy that
is intercepting the name.
The certificate is issued the moment ownership is proved, loaded on the first
request to your domain, and renewed for you — there's nothing to install
or rotate. A domain and its www count as one, so example.com and www.example.com together use a single slot.
Custom domains are a paid-plan feature — Starter includes one,
Pro five — and the free tier serves *.one9x.app only. Paid plans are not
purchasable yet, so if you need a custom domain right now, get in touch and we will enable it on
your account.
Taking a domain off a site
Two commands, and the difference matters. domains unlink example.com
stops the domain routing but you keep the name and its certificate — so
linking it again later, to this site or another one, is usually instant. It
stays in domains list as unlinked. Neither this nor release needs --site; the owning site is looked up from the name.
domains release example.com gives the name up entirely: the record and
its certificate are deleted and another account may claim it. That one
can't be undone, so it asks for confirmation first.
Deploy from an agent
A deploy is a single authorized action, so it maps cleanly to a single agent tool call. Our MCP server exposes Pages as tools an AI agent can call directly — hand it a built directory and it ships a live URL, with no terminal and no shell glue. It runs through the same signed-in account as the CLI, so an agent only ever touches your sites.
tool: release dir: ./dist site: mysite deploy: true✓ Deployed → https://mysite.one9x.app/
The server exposes release, deploy, releases_list and sites_list — the same operations as the CLI, returning structured JSON
rather than prose — and is published to the public MCP directories so agents
can discover it. See the developers page for the
agent setup.
CLI reference
One binary, one9x. Products are subcommand groups (pages); sites and domains are account-level nouns shared across products.
Everywhere a site is named it's the --site flag, and it takes the
site's name — its subdomain — rather than an id.
| Global flag | Description | |
|---|---|---|
--portal <url> | optional | Portal base URL. Also ONE9X_PORTAL; defaults to portal.one9x.com. |
--json | optional | Emit machine-readable JSON on stdout. Progress goes to stderr. |
one9x login / logout / whoami
Authorize the CLI against your one9x account and store a token locally. Run
once per machine — the login renews itself, so it doesn't expire out from
under you. On a server with no browser, add --no-browser and approve
the printed URL from any other device. In CI, skip login entirely and set ONE9X_TOKEN to a machine token.
one9x tokens
Machine tokens authenticate CI, cron and agents with no browser. tokens create <name> mints one and prints it once — store it
then, because it is hashed and can never be shown again. Tokens last 90
days by default (--expires-in to choose, up to a year); tokens extend <id> pushes that out without changing
the secret, so CI keeps working with the value it already has. tokens list shows what exists, when each was last used and when it
expires, and tokens revoke <id> kills one immediately.
one9x pages create <name> / list / rm
pages create <name> makes a new site — the name becomes <name>.one9x.app and is what you pass to --site. pages list shows your sites and their status, and pages rm --site <site> deletes one, after confirming.
one9x pages release <dir>
Upload a directory as a new release. Staged unless you pass --deploy.
| Flag | Description | |
|---|---|---|
--site <site> | required | Site name (subdomain), host, custom domain, or numeric id. |
--deploy | optional | Make this release live immediately instead of staging it. |
--spa | optional | Serve /index.html (200) for unmatched routes. |
--error <code>:<path> | repeatable | Map an HTTP status code to a page in the build. |
--tag <name> | optional | Label the release. Tagged releases survive retention. |
--exclude <glob> | repeatable | Leave matching paths out of the release. Matched against the path relative to the directory and against the file name. |
one9x pages deploy
Make an existing release live. Uploads nothing — this is both go-live and rollback.
| Flag | Description | |
|---|---|---|
--site <site> | required | Which site to re-point. |
--to <ref> | required | Release number (v6), tag (prod), or hash prefix. --to "" is the previous release. |
one9x pages releases
releases list --site <site> shows a site's history, newest
first, with each release's number, tag and status. releases rm <ref> --site <site> deletes one, freeing a tag
slot. The live release can't be deleted.
one9x domains at launch
Custom domains, top-level rather than under a product. domains link <domain> --site <site> routes a name to a site
and starts verification, domains verify <domain> --site <site>
checks DNS and advances it, and domains list shows every name on the
account (or one site's, with --site).
domains unlink <domain> stops a name routing but keeps it and its
certificate; domains release <domain> gives it up entirely. Neither
takes --site.
one9x completion
Generates a completion script for bash, zsh, fish or powershell. Beyond command and flag names, values complete against your
account: --site offers your site names, --to offers that
site's releases and tags, and the domain commands offer your domains. If
you're logged out or offline it silently offers nothing rather than
erroring into your prompt.
How deploys work
Pages uses a “git-push” style deploy: the work of figuring out what changed happens on your machine, so the network only ever carries new bytes.
- Hash locally. The CLI walks your directory, computes the
SHA-256of each file, and builds a manifest mapping every path to its content hash. - Ask what's missing. It sends the manifest up; the service replies with just the hashes it hasn't seen before.
- Upload only those. The CLI uploads only the missing blobs, addressed by hash. Unchanged files — frameworks, libraries, images shared with earlier deploys — are never re-sent.
- Publish atomically. Once every blob is present, the manifest is stored as the new release. Going live is a separate step: the site's active-version pointer flips to that release in one operation. That flip is the whole of both deploying and rolling back, which is why a staged release costs nothing to keep and a rollback is instant.
Every file is stored once by its content and served with long-lived, immutable caching, so repeat visits and files shared across sites load
instantly. Nothing is ever mutated in place, which is what makes deploys atomic
and rollbacks free.
Coming soon
Designed and on the roadmap, kept separate from the launch product above so the docs only describe what's part of the release.
Paid tiers
Starter, Pro and Max — more sites, storage and custom domains, plus rollbacks, password-protected sites and an uptime SLA — open up as billing and tier limits go live.
Analytics
Per-site request and bandwidth numbers in the dashboard, without adding a script to your pages.
Multi-region serving
Serve a site from more than one region at once — closer to your visitors, no migration. A setting you flip, not a move you make.