# 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}.saif.com                 // root app URL, production
https://{subdomain}.saif.com/{appname}       // non-root app URL, production
https://{subdomain}.{env}.saif.com           // root app URL, non-production (test, qa, uat)
https://{subdomain}.{env}.saif.com/{appname} // non-root app URL, non-production
```

> ⚠️ **Note the separator:** non-production hostnames use a **dot** before the environment (`myapp.test.saif.com`), not a hyphen. Production hostnames omit the environment entirely (`myapp.saif.com`).

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 cloud-foundations

Custom subdomains are declared centrally in [`infra/teams/azure/subdomains.tfvars`](https://github.com/saif-corp/cloud-foundations/blob/main/infra/teams/azure/subdomains.tfvars) in the shared `saif-corp/cloud-foundations` repository (GitHub)—not in a per-team repository.

**Step 1:** Fork `saif-corp/cloud-foundations` (unless you already have write access), then clone your fork:

```bash
git clone https://github.com/{your-org-or-username}/cloud-foundations.git
cd cloud-foundations
```

> 💡 **Tip:** If you already have write access to `saif-corp/cloud-foundations`, you can clone it directly and skip forking:
>
> ```bash
> git clone https://github.com/saif-corp/cloud-foundations.git
> cd cloud-foundations
> ```

**Step 2:** Open `infra/teams/azure/subdomains.tfvars` and add an entry to the `custom_subdomains` list:

```hcl title="cloud-foundations/infra/teams/azure/subdomains.tfvars"
# Custom subdomains for team services (e.g. mosaic.saif.com).
#
# NOTE: adding an entry here creates the Front Door endpoint, WAF policy, and the
# PUBLIC Route53 DNS records only. It does NOT create the record in the internal
# (corp) saif.com zone, so the subdomain will not resolve on the SAIF network or
# VPN until that record is requested from the infrastructure team.
custom_subdomains = [
  {
    team      = "Customer"        # Existing entry
    subdomain = "mosaic"
  },
  {
    team      = "MyTeam"          # Add your entry here
    subdomain = "myapp"
  }
]
```

**Fields:**

| Field                  | Required | Description                                                                                  |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------- |
| `team`                 | yes      | Owning team. Must match the team's `owner` value—entries are filtered per team at plan time. |
| `subdomain`            | yes      | Lowercase alphanumeric with hyphens, 1–63 chars. Cannot start or end with a hyphen.             |
| `internal_access_only` | no       | Defaults to **`true`**. Restricts the Front Door WAF policy to SAIF traffic only.               |

**Step 3:** Open a PR against `saif-corp/cloud-foundations` (from your fork, or directly if you have write access). It's owned by `@saif-corp/platform-engineers` per `CODEOWNERS`, so they'll review it.

> 💡 **Prefer not to touch Terraform yourself?** Open an issue on `saif-corp/cloud-foundations` requesting the entry be added, and a platform engineer will make the change for you.

Once the change is reviewed and merged, the team's pipeline applies it and provisions the Front Door endpoint for each environment.

This creates hostnames like `myapp.test.saif.com` and `myapp.uat.saif.com` in non-production environments, and `myapp.saif.com` (no environment) in production.

> ✅ **Public DNS is automated.** Terraform (via the `dns_domain_validation` module) creates the public Route53 `CNAME` and `_dnsauth` `TXT` domain-validation records automatically. You do **not** need to file a request for public DNS.

> ⚠️ **Internal DNS is still manual.** The internal `saif.com` zone is served by the AD-integrated corp domain controllers and managed outside `cloud-foundations`. Because corp DNS is authoritative for `saif.com`, it returns `NXDOMAIN`—not a fallback to public DNS—for names it doesn't hold. Your subdomain will not resolve on the SAIF office network or VPN until someone adds the internal record.
>
> Raise a ServiceNow request to the infrastructure team that administers the corp domain controllers, asking for a `CNAME` in the internal `saif.com` zone pointing at the Front Door endpoint hostname (find the exact value in the `internal_dns_records_required` output on the team's Terraform Cloud workspace). Ask for a **`CNAME`, not an `A` record**—Front Door edge IPs rotate. No internal `_dnsauth` `TXT` record is needed; ownership is already validated via the public zone.
>
> See [`docs/guides/custom-subdomains.md`](https://github.com/saif-corp/cloud-foundations/blob/main/docs/guides/custom-subdomains.md) in `cloud-foundations` for the full request process and a verification checklist.
>
> ⏱️ **Propagation Times:**
>
> - Internal DNS record creation: depends on the infrastructure team's turnaround
> - Front Door changes: up to 45 minutes

### 2. Create Your Frontend Application

Create a new frontend application project using the SAIF CLI:

```bash
saif new saif-frontend-service
```

### 🔧 Command Options

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

```bash
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

```bash
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

<details>
<summary>📁 Click to see detailed project structure</summary>

```
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

</details>

### 3. Update Infrastructure Variables in Your Project

Add the custom subdomain settings to your web infrastructure variables:

```yaml title="infra/web/vars.yml"
owner: Platform
project_id: myapp
application_name: myapp
custom_subdomain: myapp              # Must match the subdomain entry in cloud-foundations
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:

```hcl title="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 title="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:

```typescript title="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:

```tsx title="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`)

<details>
<summary>💡 Alternative: Static basename for <code>is_root_path: false</code></summary>

If you're using `is_root_path: false` (default), both endpoints use the same subpath, so a static basename also works:

```tsx title="App.tsx"
const App = () => {
  return (
    <BrowserRouter basename="/myapp">
      {/* ... */}
    </BrowserRouter>
  );
};
```

This simpler approach is valid only when `is_root_path: false`.

</details>

---

## 🔧 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):**

```bash
# 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`:**

```bash
# 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:

```yaml title="Application A: infra/web/vars.yml"
application_name: dashboard
custom_subdomain: myportal
is_root_path: false    # Routes to myportal.test.saif.com/dashboard
```

```yaml title="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:

```nginx
# 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/'`
