Fastly Wildcard Subdomain Routing for Compute@Edge
Problem
Wildcard subdomains (*.example.com) don't work on Fastly Compute@Edge services even after:
- Creating the domain with
fastly domain-v1 create
- Setting up DNS records
- The static publisher working fine for the apex domain
Symptoms include:
- 500 errors with "Fastly error: unknown domain: subdomain.example.com"
- PublisherServer returning empty responses (content-length: 0) for subdomain requests
- TLS subscriptions stuck in "pending" state
- DNS resolving to wrong targets
Context / Trigger Conditions
-
Wrong SNI endpoint: TLS certificate is "issued" but edge serves default certificate
(CN=j.sni-644-default.ssl.fastly.net instead of your domain).
CRITICAL: The TLS configuration name tells you which SNI endpoint to use:
- "HTTP/3 & TLS v1.3 + 0RTT (x.sni)" → DNS to
x.sni.global.fastly.net.
- "HTTP/3 & TLS v1.3 (w.sni)" → DNS to
w.sni.global.fastly.net.
- Default/legacy → DNS to
j.sni.global.fastly.net.
Check your TLS configuration:
FASTLY_KEY=$(fastly profile token) && curl -s -H "Fastly-Key: $FASTLY_KEY" \
"https://api.fastly.com/tls/configurations" | jq '.data[] | {id: .id, name: .attributes.name}'
Verify by connecting directly to the correct endpoint:
echo | openssl s_client -servername subdomain.yourdomain.com \
-connect x.sni.global.fastly.net:443 2>/dev/null | openssl x509 -noout -subject
-
DNS trailing dot issue: CNAME records resolve to target.com.yourdomain.com instead
of target.com because the DNS provider appends the zone name without a trailing dot.
Check with: dig @ns-server *.yourdomain.com CNAME +short
Bad: x.sni.global.fastly.net.yourdomain.com.
Good: x.sni.global.fastly.net.
-
Domain not activated: Fastly domain-v1 shows "activated": false, "verified": false
even after DNS is set up.
Check with:
curl -s -H "Fastly-Key: $(fastly profile token)" \
"https://api.fastly.com/domain-management/v1/domains/DOMAIN_ID" | jq
-
PublisherServer returns empty for subdomains: The @fastly/compute-js-static-publish
PublisherServer returns null/empty responses when the request hostname is a subdomain,
even though it works for the apex domain and edgecompute.app URL.
Solution
Part 1: Fix DNS Configuration
-
Wildcard CNAME with trailing dot (check your TLS config for correct SNI endpoint):
Name: *
Type: CNAME
Value: x.sni.global.fastly.net. <- Use endpoint from your TLS config name (x.sni, w.sni, or j.sni)
-
ACME challenge CNAME with trailing dot (for TLS validation):
Name: _acme-challenge
Type: CNAME
Value: CHALLENGE_VALUE.fastly-validations.com. <- MUST include trailing dot
-
Alternative: Use A records instead of CNAME (avoids trailing dot issues):
Name: *
Type: A
Values: 151.101.1.242, 151.101.65.242, 151.101.129.242, 151.101.193.242
Part 2: Recreate Fastly Domain (if stuck)
If domain shows activated: false even after DNS is correct:
# 1. Delete TLS subscription first (if exists)
fastly tls-subscription delete --id SUBSCRIPTION_ID --force
# 2. Delete the domain
fastly domain-v1 delete --domain-id DOMAIN_ID
# 3. Recreate domain
fastly domain-v1 create --fqdn "*.yourdomain.com" --service-id SERVICE_ID
# 4. Create new TLS subscription
fastly tls-subscription create --domain "*.yourdomain.com"
# 5. Wait 1-5 minutes for verification and TLS issuance
Part 3: Fix PublisherServer for Subdomains
The PublisherServer from @fastly/compute-js-static-publish doesn't serve content for
subdomain hostnames. You must read from the KV store directly:
if (subdomain) {
// Open the content KV store
const contentStore = new KVStore('your-content-store-name');
// Read the index file - NOTE: collection name might be 'undefined' not 'default'
const indexEntry = await contentStore.get('default_index_undefined');
const indexData = await indexEntry.json();
// Get the index.html entry - structure is { '/path': { key: 'sha256:HASH', ... } }
const indexHtmlInfo = indexData['/index.html'];
// Read the actual content
const contentHash = indexHtmlInfo.key.replace('sha256:', '');
const contentKey = `default_files_sha256_${contentHash}`;
const htmlEntry = await contentStore.get(contentKey);
const html = await htmlEntry.text();
// Inject subdomain-specific data and return
const modifiedHtml = html.replace('<head>', `<head><script>window.SUBDOMAIN_DATA = {...}</script>`);
return new Response(modifiedHtml, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
Part 4: Key Format Discovery
The static publisher uses these KV key formats:
- Index:
${publishId}_index_${collectionName} (e.g., default_index_undefined)
- Settings:
${publishId}_settings_${collectionName}
- Files:
${publishId}_files_sha256_${hash}
To discover your actual key names:
curl -s -H "Fastly-Key: $(fastly profile token)" \
"https://api.fastly.com/resources/stores/kv/STORE_ID/keys?limit=50" | jq '.data[]' | grep index
Verification
# 1. Verify DNS is correct
dig @8.8.8.8 subdomain.yourdomain.com A +short
# Should return Fastly IPs
# 2. Verify domain is activated
curl -s -H "Fastly-Key: $(fastly profile token)" \
"https://api.fastly.com/domain-management/v1/domains/DOMAIN_ID" | jq '{activated, verified}'
# Both should be true
# 3. Verify TLS is issued
fastly tls-subscription list | grep yourdomain
# State should be "issued"
# 4. Test the subdomain
curl -sI "https://subdomain.yourdomain.com"
# Should return 200 with content
Example
Complete fix for *.divine.space subdomain routing:
// In Compute@Edge handler
if (subdomain && namesStore) {
const entry = await namesStore.get(`name:${subdomain}`);
const contentStore = new KVStore('divine-space-content');
const indexEntry = await contentStore.get('default_index_undefined');
const indexData = await indexEntry.json();
const indexHtmlInfo = indexData['/index.html'];
const contentHash = indexHtmlInfo.key.replace('sha256:', '');
const htmlEntry = await contentStore.get(`default_files_sha256_${contentHash}`);
const html = await htmlEntry.text();
if (entry) {
const data = await entry.json();
const injectedHtml = html.replace('<head>', `<head>
<script>window.__DIVINE_SPACE_USER__ = {
subdomain: "${subdomain}",
pubkey: "${data.pubkey}"
};</script>`);
return new Response(injectedHtml, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
});
}
}
Notes
- The
collectionName in KV keys is often undefined (literal string) rather than
default due to how the static publisher is configured. Always check actual keys.
- DNS changes can take up to 3 hours to propagate due to TTL settings.
- Fastly edge cache may serve stale responses—use
fastly purge --all after changes.
- Static assets (/assets/*, .js, .css files) should be served BEFORE subdomain handling
using the normal PublisherServer.
- The wildcard TLS cert (*.domain.com) requires the ACME challenge DNS record to be
correct before it will issue.
References