Building a Multi-City Hub with CloudFront Routing: The Queen's Fleet Infrastructure
Over the course of a development sprint, we unified sixteen city-themed sites under a single hub domain using CloudFront routing, dynamic DNS configuration, and a centralized site registry. This post details how we architected the infrastructure, automated the deployment pipeline, and established patterns for scaling to additional properties.
The Architecture Problem
Our fleet consisted of individual queenof[city].com domains scattered across CloudFront distributions, Route53 records, and S3 origins. To unify them behind a discoverable hub, we needed:
- A central hub domain with per-city card previews
- Intelligent routing based on request hostname or subdomain
- Consistent DNS management across all properties
- Repeatable infrastructure-as-code patterns
Domain Registration and Certificate Setup
Why we chose thequeensfleet.com: The original queensfleet.com was squatted at a premium price. Rather than overpay for a legacy domain, we registered thequeensfleet.com via the Namecheap API from our Lightsail jumpbox—a pattern that keeps credential management centralized and auditable.
# Domain registration via Namecheap API
POST /namecheap/domains/create
Body: {
domain: "thequeensfleet.com",
contact_info: {existing_jada_registrant},
years: 1
}
We then requested an ACM certificate in us-east-1 (required for CloudFront) and automated DNS validation:
# ACM certificate request
aws acm request-certificate \
--domain-name thequeensfleet.com \
--domain-name "*.thequeensfleet.com" \
--validation-method DNS \
--region us-east-1
The validation CNAMEs were added to the Namecheap DNS via their API, eliminating manual DNS record entry and reducing deployment friction.
CloudFront Distribution and Routing
Rather than scatter separate distributions for each city, we created a single CloudFront distribution (ID: E176JRMB92GPAJ) with:
- Primary origin: S3 bucket
qos-city-sitesserving pre-built hub HTML - Cache behavior: Configured for aggressive caching with versioned assets (ETags verified post-deployment)
- Aliases:
thequeensfleet.com,www.thequeensfleet.com, plus city aliases likethepacific.thequeensfleet.com
Why a single distribution: While we could have used separate distributions per city, consolidation reduces operational overhead, leverages shared edge cache, and simplifies certificate management. The trade-off is that request routing logic must be more sophisticated—which we handle via CloudFront functions, not origin multiplexing.
Routing Logic: CloudFront Functions and Router.js
The core routing lives in /Users/cb/icloud-repos/sites/queenof-cities/build/router.js, which translates request hostnames into origin paths. When a request arrives for thepacific.thequeensfleet.com, the function:
- Extracts the hostname subdomain (
thepacific) - Maps it to an S3 origin path via a lookup table
- Rewrites the request URI to point to the correct index.html and assets
- Injects cache headers based on asset type (immutable for versioned assets, revalidate for HTML)
// Simplified routing logic
const cityMap = {
"thepacific": "pacific/",
"theatlantic": "atlantic/",
"mazatlan": "mazatlan/",
// ... 13 additional cities
};
const subdomain = hostname.split(".")[0];
const path = cityMap[subdomain] || "hub/";
request.uri = path + request.uri;
This approach is vastly more scalable than managing N separate distributions—adding a new city requires only updating the map and uploading its assets to S3, not provisioning new infrastructure.
Hub Page Generation and Deployment
The hub HTML is generated dynamically by build/build_hub.py, which:
- Reads the site registry from
jada-ops/registry.json(16 destination entries in theventuressection) - Fetches a 1x1 pixel from each city's S3 bucket to validate the s3-origin exists (early fail detection)
- Renders card images for each destination using a preview URL pattern:
https://[city-alias].thequeensfleet.com/assets/0.jpg - Outputs HTML to
qos-city-sitesS3 bucket underhub/index.html
# Deployment command
python build/build_hub.py \
--registry jada-ops/registry.json \
--output-bucket qos-city-sites \
--cloudfront-dist E176JRMB92GPAJ
Post-deployment, we verify ETags of uploaded assets against CloudFront's cache to ensure fresh content is served, then invalidate the CloudFront cache for /hub/*.
Site Registry and Metadata
The source of truth is jada-ops/registry.json, which contains all 16 city properties in a ventures array:
{
"ventures": [
{
"domain": "thequeensfleet.com",
"slug": "queen-fleet",
"type": "hub",
"routes": ["thequeensfleet.com", "www.thequeensfleet.com"]
},
{
"domain": "thepacific.thequeensfleet.com",
"slug": "pacific",
"s3_prefix": "pacific/",
"routes": ["thepacific.thequeensfleet.com"]
},
// ... 14 additional cities
]
}
This centralized registry powers code generation, deployment validation, and test fixtures—reducing drift and manual documentation burden.
Route53 and DNS Propagation
We created Route53 alias records for the primary domain and three additional city aliases:
# Route53 alias (UPSERT)
Name: thequeensfleet.com
Type: A / AAAA
Alias Target: [CloudFront distribution domain]
Evaluate Target Health: false
Name: thepacific.thequeensfleet.com
Type: A / AAAA
Alias Target: [same CloudFront distribution]
// ... theatlan, mazatlan, etc.
Alias records are superior to CNAME for zone apexes and eliminate extra DNS lookups. We verified propagation at multiple public resolvers before marking the deployment complete.
Testing and Validation
All infrastructure changes are validated by tests/test_hub.py, which:
- Verifies each city's index.html and card image (0.jpg) exist in S3
- Confirms CloudFront aliases resolve to the correct distribution
- Validates Route53 A/AAAA records point to CloudFront
- Tests HTTP → HTTPS redirects on the apex domain
# Run integration tests
pytest tests/test_hub.py -v --tb=short
Tests are run post-deployment and gate production traffic ramps.
Key Decisions and Trade-Offs
- Single CloudFront distribution vs. multiple: Consolidation reduces cost and operational surface area; routing complexity is handled by functions, not infrastructure sprawl.
- CloudFront functions over Lambda@Edge: Functions are faster (sub-1ms) and free for our scale; Lambda@Edge adds latency and cost for simple routing logic.
- S3 origin vs. custom origins: S3 is simple and cost-effective; all content is static HTML and images, removing the need for dynamic backends.
- Namecheap API for registration: Keeps credential management centralized on our Lightsail jumpbox and enables infrastructure-as-code for domain lifecycle.
What's Next
With the hub infrastructure in place, we are adding per-city avatar photography, expanding the city roster, and building email workflows for destination marketing. The registry-driven pattern scales—adding a new city is now a two-step process: register the domain and upload assets; infrastructure deployment is automatic.
```