Skip to content

Frontend (Azure Static Web Apps)

The frontend is deployed to Azure Static Web Apps via GitHub Actions. Separate instances run for ACC and PROD environments.


Architecture

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   GitHub Repository (ronl-business-api)  โ”‚
โ”‚                                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚  packages/frontend/                 โ”‚ โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ src/                           โ”‚ โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ public/                        โ”‚ โ”‚
โ”‚  โ”‚  โ”‚   โ”œโ”€โ”€ tenants.json               โ”‚ โ”‚
โ”‚  โ”‚  โ”‚   โ””โ”€โ”€ staticwebapp.config.json   โ”‚ โ”‚ โ† SPA routing config
โ”‚  โ”‚  โ”œโ”€โ”€ vite.config.ts                 โ”‚ โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ package.json                   โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ”‚                 โ”‚                        โ”‚
โ”‚                 โ”‚ Push to 'acc' branch   โ”‚
โ”‚                 โ–ผ                        โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
โ”‚  โ”‚  .github/workflows/                 โ”‚ โ”‚
โ”‚  โ”‚  โ”œโ”€โ”€ azure-frontend-acc.yml         โ”‚ โ”‚
โ”‚  โ”‚  โ””โ”€โ”€ azure-frontend-prod.yml        โ”‚ โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                 โ”‚
                 โ”‚ GitHub Actions
                 โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚      Azure Static Web Apps              โ”‚
โ”‚                                         โ”‚
โ”‚  ACC:  acc.mijn.open-regels.nl          โ”‚
โ”‚  PROD: mijn.open-regels.nl              โ”‚
โ”‚                                         โ”‚
โ”‚  โœ… Global CDN                          โ”‚
โ”‚  โœ… Automatic SSL                       โ”‚
โ”‚  โœ… Custom domains                      โ”‚
โ”‚  โœ… SPA fallback routing                โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

GitHub Actions Workflows

ACC Workflow

File: .github/workflows/azure-frontend-acc.yml

name: Deploy Frontend to ACC

on:
  push:
    branches:
      - acc
    paths:
      - 'packages/frontend/**'
      - 'packages/shared/**'
      - 'packages/pa-cockpit/**'
      - '.github/workflows/azure-frontend-acc.yml'
      - '.nvmrc'

jobs:
  build_and_deploy:
    runs-on: ubuntu-24.04
    name: Build and Deploy to ACC
    steps:
      - uses: actions/checkout@v3
        with:
          submodules: true

      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version-file: .nvmrc
          cache: 'npm'

      - name: Install dependencies
        run: |
          npm ci
          npm ci --workspace=packages/frontend

      - name: Build frontend
        run: npm run build --workspace=packages/frontend
        env:
          VITE_KEYCLOAK_URL: ${{ secrets.ACC_KEYCLOAK_URL }}
          VITE_API_URL: ${{ secrets.ACC_API_URL }}

      - name: Deploy to Azure Static Web Apps
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.ACC_AZURE_SWA_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: 'upload'
          app_location: 'packages/frontend'
          output_location: 'dist'
          skip_app_build: true

