Skip to content
homeport

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:

Records for the wildcard
TypeNameValue
CNAME*.yourapp (or * for a root domain)edge.homeport.run
TXT_homeport.yourapphomeport-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
curl -X POST "$HOMEPORT_DOMAINS_URL" \
  -H "Authorization: Bearer $HOMEPORT_DOMAINS_TOKEN" \
  -d '{"domain": "acme.yourapp.com"}'
TypeScript
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:

TypeScript
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
curl "$HOMEPORT_DOMAINS_URL/shop.acme.com" -H "Authorization: Bearer $HOMEPORT_DOMAINS_TOKEN"
A domain’s status
StatusMeans
pendingNot checked yet, or a customer’s subdomain waiting for its wildcard.
verifyingChecked, not proved yet. why says what its DNS says instead.
liveServed. Its first visit gets it a certificate.
failedIt was live, then stopped pointing at homeport. Not served; removed a week later unless it is fixed.
suspendedYour 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
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.

Domains API errors
StatusMeans
400Not a domain you can add (field: "domain" says why).
401No token, or it was revoked.
402Your plan has no platform domains.
403The token can’t do this.
404Not this token’s app, or no such domain.
409No room: your team has reached its share of platform domains. Contact us.
429Too many requests, or too many domains waiting on DNS. Wait Retry-After seconds.
503Try 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

Platform domains per plan
PlanIncludedPast that
FreeNot available—
StarterNot available—
Growth100$1 per 100 a month
Business10,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.