Browse the docs

Teams

Custom domains

Serve a hosted app at a domain you own (tools.acme.com). DNS records to create, verification, HTTPS, sign-in, and the CLI and dashboard commands.

A hosted app lives at https://<slug>.<apps domain>/. You can also serve it at a domain you own, such as tools.acme.com. The app behaves the same there: sign-in, live queries, endpoints, storage, and inspection all work, and nothing in the capsule changes.

fuast domains add tools.acme.com        # prints the DNS records to create
fuast domains check tools.acme.com      # check DNS now; exits 1 until the domain is live
fuast domains list                      # status and records of every domain (alias: ls)
fuast domains remove tools.acme.com     # alias: rm

Each command targets the app in fuast.json, or takes a slug after the hostname: fuast domains add tools.acme.com my-app. Add --json for machine-readable output. In the dashboard, open the app and use the Domains tab.

Only the app owner and workspace admins can add, check, or remove domains. App deploy tokens (fuast token create --app) cannot.

DNS records

Create two records at your DNS provider. fuast domains add and the Domains tab show the exact values, with copy buttons.

Type Name Value Why
TXT _fuast.tools.acme.com fuast-verify=<token> Proves you control the domain. The token is unique to this domain and app.
CNAME tools.acme.com the platform's target, e.g. fuast-production.up.railway.app Sends the domain's traffic to fuast.
  • Apex domains (acme.com) cannot have a CNAME. Use your provider's ALIAS, ANAME, or flattened CNAME record pointing at the same target. fuast accepts the domain when its addresses match the target's.
  • Cloudflare: set the record to DNS only (grey cloud) unless the platform operator tells you otherwise. A proxied record hides the CNAME and the check fails.
  • When the platform issues certificates through its host (see HTTPS), the list includes one or two more records from that provider: usually a TXT record that lets it issue a certificate, and its own CNAME value. Create every record in the list.

Keep the records in place after the domain is live. fuast re-checks live domains every few hours.

Verification

After add, the domain is pending. fuast checks DNS in the background: often at first, then less often (backing off to once an hour). fuast domains check or Check now checks immediately. DNS changes can take minutes, sometimes hours, to show up.

Status Meaning
pending Waiting for the records. The response says which record is missing or wrong.
active Both records are right. The app is served on the domain.
error The certificate provider refused the domain, or a live domain lost its records (two failed checks in a row). The error field says why. Fix DNS and check again.

A domain that is never verified is released after 7 days, so nobody can hold a hostname they don't control. A DNS timeout never takes a live domain down; only definite answers ("no such record") do.

Rules for hostnames:

  • A full hostname with a dot. Internationalized names are converted to punycode. Uppercase and a trailing dot are fine.
  • Not an IP address, not localhost, not a reserved name (.local, .test, .internal, ...), no wildcards (*.acme.com).
  • Not the platform's own hosts, and nothing under the apps domain (for <name>.<apps domain>, use an alias).
  • One app per hostname. A hostname connected to another app is refused until it is removed there.
  • At most 5 custom domains per app (the platform can change this).

HTTPS

Custom domains are served over HTTPS. Who issues the certificate depends on the platform:

  • On fuast.com and Railway-hosted platforms, fuast registers the domain with Railway, which issues a Let's Encrypt certificate once DNS is in place, usually within an hour. The Domains tab shows the certificate status (issuing, valid, failed).
  • Other self-hosted platforms terminate TLS in front of fuast (for example Cloudflare for SaaS). The operator handles certificates.

What changes for the app

  • Primary domain. The first domain that is live with a valid certificate becomes the app's primary domain. It is the app's URL in the dashboard, in fuast deploy output, in share emails, and in the URLs ctx.storage.url() returns.
  • The old address keeps working. Pages opened at <slug>.<apps domain> redirect to the primary domain. The call API, WebSocket, endpoints, storage, and inspection routes keep answering on the old address too, so webhooks and scripts don't break.
  • Each hostname is its own origin. Sign-in works on every domain through the same one-time-code handshake as the subdomain, and each domain gets its own app session cookie (__Host- prefixed on HTTPS). Someone signed in on <slug>.<apps domain> signs in once more on tools.acme.com (one click if they are signed in to fuast). Guests get a separate guest session per hostname.
  • Origin checks accept requests from the hostname the request arrived on. A page on another domain cannot call the app's API with a visitor's cookies.
  • req.url in endpoints uses the hostname the request arrived on. In the UI, build links with appUrl(path) from fuast/client: it returns a path on whatever origin the page was opened on.

