Subdomain Routing (multi-tenant)
Serve many tenants from one CDN distribution and one S3 bucket. The request subdomain becomes a folder prefix in the bucket, so abc.yourdomain.com maps to /abc/ - no per-tenant bucket, distribution, or certificate.
Subdomain routing lets a single CDN resource with a wildcard domain
(*.yourdomain.com) serve unlimited tenants out of one S3 bucket, each isolated
in its own folder. The request subdomain is rewritten to a path prefix in the
bucket at the edge.
abc.yourdomain.com/style.css -> bucket /abc/style.css
abc.yourdomain.com/ -> bucket /abc/index.html
abc.yourdomain.com/advogado -> bucket /abc/advogado/index.html
www.yourdomain.com/ and apex -> bucket /index.html (root, no prefix)
This replaces the "one bucket + one distribution + one certificate per site"
pattern. Add a tenant by writing files to a new /{subdomain}/ folder - no new
infrastructure.
How it works
A viewer-request CloudFront function (mamute-index-rewrite-subdomain) runs on
every request:
- Reads the
Hostheader, lowercased. - Takes the first label (
abcfromabc.yourdomain.com). - Clean URLs: if the path ends in
/, or has no file extension, it serves the
directory'sindex.html(/advogadoand/advogado/both map to
/advogado/index.html). Paths with an extension (.css,.js) pass through. - If the subdomain is not
wwwor the apex, prefixes/{subdomain}to the path.
It only makes sense with an S3 / storage origin - the function rewrites the
path into the bucket.
Enabling it
subdomain_routing is a boolean flag on a behavior (sibling of index_rewrite).
It is not accepted on POST /cdn/resource; the default behavior is created
with subdomain_routing = false. Enable it with a PUT on the default behavior.
The update action requires the behavior's id, so first read the resource and
grab the id of the priority-0 (default) behavior:
curl https://api.mamutecloud.com/cdn/resource/$RESOURCE_ID \
-H "Authorization: $MAMUTECLOUD_API_KEY"
# -> response.behaviors[] -> take the object with "priority": 0, read its "id"Then enable the flag, passing that id:
curl -X PUT https://api.mamutecloud.com/cdn/resource/$RESOURCE_ID \
-H "Authorization: $MAMUTECLOUD_API_KEY" \
-H "content-type: application/json" \
-d '{
"behaviors": [
{ "action": "update", "id": "<behavior-id>", "priority": 0, "subdomain_routing": true }
]
}'Read it back with GET /cdn/resource/{resourceId} - each behavior object returns
subdomain_routing as true, false, or null.
Edge function precedence (viewer-request slot)
For a behavior, the function attached is chosen in this order:
subdomain_routing = true->mamute-index-rewrite-subdomain- else
index_rewritetrue (or plain S3 origin) ->mamute-index-rewrite - else custom origin ->
mamute-block-websocket - else -> none
Purging the cache (important)
Because the subdomain is rewritten into the path before the cache key, objects
are cached under /{subdomain}/.... A purge of the public path (/advogado/*)
would not match the cached /{subdomain}/advogado/* and would silently miss.
Pass the tenant subdomain on the purge so it targets the right path:
curl -X POST https://api.mamutecloud.com/cdn/resource/$RESOURCE_ID/purge/url \
-H "Authorization: $MAMUTECLOUD_API_KEY" \
-H "content-type: application/json" \
-d '{ "subdomain": "abc", "urls": ["/advogado/*"] }'
# -> invalidates /abc/advogado/*On a subdomain-routing resource, a purge without subdomain returns a 400 telling
you to pass it. Use "urls": ["/*"] (still with the subdomain, or as a plain
purge/all) to clear an entire tenant. The same subdomain field works on
purge/pattern.
Prerequisites
- A wildcard SSL certificate
*.yourdomain.comattached to the resource's
custom domain (see Hosting a Static Site for the cert- custom-domain flow).
- The bucket laid out with one folder per subdomain:
/{subdomain}/index.html,
/{subdomain}/assets/..., etc. A request for a key that does not exist returns
the S3404/403- the client's upload must write into this layout. - Files uploaded via the S3 data plane (
https://s3-us-east-1.mamutecloud.com,
path-style, regionus-east-1).
End-to-end
- Create a bucket and S3 credentials (see Hosting a Static Site).
- Upload each tenant under its own prefix:
s3://<bucket>/<subdomain>/index.html. - Issue a wildcard cert:
POST /cdn/ssl-certificate {"domain_name":"*.yourdomain.com"},
validate the DNS records, wait forISSUED. - Create the resource with a storage origin:
POST /cdn/resource. - Attach the wildcard domain + cert:
PUT /cdn/resource/{id}withcustom_domains,
then activate it. - Turn on routing: read the resource, then
PUT /cdn/resource/{id}with the
subdomain_routingbehavior (above, with the behaviorid). - Point
*.yourdomain.com(wildcard CNAME) at the resourcedistribution_domain.
Any <sub>.yourdomain.com now serves s3://<bucket>/<sub>/.
Custom domain with its own certificate per folder
By default subdomain routing derives the folder from the first label of the
host: advogado.yourdomain.com serves /advogado/. A tenant that brings its
own domain (cliente.com.br) needs the folder stated explicitly, because the
host no longer carries it.
The resource, the bucket and the wildcard keep working as they are. Each custom
domain becomes a CloudFront distribution tenant on the same resource, carrying
its own certificate, and the edge maps the full host to its folder.
1. Issue the certificate for the tenant domain
curl -X POST https://api.mamutecloud.com/cdn/ssl-certificate \
-H "Authorization: $MAMUTECLOUD_API_KEY" \
-H "content-type: application/json" \
-d '{"domain_name": "cliente.com.br", "subject_alternative_names": ["www.cliente.com.br"]}'Publish the validation CNAME returned in validation_records and wait for
status: issued on GET /cdn/ssl-certificate/{id}.
2. Attach the domain and say which folder it serves
curl -X PUT https://api.mamutecloud.com/cdn/resource/$RESOURCE_ID \
-H "Authorization: $MAMUTECLOUD_API_KEY" \
-H "content-type: application/json" \
-d '{ "custom_domains": [ { "action": "add",
"domain_name": "cliente.com.br",
"additional_domains": ["www.cliente.com.br"],
"certificate_id": "'$CERT_ID'",
"folder": "advogado" } ] }'folder accepts letters, numbers, dot, dash and underscore. It can also be set
later with PUT /cdn/resource/{id}/custom-domain/{customDomainId}.
3. Activate
curl -X POST \
https://api.mamutecloud.com/cdn/resource/$RESOURCE_ID/custom-domain/$CUSTOM_DOMAIN_ID/activate \
-H "Authorization: $MAMUTECLOUD_API_KEY"Activation is what publishes the host to folder mapping at the edge. On a
resource with subdomain_routing, activating without folder returns 400.
4. Point the DNS
Create a CNAME for cliente.com.br and www.cliente.com.br pointing at the
resource distribution_endpoint (returned by GET /cdn/resource/{id}).
https://cliente.com.br/ then serves /advogado/index.html from the bucket,
with the tenant's own certificate, while advogado.yourdomain.com keeps being
served by the wildcard.
Notes
- Every domain in the same entry (
domain_nameplusadditional_domains) maps
to the same folder and shares one certificate, so the apex andwwwserve the
same content. - Purging still targets the folder: send
subdomainwith the folder name on
POST /cdn/resource/{id}/purge/url, or"/*"to purge every tenant. - Deactivating (
POST .../custom-domain/{id}/deactivate) removes the mapping and
the host falls back to the wildcard behaviour (first label as the folder). - The mapping is eventually consistent: right after activating, the first
requests can still hit the fallback for a few seconds.
Updated 5 days ago
