Skip to content

Custom Subdomains

Configure custom subdomains for frontend applications on Azure Front Door.


πŸ“‹ Overview

By default, frontend applications are accessible via the shared organizational endpoint:

https://app-int-{env}.saif.com/{appname}

You can add a custom subdomain to provide a dedicated URL:

https://{subdomain}-{env}.saif.com           // root app URL
https://{subdomain}-{env}.saif.com/{appname} // non-root app URL

Both URLs work simultaneouslyβ€”the shared endpoint remains available after adding a custom subdomain.

πŸ’‘ Best Practice: Use custom subdomains sparingly, primarily for grouping related applications rather than creating a subdomain per application. This approach simplifies DNS management and SSL certificate provisioning.


βš™οΈ Configuration

1. Configure Custom Subdomain in Your Team's azure.terraform Repository

Custom subdomains are managed in your team's azure.terraform repository in Azure DevOps. This repository was created when your team's project was provisioned.

Step 1: Clone your team's azure.terraform repository (if you haven't already):

git clone https://dev.azure.com/SAIFCorp/{YourTeamProject}/_git/azure.terraform
cd azure.terraform

πŸ’‘ Tip: Replace {YourTeamProject} with your Azure DevOps project name (e.g., Platform, Claims, HR).

Step 2: Open infra/azure/subdomains.yml and add your subdomain to the custom_subdomains list:

azure.terraform/infra/azure/subdomains.yml
# Custom domains configuration
# Each entry creates a Front Door endpoint with custom domain
# that app repos can attach their routes to.

custom_subdomains:
  - subdomain: myapp              # Add your subdomain here
    is_external: false            # false: internal; true: external/partner

πŸ’‘ Tip: If other subdomains already exist, add yours to the list:

custom_subdomains:
  - subdomain: existingapp
    is_external: false
  - subdomain: myapp            # Your new subdomain
    is_external: false

Step 3: Commit and push your changes, then run the Azure DevOps pipeline to deploy.

This creates Front Door endpoints for each subdomain (e.g., myapp-test.saif.com, myapp-uat.saif.com, etc.)

⚠️ DNS Configuration Required: DNS is not currently automated. Submit a ServiceNOW request to the Cloud Services team with the following information:

Request Template:

Subject: DNS Setup for [SUBDOMAIN]

Setup DNS on Route53 and Internal DNS.

CNAME Record:
  [subdomain]-[environment].saif.com -> [frontend-endpoint-fqdn]

Domain Validation (TXT Record):
  Name: _dnsauth.[subdomain]-[environment].saif.com
  Value: [validation-token-from-front-door]

The Front Door endpoint FQDN and validation token are available in the Azure Portal under Front Door > Custom domains after adding the custom domain.

⏱️ Propagation Times:

  • Initial domain validation: up to 24 hours
  • Subsequent Front Door changes: up to 45 minutes

2. Create Your Frontend Application

Create a new frontend application project using the SAIF CLI:

saif new saif-frontend-service

πŸ”§ Command Options

You can provide all parameters upfront to skip the interactive prompts:

saif new saif-frontend-service --application_name <app-name> --business_domain <domain> --owner <owner-name>

Parameters:

  • --application_name: Your application name (used in project and subdomain naming)
  • --business_domain: The business domain (e.g., it, claims, hr)
  • --owner: The team or individual responsible for the application

πŸ’‘ Example

saif new saif-frontend-service --application_name myapp --business_domain it --owner Platform

This creates a frontend project with:

  • Application Name: myapp
  • Business Domain: it
  • Owner: Platform
  • Project ID: it-web-myapp (auto-generated from {business_domain}-web-{application_name})

βœ… Success Indicators

After running the command, you should see:

  • βœ… New project folder created with your app name
  • βœ… Docker configuration included for nginx
  • βœ… Terraform infrastructure files ready to configure
  • βœ… Frontend application scaffolding complete
  • βœ… No error messages in the terminal

🎯 What Happens Next?

  1. Navigate into your new project directory: cd <your-project-name>
  2. Verify the project structure matches your expectations
  3. Proceed to Step 3 to update infrastructure variables for custom subdomain support
