Custom Domains & TLS
Every deployed app already gets https://<slug>.apps.openfactory.tech. Custom
domains let you serve the same app from a domain you own (e.g., app.example.com,
docs.acme.io, shop.mybrand.com) without touching gateway config, generating
CSRs, or copy-pasting cert files. Attach the domain, follow the DNS
instructions, and the platform handles verification end-to-end.
Today: Domain storage, the REST API, real DNS verification against public resolvers, and the
tls-askissuance gate are production-ready. Automatic certificate issuance and serving on your custom domain are coming soon. Until then, verified domains are reachable via the platform subdomain, and the gate is already enforced so cert issuance cannot race ahead of verification.
When to use this
- You want a branded URL for a shipped app instead of the platform subdomain.
- You operate a CI pipeline that attaches domains for preview environments and needs to poll verification programmatically.
- You need a “no squatting” guarantee. A tenant cannot point arbitrary domains at the gateway and trigger ACME issuance for someone else’s name.
If you only need the default https://<slug>.apps.openfactory.tech URL, you do
not need this feature. See App Deployment.
How it works
- Attach the domain.
POST /api/app-gateway/domainswithapp_sluganddomain. The service validates the FQDN, rejects platform-owned zones (*.openfactory.tech), and persists a record inpendingstate. The response includes DNS instructions tailored to the name:- Subdomain (3+ labels): a single CNAME pointing at
<slug>.apps.openfactory.tech. - Apex (2 labels): a TXT verification token plus an A record once the TXT resolves.
- Subdomain (3+ labels): a single CNAME pointing at
- Add the DNS record at your provider.
- Verifier reconciles. A background loop checks every 60 seconds for the
first 15 minutes, then every 5 minutes, against public resolvers
(
1.1.1.1,8.8.8.8). When the record resolves, the status flips toverified. You can also force a check withPOST .../verify. - TLS issuance is gated. The gateway’s ask endpoint
(
GET /api/app-gateway/tls-ask) returns200only for domains inverified,active, orexpiring. Unverified names get403and never trigger certificate issuance. - Cert lifecycle. Once the gateway issues a cert, the verifier records
cert_expires_at. Inside the 14-day renewal window the status flips toexpiringso App Observability can alert.
Domain states
| State | Meaning |
|---|---|
pending | Record stored; DNS not yet observed at public resolvers |
verified | DNS matches; tls-ask will allow issuance |
active | Cert issued and cached by the gateway |
expiring | Cert is inside the 14-day renewal window |
failed | Verification kept failing past the retry budget; see last_check_error |
REST API
All endpoints live under /api/app-gateway/. Authenticated routes use the
standard UserContext (session cookie or bearer token).
Attach a domain
POST /api/app-gateway/domains
Content-Type: application/json
{
"app_slug": "myapp",
"domain": "app.example.com"
}{
"domain": "app.example.com",
"app_slug": "myapp",
"status": "pending",
"kind": "subdomain",
"method": "cname",
"instructions": {
"records": [
{
"type": "CNAME",
"name": "app",
"value": "myapp.apps.openfactory.tech"
}
],
"notes": "Add a CNAME at 'app' pointing to myapp.apps.openfactory.tech. DNS propagation may take a few minutes; verification polls every minute for the first 15 minutes, then every 5 minutes."
}
}For an apex domain, the response includes a txt_token in the instructions.records array with both the TXT and A record objects.
List domains
GET /api/app-gateway/domains?app_slug=myapp{
"domains": [
{
"domain": "app.example.com",
"app_slug": "myapp",
"status": "verified",
"kind": "subdomain",
"method": "cname",
"txt_token": null,
"created_at": "2026-06-26T14:02:11Z",
"verified_at": "2026-06-26T14:08:42Z",
"last_checked_at": "2026-06-26T14:08:42Z",
"last_check_error": null,
"failure_count": 0,
"cert_expires_at": null
}
]
}Omit ?app_slug= to list every domain you own.
Inspect one domain
GET /api/app-gateway/domains/app.example.comReturns the same shape as a single entry from the list, including all fields shown above.
Force a verification pass
POST /api/app-gateway/domains/app.example.com/verify{
"verified": true,
"status": "verified",
"last_check_error": null,
"last_checked_at": "2026-06-26T14:08:42Z"
}Useful as a “Check now” button or after fixing a DNS typo. Don’t wait for the reconciler.
Detach a domain
DELETE /api/app-gateway/domains/app.example.comReturns 204 No Content. The next gateway sync stops serving the name and the
tls-ask gate refuses any future issuance for it.
TLS ask gate (internal)
GET /api/app-gateway/tls-ask?domain=app.example.com200 if the domain is verified, active, or expiring. 403 otherwise.
This endpoint is not reachable from the public internet; the gateway consults
it during the TLS handshake before requesting any certificate. It is the
squatting defense: a stranger cannot point victim.com at the gateway and
trick the platform into requesting a cert.
Putting it together
A typical attach-to-serving flow:
- Call
POST /api/app-gateway/domainswithapp_slug="myapp"anddomain="app.example.com". The service validates the FQDN, detects it is a subdomain, generates the CNAME targetmyapp.apps.openfactory.tech, and stores the record aspending. Response is201with the CNAME instructions. - Add the CNAME at your DNS provider. The verifier loop runs every 60 seconds
for the first 15 minutes, then every 5 minutes. When the CNAME resolves via
1.1.1.1and8.8.8.8, the status flips toverified. - Load the app detail panel and watch the status change from
pendingtoverified. A CI pipeline can hitPOST .../verifyto force the check instead of polling. - The gateway checks the
tls-askgate forapp.example.com. Because the domain isverified, the gate answers200. Once automatic issuance ships (coming soon), that200is what allows the certificate request. - Open
https://app.example.comin a browser. The gateway intercepts the TLS handshake, consultstls-ask, gets200, and obtains a Let’s Encrypt certificate (HTTP-01 or TLS-ALPN-01). The cert is cached. - On subsequent reconciler passes,
cert_expires_atis populated from the gateway’s cert store. Inside the 14-day window the status becomesexpiringand App Observability raises an alert.
Notes
- Platform zones are reserved. Any name ending in
openfactory.tech(or a configured platform suffix) is rejected at attach time. Use the default<slug>.apps.openfactory.techURL for those. - Apex domains need a TXT before the A. The verifier will not accept the A record until the TXT token resolves, to prove zone control.
- DELETE is reversible by re-attaching. Detaching removes the record and
blocks issuance. Re-attaching starts the verification cycle over from
pending. - Verification uses public resolvers. Local
/etc/hostsoverrides will not satisfy the check. Test against the real DNS provider. - Automatic certificate serving is coming soon. Until it lands, verified domains route through the platform subdomain. The gate is already enforced, so the rollout is a config flip, not a data migration.
Related
- App Deployment: register an app and get its
default
https://<slug>.apps.openfactory.techURL. - App Observability: surface
expiringcert alerts and DNS verification failures.