A full-stack dashboard application deployed to Azure Static Web Apps. The frontend is a React SPA authenticated via Microsoft Entra ID (PKCE). The backend is a C# Azure Functions API that uses the On-Behalf-Of (OBO) flow to execute SQL queries under the calling user's identity.
Browser (React + MSAL)
│
│ PKCE login -> acquires access token scoped to the Backend API
│
▼
Azure Static Web Apps
├── Static files (React build output)
└── /api/* -> Azure Functions (C# .NET 8 Isolated Worker)
│
│ OBO flow: exchanges user's API token
│ for a SQL token (database.windows.net)
│
▼
Azure SQL Database
(permissions enforced per-user by SQL RBAC)
Install these before starting local development:
- .NET 8 SDK
- Node.js 20+
- Azure Functions Core Tools v4
- Azure Static Web Apps CLI:
npm install -g @azure/static-web-apps-cli - Azure CLI (optional, for managing Azure resources)
You also need these Azure resources already provisioned:
- An Azure SQL Database
- Two Microsoft Entra ID App Registrations (Frontend SPA and Backend API)
- An Azure Static Web Apps resource (for deployment; not required for local dev)
- Go to Azure Portal > Microsoft Entra ID > App registrations > New registration.
- Name it something like
Dashboard-API. - Set Supported account types to "Accounts in this organizational directory only."
- No redirect URI is needed for the backend.
- After creation, note the Application (client) ID. This is your
BACKEND_CLIENT_ID.
Expose an API:
- Go to Expose an API.
- Set the Application ID URI to
api://<BACKEND_CLIENT_ID>. - Add a scope:
- Scope name:
access_as_user - Who can consent: Admins and users
- Admin consent display name: "Access Dashboard API as user"
- Admin consent description: "Allows the SPA to call the Dashboard API on behalf of the signed-in user."
- State: Enabled
- Scope name:
Certificates & secrets:
- Go to Certificates & secrets > New client secret.
- Copy the secret value. This is your
BACKEND_CLIENT_SECRET.
API permissions:
- Go to API permissions > Add a permission > APIs my organization uses.
- Search for "Azure SQL Database" or use the well-known scope:
https://database.windows.net/user_impersonation. - Select Delegated permissions > user_impersonation.
- Click Grant admin consent for your tenant.
- Create another app registration, e.g.
Dashboard-SPA. - Set Supported account types to "Accounts in this organizational directory only."
- Under Authentication > Platform configurations > Add a platform > Single-page application.
- Add redirect URIs:
http://localhost:4280(local SWA CLI)https://<your-swa-hostname>.azurestaticapps.net(production)
- Note the Application (client) ID. This is your
FRONTEND_CLIENT_ID.
API permissions:
- Go to API permissions > Add a permission > My APIs.
- Select the Backend API registration.
- Check the
access_as_userscope. - Click Grant admin consent for your tenant.
- In the Azure Portal, go to your SQL Server (not the database, the server).
- Under Settings > Microsoft Entra admin, set an Entra admin user or group.
Connect to your database as the Entra admin (e.g., via Azure Data Studio or SSMS) and run the setup.sql script. Edit it first to replace the placeholder user/group names with your actual Entra identities.
# Example using sqlcmd with Entra auth:
sqlcmd -S yourserver.database.windows.net -d yourdb --authentication-method=ActiveDirectoryInteractive -i setup.sqlCopy the example settings file and fill in your values:
cp api/local.settings.json.example api/local.settings.jsonEdit api/local.settings.json with your actual Tenant ID, Backend Client ID, Backend Client Secret, and SQL connection string.
The SQL connection string must not contain a username or password. Example:
Server=tcp:yourserver.database.windows.net,1433;Database=yourdb;Encrypt=True;TrustServerCertificate=False;
Copy the example env file and fill in your values:
cp client/.env.example client/.envEdit client/.env with your Tenant ID, Frontend Client ID, and Backend Client ID.
# Frontend
cd client
npm install
# Backend
cd ../api
dotnet restoreFrom the project root:
swa start client/dist --api-location api --run "cd client && npm run dev" --api-devserver-url http://localhost:7071Or run the pieces separately in different terminals:
Terminal 1: Frontend dev server
cd client
npm run devTerminal 2: Azure Functions
cd api
func startTerminal 3: SWA CLI proxy
swa start http://localhost:3000 --api-devserver-url http://localhost:7071The SWA CLI serves on http://localhost:4280 by default. Use that URL in your browser (it must match your SPA redirect URI).
├── api/ C# Azure Functions backend
│ ├── Api.csproj Project file with dependencies
│ ├── Program.cs Host builder and DI setup
│ ├── host.json Functions host configuration
│ ├── local.settings.json.example Template for local secrets
│ ├── Functions/
│ │ ├── RecordsFunction.cs POST /api/records (Table A insert)
│ │ ├── HistoryFunction.cs GET /api/history (Table B read)
│ │ └── MetricsFunction.cs GET /api/metrics (Table C aggregation)
│ └── Services/
│ └── DatabaseService.cs OBO token exchange + SQL connection
├── client/ React SPA frontend
│ ├── package.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ ├── index.html
│ ├── .env.example Template for frontend env vars
│ └── src/
│ ├── main.tsx Entry point, MSAL provider
│ ├── App.tsx Auth gate + tab navigation
│ ├── App.css Styles
│ ├── authConfig.ts MSAL configuration
│ ├── components/
│ │ ├── Dashboard.tsx Metrics KPI cards + charts (auto-refresh)
│ │ ├── RecordForm.tsx Form to insert into Table A
│ │ └── HistoryTable.tsx Table showing Table B rows
│ └── services/
│ └── apiClient.ts Token acquisition + API calls
├── staticwebapp.config.json SWA routing and auth config
├── setup.sql SQL DDL + user/permission grants
├── .github/
│ └── workflows/
│ └── azure-static-web-apps.yml CI/CD pipeline
├── .gitignore
└── README.md
-
In the Azure Portal, go to your Static Web App and copy the deployment token from the Overview page.
-
In your GitHub repo, go to Settings > Secrets and variables > Actions and create these secrets:
Secret Name Value AZURE_STATIC_WEB_APPS_API_TOKENDeployment token from the SWA resource VITE_TENANT_IDYour Entra tenant ID VITE_FRONTEND_CLIENT_IDFrontend SPA app registration client ID VITE_BACKEND_CLIENT_IDBackend API app registration client ID -
Optionally, create a repository variable (not secret):
Variable Name Value VITE_REFRESH_INTERVAL_SECONDS60 -
Set the backend environment variables in the SWA resource:
az staticwebapp appsettings set \ --name <your-swa-name> \ --resource-group <your-rg> \ --setting-names \ TENANT_ID=<value> \ BACKEND_CLIENT_ID=<value> \ BACKEND_CLIENT_SECRET=<value> \ AZURE_SQL_CONNSTR="Server=tcp:yourserver.database.windows.net,1433;Database=yourdb;Encrypt=True;TrustServerCertificate=False;"
-
Push to
main. The workflow triggers automatically.
# Build frontend
cd client
npm ci
npm run build
# Deploy
swa deploy client/dist --api-location api --deployment-token <YOUR_TOKEN>| Key | Description |
|---|---|
TENANT_ID |
Microsoft Entra ID tenant ID |
BACKEND_CLIENT_ID |
Client ID of the Backend API app registration |
BACKEND_CLIENT_SECRET |
Client secret for the Backend API app registration |
AZURE_SQL_CONNSTR |
SQL connection string, no username/password (token injected at runtime) |
| Key | Description |
|---|---|
VITE_TENANT_ID |
Microsoft Entra ID tenant ID |
VITE_FRONTEND_CLIENT_ID |
Client ID of the Frontend SPA app registration |
VITE_BACKEND_CLIENT_ID |
Client ID of the Backend API app registration |
VITE_REFRESH_INTERVAL_SECONDS |
Dashboard auto-refresh interval in seconds (default: 60) |
The table names and column names are defined as constants at the top of each function file in api/Functions/. The SQL aggregation query in MetricsFunction.cs can be modified to match your actual schema.
The dashboard layout, KPI cards, and chart types are defined in client/src/components/Dashboard.tsx. The component uses Recharts for visualization. Modify the JSX and chart configuration to match your data shape.
Set VITE_REFRESH_INTERVAL_SECONDS in client/.env (local) or as a GitHub Actions variable (CI). The dashboard polls the /api/metrics endpoint at this interval.