Removing a domain stops serving it at once. The app falls back to its next live domain, or to the subdomain.

Aliases

A name without a dot adds an alias under the platform's apps domain instead: fuast domains add my-tool serves the app at my-tool.<apps domain> too, with no DNS work. Aliases appear in fuast domains list next to custom domains.

HTTP API

The CLI and dashboard call these platform routes with your session or a personal token (Authorization: Bearer <token>).

Method Path Body Returns
GET /api/apps/<slug>/domains { custom: CustomDomain[], domains: Alias[], url, config }
POST /api/apps/<slug>/domains { "hostname": "tools.acme.com" } { ok, domain: CustomDomain }
POST /api/apps/<slug>/domains/<hostname>/check { ok, domain: CustomDomain }
DELETE /api/apps/<slug>/domains/<hostname> { ok, warning? }
type CustomDomain = {
  hostname: string; url: string; status: "pending" | "active" | "error"; primary: boolean; error: string | null;
  records: { type: string; name: string; value: string; purpose: "ownership" | "routing" | "provider-verification" | "acme";
             status: "ok" | "pending"; current: string | null; note?: string }[];
  tls: { provider: "manual" | "railway"; certificate: "pending" | "issuing" | "valid" | "failed" | "external"; message: string | null };
  verifiedAt: number | null; lastCheckedAt: number | null; nextCheckAt: number | null; createdAt: number;
};

Errors use the usual shape ({ "error": "...", "code": "..." }): invalid_hostname, ip_address, wildcard, reserved (400), limit (400), taken and already_added (409), not_found (404), rate_limited (429).

Running your own platform

Custom domains are on by default. Set these on the platform (and FUAST_BASE_URL on the web process, which already gets it from start.sh):

Variable Effect
FUAST_CUSTOM_DOMAIN_TARGET Where users point their CNAME, e.g. your Railway public domain. Comma-separate several hostnames or IPs. Unset: only the TXT record is checked.
FUAST_CUSTOM_DOMAIN_TLS manual (default) or railway (default when RAILWAY_API_TOKEN is set).
RAILWAY_API_TOKEN Railway account or workspace token (sent as Authorization: Bearer). A project token works as RAILWAY_TOKEN with FUAST_CUSTOM_DOMAIN_TLS=railway (sent as Project-Access-Token).
RAILWAY_PROJECT_ID, RAILWAY_ENVIRONMENT_ID, RAILWAY_SERVICE_ID The service domains are attached to. Railway injects these into running services.
FUAST_RAILWAY_TARGET_PORT Optional port Railway routes custom domains to (default: the service's).
FUAST_CUSTOM_DOMAIN_LIMIT Custom domains per app (5).
FUAST_CUSTOM_DOMAIN_PENDING_DAYS Days before a never-verified domain is released (7; 0 keeps them).
FUAST_WEB_HOSTS Extra hostnames that serve the dashboard (never treated as custom domains).
FUAST_CUSTOM_DOMAINS off turns the feature off.

With railway, fuast calls Railway's public GraphQL API (customDomainCreate when a domain is added, customDomain on each check, customDomainDelete on removal or when the app is deleted or terminated) and shows the records Railway asks for. Railway limits custom domains per service (2 on Hobby, 20 on Pro by default), and that limit applies to all apps on the platform together.

With manual, put something in front of fuast that terminates TLS for any hostname that reaches it (Cloudflare for SaaS custom hostnames, Caddy on-demand TLS, ...) and forwards the original Host. The web front end passes every hostname that is not the dashboard's own through to the platform, which serves the app that owns the verified domain or a "Domain not connected" page.