πŸ“ Click to see detailed project structure
your-project/
β”œβ”€β”€ .azdo/                              # Azure Pipelines configuration
β”‚   β”œβ”€β”€ azure-pipelines.yml             # Main deployment pipeline
β”‚   β”œβ”€β”€ azure-pipelines-pr.yml          # Pull request validation pipeline
β”‚   └── vars/                           # Pipeline variables
β”œβ”€β”€ infra/                              # Infrastructure as Code
β”‚   β”œβ”€β”€ bootstrap/                      # Environment bootstrapping
β”‚   └── web/                            # Frontend infrastructure
β”‚       β”œβ”€β”€ main.tf                     # Terraform configuration
β”‚       β”œβ”€β”€ vars.yml                    # Infrastructure variables
β”‚       └── settings.yml                # Deployment settings
β”œβ”€β”€ src/                                # Source code
β”‚   β”œβ”€β”€ [AppName].AppHost/              # .NET Aspire orchestration
β”‚   └── [AppName].Frontend/             # React frontend application
β”‚       β”œβ”€β”€ src/                        # React components and pages
β”‚       β”œβ”€β”€ nginx.conf                  # nginx configuration for routing
β”‚       β”œβ”€β”€ Dockerfile                  # Container configuration
β”‚       └── vite.config.ts              # Vite build configuration
└── .gitignore
**Key Components:** - **πŸ”„ `.azdo`**: Azure Pipelines for CI/CD - **πŸ—οΈ `infra`**: Terraform configurations for App Service, Front Door, and DNS - **πŸ’» `src`**: .NET Aspire host and React frontend application

3. Update Infrastructure Variables in Your Project

Add the custom subdomain settings to your web infrastructure variables:

infra/web/vars.yml
owner: Platform
project_id: myapp
application_name: myapp
custom_subdomain: myapp              # Must match subdomain in azure.terraform
is_root_path: false                  # true: root path; false: /appname path

Path options:

  • is_root_path: true: Custom subdomain at root (myapp-test.saif.com), shared endpoint at app-int-test.saif.com/myapp
  • is_root_path: false (default): Both endpoints use /myapp path (e.g., myapp-test.saif.com/myapp + app-int-test.saif.com/myapp)

The subpath is automatically set to the application nameβ€”this enforces a consistent pattern and prevents routing conflicts.

These variables are passed to the saif-web-service Terraform module. In your main.tf, ensure the module references these variables:

infra/web/main.tf
module "saif-webapp" {
  source  = "app.terraform.io/SAIFCorp/web-service/saif"
  version = ">= 3.0.0, < 4.0.0"

  # ... existing configuration ...

  custom_subdomain = lookup(local.variables, "custom_subdomain", "")
  is_root_path     = lookup(local.variables, "is_root_path", false)
}

4. Verify nginx Configuration

The nginx configuration is included by default in templates with routing pre-configured for is_root_path: false (the default):

nginx.conf
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html/myapp;
    index index.html;

    # Always needed: Shared endpoint (app-int-{env}.saif.com/myapp)
    location /myapp/ {
        rewrite ^/myapp(/.*)$ $1 break;
        try_files $uri /index.html =404;
    }

    # Handles URLs without trailing slash (e.g., myapp-test.saif.com/myapp)
    # Required for custom subdomain with is_root_path: false (default)
    location = /myapp {
        try_files /index.html =404;
    }

    # Uncomment when is_root_path: true in infra/web/vars.yml
    # Also comment out the location = block above when using root path
    # location / {
    #     try_files $uri $uri/ /index.html;
    # }
}

5. Verify Vite Configuration

Your vite.config.ts should already use an absolute base path:

vite.config.ts
export default defineConfig({
  base: '/myapp/',
  // ... rest of config
});

6. Verify React Router Configuration

The template includes a dynamic getBasename() function that works for both is_root_path settings:

App.tsx
import { BrowserRouter } from "react-router";

// Base path for routing - matches application_name in infra/web/vars.yml
// This value is set during template instantiation and matches the Vite base path
const APP_BASE_PATH = '/myapp';

// Detect base path at runtime to support both shared endpoint and custom domain
const getBasename = () => {
  if (typeof window !== "undefined" && window.location.pathname.startsWith(APP_BASE_PATH)) {
    return APP_BASE_PATH;
  }
  return "/";
};

const App = () => {
  return (
    <BrowserRouter basename={getBasename()}>
      {/* ... */}
    </BrowserRouter>
  );
};

This pattern:

  • Returns /myapp when accessed via subpath (shared endpoint, or custom subdomain with is_root_path: false)
  • Returns / when accessed at root (custom subdomain with is_root_path: true)
