Custom Domains
Every Cell is reachable at a *.tissue.dev address the moment it is deployed. You can also serve it from your own domain — api.yourdomain.com, app.example.com, or a bare apex like example.com — by registering the domain on your Cell, proving you own it, and pointing DNS at the Cell. The whole flow is self-serve — no support ticket, no waiting on an operator.
How it works
Connecting a custom domain takes three steps:
- Register the domain on your Cell. Tissue returns a one-time TXT ownership challenge and a CNAME target.
- Prove ownership by publishing the TXT record at your DNS provider, then asking Tissue to verify it.
- Point the domain at your Cell with a
CNAME(subdomain) orALIAS/ANAME(apex), then verify once more.
Once DNS resolves to your Cell, TLS is provisioned automatically (Let's Encrypt, via on-demand TLS) on the first HTTPS request — there is nothing to configure and no certificate to upload.
The Cell receives the request with your custom hostname intact in request.url — the platform does not rewrite the URL.
Status lifecycle
A registered domain moves through these states:
| Status | Meaning |
|---|---|
pending |
Registered, but the TXT ownership challenge has not been seen yet. |
verified |
Ownership proven. DNS is not yet pointing at the Cell. |
active |
Live and serving over HTTPS. TLS has been provisioned. |
failed |
Ownership could no longer be confirmed (e.g. the TXT record was removed). |
Run ribo domain verify <domain> (or click Verify in the dashboard) to re-check and advance the status.
Step 1 — Register the domain
Registering the domain reserves it on your Cell and returns the records you'll need. A domain is globally unique — it can be attached to only one account at a time.
CLI
ribo domain add app.example.com
The Cell is taken from [cell] name in your ribo.toml, or pass --cell <name> to target a different Cell. The command prints the TXT ownership challenge and the CNAME target:
✓ Registered app.example.com for cell my-api
status: pending
1. Ownership (TXT)
name _tissue-challenge.app.example.com
value 3f8c1a...e2d9 (64-hex token)
2. Routing (CNAME — or ALIAS/ANAME for an apex)
target my-api.strand-9c.tissue.dev
Dashboard
Open your Cell, go to Custom Domains, and click Add domain. The same TXT challenge and CNAME target are shown on the domain's row.
MCP / API
Via the MCP server, call the domain_add tool. Over the REST API:
curl -X POST https://api.tissue.systems/v1/domains \
-H "Authorization: Bearer $RIBO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cell": "my-api", "domain": "app.example.com"}'
Step 2 — Prove ownership
Ownership verification is required for security: without it, anyone could point an already-registered domain at Tissue and hijack traffic to it. At your DNS provider, add a TXT record:
| Field | Value |
|---|---|
| Type | TXT |
| Name / Host | _tissue-challenge.app.example.com |
| Value | The 64-hex token shown at registration |
There is one TXT token per registration, and it stays stable for the life of the registration — you don't need to update it on redeploys.
Then verify:
ribo domain verify app.example.com
or click Verify in the dashboard. Once Tissue sees the record, the status moves from pending to verified. DNS changes can take a few minutes to propagate — if verification doesn't succeed immediately, wait and re-run it.
Step 3 — Point the domain at your Cell
With ownership proven, point the hostname at your Cell's tissue.dev address. Find that address with ribo list (or from the output of your last ribo deploy) — it's the CNAME target shown at registration, e.g. my-api.strand-9c.tissue.dev.
Subdomains (app.example.com)
Add a CNAME record:
| Field | Value |
|---|---|
| Type | CNAME |
| Name / Host | The subdomain, e.g. app |
| Value / Target | my-api.strand-9c.tissue.dev |
| TTL | 300 (5 minutes) or your provider's default |
Apex / root domains (example.com)
Standard DNS does not permit a CNAME at the apex of a domain. Most providers offer an ALIAS or ANAME record (or, on Cloudflare, CNAME flattening) that points the apex at a hostname instead — use that, with the same my-api.strand-9c.tissue.dev target.
Then verify one last time:
ribo domain verify example.com
Once DNS resolves to your Cell, the status moves to active and TLS is provisioned automatically on the first HTTPS request. https://example.com is then live.
Cloudflare users — use a grey cloud. The record pointing at Tissue must be set to "DNS only" (grey cloud icon), not proxied (orange cloud). A proxied record terminates TLS at Cloudflare and breaks the HTTP-01 certificate challenge, so your domain will never obtain a certificate. This applies to both the
CNAME(subdomain) and the CNAME-flattened apex record.
DNS provider setup
Step-by-step for the most common providers. In every case you add two records: the
_tissue-challenge… TXT (Step 2, ownership) and the CNAME (Step 3, routing). Throughout,
<token> is the value shown at registration and my-api.strand-9c.tissue.dev is your Cell's
hostname (yours will differ — use the one from ribo list). Most providers auto-append your
domain to the Name/Host field, so you enter only the prefix (_tissue-challenge.app, app);
if yours wants the full name, type _tissue-challenge.app.example.com.
Cloudflare
DNS → Records → Add record.
- TXT — Name
_tissue-challenge.app, Content<token>. - CNAME — Name
app, Targetmy-api.strand-9c.tissue.dev, Proxy status: DNS only (grey cloud). - Apex — Name
@; Cloudflare flattens the CNAME automatically. Still grey cloud.
The grey cloud is mandatory — a proxied (orange) record breaks certificate issuance.
GoDaddy
My Products → DNS → Add.
- TXT — Host
_tissue-challenge.app, Value<token>, TTL 600s (Custom). - CNAME — Host
app, Points tomy-api.strand-9c.tissue.dev. (GoDaddy may require a trailing dot:my-api.strand-9c.tissue.dev.) - Apex — GoDaddy has no
ALIAS/ANAMEat the root. Use a subdomain such aswwworapp.
Namecheap
Domain List → Manage → Advanced DNS → Add New Record.
- TXT Record — Host
_tissue-challenge.app, Value<token>. - CNAME Record — Host
app, Valuemy-api.strand-9c.tissue.dev. - Apex — use an ALIAS Record, Host
@, Targetmy-api.strand-9c.tissue.dev.
Google Domains / Squarespace Domains
DNS → Manage custom records → Create new record (Squarespace: Domains → DNS → Add record).
- TXT — Host
_tissue-challenge.app, Data<token>. - CNAME — Host
app, Datamy-api.strand-9c.tissue.dev. - Apex — set Host
@; CNAME at the root is synthesized/flattened.
AWS Route 53
Hosted zones → your zone → Create record.
- TXT — Record name
_tissue-challenge.app, value"<token>"(quotes required in Route 53). - CNAME — Record name
app, valuemy-api.strand-9c.tissue.dev. - Apex — Record type A, toggle Alias on, route to "another value" →
my-api.strand-9c.tissue.dev.
Porkbun
Domain Management → your domain → DNS.
- TXT — Subdomain
_tissue-challenge.app, Answer<token>. - CNAME — Subdomain
app, Answermy-api.strand-9c.tissue.dev. - Apex — add an ALIAS record, Subdomain blank (
@), Answermy-api.strand-9c.tissue.dev.
DNSimple
Domains → your domain → DNS → Add Record.
- TXT — Name
_tissue-challenge.app, Content<token>. - CNAME — Name
app, Targetmy-api.strand-9c.tissue.dev.(trailing dot recommended). - Apex — use an ALIAS record, Name blank, Target
my-api.strand-9c.tissue.dev..
Hover
Your domain → DNS → Add A Record (the form adds any record type).
- TXT — Hostname
_tissue-challenge.app, Value<token>. - CNAME — Hostname
app, Targetmy-api.strand-9c.tissue.dev. - Apex — Hover has no
ALIAS/ANAME. Use a subdomain (e.g.www).
Gandi
Domain → DNS Records → Add.
- TXT — Name
_tissue-challenge.app, Value<token>. - CNAME — Name
app, Hostnamemy-api.strand-9c.tissue.dev.(trailing dot). - Apex — Gandi LiveDNS has no native
ALIAS. Use a subdomain, or Gandi's web-redirect from the apex towww.
DigitalOcean
Networking → Domains → your domain.
- TXT — Hostname
_tissue-challenge.app, Value<token>. - CNAME — Hostname
app, "Is an alias of"my-api.strand-9c.tissue.dev.. - Apex — DigitalOcean DNS has no
ALIAS/ANAME. Use a subdomain.
Another provider? Any provider with CNAME support works for subdomains. For an apex/root
domain you need ALIAS/ANAME support or CNAME flattening; if your provider has neither, point a
subdomain (e.g. www) at your Cell and redirect the apex to it. Look for a "DNS", "DNS Zone
Editor", or "Advanced DNS" section in your control panel.
Managing domains
List the domains registered on your Cell and their current status:
ribo domain list
# app.example.com active dd0d1cd5k2mx7
# example.com verified dd0d1cd5k2mx7
(The CELL column shows the cell's 13-character address, not its name.)
Remove a domain (it stops serving immediately, and the name is freed for reuse):
ribo domain rm app.example.com
See the ribo CLI reference for the full command set.
The Cell sees your custom hostname
Once a domain is active, your Cell sees requests arriving at the custom hostname:
export default {
async fetch(request, env) {
const url = new URL(request.url);
// url.hostname → "app.example.com"
// url.pathname → whatever path the client requested
return Response.json({ host: url.hostname, path: url.pathname });
},
};
A single Cell can serve any number of custom domains — repeat the three steps for each one. No configuration in ribo.toml is required.
Troubleshooting
Verification never advances past pending. Tissue can't see the TXT record yet. Confirm the record name is exactly _tissue-challenge.<your-domain> and the value matches the token from registration, then allow a few minutes for DNS propagation and re-run ribo domain verify. Check the record directly with dig _tissue-challenge.app.example.com TXT.
Verification reaches verified but never active. The domain's ownership is proven but DNS isn't yet pointing at your Cell. Confirm the CNAME (or apex ALIAS) target matches your Cell's tissue.dev hostname exactly, then verify again.
Browser shows a certificate error for the custom hostname. The certificate is provisioned on the first HTTPS request after DNS resolves — give it a moment. If it persists, the most common cause is a proxied (orange-cloud) Cloudflare record intercepting the HTTP-01 challenge. Switch the record to DNS only.
Your domain uses a CAA record. If your domain has a CAA record restricting certificate authorities, add an entry allowing letsencrypt.org — Tissue issues custom-domain certificates via Let's Encrypt. Without it, issuance fails even after the domain is verified.
Status flipped to failed. Ownership could no longer be confirmed — usually the TXT challenge record was removed. Re-add it and run ribo domain verify to recover.
You can't add a CNAME because the name already has other records. A name with a CNAME cannot carry any other record type (MX, TXT, A, etc.) — this is a DNS-wide rule. Use a different subdomain, or move the conflicting records.
A note on certificate transparency
When Tissue issues a certificate for your domain, the hostname is recorded in public Certificate Transparency logs. This is standard for every TLS certificate from every provider, not specific to Tissue — but it does mean the domain name becomes publicly discoverable once a certificate is issued.
See also
- Cells Overview — Cell addresses and default URLs
- Writing Cells — reading
request.urland routing - ribo CLI Reference —
ribo domaincommands - ribo.toml Reference — deploying Cells