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:

  1. Reads the Host header, lowercased.
  2. Takes the first label (abc from abc.yourdomain.com).
  3. Clean URLs: if the path ends in /, or has no file extension, it serves the
    directory's index.html (/advogado and /advogado/ both map to
    /advogado/index.html). Paths with an extension (.css, .js) pass through.
  4. If the subdomain is not www or 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:

  1. subdomain_routing = true -> mamute-index-rewrite-subdomain
  2. else index_rewrite true (or plain S3 origin) -> mamute-index-rewrite
  3. else custom origin -> mamute-block-websocket
  4. 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.com attached 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 S3 404/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, region us-east-1).

End-to-end

  1. Create a bucket and S3 credentials (see Hosting a Static Site).
  2. Upload each tenant under its own prefix: s3://<bucket>/<subdomain>/index.html.
  3. Issue a wildcard cert: POST /cdn/ssl-certificate {"domain_name":"*.yourdomain.com"},
    validate the DNS records, wait for ISSUED.
  4. Create the resource with a storage origin: POST /cdn/resource.
  5. Attach the wildcard domain + cert: PUT /cdn/resource/{id} with custom_domains,
    then activate it.
  6. Turn on routing: read the resource, then PUT /cdn/resource/{id} with the
    subdomain_routing behavior (above, with the behavior id).
  7. Point *.yourdomain.com (wildcard CNAME) at the resource distribution_domain.

Any <sub>.yourdomain.com now serves s3://<bucket>/<sub>/.


Did this page help you?