πŸ’‘ Alternative: Static basename for is_root_path: false If you're using `is_root_path: false` (default), both endpoints use the same subpath, so a static basename also works:
App.tsx
const App = () => {
  return (
    <BrowserRouter basename="/myapp">
      {/* ... */}
    </BrowserRouter>
  );
};
This simpler approach is valid only when `is_root_path: false`.

πŸ”§ How It Works

When using is_root_path: false (default), both endpoints use the same subpath:

Endpoint URL Pattern nginx Location React Router Base
Shared app-int-{env}.saif.com/myapp /myapp/ (rewrite) /myapp
Shared app-int-{env}.saif.com/myapp/ /myapp/ (rewrite) /myapp
Custom myapp-{env}.saif.com/myapp = /myapp (exact) /myapp
Custom myapp-{env}.saif.com/myapp/ /myapp/ (rewrite) /myapp

When using is_root_path: true, the custom subdomain serves from root:

Endpoint URL Pattern nginx Location React Router Base
Shared app-int-{env}.saif.com/myapp /myapp/ (rewrite) /myapp
Shared app-int-{env}.saif.com/myapp/ /myapp/ (rewrite) /myapp
Custom myapp-{env}.saif.com / (root) /
Custom myapp-{env}.saif.com/about / (root) /

Asset loading:

Both endpoints load assets from /myapp/assets/... because:

  1. Vite builds assets with absolute paths (/myapp/assets/main.js)
  2. On the custom subdomain, requests to /myapp/* are served by nginx from the same files
  3. On the shared endpoint, Front Door routes /myapp/* to the container

βœ… Testing

After deployment, verify both endpoints work correctly.

⏱️ Note: Deploying your application attaches Front Door routes to the custom subdomain. Changes can take up to 45 minutes to propagate.

If using is_root_path: false (default):

# Both endpoints use the same /myapp subpath
curl -I https://app-int-test.saif.com/myapp
curl -I https://app-int-test.saif.com/myapp/
curl -I https://myapp-test.saif.com/myapp
curl -I https://myapp-test.saif.com/myapp/

# SPA routes (with trailing slash)
curl -I https://app-int-test.saif.com/myapp/about
curl -I https://myapp-test.saif.com/myapp/about

If using is_root_path: true:

# Shared endpoint uses /myapp subpath
curl -I https://app-int-test.saif.com/myapp
curl -I https://app-int-test.saif.com/myapp/about

# Custom subdomain serves from root
curl -I https://myapp-test.saif.com
curl -I https://myapp-test.saif.com/about

πŸš€ Advanced Scenarios

Multiple Applications on One Subdomain

A single custom subdomain can host multiple applications. Each application attaches to the subdomain at its own subpath:

Application A: infra/web/vars.yml
application_name: dashboard
custom_subdomain: myportal
is_root_path: false    # Routes to myportal-test.saif.com/dashboard
Application B: infra/web/vars.yml
application_name: settings
custom_subdomain: myportal
is_root_path: false    # Routes to myportal-test.saif.com/settings

⚠️ Important: Only one application per subdomain can use is_root_path: true. All other applications on the same subdomain must use is_root_path: false.


πŸ” Troubleshooting

Blank page on custom subdomain

Symptom: Custom subdomain shows blank page, browser console shows 404 for assets.

Cause: nginx missing location / block or assets not being served.

Solution: If using is_root_path: true, ensure the location / block is uncommented in nginx.conf.

URLs without trailing slash return 404

Symptom: myapp-test.saif.com/myapp returns 404, but myapp-test.saif.com/myapp/ works.

Cause: The location /myapp/ block only matches paths with a trailing slash. nginx needs an exact match block for the subpath without trailing slash.

Solution: The template includes the location = /myapp block by default. If you're seeing this issue, verify the block exists and is not commented out:

# Handles URLs without trailing slash
location = /myapp {
    try_files /index.html =404;
}

πŸ’‘ Note: If you're using is_root_path: true, comment out this block and uncomment the location / block instead.

SPA routes return 404

Symptom: Direct navigation to /about returns 404.

Cause: nginx not falling back to index.html for SPA routes.

Solution: Verify try_files directive includes /index.html fallback.

Assets load from wrong path

Symptom: Assets requested from /assets/... instead of /myapp/assets/....

Cause: Vite base path configured as relative (./) instead of absolute.

Solution: Use absolute base path: base: '/myapp/'