Triggers: - Push to acc branch - Changes in packages/frontend/** - Changes to workflow file itself

PROD Workflow

File: .github/workflows/azure-frontend-prod.yml

Same build structure as ACC, but it is not triggered by a branch at all:

  • Triggers: workflow_call and workflow_dispatch only. A push to main starts promote-to-production.yml, which calls this workflow once the backend deploy has succeeded or been skipped โ€” see How a promotion reaches production.
  • Secrets: AZURE_STATIC_WEB_APPS_API_TOKEN_PROD, passed explicitly by the promotion (never secrets: inherit), plus GITHUB_TOKEN.
  • Build provenance: VITE_BUILD_SHA and VITE_BUILD_RUN are injected on the build step, which is what the running application prints as build <sha> ยท #<run>. A called workflow inherits the caller's run number, so that # is the promotion's run number, not a per-app one.
  • No approval gate. The production environment carries no protection rule; nothing pauses waiting for a reviewer.

SPA Routing Configuration

Critical for React Router: Azure Static Web Apps needs staticwebapp.config.json to handle client-side routing.

File: packages/frontend/public/staticwebapp.config.json

{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/assets/*", "/*.js", "/*.css", "/*.json", "/*.png", "/*.jpg", "/*.svg"]
  },
  "globalHeaders": {
    "cache-control": "no-cache, no-store, must-revalidate"
  },
  "mimeTypes": {
    ".json": "application/json",
    ".css": "text/css",
    ".js": "application/javascript"
  }
}

What it does:

  • โœ… All routes (/, /auth, /dashboard) โ†’ served by index.html
  • โœ… React Router handles routing client-side
  • โœ… Static assets excluded from fallback
  • โœ… Proper cache headers
  • โœ… Correct MIME types

Without this file: Routes like /auth and /dashboard return 404 errors after Keycloak redirect.


Build and Deployment Steps

Step 1 โ€” Trigger

Push changes to acc branch:

cd ~/Development/ronl-business-api

# Make frontend changes
nano packages/frontend/src/pages/Dashboard.tsx

# Commit and push
git add packages/frontend/
git commit -m "feat: update dashboard styling"
git push origin acc

Step 2 โ€” GitHub Actions Build

The workflow automatically:

  1. Checks out code
  2. Sets up Node.js from .nvmrc (22.23.2)
  3. Installs dependencies (monorepo aware)
  4. Builds frontend with environment variables
  5. Outputs to packages/frontend/dist/

Build artifacts:

dist/
โ”œโ”€โ”€ index.html                 # Entry point
โ”œโ”€โ”€ assets/
โ”‚   โ”œโ”€โ”€ index-[hash].js        # Main bundle
โ”‚   โ”œโ”€โ”€ index-[hash].css       # Styles
โ”‚   โ””โ”€โ”€ [other assets]
โ”œโ”€โ”€ tenants.json               # Municipality config (copied from public/)
โ””โ”€โ”€ staticwebapp.config.json   # SPA routing (copied from public/)

Step 3 โ€” Azure Deployment

The Azure/static-web-apps-deploy@v1 action:

  1. Authenticates with Azure using API token
  2. Uploads dist/ folder to Azure Storage
  3. Invalidates CDN cache
  4. Updates routing rules from staticwebapp.config.json
  5. Makes new version live

Deployment time: ~2-3 minutes

Step 4 โ€” Verification

# Check deployment status
# GitHub Actions โ†’ Workflows โ†’ Deploy Frontend to ACC

# Test URLs
curl -I https://acc.mijn.open-regels.nl
# Should return: 200 OK

curl -I https://acc.mijn.open-regels.nl/auth
# Should return: 200 OK (not 404!)

# Test in browser
# Visit: https://acc.mijn.open-regels.nl
# Click "Inloggen met DigiD"
# After Keycloak auth, /auth route should work
# Dashboard should load at /dashboard

Environment Files

Environment variables are configured as GitHub Secrets:

ACC Secrets

ACC_KEYCLOAK_URL=https://acc.keycloak.open-regels.nl
ACC_API_URL=https://acc.api.open-regels.nl/v1
ACC_AZURE_SWA_TOKEN=<deployment-token>

PROD Secrets

PROD_KEYCLOAK_URL=https://keycloak.open-regels.nl
PROD_API_URL=https://api.open-regels.nl/v1
PROD_AZURE_SWA_TOKEN=<deployment-token>

Adding/Updating Secrets

  1. GitHub Repository โ†’ Settings โ†’ Secrets and variables โ†’ Actions
  2. Click New repository secret
  3. Name: ACC_KEYCLOAK_URL
  4. Value: https://acc.keycloak.open-regels.nl
  5. Click Add secret

Azure Static Web App Configuration

Resource Details

ACC:

  • Name: ronl-frontend-acc
  • Resource Group: rg-ronl-acc
  • Region: West Europe
  • Custom Domain: acc.mijn.open-regels.nl
  • Pricing: Free tier (sufficient for this application)

PROD:

  • Name: ronl-frontend-prod
  • Resource Group: rg-ronl-prod
  • Region: West Europe
  • Custom Domain: mijn.open-regels.nl
  • Pricing: Standard tier (for production SLA)

Deployment Token

Retrieve token from Azure Portal:

  1. Navigate to Static Web App resource
  2. Left menu โ†’ Deployment tokens
  3. Copy Deployment token
  4. Store in GitHub Secrets as ACC_AZURE_SWA_TOKEN

Token permissions:

  • Upload build artifacts
  • Update routing configuration
  • Invalidate CDN cache

Custom Domain Setup

ACC domain (acc.mijn.open-regels.nl):

  1. Azure Portal โ†’ Static Web App โ†’ Custom domains
  2. Click Add
  3. Enter: acc.mijn.open-regels.nl
  4. Choose: Other DNS
  5. Add DNS records:
    Type: CNAME
    Name: acc.mijn
    Value: <generated-url>.azurestaticapps.net
    
  6. Verify and add

SSL certificate: Automatically provisioned by Azure (Let's Encrypt)


Post-Deployment Verification

Automated Tests (Future)

# Add to workflow after deployment
- name: Test deployed app
  run: |
    # Test root route
    curl -f https://acc.mijn.open-regels.nl

    # Test SPA routes
    curl -f https://acc.mijn.open-regels.nl/auth
    curl -f https://acc.mijn.open-regels.nl/dashboard

    # Test static assets
    curl -f https://acc.mijn.open-regels.nl/tenants.json

Manual Testing Checklist

  • Landing page loads (https://acc.mijn.open-regels.nl)
  • Three IDP buttons visible and styled correctly
  • Changelog panel opens/closes
  • DigiD button โ†’ redirects to Keycloak
  • After auth โ†’ /auth route works (no 404)
  • Dashboard loads at /dashboard
  • Municipality theme applied correctly
  • Calculator form submits
  • Mobile responsive (test on phone)
  • No console errors in browser DevTools

Manual Deployment

If GitHub Actions fails or for emergency hotfix:

Step 1 โ€” Build locally

cd ~/Development/ronl-business-api/packages/frontend

# Set environment variables
export VITE_KEYCLOAK_URL=https://acc.keycloak.open-regels.nl
export VITE_API_URL=https://acc.api.open-regels.nl/v1

# Build
npm run build

# Output: dist/ directory
ls -la dist/

Step 2 โ€” Deploy via Azure CLI

# Install Azure CLI (if not installed)
curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

# Login to Azure
az login

# Deploy to ACC
az staticwebapp deploy \
  --name ronl-frontend-acc \
  --resource-group rg-ronl-acc \
  --source ./dist \
  --no-use-keychain

# Wait for deployment (~2-3 minutes)

Step 3 โ€” Verify

# Test deployment
curl -I https://acc.mijn.open-regels.nl
# Should return: 200 OK

# Open in browser
# Visit: https://acc.mijn.open-regels.nl

Keycloak Redirect URIs

Critical: Keycloak client must allow redirects from deployed URLs.

ACC Configuration

Keycloak Admin Console:

  1. Realm: ronl
  2. Clients โ†’ ronl-business-api
  3. Valid Redirect URIs: https://acc.mijn.open-regels.nl/*
  4. Valid Post Logout Redirect URIs: + (inherits from redirect URIs)
  5. Web Origins: https://acc.mijn.open-regels.nl

Screenshot: Keycloak Client Redirect Configuration

Kecloak Admin Console showing redirects

PROD Configuration

Same as ACC but with:

  • Valid Redirect URIs: https://mijn.open-regels.nl/*
  • Web Origins: https://mijn.open-regels.nl

Rollback Procedure

If deployment causes issues:

Option 1 โ€” Revert via GitHub

# Find last working commit
git log --oneline packages/frontend/

# Revert to that commit
git revert <commit-hash>

# Push to trigger new deployment
git push origin acc

Option 2 โ€” Rollback in Azure Portal

  1. Azure Portal โ†’ Static Web App
  2. Left menu โ†’ Environments
  3. Select previous deployment
  4. Click Promote

Troubleshooting

404 on routes after deployment

Symptoms: Landing page works, but /auth and /dashboard return 404.

Cause: staticwebapp.config.json missing or not deployed.

Solution:

# Verify file exists in source
ls -la packages/frontend/public/staticwebapp.config.json

# File must be in public/ to be copied to dist/ during build

# If missing, create it:
cat > packages/frontend/public/staticwebapp.config.json << 'EOF'
{
  "navigationFallback": {
    "rewrite": "/index.html",
    "exclude": ["/assets/*", "/*.js", "/*.css", "/*.json"]
  }
}
EOF

# Commit and redeploy
git add packages/frontend/public/staticwebapp.config.json
git commit -m "fix: add SPA routing configuration"
git push origin acc

Environment variables not applied

Symptoms: App loads but can't connect to Keycloak or API.

Cause: Build-time environment variables not set.

Solution:

# Check GitHub Secrets are configured
# Repository โ†’ Settings โ†’ Secrets and variables โ†’ Actions

# Required secrets:
# - ACC_KEYCLOAK_URL
# - ACC_API_URL

# Verify workflow uses them:
# .github/workflows/azure-frontend-acc.yml
env:
  VITE_KEYCLOAK_URL: ${{ secrets.ACC_KEYCLOAK_URL }}
  VITE_API_URL: ${{ secrets.ACC_API_URL }}

# Re-run workflow after fixing

CORS errors in browser console

Symptoms: Browser shows CORS policy errors when calling API.

Cause: Keycloak or API not configured for frontend origin.

Solution:

# Keycloak: Web Origins should be '+'
# This inherits all Valid Redirect URIs as allowed origins

# API: Backend must allow frontend origin in CORS config
# packages/backend/src/index.ts
app.use(cors({
  origin: [
    'https://acc.mijn.open-regels.nl',
    'https://mijn.open-regels.nl'
  ],
  credentials: true
}));

Deployment takes too long or fails

Symptoms: GitHub Actions workflow runs for 10+ minutes or fails.

Cause: Azure Static Web Apps service issues or incorrect token.

Solution:

# Check Azure service status
# Visit: https://status.azure.com/

# Verify deployment token is valid
# Azure Portal โ†’ Static Web App โ†’ Deployment tokens
# Regenerate if needed and update GitHub Secret

# Check workflow logs for specific error
# GitHub Actions โ†’ Failed workflow โ†’ View logs

# Common errors:
# - "401 Unauthorized" โ†’ Token invalid, regenerate
# - "429 Too Many Requests" โ†’ Wait and retry later

Build succeeds but app doesn't update

Symptoms: Deployment completes successfully but changes not visible.

Cause: Browser cache or CDN cache.

Solution:

# Hard refresh browser
# Chrome: Ctrl+Shift+R (Windows/Linux) or Cmd+Shift+R (Mac)
# Firefox: Ctrl+F5 (Windows/Linux) or Cmd+Shift+R (Mac)

# Check deployed version
# View source: https://acc.mijn.open-regels.nl
# Look for timestamp or version comments

# CDN cache usually invalidates within 5 minutes
# Wait and try again if issue persists

Performance Optimization

Bundle Size

Current build stats (~production):

  • Total bundle: ~250 KB gzipped
  • index.js: ~200 KB (React, React Router, Keycloak, Axios)
  • index.css: ~50 KB (Tailwind CSS)

Optimization opportunities:

  • โœ… Vite code splitting (already enabled)
  • โœ… Tree shaking (already enabled)
  • โš ๏ธ Consider lazy loading routes for larger apps
  • โš ๏ธ Consider removing unused Tailwind classes

CDN Performance

Azure Static Web Apps provides:

  • โœ… Global CDN distribution
  • โœ… Automatic gzip/brotli compression
  • โœ… HTTP/2 support
  • โœ… Edge caching (configured via staticwebapp.config.json)

Cache headers:

{
  "globalHeaders": {
    "cache-control": "no-cache, no-store, must-revalidate"
  }
}

Currently set to no-cache for development. For production, consider:

{
  "routes": [
    {
      "route": "/assets/*",
      "headers": {
        "cache-control": "public, max-age=31536000, immutable"
      }
    }
  ]
}



Questions or issues? See Troubleshooting or contact the DevOps team.