Custom domains
Serve an app at your own domain, with HTTPS. Every app always keeps its own address at <app>.deplane.app.
Connect a domain
In the dashboard, open the app's Domains tab and add the hostname, or run deplane domains add www.example.com. Deplane shows the two DNS records to set at your DNS provider:
| Domain | Records |
|---|---|
A subdomain, such as www.example.com | CNAME to <app>.deplane.app, and TXT _deplane-challenge.www.example.com with the value shown |
A root domain, such as example.com (it cannot have a CNAME) | A to Deplane's address, and TXT _deplane-challenge.example.com with the value shown |
Deplane checks every 2 minutes for 48 hours (or press Check now). When DNS points at the app and the TXT value matches, Deplane routes the domain and gets a certificate from Let's Encrypt, usually within a minute: Waiting for DNS, then Issuing certificate, then Active. Certificates renew automatically.
Switch DNS for me
Deplane can set the records for you at your DNS provider, show you the exact changes first, and undo them: from the dashboard (the domain's Switch DNS for me button), or with deplane domains switch on your own computer. It works with Cloudflare, Amazon Route 53, DigitalOcean, Gandi, Porkbun and GoDaddy (each switched on after a test with a real account there; the dashboard lists the ones that are), and by hand with any other provider. The examples below use Cloudflare; Other providers has the keys for the rest.
$ export CLOUDFLARE_API_TOKEN=... # or pipe it in
$ deplane domains switch www.example.com --app shop --check # preview, changes nothing
$ deplane domains switch www.example.com --app shop # asks, then switches
$ deplane domains switch www.example.com --rollback deplane-dns-www.example.com-20261031-140501.json
The token. Create a Cloudflare API token at https://dash.cloudflare.com/profile/api-tokens with the Edit zone DNS template, and under Zone Resources pick only this domain's zone. The CLI reads it from stdin or CLOUDFLARE_API_TOKEN, never from a flag. It talks to Cloudflare itself, from your computer: the token is never sent to Deplane, never printed, and never written to a file. If the token can see more than one zone, the preview says so: a token for the one zone is safer. You can delete the token in Cloudflare when you are done. A piped token takes stdin, so the switch cannot ask its question there and needs --yes: run it with --check first to read the preview, or set CLOUDFLARE_API_TOKEN and answer the question in your terminal.
The preview (--check) shows the records now at the domain and at _deplane-challenge.<domain>, and each change with its reason:
- The
TXTrecord that proves the domain is yours comes first, unless it is already there. - A subdomain gets a
CNAMEto<app>.deplane.app. An existing CNAME is updated in place, for example one that still points at your old host. AnyAorAAAArecord at the name conflicts with it and is deleted. - A root domain gets a
CNAMEtoo: Cloudflare flattens it, answering with Deplane's addresses and following them if they change. ItsAandAAAArecords are deleted. If<app>.deplane.appdoes not resolve to Deplane's edge addresses alone, the root getsArecords to them instead. - A subdomain that also has other records (an
MX, aTXT) getsArecords to Deplane's addresses instead of a CNAME, which would hide those records from some resolvers.Arecords name today's addresses: if Deplane announces new ones, run the switch again. - Records that Cloudflare proxies (orange cloud) are flagged: the new records are DNS only, so Cloudflare's WAF, caching, DDoS protection, Workers routes and Page Rules stop applying to that name. Deplane serves HTTPS itself and checks that the name points at it, which a proxied name does not.
MXrecords, every other record type at the name, and every other name in the zone are never changed. Only the domain you name and its_deplane-challengerecord are read, plus theNSandCAArecords above it.- If the records that change have a long TTL, the preview recommends setting it to Auto in Cloudflare first and waiting that long, so that the switch and a rollback both take effect within minutes. The new records use Auto (5 minutes).
- A
CAArecord that allows certificate authorities but not Let's Encrypt is flagged: Deplane could not get the certificate.
If the domain is not on the app yet, the switch adds it first. A zone that is not active on Cloudflare yet (its nameservers are not Cloudflare's) is refused, and so is a name delegated to other nameservers: records there do nothing.
The switch asks for confirmation (--yes skips it; with --json the preview goes to stderr before the question), then:
- Saves the records as they are to
deplane-dns-<domain>-<date>.jsonin the current folder, readable only by you, without the token, and flushed to disk. If the records changed while you were reading the preview, it stops instead (dns_records_changed). - Adds the
TXTrecord on its own, and waits (up to 3 minutes) until public DNS shows it. Nothing visitors see has changed yet. - Changes the records at the domain in one request to Cloudflare's batch API, which applies them all together or not at all: a failure leaves the domain as it was. If the zone cannot use the batch API, the changes are made one by one, adding before removing wherever Cloudflare allows it, and the command says so.
- Waits (up to 5 minutes) for Deplane to verify the domain, and says when it has. Until Deplane sees the new records (a few minutes, or up to the old TTL where resolvers cached the old answer) and gets the certificate, HTTPS to the domain may show errors.
--no-wait skips both waits, with a warning. If a change fails, the command stops and shows the rollback command.
The rollback (--rollback FILE) puts the saved records back exactly: content, TTL, proxy setting and comment, in one batch where Cloudflare allows it. It deletes what the switch created, restores what it changed, and recreates what it deleted (with new Cloudflare ids; tags on deleted records are not saved), starting from the records as they are now, so it also undoes a switch that stopped part way and is safe to run twice. --check previews it. The domain stays on the app; remove it with deplane domains remove if you no longer want it.
Other providers
deplane domains switch finds the provider from the domain's nameservers; --provider chooses one (cloudflare, route53, digitalocean, gandi, porkbun, godaddy, or manual for by hand). A provider that does not serve the domain's nameservers is refused (dns_provider_mismatch): records changed there would do nothing. Make the narrowest key the provider allows, and delete it when you are done. The CLI reads it from stdin (two lines where it has two parts) or the variables below, never from a flag.
| Provider | Key, and the narrowest form | Variables | All together |
|---|---|---|---|
| Amazon Route 53 | An IAM user's access key, with only the policy below | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY (and AWS_SESSION_TOKEN) | Yes |
| DigitalOcean | A personal access token with Custom Scopes domain: read and domain: update only. It reaches every domain in the account. | DIGITALOCEAN_TOKEN | No |
| Gandi | A personal access token with only Manage domain name technical configurations, restricted to this domain | GANDI_PAT | No |
| Porkbun | An API key and secret, with API Access switched on only for this domain. The key reaches the whole account. | PORKBUN_API_KEY, PORKBUN_SECRET_API_KEY | No |
| GoDaddy | A production API key and secret. It reaches the whole account, and GoDaddy refuses its API for accounts with fewer than 10 domains (dns_api_refused): use the by-hand mode there. | GODADDY_API_KEY, GODADDY_API_SECRET | No |
The Route 53 policy, with your hosted zone's id and your hostname:
{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": "route53:ListHostedZonesByName", "Resource": "*"},
{"Effect": "Allow", "Action": "route53:ListResourceRecordSets", "Resource": "arn:aws:route53:::hostedzone/ZONEID"},
{"Effect": "Allow", "Action": "route53:ChangeResourceRecordSets", "Resource": "arn:aws:route53:::hostedzone/ZONEID",
"Condition": {"ForAllValues:StringEquals": {"route53:ChangeResourceRecordSetsNormalizedRecordNames": ["www.example.com", "_deplane-challenge.www.example.com"]}}}
]
}
With this policy the key cannot count the account's hosted zones, so the preview says it cannot tell how many the key sees: that is expected. The CLI never reads ~/.aws or an AWS profile: only the key you give it.
What differs from Cloudflare:
- A root domain gets
Arecords to Deplane's addresses: only Cloudflare can hold a CNAME there. - "All together": Route 53 applies the changes at the domain in one all-or-nothing request, like Cloudflare. The others make them one at a time, adding before removing wherever DNS allows; the preview says so. If one fails part way, the rollback puts everything back.
- Route 53 records with a routing policy (weighted, latency, geolocation, failover) at the domain are refused (dns_records_unsupported). An alias record is replaced and restored exactly by the rollback.
- TTL: new records use 5 minutes (10 minutes at Porkbun and GoDaddy, their shortest).
Lower the TTL first
If the records that change have a long TTL, resolvers may keep the old answer that long after the switch, and after a rollback. --lower-ttl (the Lower TTL first box in the dashboard) lowers the TTL of only the records the switch will change, saves them first, and says when to run the switch: after the longest old TTL. Then run the switch as usual. The rollback file of the TTL step undoes both the TTL change and a later switch; the switch's own file undoes only the switch.
By hand
For any other provider, or one that refuses its API, the by-hand mode (--provider manual, or Set the records by hand in the dashboard) needs no key and changes nothing itself. It reads the records at the domain from public DNS and saves them, shows the records to set and the ones to remove (the TXT record first), then watches public DNS (up to 30 minutes on the CLI; --no-wait skips) until it shows them, and until Deplane verifies the domain. Public DNS does not show TTLs, so the saved records have none. Public DNS also shows where a chain of CNAMEs ends, not the first CNAME you set: the mode warns, so write down the CNAME at your provider before you remove it. --rollback with that file shows the records to set back, and watches for them the same way.
From the dashboard
On the app's Domains tab, a domain waiting for DNS has Switch DNS for me: choose the provider (Deplane picks the one the nameservers name), paste the key (the page shows how to make the narrowest one), and Preview. The preview is the same as the CLI's. Switch asks for your passkey, writes the TXT record, waits until public DNS shows it, then moves traffic, and shows each step. Keep the page open until it says the switch is done: the key stays only in the page's memory, and is needed again to move traffic.
The key is sent to Deplane in the requests that use it, and used there to call the provider. It is never stored, logged or shown, and the page forgets it when the switch ends or you leave. Deplane keeps the records as they were before the switch (never the key) for 30 days: Roll back puts them back during that time, with the key and your passkey again. Only the person who started a switch moves its traffic; they or a team owner can roll it back.
Why the TXT record
A domain is connected only when DNS proves that whoever controls it chose this connection. The TXT value is new each time a domain is added, so it proves the records were set for this app and this team, not left over from an app someone deleted: nobody can take over a domain whose DNS still points at Deplane after its owner moved on.
The TXT record is needed to connect the domain. After that you may keep it or remove it: a connected domain stays connected while its CNAME names the app (or, for a root domain, its A record points at Deplane).
Re-checks
Deplane checks every connected domain again every 6 hours. If its DNS no longer points at the app, the domain shows DNS changed and your team gets an alert (Custom domain no longer points here). Deplane keeps serving it for 7 days, sends a second alert a day before the end, and then stops serving the domain and its certificate; it goes back to Waiting for DNS. When DNS points back at the app, it is served again on the next check. A DNS server that does not answer changes nothing.
If another Deplane team adds the domain and proves it with its own TXT value (the domain changed hands, and its new owner points it at their app), that team's app gets the domain at once and the old connection stops. Your team gets an alert (Custom domain taken over) saying what happened and what to check: anyone who can write the domain's _deplane-challenge TXT record while it points at Deplane can do this, so keep that record's DNS access to your own people.
Limits
Up to 10 custom domains per app, and your plan's number for the whole team, all apps together: 3 on Free, 5 on Hobby, 25 on Pro, 100 on Team (see limit_domains). A domain can be connected to one app on Deplane at a time. Deplane's own domains cannot be added. Removing a domain stops it serving the app right away; deleting an app frees its domains.
When a team removes a domain or deletes its app, another team cannot add that domain while its CNAME still points at the old app's address (domain_dangling): change the CNAME first. A deleted app's name stays reserved for its team for 90 days, so nothing that still points at <app>.deplane.app (DNS, OAuth callbacks, webhooks) reaches another team's app.
This page is also available as Markdown. AI agents can start at /llms.txt.