Skip to content

Tenant Subdomain Provisioning

When a new tenant is provisioned, the API automatically registers a custom subdomain for them — both on Vercel (to route traffic) and on Cloudflare (to create the DNS CNAME record). This happens concurrently, outside the MongoDB transaction, so a DNS failure never blocks tenant creation.


Full Provisioning Flow

sequenceDiagram
    participant Admin as Super Admin (Dashboard)
    participant API as dot-foot-factory-api
    participant DB as MongoDB Atlas
    participant DP as domainProvisioning.js
    participant Vercel as Vercel API
    participant CF as Cloudflare DNS API

    Admin->>API: POST /api/v1/tenant
    API->>DB: Start MongoDB transaction
    DB-->>API: Session open
    API->>DB: Create Tenant document
    API->>DB: Create admin User document
    API->>DB: Set Tenant.owner = newUser._id
    API->>DB: Commit transaction
    DB-->>API: Transaction committed

    API->>DP: provisionSubdomain(slug)
    DP->>Vercel: addDomain(subdomain) [10s timeout]
    DP->>CF: createCnameRecord(subdomain, cnameTarget) [10s timeout]
    Vercel-->>DP: 200 OK / 409 Already exists
    CF-->>DP: 200 OK / 409 Already exists

    alt Both services succeeded
        DP-->>API: { vercel: success, cloudflare: success }
        API->>DB: tenant.domainStatus = "active"
        API-->>Admin: 201 Created
    else One or both services failed
        DP-->>API: { vercel: failed/success, cloudflare: failed/success }
        API->>DB: tenant.domainStatus = "pending"
        API-->>Admin: 207 Multi-Status (partial)
    end

201 vs 207 Response Decision

flowchart TD
    A[provisionSubdomain resolves] --> B{vercel.status === success?}
    B -->|No| D[domainStatus = pending]
    B -->|Yes| C{cloudflare.status === success?}
    C -->|No| D
    C -->|Yes| E[domainStatus = active]
    D --> F[HTTP 207 — partial success]
    E --> G[HTTP 201 — full success]

A 207 response means the tenant was created successfully in the database, but one or both DNS services failed. The tenant is fully operational — only the custom subdomain is pending.


Retry Flow

When a tenant has domainStatus: "pending", the super admin can trigger a retry from the dashboard.

sequenceDiagram
    participant Admin as Super Admin (Dashboard)
    participant API as dot-foot-factory-api
    participant DB as MongoDB Atlas
    participant DP as domainProvisioning.js
    participant Vercel as Vercel API
    participant CF as Cloudflare DNS API

    Admin->>API: POST /api/v1/tenant/:id/retry-domain
    API->>DB: Tenant.findById(id)
    DB-->>API: Tenant document

    alt domainStatus === "active"
        API-->>Admin: 400 — Domain is already active
    else domainStatus === "pending"
        API->>DP: retrySubdomain(slug)
        DP->>Vercel: addDomain(subdomain) [10s timeout]
        DP->>CF: createCnameRecord(subdomain, cnameTarget) [10s timeout]
        Vercel-->>DP: 200 OK / 409 Already exists
        CF-->>DP: 200 OK / 409 Already exists
        DP-->>API: ProvisioningResult
        API->>DB: Update tenant.domainStatus
        API-->>Admin: 200 OK with updated tenant + provisioningResult
    end

Both addDomain and createCnameRecord treat 409 Already exists as success — retries are idempotent.


Architecture: Domain Provisioning Utilities

graph TD
    TC[tenantController.js] -->|provisionSubdomain / retrySubdomain| DP[domainProvisioning.js]
    DP -->|Promise.allSettled + 10s timeout| VC[vercelClient.js]
    DP -->|Promise.allSettled + 10s timeout| CC[cloudflareClient.js]
    VC -->|POST /v10/projects/:id/domains| VAPI[Vercel REST API]
    CC -->|POST /zones/:id/dns_records| CFAPI[Cloudflare DNS API]
    CC -->|CLOUDFLARE_ENABLED=false| NOOP[No-op mode]

Module Responsibilities

Module Responsibility
domainProvisioning.js Orchestrator — runs Vercel + Cloudflare concurrently via Promise.allSettled, enforces 10s timeouts, logs failures
vercelClient.js Calls Vercel REST API to add/remove domains from the project. Throws ConfigError if env vars missing
cloudflareClient.js Calls Cloudflare DNS API to create/delete CNAME records. Supports no-op mode via CLOUDFLARE_ENABLED=false

Error Handling Strategy

Scenario Behaviour
Vercel API timeout (>10s) Logged as error; vercel.status = "failed"; tenant saved with domainStatus: "pending"
Cloudflare API timeout (>10s) Logged as error; cloudflare.status = "failed"; tenant saved with domainStatus: "pending"
Vercel 409 (domain already exists) Treated as success — idempotent
Cloudflare 409 (record already exists) Treated as success — idempotent
Missing VERCEL_API_TOKEN / VERCEL_PROJECT_ID ConfigError thrown; provisioning fails; tenant saved with domainStatus: "pending"
Missing CLOUDFLARE_API_TOKEN / CLOUDFLARE_ZONE_ID ConfigError thrown (unless CLOUDFLARE_ENABLED=false)
CLOUDFLARE_ENABLED=false Cloudflare calls are skipped entirely (no-op); only Vercel is called
MongoDB transaction failure Transaction rolled back; no tenant created; domain provisioning never runs

Failures in domain provisioning never cause the tenant creation to fail — the tenant is always persisted. The domainStatus field reflects the current state.


Admin UI: Domain Status

The DomainStatusBadge component in dot-foot-factory-admin renders the domain state inline on the Tenants page:

  • active — green pill showing the subdomain URL
  • pending — amber warning banner with two actions:
  • Retry — calls POST /api/v1/tenant/:id/retry-domain
  • Manual Setup — opens DomainManualSetupModal with step-by-step Vercel + Cloudflare instructions

Environment Variables

Variable Required Description
SUBDOMAIN_BASE_DOMAIN Yes Base domain suffix appended to slug (e.g. .dotevolve.net)
VERCEL_API_TOKEN Yes Vercel API token with domain management permissions
VERCEL_PROJECT_ID Yes Vercel project ID to register domains against
VERCEL_CNAME_TARGET Yes CNAME target for DNS records (e.g. 6ede78a9c15a1c22.vercel-dns-017.com)
CLOUDFLARE_API_TOKEN Yes* Cloudflare API token with DNS edit permissions
CLOUDFLARE_ZONE_ID Yes* Cloudflare zone ID for the base domain
CLOUDFLARE_ENABLED No Set to false to disable Cloudflare integration (no-op mode)

*Not required when CLOUDFLARE_ENABLED=false.


Subdomain Format

Given a tenant with slug: "acme-corp" and SUBDOMAIN_BASE_DOMAIN=".dotevolve.net":

  • Subdomain: acme-corp.dotevolve.net
  • Vercel: domain added to the project → traffic routed to the factory floor app
  • Cloudflare: proxied CNAME record acme-corp.dotevolve.net → cname.vercel-dns.com