Skip to Content
Custom Domains

Custom Domains

Every PickLayer application comes with a managed URL on picklayer.app. You can optionally route traffic through your own verified domain to expose branded auth.yourdomain.com and api.yourdomain.com subdomains.

URL patterns

Managed (default)

The managed URL is derived from your organization slug and application handler:

https://{org-slug}-{app-handler}.picklayer.app

Regardless of domain mode, every application is always reachable via the shared gateway pattern:

https://api.picklayer.ai/api/org/{org-slug}/app/{app-handler}/{env?}/v1/*
SegmentMeaning
org-slugYour organization’s URL-safe slug
app-handlerApplication handler name
envOptional environment; defaults to prod

Example:

https://api.picklayer.ai/api/org/acme/app/support/prod/v1/chat/completions

Custom domain

When custom mode is active and a domain is verified, end-user traffic can use:

https://api.{yourdomain.com}/v1/*

Hosted auth reset links point to auth.{yourdomain.com}.

Domain modes

hosted_domain_mode on the application_settings view controls which mode is active:

ValueBehaviour
managedUse the managed picklayer.app URL. hosted_domain_id is null.
customRoute through the selected verified custom domain. hosted_domain_id references the domain record.

Setup flow

1. Verify a domain

Before you can attach a custom domain to an application, verify it at the organization level (Builder UI → Settings → Domains, or via the GraphQL API below). Verification creates a domain record and provisions it against AWS SES for email delivery.

DNS records required for verification:

RecordPurpose
TXT _amazonses.{domain}SES domain ownership verification
CNAME {token}._domainkey.{domain}DKIM signing (3 records)
TXT {domain}SPF: v=spf1 include:amazonses.com ~all
MX {mailFromDomain}Bounce handling: 10 feedback-smtp.{region}.amazonses.com

The verification token and DKIM tokens are returned by the provision endpoint and shown in the DNS wizard. DNS propagation typically takes under a minute; allow up to 48 hours for slow registrars.

2. Add CNAME records for API routing

After domain verification, add two additional CNAME records for API traffic routing:

TypeHostValue
CNAMEauth.{yourdomain.com}hosted.picklayer.app
CNAMEapi.{yourdomain.com}api.picklayer.app

TTL 300 is recommended.

3. Attach to an application

Select the application in Builder UI → Application URLs and switch to “Use a custom domain”. Choose the verified domain and click Save domain preference. The setting is written to application_settings.hosted_domain_mode and hosted_domain_id.

Builder API

Create and provision a domain

Domain records are managed via the email_domains Hasura table and the REST provisioning endpoint.

Step 1 — insert the record:

mutation CreateDomain($orgId: uuid!, $domain: String!, $sesRegion: String!, $appliesToAllApps: Boolean!) { insert_email_domains_one(object: { organization_id: $orgId domain: $domain ses_region: $sesRegion applies_to_all_apps: $appliesToAllApps mode: "custom" status: "pending" }) { id domain status mode } }

Step 2 — provision with SES:

POST /email/domains/{domainId}/provision Content-Type: application/json Authorization: Bearer <organizationToken> { "organizationId": "<orgId>" }

Response includes:

{ "data": { "domain": "yourdomain.com", "configurationSet": "...", "mailFromDomain": "bounce.yourdomain.com", "dkimTokens": ["abc", "def", "ghi"], "verificationToken": "base64token==" } }

Refresh / re-check verification

POST /email/domains/{domainId}/refresh Authorization: Bearer <organizationToken>

Delete a domain

DELETE /email/domains/{domainId} Authorization: Bearer <organizationToken>

Query domain list

query GetDomains($orgId: uuid!) { email_domains( where: { organization_id: { _eq: $orgId } } order_by: { created_at: desc } ) { id domain mode status applies_to_all_apps verification_token dkim_tokens mail_from_domain ses_region verified_at last_error updated_at } }

Assign domain to specific applications

Scope a domain to a subset of applications instead of all apps:

mutation AddDomainApplications($domainId: uuid!, $orgId: uuid!, $appIds: [uuid!]!) { insert_email_domain_applications( objects: [{ email_domain_id: $domainId, application_id: $appIds }] on_conflict: { constraint: email_domain_applications_pkey, update_columns: [] } ) { affected_rows } }

To set applies_to_all_apps:

mutation SetDomainScope($domainId: uuid!, $allApps: Boolean!) { update_email_domains_by_pk( pk_columns: { id: $domainId } _set: { applies_to_all_apps: $allApps } ) { id applies_to_all_apps } }

Configure hosted_domain_mode for an application

mutation SetHostedDomainMode( $appId: uuid! $mode: String! $domainId: uuid ) { update_applications_by_pk( pk_columns: { id: $appId } _set: { hosted_domain_mode: $mode hosted_domain_id: $domainId } ) { id hosted_domain_mode hosted_domain_id } }

Set $mode = "managed" and $domainId = null to revert to the managed URL.

Read current domain settings for an application

query GetApplicationDomainSettings($appId: uuid!) { application_settings(where: { id: { _eq: $appId } }) { id handler name hosted_domain_mode hosted_domain_id } }

Domain statuses

StatusMeaning
pending / pending_dnsDomain created; DNS records not yet detected
verifyingAWS SES verification in progress
verifiedDNS records validated; domain is ready
failedVerification failed — check last_error for details
Last updated on