Skip to content

Custom Domains Design

Overview

Add custom domain support so Pro users can serve shortlinks from their own branded domains (e.g. go.acme.com/abc123 instead of usnp.me/abc123). Uses Cloudflare for SaaS for TLS provisioning and proxying.

Decisions

  • Infrastructure: Cloudflare for SaaS handles TLS + proxying. No changes to DigitalOcean App Platform.
  • ID uniqueness: Globally unique (unchanged). A short_id maps to exactly one link regardless of domain.
  • Verification: CNAME + TXT record. TXT proves ownership, CNAME enables routing.
  • Domain limit: Free: 0 (upgrade CTA). Pro: unlimited.
  • Webhook payloads: Include the domain the click came through from day one.

Data Model

New domains table

CREATE TABLE domains (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    api_key_id UUID NOT NULL REFERENCES api_keys(id),
    domain TEXT NOT NULL UNIQUE,
    verification_token TEXT NOT NULL,
    txt_verified BOOLEAN NOT NULL DEFAULT false,
    cname_verified BOOLEAN NOT NULL DEFAULT false,
    cf_custom_hostname_id TEXT,
    tls_status TEXT NOT NULL DEFAULT 'pending',
    created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_domains_api_key_id ON domains(api_key_id);
CREATE INDEX idx_domains_domain ON domains(domain);

ALTER TABLE domains ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow all operations on domains" ON domains FOR ALL USING (true) WITH CHECK (true);

Changes to existing tables

-- Link-to-domain association (nullable; NULL = usnp.me)
ALTER TABLE links ADD COLUMN domain_id UUID REFERENCES domains(id);

-- Track which domain a hit came through
ALTER TABLE hits ADD COLUMN domain TEXT;

No migration needed for existing data. Null domain_id means the link is on usnp.me.

Verification states

txt_verified cname_verified tls_status Meaning
false false pending Just registered, waiting for DNS
true false pending Ownership proved, CNAME not found yet
true true provisioning Both records verified, Cloudflare issuing cert
true true active Live, serving traffic
true true failed Cloudflare cert provisioning failed

Cloudflare for SaaS Integration

Setup (one-time)

  • Cloudflare zone with "SSL for SaaS" enabled
  • Fallback origin: fallback-origin.usnp.me pointed at the DO App Platform app
  • New env vars: CLOUDFLARE_API_TOKEN, CLOUDFLARE_ZONE_ID

Domain lifecycle

  1. Register: User calls POST /domains with their subdomain. App generates a verification_token and stores the domain.
  2. Verify: User calls POST /domains/{id}/verify. App performs DNS lookups:
  3. TXT: _yousnap-verify.{domain} must contain the token
  4. CNAME: {domain} must point to fallback-origin.usnp.me
  5. Provision: Both pass -> app calls Cloudflare Create Custom Hostname API. Stores cf_custom_hostname_id, sets tls_status = 'provisioning'.
  6. Active: App polls Cloudflare hostname status (or checks on next verify call) to confirm TLS. Flips tls_status = 'active'.
  7. Delete: App calls Cloudflare Delete Custom Hostname API, deletes domains row. Links with that domain_id fall back to usnp.me (set domain_id = NULL).

Failure handling

If Cloudflare cert provisioning fails, tls_status stays failed. Dashboard shows the error and a retry button that re-triggers verification + provisioning.

API Endpoints

New endpoints

Method Path Auth Description
POST /domains JWT or API key (Pro only) Register a custom domain
GET /domains JWT or API key List user's domains with status
POST /domains/{domain_id}/verify JWT or API key Trigger DNS verification
DELETE /domains/{domain_id} JWT or API key Remove domain + CF hostname

POST /domains

Request:

{ "domain": "go.acme.com" }

Response (201):

{
  "id": "uuid",
  "domain": "go.acme.com",
  "txt_record": "_yousnap-verify.go.acme.com",
  "txt_value": "yousnap-verify=abc123...",
  "cname_target": "fallback-origin.usnp.me",
  "tls_status": "pending"
}

Free-tier users get 403 with upgrade message.

POST /domains/{domain_id}/verify

Response (200):

{
  "txt_verified": true,
  "cname_verified": true,
  "tls_status": "active"
}

Partial verification returns which records are missing with instructions.

Changes to existing endpoints

POST /shorten: Add optional domain_id to LinkCreate. When set: - Validate domain belongs to caller and tls_status = 'active' - Build short_url using the custom domain (e.g. https://go.acme.com/abc123) - Store domain_id on the link

GET /{short_id}: No lookup change (globally unique IDs). Pass Host header to record_hit_and_notify for analytics.

GET /{short_id}/qr: Build QR URL using the link's domain instead of hardcoded usnp.me.

Webhook payloads: Add domain field containing the domain the click came through.

Routing & Middleware

Domain cache

In-memory set of verified domain strings, refreshed every 60 seconds via background task. Avoids hitting Supabase on every custom-domain request.

Middleware changes

known_hosts = {"yousnap.me", "www.yousnap.me", "usnp.me"}

if host in ("yousnap.me", "www.yousnap.me") and method == "GET":
    # serve landing page (unchanged)
elif host == "usnp.me" or host in verified_domains_cache:
    # pass through to API
else:
    return 404

The cache refresh runs as an asyncio background task started on app startup.

Dashboard UI

New "Domains" tab

  • Domain list table: domain, status badge, created date, delete button
  • "Add Domain" button opens modal
  • Free-tier users see upgrade CTA instead of add button

Add domain modal

  1. User enters subdomain
  2. On submit, shows DNS instructions with copy buttons:
  3. TXT record: _yousnap-verify.go.acme.com -> value
  4. CNAME: go.acme.com -> fallback-origin.usnp.me
  5. "Verify" button triggers verification
  6. Inline status icons per record (check/x)
  7. Status flips to active once Cloudflare provisions the cert
  • Domain dropdown in create modal (defaults to usnp.me)
  • Only shows verified/active domains as options
  • Link list shows which domain each link is on

Analytics

  • New domain column on hits table stores the Host header value
  • Analytics endpoints can add a domains breakdown (optional for v1, data is captured either way)
  • Webhook payloads include domain field

New Environment Variables

Variable Required Purpose
CLOUDFLARE_API_TOKEN Yes (for custom domains) Cloudflare API token with Custom Hostnames permission
CLOUDFLARE_ZONE_ID Yes (for custom domains) Cloudflare zone ID