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>/.

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_name plus additional_domains) maps
    to the same folder and shares one certificate, so the apex and www serve the
    same content.
  • Purging still targets the folder: send subdomain with 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.

Did this page help you?