Tenant Vanity Domains
A vanity domain is a hostname that belongs to one of your tenants, such as surveys.example.com for a customer named Example Corp. With vanity domains, Descope authentication runs on that tenant's hostname. The login screens, session cookies, passkeys, and redirects all stay on surveys.example.com, and your tenant never adds a DNS record that points to Descope.
This page is for B2B applications that already serve each tenant on its own hostname through an edge proxy such as Akamai or Cloudflare. If you want Descope on a single hostname that you own, such as auth.app.example.com, see Custom Domain instead.
Why vanity domains use a reverse proxy
A custom domain works through a CNAME record that points your hostname at Descope. That model fits one hostname per project. It doesn't fit a B2B application that serves hundreds of tenants, because each tenant would need to change its own DNS, and many tenants can't or won't do that.
Vanity domains reuse the routing that you already run. Your edge proxy already receives traffic for every tenant hostname. You add one route that forwards /auth/* on each vanity domain to your project's Descope custom domain. The proxy tells Descope which hostname the user is actually on and what the user's IP address is. The proxy also sends a shared secret that proves the request came from your proxy and not from a client that forged the headers.
Descope then treats the request as if it arrived on the vanity domain. Descope also binds the vanity domain to one tenant, so a request on surveys.example.com can only sign users into Example Corp.
How a request flows
When a user signs in on a vanity domain, the browser talks only to that hostname. Your proxy adds three headers and forwards the request to Descope.
Responsibilities
The setup splits work between your team and Descope. Your team owns everything at the edge, and Descope owns the validation that happens after the request arrives.
| Your team | Descope |
|---|---|
| DNS and a TLS certificate for each vanity domain | Stores only a salted hash of the proxy secret and never returns the secret |
Routing <vanity domain>/auth/* to Descope | Validates that each request comes from your proxy |
| Adding the forwarded headers and overwriting any value the client sent | Enforces the tenant binding on every request |
| Generating, storing, and rotating the proxy secret | Uses the forwarded user IP for audit, flows, and IP-based checks |
| Binding each vanity domain to its tenant through the Management API | Sets cookies, allows redirects, and registers passkeys on the vanity domain |
| Configuring the frontend SDK on each vanity domain |
Prerequisites
Vanity domains forward traffic to your project's Descope custom domain, so you need a working custom domain first. You also need a management key to call the Management API.
Set up vanity domains
The setup has four parts. You set the proxy secret once per project. You then configure the proxy route, bind each vanity domain to its tenant, and point the frontend SDK at the vanity domain.
Set the proxy secret
The proxy secret proves to Descope that a request came from your proxy. Generate a random secret of at least 64 characters, for example with openssl rand -base64 48. Store the secret in your proxy's secret store. The secret must never reach a browser.
You can then set the secret on the project with the Management API:
curl -X POST "https://<your Descope custom domain>/v1/mgmt/project/settings" \
-H "Authorization: Bearer <ProjectID>:<ManagementKey>" \
-H "Content-Type: application/json" \
-d '{"trustedProxySecret": "<the secret>"}'Descope stores a hash of the secret, not the secret itself. When you read the project settings back, Descope returns a placeholder in place of the secret. If you send the placeholder back in a later update, Descope keeps the stored secret. If you send a new value, Descope replaces it.
Configure the proxy route
Each vanity domain needs a route on your proxy that forwards https://<vanity domain>/auth/* to https://<your Descope custom domain>. The proxy keeps the path as it is, so /auth/v1/flow/start stays /auth/v1/flow/start. The proxy also sends the Host header and TLS SNI of your Descope custom domain.
The proxy sets three request headers. It overwrites any value the client sent for these headers.
| Header | Value |
|---|---|
x-descope-forwarded-hostname | The vanity hostname only, with no scheme or port. For example, surveys.example.com. |
x-descope-forwarded-ip | The end user's IP address, as a single IPv4 or IPv6 address. For example, 203.0.113.9. |
x-descope-proxy-secret | The secret from the previous step. |
Everything else passes through unchanged. That includes all methods (GET, HEAD, OPTIONS, POST, PUT, PATCH, DELETE), query strings, bodies, and the Origin, Referer, User-Agent, and Cookie headers. Responses also pass through unchanged, including Set-Cookie headers and redirects.
Three more rules apply to the route. The proxy shouldn't cache responses under /auth/*. The proxy should add the secret only to requests that go to Descope, and never return the secret in a response. The proxy should also keep the x-descope-proxy-secret header out of request logs, log streams, and debug output.
The configuration for Akamai and Cloudflare follows.
In Property Manager, add a rule that matches the path /auth/* on your vanity hostnames, with these behaviors:
- Origin Server: The origin hostname is your Descope custom domain, and Forward Host Header uses Origin Hostname.
- Allowed methods: The rule enables the
POST,OPTIONS,PUT,PATCH, andDELETEbehaviors. - Caching: The rule uses no store.
- Modify Outgoing Request Header: The rule adds one behavior per header, with the action Add or Modify.
The header behaviors use these values:
| Header | Value |
|---|---|
x-descope-forwarded-hostname | {{builtin.AK_HOST}} |
x-descope-forwarded-ip | {{builtin.AK_CLIENT_IP}} |
x-descope-proxy-secret | {{user.PMUSER_DESCOPE_PROXY_SECRET}}, a user variable marked as sensitive |
The behavior names come from Property Manager. Your Akamai admin may use equivalent behaviors.
Bind each vanity domain to its tenant
Each tenant has an approvedDomains field that holds the tenant's vanity domain. The value is one exact hostname, with no wildcards and no subdomains. Each hostname must be unique within the project.
You can set the field when you create the tenant:
curl -X POST "https://<your Descope custom domain>/v1/mgmt/tenant/create" \
-H "Authorization: Bearer <ProjectID>:<ManagementKey>" \
-H "Content-Type: application/json" \
-d '{
"name": "Example Corp",
"id": "example-corp",
"approvedDomains": ["surveys.example.com"]
}'For an existing tenant, you can use patch (PATCH /v1/mgmt/tenant/patch), which changes only the fields you send. If you use update (POST /v1/mgmt/tenant/update) instead, you would send the tenant's full configuration, because update overwrites any field you leave out. An empty approvedDomains array ([]) removes the binding. Descope rejects a hostname that another tenant already has.
You should bind the domain before you route traffic for it. Descope rejects requests from a hostname that isn't bound to a tenant with a 403 error.
Point the frontend SDK at the vanity domain
On each vanity domain, the Descope SDK uses the vanity domain plus the /auth path as its base URL:
<AuthProvider projectId="<ProjectID>" baseUrl="https://surveys.example.com/auth">Users must sign in with Descope flows on a vanity domain. Descope blocks direct auth API calls on a vanity domain, such as otp.signIn from the SDK. Session endpoints work as usual, including refresh, logout, me, tenant selection, and session validation.
How Descope enforces the tenant binding
The tenant binding is what keeps one tenant's vanity domain from signing users into another tenant. Descope resolves the forwarded hostname to its tenant and validates every tenant-related request against that tenant.
- Flows: A flow that starts on
surveys.example.comalways signs users into that hostname's tenant. - Session endpoints: The tenant in a session token must match the hostname's tenant.
- Unbound hostnames: Descope rejects requests from a hostname that isn't bound to any tenant.
- Management API: Calls with a management key run server-side, so Descope doesn't limit them to the vanity domain's tenant. A call that takes a tenant ID still acts on that tenant only. You should call the Management API on your Descope custom domain, not through a vanity domain.
Behavior on vanity domains
A few Descope behaviors change when a request arrives on a vanity domain.
- Cookies: If your project returns tokens in cookies, Descope sets them with
Domain=<vanity hostname>. The browser also sends those cookies to the hostname's subdomains. The project's cookie domain setting doesn't apply on vanity domains. If your project returns tokens in the response body, the SDK stores them as usual. - Passkeys: A passkey that a user creates on
surveys.example.combelongs to that hostname and works only there. - Redirects: Descope allows redirects back to
https://<vanity domain>/...for magic links, social login returns, and logout. - User IP: Descope uses
x-descope-forwarded-iponly when the request has a valid proxy secret. If the secret is missing or wrong, or the value isn't a single valid IP, Descope sees your proxy's IP.
Current limitations
Some parts of authentication still run on your Descope custom domain rather than on the vanity domain.
- Social login and SSO: The provider callback lands on your Descope custom domain, and Descope then returns the user to the vanity domain. The provider's consent screen shows your Descope custom domain.
- SAML: The service provider ACS and metadata URLs are on your Descope custom domain, not on the vanity domain.
- OIDC issuer: Descope issues tokens from your Descope custom domain, and serves discovery and JWKS there too.
- Location and network signals: Country, city, ASN, and bot signals still describe your proxy's edge location, not the user's location.
- Secret rotation: A project holds one proxy secret at a time. While Descope and the proxy hold different secrets, Descope ignores the forwarded headers, so requests from vanity domains fail until both sides match.
- Sub-tenants: You bind a vanity domain to a tenant. Sub-tenants don't get their own vanity domain.
Next steps
Once the first vanity domain works, you can bind more tenants through the same Management API calls. To manage the tenants themselves, see Tenant Management. For the cookie settings that apply on your Descope custom domain, see Custom Domain.