A monorepo template for the standard application stack: Next.js · C#/.NET API · Keycloak · PostgreSQL · Azure · Azure DevOps
/
├── frontend/ Next.js 16 App Router (TypeScript, NextAuth + Keycloak)
├── backend/ ASP.NET Core 10 Web API (EF Core + Npgsql, JWT Bearer)
├── infra/ Bicep modules for Azure provisioning
└── pipelines/ Azure DevOps YAML pipelines
├── ci-backend.yml — build & test .NET
├── ci-frontend.yml — build Next.js
├── cd-infrastructure.yml — deploy Bicep
├── cd-backend.yml — deploy API (uat→prod)
├── cd-frontend.yml — deploy frontend (uat→prod)
└── azure-pipelines.yml — manual orchestrator
| Tool | Minimum version |
|---|---|
| Node.js | 20 LTS |
| .NET SDK | 10.0 |
| Azure CLI + Bicep | latest |
| Azure DevOps | any |
| Keycloak | 24+ |
| PostgreSQL | 16+ (local dev) |
If you use Claude or an OpenAI Codex-compatible agent, ask it to run the Project Setup from CLAUDE.md / AGENTS.md before doing any other work. It will collect your project details and perform all the renaming and file restructuring in one go.
┌─────────────────┐
Browser ────────▶│ Next.js (App │
│ Router) │
│ frontend/ │◀──── Keycloak (OIDC)
└────────┬────────┘ ▲
│ Bearer JWT │
▼ │
┌─────────────────┐ │
│ ASP.NET Core │────────────┘
│ Web API │ validates JWT
│ backend/ │
└────────┬────────┘
│
┌────────▼────────┐
│ PostgreSQL │
│ (EF Core / │
│ Npgsql) │
└─────────────────┘
Azure resources:
App Service Plan · Frontend App Service · Backend App Service
Key Vault (RBAC) · Log Analytics Workspace · App Insights
PostgreSQL Flexible Server · Storage Account · VNet (optional)
frontend/ — Next.js App Router with NextAuth.js handling the Keycloak Authorization Code + PKCE flow. The session stores the Keycloak access token, which is forwarded as a Bearer JWT to the backend.
backend/ — ASP.NET Core Web API with JWT Bearer middleware that validates Keycloak tokens. Uses EF Core + Npgsql for PostgreSQL access and pulls secrets from Azure Key Vault via Managed Identity.
infra/ — Bicep modules split into components/ (create resources), config/ (configure App Service settings), and utils/ (shared types and helpers).
pipelines/ — Azure DevOps YAML pipelines for CI (build + test) and CD (infrastructure + app deployment) across two cloud environments: uat and prod.
All resource names are derived from appBaseName and environment in each .bicepparam file:
| Resource | Pattern | Example (appBaseName=myapp, environment=uat) |
|---|---|---|
| App Service Plan | {appBaseName}-{environment}-asp |
myapp-uat-asp |
| Frontend App Service | {appBaseName}-{environment}-frontend |
myapp-uat-frontend |
| Backend App Service | {appBaseName}-{environment}-api |
myapp-uat-api |
| Key Vault | {appBaseName}-{environment}-kv |
myapp-uat-kv |
| Log Analytics Workspace | {appBaseName}-shared-law |
myapp-shared-law |
| App Insights | {appBaseName}-{environment}-ai |
myapp-uat-ai |
| PostgreSQL Server | {appBaseName}-{environment}-postgres |
myapp-uat-postgres |
| Storage Account | {appBaseName}{environment}storage |
myappuatstorage |
| VNet | {appBaseName}-{environment}-vnet |
myapp-uat-vnet |
Development runs entirely on local machines. Each developer runs the frontend and backend locally against a local PostgreSQL instance and a shared (or local) Keycloak instance.
Frontend
cd frontend
cp .env.example .env.local # fill in values
npm install
npm run devBackend
cd backend
# Ensure a local PostgreSQL instance is running
dotnet restore
dotnet run --project src/TemplateApp.ApiDo a case-sensitive global find-and-replace across the whole repo:
| Find | Replace with | Notes |
|---|---|---|
TemplateApp |
MyProject |
PascalCase — used in .NET solution/project names and C# namespaces |
templateapp |
myproject |
lowercase, no spaces — used in Bicep appBaseName, npm name, ADO environment names |
templateapp-azure-sc |
your ADO service connection name | |
keycloak.example.com |
your Keycloak hostname | appears in infra/main.*.bicepparam and pipeline variable files |
australiaeast |
your Azure region | appears in pipelines/variables/*.yml |
templateapp-*-rg |
your resource group names | appears in pipelines/variables/*.yml |
Then:
- Rename solution and project files from
TemplateApp.*to<YourName>.* - Update
backend/TemplateApp.slnproject references after renaming
Squash the template's commit history so your project starts with a clean slate:
git checkout --orphan fresh-start
git add -A
git commit -m "Initial commit"
git branch -D main
git branch -m mainPoint the repo at your new project remote and push:
git remote set-url origin <your-new-repo-url>
git push -u origin mainCreate two clients in your Keycloak realm:
templateapp-frontend — confidential client used by the Next.js backend (NextAuth)
| Setting | Value |
|---|---|
| Client authentication | On |
| Authentication flow | Standard flow only |
| Valid redirect URIs | <frontend-url> e.g., http://localhost:3000/* |
| Web origins | <frontend-url> e.g., http://localhost:3000 |
Add an Audience mapper to the frontend client so that the access tokens it receives include templateapp-api in the aud claim. The backend validates this claim on every request — without it you will get an audience validation error.
- Clients →
templateapp-frontend→ Client scopes →templateapp-frontend-dedicated→ Add mapper → Audience - Included client audience:
templateapp-api— Add to access token: On
Copy the client secret from the Credentials tab into AUTH_KEYCLOAK_SECRET (.env locally, Key Vault in Azure).
templateapp-api — confidential client representing the backend API (used as the JWT audience)
| Setting | Value |
|---|---|
| Client authentication | On |
| Authentication flow | Standard flow only |
Copy the client secret into Keycloak__Secret in App Service config (or Key Vault reference).
templateapp-public — public client for developer API docs via Scalar — local development only
| Setting | Value |
|---|---|
| Client authentication | Off (public client) |
| Authentication flow | Standard flow only |
| Valid redirect URIs | http://localhost:5000/* |
| Web origins | http://localhost:5000 |
No secret is required. PKCE (SHA-256) is enforced by the Scalar configuration.
Update Keycloak URLs in infra/main.*.bicepparam files and AUTH_KEYCLOAK_ISSUER in frontend .env.example and App Service config.
Resource groups are provisioned by IT. Update the names in pipelines/variables/*.yml.
# Validate the Bicep template
az deployment group validate \
--resource-group templateapp-rg \
--template-file infra/main.bicep \
--parameters infra/main.uat.bicepparam \
--parameters postgresAdminPassword=<password>
# Deploy
az deployment group create \
--resource-group templateapp-rg \
--template-file infra/main.bicep \
--parameters infra/main.uat.bicepparam \
--parameters postgresAdminPassword=<password>- Confirm resource groups exist for uat and prod
- Run validate + deploy for uat first, then prod
- Store secrets in Key Vault:
nextauth-secret,keycloak-frontend-client-secret,postgres-connection-string - Verify App Service Managed Identities have the
Key Vault Secrets Userrole on the vault
- Create service connection
templateapp-azure-sc(Azure Resource Manager, scoped to subscription) - Create two ADO Environments:
-
templateapp-uat(1 approver required) -
templateapp-prod(2 approvers required)
-
- Create variable groups in the ADO Library:
-
templateapp-common— non-secret shared variables -
templateapp-uat-secrets—postgresAdminPassword(secret),keycloakClientSecret(secret),nextauthSecret(secret) -
templateapp-prod-secrets— same structure
-
- Import pipeline YAML files into ADO:
-
pipelines/ci-backend.yml→ name: "CI - Backend" -
pipelines/ci-frontend.yml→ name: "CI - Frontend" -
pipelines/cd-infrastructure.yml→ name: "CD - Infrastructure" -
pipelines/cd-backend.yml→ name: "CD - Backend" -
pipelines/cd-frontend.yml→ name: "CD - Frontend" -
pipelines/azure-pipelines.yml→ name: "CD - Orchestrator"
-
- Link
templateapp-commonvariable group to each pipeline - Link
templateapp-uat-secretsandtemplateapp-prod-secretstocd-infrastructure,cd-backend,cd-frontend
- Trigger
CI - Backendon a feature branch to verify build and tests pass - Trigger
CI - Frontendon a feature branch to verify Next.js build passes - Run
CD - Infrastructuretargetinguatto provision all Azure resources - Run EF Core migrations against the uat PostgreSQL instance:
dotnet ef database update --project backend/src/TemplateApp.Api
- Merge to
mainto triggerCD - BackendandCD - Frontendfor uat deployment - Verify end-to-end: frontend → Keycloak login → JWT forwarded to backend → 200 OK
-
GET https://{backend-uat-url}/api/health→ 200 (unauthenticated) -
GET https://{backend-uat-url}/api/health/authwith valid JWT → 200 - Frontend sign-in flow completes without errors
- Application Insights receives telemetry from both frontend and backend
The Key Vault uses enableRbacAuthorization: true. App Service Managed Identities are granted the Key Vault Secrets User role via infra/modules/config/keyvault-access.bicep. This is the current Microsoft recommendation and is auditable via Azure Policy.
Connection strings and client secrets are stored as Key Vault secrets and referenced in App Service settings as @Microsoft.KeyVault(SecretUri=...). The plain-text values never appear in app settings or source control.
The PostgreSQL admin password is a @secure() Bicep parameter supplied only at deployment time via an Azure DevOps secret variable. It does not appear in any .bicepparam file.
Required for deployment to Azure App Service on Linux. The deployment pipeline copies the public/ and .next/static/ directories into the standalone output before packaging, as Next.js does not do this automatically.
The CD pipelines use Azure DevOps deployment jobs linked to ADO Environments. Approval gates are configured in the ADO UI — not in YAML — keeping pipeline code clean and approval policy in one place.
Every Azure resource name is derived from appBaseName + environment variables defined in main.bicep. This ensures consistent naming and means renaming the project only requires changing the parameter files.