Custom Subdomains¶
Configure custom subdomains for frontend applications on Azure Front Door.
π Overview¶
By default, frontend applications are accessible via the shared organizational endpoint:
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):
π‘ 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:
# 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:
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:
π§ 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¶
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?¶
- Navigate into your new project directory:
cd <your-project-name> - Verify the project structure matches your expectations
- 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
3. Update Infrastructure Variables in Your Project¶
Add the custom subdomain settings to your web infrastructure variables:
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 atapp-int-test.saif.com/myappis_root_path: false(default): Both endpoints use/myapppath (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:
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):
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:
6. Verify React Router Configuration¶
The template includes a dynamic getBasename() function that works for both is_root_path settings:
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
/myappwhen accessed via subpath (shared endpoint, or custom subdomain withis_root_path: false) - Returns
/when accessed at root (custom subdomain withis_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:
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:
- Vite builds assets with absolute paths (
/myapp/assets/main.js) - On the custom subdomain, requests to
/myapp/*are served by nginx from the same files - 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_name: dashboard
custom_subdomain: myportal
is_root_path: false # Routes to myportal-test.saif.com/dashboard
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 useis_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:
π‘ Note: If you're using
is_root_path: true, comment out this block and uncomment thelocation /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/'