Subdomain Management for Multi-Tenant SaaS 🚀

Subdomain Management for Multi-Tenant SaaS :rocket:

Imagine this: Your SaaS platform powers over 500 healthcare facilities, each on a custom subdomain like clinic.healthcorp.com.

Then HealthCorp calls —
“We’re rebranding the clinic to sunrise. Can it go live by tomorrow with zero or with minimal downtime?”

Traditional approaches might involve certificate reissuance, infrastructure changes, or complex DNS juggling.


:puzzle_piece: The Business Challenge

Multi-tenant SaaS platforms face a unique challenge: dynamic subdomain management at scale. Most platforms use a hierarchical subdomain structure:

<facility>.<enterprise>.com

Real-world scenarios demand support for:

  • :hospital: Branded subdomains for each tenant
  • :counterclockwise_arrows_button: Frequent changes due to rebranding, mergers, and compliance
  • :locked_with_key: Security and uptime guarantees

:gear: Architecture Overview

Here’s how we’ve structured our platform to support this dynamic need:

  • A single CloudFront distribution serves all tenants.
  • A wildcard ACM certificate (*.healthcorp.com) covers all subdomains.
  • Route 53 ALIAS A records point subdomains to the CloudFront distribution.
  • SSM Parameter Store holds environment-specific identifiers (e.g., hosted zone ID, CloudFront distribution ID).

This setup allows us to change subdomains dynamically without redeploying CloudFront or reissuing certificates.


:counterclockwise_arrows_button: Workflow: Handling a Subdomain Change

When a facility requests a subdomain change, our backend executes the following steps:

1. :inbox_tray: Fetch Configuration from SSM

Use SSM Parameter Store to fetch:

  • Hosted zone ID
  • CloudFront domain name
  • CloudFront distribution ID
const hosted_zone = await fetchSSMParameter({ parameter_name: 'hostZoneId', withDecryption: false });
const cloudfront_domain = `${await fetchSSMParameter({ parameter_name: 'FACILITY_DISTRIBUTION_DOMAIN', withDecryption: false })}.`;
const cloudfront_hosted_zone_id = await verifyExistingRecord({
  hosted_zone_id,
  record_name: current_sub_domain,
  record_type: 'A',
  max_items: 1,
});
const cloudfront_distribution_id = await fetchSSMParameter({ parameter_name: 'FACILITY_DISTRIBUTION_ID', withDecryption: false });

2. :card_index_dividers: Update Route 53 Records

Replace the old DNS record with a new ALIAS record that points to the same CloudFront distribution.

Changes: [
  {
    "Action": "DELETE",
    "ResourceRecordSet": {
      "Name": "clinic.healthcorp.com.",
      "Type": "A",
      "AliasTarget": {
        "HostedZoneId": "<cloudfront_hosted_zone_id>",
        "DNSName": "<cloudfront_domain>",
        "EvaluateTargetHealth": false
      }
    }
  },
  {
    "Action": "CREATE",
    "ResourceRecordSet": {
      "Name": "sunrise.healthcorp.com.",
      "Type": "A",
      "AliasTarget": {
        "HostedZoneId": "<cloudfront_hosted_zone_id>",
        "DNSName": "<cloudfront_domain>",
        "EvaluateTargetHealth": false
      }
    }
  }
]

:white_check_mark: Note: This is a controlled replace pattern, not a pure UPSERT.

  • Explicitly deleting the old DNS record before creating the new one ensures a clean and deterministic transition.
  • This avoids scenarios where an UPSERT might silently fail or conflict with existing entries—especially during tenant migrations or renames.
  • It also ensures that stale or duplicate records don’t linger, which could lead to DNS resolution issues or security risks.
  • In regulated environments like healthcare, this pattern provides better auditability and rollback control.

3. :hourglass_not_done: Wait for Route 53 to Sync

Use polling to ensure the DNS change has fully propagated.

const status = await waitForChangePropagation({ change_id });

4. :police_car_light: Invalidate CloudFront Cache

Even after DNS updates, CloudFront may serve cached content from the old subdomain. This can cause:

  • Session inconsistencies
  • Stale UI assets
  • HTTP errors (e.g., 404s)

To prevent this, invalidate the CloudFront cache:

await createInvalidation({
  distribution_id: cloudfront_distribution_id,
  paths: ['/*'],
});

This ensures new content is served immediately after the subdomain change.

:brain: Final Thoughts

Managing tenant-specific subdomains in a multi-tenant SaaS can be a complex endeavor—but it doesn’t have to be. By leveraging AWS tools like Route 53, CloudFront, ACM, and SSM Parameter Store, we’ve streamlined the process into a scalable, reliable system.

“Just remember: Always invalidate the CloudFront cache after changing DNS. It’s the secret ingredient to a smooth subdomain transition.”

3 Likes

Thanks @Sonal_Monis This is indeed insightful.

Thank you @ashu-kajekar