Subdomain Routing
Aero2 uses subdomain-based routing to serve multiple applications from a single Cloudflare Worker deployment. Each application gets its own subdomain for proper cookie, CORS, and issuer isolation.
URL Structure
dashboard.aero2.dev -> Developer Dashboard (sign up, create apps, manage)
swift-maple.aero2.dev -> App's auth pages + API (end-user facing)
brave-falcon.aero2.dev -> App's auth pages + API
auth.myapp.com -> Custom domain (CNAME -> swift-maple.aero2.dev)
aero2.dev -> Marketing / landing pageWhy Subdomains (Not Paths)
- Cookie isolation: Each app gets its own cookies naturally (security requirement). No risk of cross-app cookie leakage.
- CORS isolation: Each app is its own origin, preventing cross-app API calls.
- Custom domains: Easy CNAME mapping via Cloudflare for SaaS.
- OIDC issuer: Each app has a unique issuer URL (
https://{slug}.{domain}).
Worker Routing Logic
The subdomain routing middleware parses the Host header and resolves the application context:
Incoming request
|
+-- Host: aero2.dev
| -> Marketing landing page / redirect to dashboard
|
+-- Host: dashboard.aero2.dev
| -> Developer Dashboard (built-in app, app_id = DASHBOARD_APP_ID)
| -> Serve dashboard SPA + dashboard API
|
+-- Host: swift-maple.aero2.dev
| -> DB lookup: SELECT * FROM applications WHERE slug = 'swift-maple'
| -> Set app_id context on request
| -> Serve that app's branded auth pages + API
|
+-- Host: auth.myapp.com (custom domain)
| -> DB lookup: SELECT * FROM applications WHERE custom_domain = 'auth.myapp.com'
| -> Same as above
|
+-- Host: unknown.aero2.dev
-> 404: "Application not found"Middleware Implementation
The tenant middleware (src/backend/middleware/tenant.ts) performs these steps:
- Parse hostname from
Hostheader - Extract slug (first subdomain segment)
- If slug equals
DASHBOARD_SLUGenvironment variable, use dashboardapp_id - Otherwise, query D1:
SELECT * FROM applications WHERE slug = ? OR custom_domain = ? - If not found, return 404 "Application not found"
- Set
c.set('app', applicationRow)for downstream handlers
Application Lookup Caching
To avoid a D1 query on every request, application lookups are cached in an in-memory Map with a 60-second TTL:
- Cache key: hostname (subdomain or custom domain)
- Cache value: full application row
- TTL: 60 seconds
- Invalidation: on application update or delete
This keeps cold-start database lookups to a minimum while ensuring changes propagate within one minute.
Per-App CORS
Each application's subdomain is its own origin. The CORS middleware dynamically sets the allowed origin:
- Dashboard app allows
https://dashboard.{PLATFORM_DOMAIN} - Each app allows
https://{slug}.{PLATFORM_DOMAIN} - If the app has a
custom_domain, that origin is also allowed
Per-App Cookies
Cookie Domain is set to the app's specific subdomain (not a wildcard). This ensures browser-level cookie isolation:
dashboard.aero2.devcookies are only sent todashboard.aero2.devswift-maple.aero2.devcookies are only sent toswift-maple.aero2.dev- No wildcard
.aero2.devcookies that would leak across apps
Per-App OIDC Issuer
Each application has its own OIDC issuer URL:
- Issuer:
https://{slug}.{PLATFORM_DOMAIN}(e.g.,https://swift-maple.aero2.dev) - If a custom domain is configured and verified:
https://{custom_domain}(e.g.,https://auth.myapp.com) - OIDC Discovery at
/.well-known/openid-configurationreturns the per-app issuer - JWKS at
/oauth2/jwks.jsonis shared but tokenissclaim uses per-app issuer - Token verification checks
issagainst the requesting app's issuer
Custom Domains via Cloudflare for SaaS
Custom domains use Cloudflare's Custom Hostnames feature:
- Developer adds a custom domain in the dashboard (e.g.,
auth.myapp.com) - Aero2 calls the Cloudflare API to create a Custom Hostname:
POST /zones/{zone_id}/custom_hostnames { hostname: "auth.myapp.com", ssl: { method: "http", type: "dv" } } - SSL is auto-provisioned by Cloudflare
- Developer adds a CNAME record:
auth.myapp.com -> swift-maple.aero2.dev - Traffic flows through Cloudflare to the same Worker
- The tenant middleware resolves the custom domain to the correct application
Required Environment Variables
| Variable | Purpose |
|---|---|
PLATFORM_DOMAIN | Base domain (e.g., aero2.dev) |
DASHBOARD_SLUG | Slug for the dashboard app (e.g., dashboard) |
CF_ZONE_ID | Cloudflare zone ID for custom hostname API calls |
CF_API_TOKEN | Cloudflare API token with Custom Hostnames permission |
DNS Configuration
Type Name Content Proxy
A @ 192.0.2.1 Proxied (bare domain, placeholder IP)
CNAME * aero2.dev Proxied (wildcard catches all subdomains)Worker Route Configuration
In wrangler.json:
{
"env": {
"production": {
"routes": [
{ "pattern": "aero2.dev/*", "zone_name": "aero2.dev" },
{ "pattern": "*.aero2.dev/*", "zone_name": "aero2.dev" }
],
"vars": {
"PLATFORM_DOMAIN": "aero2.dev",
"DASHBOARD_SLUG": "dashboard"
}
},
"qa": {
"routes": [
{ "pattern": "qa.aero2.dev/*", "zone_name": "aero2.dev" },
{ "pattern": "*.qa.aero2.dev/*", "zone_name": "aero2.dev" }
],
"vars": {
"PLATFORM_DOMAIN": "qa.aero2.dev",
"DASHBOARD_SLUG": "dashboard"
}
}
}
}