# Custom domains: your name, the same app Gateway contract v1, domain schema `jmpkit.domains.v1`. This is an app-control feature implemented by Gateway, with its own Domains starter. Attach an exact hostname to an existing stable app ID. Multiple domains and subdomains may point at one app. Its canonical JMPKit address remains valid. ## Authority and availability Use the app owner's bearer on a trusted agent/server. Delegated editor grants do not authorize domain changes in this contract. Never embed the bearer in the published app. A new owner must create fresh DNS proof after a transfer. `GET /api/domains` returns `{schema, enabled, bindings}`. An unconfigured gateway refuses creation with 503. Do not advertise an attachment as active from static docs alone. No plan-specific domain gate is applied by this v1; attachment does not upgrade the app or change its expiry or resource limits. ## HTTP flow Use `https://` as the base. Equivalent central control paths start at `https://jmpkit.com/api/apps//domains`. | Method | App-relative path | Result | | --- | --- | --- | | POST | `/api/domains` | Create or reuse a pending binding | | GET | `/api/domains` | Inspect all bindings and availability | | GET | `/api/domains/` | Inspect one binding | | POST | `/api/domains//verify` | Recheck DNS and HTTPS | | DELETE | `/api/domains/` | Detach, keeping the original app | Creation JSON: `{"hostname":"www.example.com","recordType":"CNAME"}`. Choose `"A"` (the default) for A/AAAA address instructions. It returns all configured ingress addresses; add the provided records. For subdomains, CNAME can be convenient. Do not use ordinary CNAME at the zone apex unless your DNS provider explicitly supports it; use the returned A/AAAA records. Flattened CNAME should be requested as A mode because its public DNS answer contains addresses, not a CNAME. An apex plus `www` are two separate bindings. Response fields: - `bindingId`, `appId`, `appHost`, `hostname`, `url`, `statusUrl`, `recordType`. - `verification`: exact `{type:"TXT", name, value, keepRecord:true}`. - `dns`: exact routing `{type, name, value}` records. - `state`, `routingReady`, `tlsReady`, `checkedAt`, `leaseUntil`, `lastError`. - `lastVerifiedAt`, `nextCheckAt`, `failureSince`, `failureCount` for scheduled renewal and warnings. These fields may be absent until an older binding is checked. The TXT name is `_jmpkit.`, with a unique unpredictable `jmpkit-verify=…` value. Copy the returned value, not an example. DNS consoles may want only the relative record name and append the zone automatically. Keep TXT as well as routing records: the service periodically rechecks control. Use DNS-only for initial activation: the first check requires the returned direct ingress addresses and, in CNAME mode, the returned target. Once active with trusted HTTPS, you may enable Cloudflare orange cloud or another proxy. Ongoing ownership checks use TXT, not public A/AAAA/CNAME matching. Keep the proxy's origin pointed at the provided JMPKit ingress and preserve the hostname. Use Cloudflare Full (strict), not Flexible. Do not toggle an existing production DNS setup without the user's agreement. Ask before replacing existing conflicting records; explain that DNS propagation is not immediate. No registrar API key or JMPKit plugin is needed to give DNS instructions. ## Readiness and errors `pending_verification` → `pending_dns` → `pending_tls` → `active`. Some checks can advance directly past intermediate states. `routingReady` allows the certificate handshake after DNS proof; it does **not** mean the browser will already have trusted HTTPS. Announce the custom URL only when `state === "active" && tlsReady === true`. Before activation, `degraded` means DNS lookup failed; `suspended` means its current lease or app authority no longer allows routing. `needs_reverification` follows an owner change. `conflict` means another active app has the hostname. Pending requests alone do not reserve somebody else's domain. `detached` cannot be verified; create a fresh binding to reconnect. Initial HTTPS failures return `pending_tls` with `https_not_ready`; check DNS, reachable ports, CAA policy, and certificate logs. Wait at least 30 seconds between explicit verification calls. A faster retry returns the last check. Global concurrent checks are bounded; 429 requests a retry. Background scheduling: - Initial setup retries after 1, 2, 4, 8, 16, 32, then 60 minutes, for up to 24 hours after creation. After that, use explicit `/verify` when DNS is ready. - Healthy bindings are checked every 23–25 hours (daily, staggered per domain). - A successful TXT and origin HTTPS check grants a 72-hour lease from that success. Normally this leaves about 48 hours of grace after a failed daily check. - Established failures retry after 1, 2, 4, then 6 hours. The binding stays `active` with `lastError` and `failureSince` while the last good lease remains valid. Repeated failures never extend it. At expiry routing stops; checks continue so restored proof and HTTPS can recover the binding. - Detachment, ownership changes, app expiry/suspension and ingress configuration changes take effect without waiting for the DNS grace period. HTTPS probes connect only to configured JMPKit ingress IPs, with the custom hostname and normal certificate validation. They never fetch arbitrary proxy addresses. `active` verifies the JMPKit origin, not a customer's proxy/cache configuration; test the public URL after switching the proxy. Allow ACME HTTP challenges through the proxy so Caddy can renew its origin certificate. Avoid cache-everything rules on `/api/*`, authentication, dynamic responses and `/.well-known/*`; appropriate static assets can be cached. Detached names cannot keep serving an app even if a certificate remains cached at Caddy. External CDN copies already cached are outside JMPKit's control. 401 means missing owner auth; 403 means wrong authority; 400 means invalid hostname/input; 404 means missing app/binding; 409 means a changed binding, detached binding, or changed record type; 429 means capacity/busy; 503 means the gateway's domain ingress is not configured. Limit: 20 non-detached names per app in v1. Wildcards, buying domains, and arbitrary registrar management are outside this flow. ## What stays the same Static files, dynamic Origin routes, Machine fallback, Q, Path and other app-relative APIs still resolve the same app. Use relative HTTP paths and `wss://${location.host}/api/uws10` in browser code instead of hardcoding a name. Cookies, localStorage, service workers, Web Push permissions and subscriptions are origin-specific and do not migrate between URLs. Update external OAuth callbacks, allowed-origin checks or absolute links when the app uses them. Attaching a domain is not a data migration, ownership transfer, or extension of app life. Keep the original link for recovery and future administration.