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.appRegardless 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/*| Segment | Meaning |
|---|---|
org-slug | Your organization’s URL-safe slug |
app-handler | Application handler name |
env | Optional environment; defaults to prod |
Example:
https://api.picklayer.ai/api/org/acme/app/support/prod/v1/chat/completionsCustom 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:
| Value | Behaviour |
|---|---|
managed | Use the managed picklayer.app URL. hosted_domain_id is null. |
custom | Route 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:
| Record | Purpose |
|---|---|
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:
| Type | Host | Value |
|---|---|---|
CNAME | auth.{yourdomain.com} | hosted.picklayer.app |
CNAME | api.{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
| Status | Meaning |
|---|---|
pending / pending_dns | Domain created; DNS records not yet detected |
verifying | AWS SES verification in progress |
verified | DNS records validated; domain is ready |
failed | Verification failed — check last_error for details |