Cells/Custom Domains

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:

  1. Register the domain on your Cell. Tissue returns a one-time TXT ownership challenge and a CNAME target.
  2. Prove ownership by publishing the TXT record at your DNS provider, then asking Tissue to verify it.
  3. Point the domain at your Cell with a CNAME (subdomain) or ALIAS/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, Target my-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 to my-api.strand-9c.tissue.dev. (GoDaddy may require a trailing dot: my-api.strand-9c.tissue.dev.)
  • Apex — GoDaddy has no ALIAS/ANAME at the root. Use a subdomain such as www or app.

Namecheap

Domain List → Manage → Advanced DNS → Add New Record.

  • TXT Record — Host _tissue-challenge.app, Value <token>.
  • CNAME Record — Host app, Value my-api.strand-9c.tissue.dev.
  • Apex — use an ALIAS Record, Host @, Target my-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, Data my-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, value my-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, Answer my-api.strand-9c.tissue.dev.
  • Apex — add an ALIAS record, Subdomain blank (@), Answer my-api.strand-9c.tissue.dev.

DNSimple

Domains → your domain → DNS → Add Record.

  • TXT — Name _tissue-challenge.app, Content <token>.
  • CNAME — Name app, Target my-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, Target my-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, Hostname my-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 to www.

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