Domains
Custom domain setup and subdomain configuration for merchant storefronts.
The dashboard Domains page lets merchants connect custom domains to their storefront. The storefront resolves the tenant from the request host, so each domain must be mapped to an organization.
Domain types
| Type | Example | Setup |
|---|---|---|
| Platform subdomain | acme-store.prood.app | Automatic — org slug + NEXT_PUBLIC_PLATFORM_DOMAIN=prood.app |
| Store custom domain | shop.merchant.com | Manual — add in dashboard, verify DNS, provision on storefront Vercel project |
Platform admin always lives at dashboard.prood.com (not on prood.app). A future admin white-label domain (e.g. cms.my-brand.com on Scale+) is documented in Multi-tenant platform — not implemented yet.
Subdomain (automatic)
When a merchant registers, their organization gets a slug (e.g. acme-store). The storefront is automatically available at {slug}.{NEXT_PUBLIC_PLATFORM_DOMAIN} — no dashboard action required.
Drag to pan · Scroll to zoom
Subdomain resolution happens in apps/storefront/lib/tenant.ts.
Custom domain setup
Route: /domains
Step 1 — Add domain
Merchant enters their custom domain (e.g. shop.acme.com). The dashboard:
- Inserts a row in
tenant_domainwithverified = false - Calls Vercel SDK to add the domain to the storefront Vercel project
- Returns DNS records the merchant must configure
Step 2 — DNS verification
The merchant adds DNS records at their registrar:
| Type | Name | Value |
|---|---|---|
| CNAME | shop.acme.com | cname.vercel-dns.com |
| TXT | _prood-verify.shop.acme.com | Verification token |
Step 3 — Verify
The dashboard polls Vercel's domain verification API. When verified:
tenant_domain.verifiedset totrue- Storefront begins resolving the org from this host
- SSL certificate provisioned automatically by Vercel
Vercel integration
lib/vercel.ts wraps the Vercel SDK:
import { Vercel } from '@vercel/sdk'
const vercel = new Vercel({ bearerToken: process.env.VERCEL_TOKEN })
export async function addDomain(domain: string) {
return vercel.projects.addProjectDomain({
idOrName: process.env.STOREFRONT_VERCEL_PROJECT_ID!,
teamId: process.env.VERCEL_TEAM_ID,
requestBody: { name: domain },
})
}Required env vars (optional in local dev):
| Variable | Purpose |
|---|---|
VERCEL_TOKEN | Vercel API bearer token |
STOREFRONT_VERCEL_PROJECT_ID | Storefront Vercel project ID (store custom domains attach here) |
VERCEL_TEAM_ID | Vercel team ID (if applicable) |
Storage schema
CREATE TABLE tenant_domain (
id TEXT PRIMARY KEY,
organization_id TEXT NOT NULL,
domain TEXT NOT NULL UNIQUE,
verified BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);Server actions
app/(dashboard)/domains/actions.ts:
'use server'
export async function addDomain(domain: string) {
return withActiveOrg(async (orgId) => {
await createTenantDomain(orgId, domain)
await vercelAddDomain(domain)
})
}
export async function removeDomain(domainId: string) {
return withActiveOrg(async (orgId) => {
const domain = await getTenantDomain(domainId, orgId)
await vercelRemoveDomain(domain.domain)
await deleteTenantDomain(domainId, orgId)
})
}Local development
Custom domains are not required for local dev:
- Run
pnpm db:setupand copy the seeded organization id intoDEFAULT_TENANT_ORG_ID - Access http://localhost:3000
- For subdomain testing:
/etc/hostsentry +NEXT_PUBLIC_PLATFORM_DOMAIN
Plan limits: Free includes one custom domain; enforcement runs in addDomainAction.