Docs · Configure
Platforms: a domain for each customer
Build a SaaS on homeport and each of your customers can have their own address, all served by your one app: a subdomain under yours, like acme.yourapp.com, or a domain of their own, like shop.acme.com. Each gets free automatic certificates. Your app registers them through the Domains API as customers sign up.
Your app tells customers apart by the domain each request was made to. It arrives as the request’s Host, as if each customer had an app of their own. These are called platform domains, and they are on Growth (100 included) and Business (10,000 included). What it costs.
1. Your wildcard, once
On your app’s Domains tab, under Platform, add *.yourapp.com and set the two records it shows at your DNS provider:
| Type | Name | Value |
|---|---|---|
| CNAME | *.yourapp (or * for a root domain) | edge.homeport.run |
| TXT | _homeport.yourapp | homeport-verify=… |
It goes live once both resolve, usually within minutes. Keep the CNAME “DNS only”, not proxied. An app can have up to 5 wildcards.
2. A token for your app
On the same tab, under A token for your app, name a token (signups, say) and copy it. Add it to your app’s variables as HOMEPORT_DOMAINS_TOKEN, and the Domains API address the tab shows as HOMEPORT_DOMAINS_URL:
https://api.homeport.sh/v1/cli/teams/<team>/apps/<app>/domains- It is bound to this one app and manages its customer domains and nothing else: no variables, no deploys, no other app.
- It belongs to your team, so it keeps working if whoever made it leaves.
- It never expires on its own, so signups don’t stop a year in. Revoke it on the Domains tab, which shows when each token was last used. An app can have up to 10.
3. When a customer signs up
Register their domain. Only registered names are served.
curl -X POST "$HOMEPORT_DOMAINS_URL" \
-H "Authorization: Bearer $HOMEPORT_DOMAINS_TOKEN" \
-d '{"domain": "acme.yourapp.com"}'const api = process.env.HOMEPORT_DOMAINS_URL!
const auth = { Authorization: `Bearer ${process.env.HOMEPORT_DOMAINS_TOKEN}` }
type Domain = {
domain: string
status: 'pending' | 'verifying' | 'live' | 'failed' | 'suspended'
why?: string
wildcard?: string
records: { type: string; name: string; full_name: string; value: string }[]
}
export async function addDomain(domain: string): Promise<Domain> {
const res = await fetch(api, { method: 'POST', headers: auth, body: JSON.stringify({ domain }) })
if (res.status === 429) throw new Error(`slow down: retry in ${res.headers.get('Retry-After')}s`)
if (!res.ok) throw new Error((await res.json()).error)
return res.json() // 201 new, 200 already yours: the same domain either way
}Safe to retry. Posting a name your app already has returns it as it stands, with 200, instead of an error. A retried or doubled signup is harmless.
A name one level under your verified wildcard is live at once. A customer’s own domain is pending until its DNS points at homeport.
4. A customer’s own domain
Show your customer the records from the answer: a CNAME to edge.homeport.run (an A record for a root domain), or the TXT record if their DNS is proxied. Then poll its status every 10 to 30 seconds:
export async function domainStatus(domain: string): Promise<Domain> {
const res = await fetch(`${api}/${encodeURIComponent(domain)}`, { headers: auth })
if (!res.ok) throw new Error((await res.json()).error)
return res.json()
}curl "$HOMEPORT_DOMAINS_URL/shop.acme.com" -H "Authorization: Bearer $HOMEPORT_DOMAINS_TOKEN"| Status | Means |
|---|---|
pending | Not checked yet, or a customer’s subdomain waiting for its wildcard. |
verifying | Checked, not proved yet. why says what its DNS says instead. |
live | Served. Its first visit gets it a certificate. |
failed | It was live, then stopped pointing at homeport. Not served; removed a week later unless it is fixed. |
suspended | Your plan has no platform domains now. Kept, not served. |
- A domain that never proves itself is dropped after a day.
- If your customer’s zone has a wildcard record pointing at homeport (
*.acme.com), their domain needs its TXT record: pointing at homeport proves nothing there.
5. When a customer leaves
curl -X DELETE "$HOMEPORT_DOMAINS_URL/acme.yourapp.com" -H "Authorization: Bearer $HOMEPORT_DOMAINS_TOKEN"204 when removed, 404 if it wasn’t there, which is fine to treat as done. It stops answering within seconds. Removing your wildcard removes every customer under it.
Listing them
GET $HOMEPORT_DOMAINS_URL?limit=100&after=<domain> returns a page: {domains, next, live, total, included}. Pass next as after for the next page. A page holds up to 1,000.
Errors and limits
Errors are JSON, {"error": "…"}, with a field when one input is wrong.
| Status | Means |
|---|---|
| 400 | Not a domain you can add (field: "domain" says why). |
| 401 | No token, or it was revoked. |
| 402 | Your plan has no platform domains. |
| 403 | The token can’t do this. |
| 404 | Not this token’s app, or no such domain. |
| 409 | No room: your team has reached its share of platform domains. Contact us. |
| 429 | Too many requests, or too many domains waiting on DNS. Wait Retry-After seconds. |
| 503 | Try again shortly. |
- 120 adds and removes a minute per token, and 300 calls a minute in all.
- 1,000 new platform domains an hour per team.
- Up to your plan’s included number, and 1,000 at most, waiting on their DNS at once. Each is dropped after a day if it never points at homeport.
- Up to 50,000 platform domains and 5 wildcards per app.
What it costs
| Plan | Included | Past that |
|---|---|---|
| Free | Not available | — |
| Starter | Not available | — |
| Growth | 100 | $1 per 100 a month |
| Business | 10,000 | $1 per 100 a month |
Live customer domains are counted by the hour, so one live for half a month counts for half. Your wildcard itself isn’t counted, and certificates cost nothing. Platform domains don’t count toward your plan’s custom domains. Moving to a plan without them suspends them: kept, but not served or billed.