Multi-tenant Razor Pages frontend for FlexForms — a SaaS form platform that turns JSON templates into GOV.UK task-list applications.
Each tenant (Transfers, Visits, LSRP, …) is resolved from hostname or X-Tenant-ID. Configuration is loaded from flexforms-api TenantConfig (not from per-product folders). Persistence and business rules live in the API; this repo owns UI, auth cookies, form orchestration, and admin tools.
Template authoring guide: docs/Form-Template-Designer-Manual.md.
- Platform bootstrap — Host config from API at startup; per-request tenant config for Target
Web - Template-driven form engine — Tasks, pages, fields, conditional logic, collection & derived flows
- Auth — DfE Sign-In (OIDC) and optional Entra SSO, with cookie sessions and API token exchange
- Admin area — Template Manager, User Manager, Role Manager, Tenant Settings (SuperAdmin)
- Contributors — Invite collaborators when
contributorPatternis enabled on the template - Files — Upload via API; scan results consumed from Service Bus
- Notifications — API-backed notification centre
- GOV.UK Frontend — Design System components via GovUk.Frontend.AspNetCore
| Layer | Project | Purpose |
|---|---|---|
| Web | GovUK.Dfe.FlexForms.Web |
Razor Pages, middleware, auth overlays, admin |
| Application | GovUK.Dfe.FlexForms.Application |
Interfaces; references GovUK.Dfe.FlexForms.Api.Client |
| Domain | GovUK.Dfe.FlexForms.Domain |
FormTemplate models, conditional logic, complex fields |
| Infrastructure | GovUK.Dfe.FlexForms.Infrastructure |
API template store, form services, MassTransit consumers |
flowchart TB
Browser["Browser"]
Web["FlexForms Web"]
API["FlexForms API"]
IdP["DfE Sign-In / Entra"]
TC["TenantConfig DB"]
EA["EA data DB"]
Browser -->|HTTPS + Host| Web
Web -->|App-only Bearer<br/>host-config / tenant-config| API
API --> TC
Web -->|OIDC challenge| IdP
IdP -->|tokens| Web
Web -->|Exchanged JWT + X-Tenant-ID<br/>applications / templates / users| API
API --> EA
PlatformBootstrap:Enabled must be true. On start:
- Load
appsettings.bootstrap.json+ environment + user secrets. - Acquire an app-only Entra token (
PlatformAccessTokenProvider). - Call
GET /v1/host-config?target=Web. - Merge host keys into
IConfiguration.
Legacy configurations/{APPLICATION_NAME}/ folders are not used.
sequenceDiagram
participant Browser
participant MW as TenantConfigurationMiddleware
participant Resolver as TenantIdResolver
participant API as flexforms-api
participant Ctx as ITenantRequestContext
Browser->>MW: Request (+ Host / X-Forwarded-Host)
MW->>Resolver: Resolve tenant id
alt X-Tenant-ID or ?tenantId=
Resolver-->>MW: Guid
else Hostname
Resolver->>API: GET /v1/tenant-config/resolve?hostname=
API-->>Resolver: TenantId
end
MW->>API: GET /v1/tenant-config/tenants/{id}?target=Web
API-->>MW: Merged Shared + Web settings
MW->>Ctx: TenantId, Name, Configuration
Note over MW: Then auth, token exchange, page handlers
Tenant id order (TenantIdResolver):
- Header
X-Tenant-ID - Query
tenantId - Public hostname (
X-Forwarded-Host→Request.Host) → API resolve
Prefer launch profiles that use *.localhost hostnames mapped in TenantConfig (e.g. lsrp.localhost, rgvisits.localhost).
API business calls get X-Tenant-ID from TenantApiClientSettingsProvider / Api.Client HeaderForwardingHandler.
DynamicAuthenticationSchemeProvider / composite strategy (priority):
- Internal service headers (
x-service-email+ API key) - Test authentication (when enabled)
- Entra SSO when tenant
EntraSso:Enabled - Else DfE Sign-In OIDC
Cookies are the authenticate / sign-in / sign-out scheme. Challenge uses Entra or OpenIdConnect.
- Registered via CoreLibs custom OIDC.
- Per-request overlay:
TenantAwareOpenIdConnectConfigurator(ClientId, authority, redirects from tenant settings). - Callbacks:
/signin-oidc,/signout-callback-oidc.
- Always registered; activated when tenant enables it.
- Overlay:
TenantAwareEntraSsoConfigurator. - Callbacks:
/signin-entra,/signout-callback-entra.
sequenceDiagram
participant User
participant Web
participant IdP
participant API
User->>Web: Protected page
Web->>IdP: OIDC challenge
IdP-->>Web: Auth cookie + id_token
Web->>API: POST /v1/tokens/exchange
API-->>Web: Tenant API JWT
Web->>API: Business APIs with JWT + X-Tenant-ID
Note over Web: TokenRefresh + ActivityBasedTokenRefreshMiddleware<br/>idle / absolute timeout / proactive refresh
ExternalApplicationsApiClient:RequestTokenExchangeis forced on when platform bootstrap is enabled.- Session tickets use distributed cache ticket store.
TokenRefreshsettings (tenant-aware): refresh lead time, force logout window, inactivity and absolute timeouts.- Stay-signed-in:
SessionController+ antiforgery.
| Role / claim | Capabilities |
|---|---|
| SuperAdmin | All admin + Tenant Settings; can assign tenant Admin |
| Admin | Template / User / Role managers within tenant |
| Custom Manage claims | e.g. Template:Any:Manage, User:Any:Manage open Admin hub / tools |
| User | Applications, form fill, contributors (if enabled) |
See Security/AdminAccessHelper.cs.
classDiagram
direction TB
FormTemplate "1" --> "*" TaskGroup
TaskGroup "1" --> "*" Task
Task "1" --> "*" Page : linear
Task "0..1" --> TaskSummaryConfiguration : summary
TaskSummaryConfiguration --> MultiCollectionFlowConfiguration : flows
TaskSummaryConfiguration --> DerivedCollectionFlowConfiguration : derivedFlows
MultiCollectionFlowConfiguration --> Page
DerivedCollectionFlowConfiguration --> Page
Page "1" --> "*" Field
FormTemplate --> ConditionalLogic : conditionalLogic
class FormTemplate {
+string TemplateId
+string TemplateName
+string? DefaultFieldRequirementPolicy
+bool HideFieldLabelWhenOnlyOneField
+bool ContributorPattern
}
class Task {
+string TaskId
+string TaskName
+string? Caption
+TaskSummaryConfiguration? Summary
}
class Field {
+string FieldId
+string Type
+Label Label
+List~ValidationRule~ Validations
+ComplexField? ComplexField
}
Full authoring reference: docs/Form-Template-Designer-Manual.md.
| Concern | Implementation |
|---|---|
| Entry route | /applications/{referenceNumber}/{taskId?}/{*pageId} → RenderForm |
| Template load | ITemplatesClient → ApiTemplateStore → JsonFormTemplateParser |
| Navigation | FormStateManager, FormNavigationService |
| Save | Session accumulate → Base64 JSON → AddApplicationResponseAsync |
| Conditional logic | ConditionalLogicEngine / orchestrator |
| Collections | Multi + derived flow handlers on RenderForm |
| Complex fields | Tenant FormEngine:ComplexFields (Trust/Academy search, uploads) |
| Submit | SubmitApplicationAsync + ApplicationSubmissionOrchestrator (e.g. publish event) |
TemplateSelectionMiddleware:
- Multiple live templates →
/templates?liveOnly=true - Single live → auto-select
- Admins can preview non-live templates
Hub: /admin (CanAccessAdminArea).
| Tool | Route | Who |
|---|---|---|
| Template Manager | /admin/template-manager |
Admin / SuperAdmin / Template Manage |
| Create Template | /admin/create-template |
Same |
| Custom status labels | /admin/custom-status-label-overrides |
Same |
| User Manager | /admin/user-manager |
Admin / SuperAdmin / User Manage |
| Role Manager | /admin/role-manager |
Admin / SuperAdmin |
| Tenant Settings | /admin/tenant-settings |
SuperAdmin only |
Tenant Settings uses ITenantAdminClient (Base64 settings payloads), then refreshes API tenant cache and local ITenantConfigurationCache.
| Feature | Routes / notes |
|---|---|
| Dashboard | /applications/dashboard — list, filter, create |
| Form | /applications/{ref}/… |
| Contributors | /applications/{ref}/contributors, …/invite when contributorPattern: true |
| Submitted | /application-submitted/{referenceNumber} |
| Notifications | /Notifications UI + notifications/* API proxy |
| Feedback | /Feedback/* (often anonymous) |
Terminology (application vs case, etc.) comes from tenant ApplicationTerminology settings.
| Topic | Behaviour |
|---|---|
| AuthN | Cookie + OIDC/Entra; API via exchanged JWT |
| AuthZ | Folder policy OpenIdConnectPolicy; admin policies from AdminAccessHelper |
| CSRF | Antiforgery on POSTs (SessionController, notifications, forms) |
| Tenant binding | Hostname / header → config; API calls send X-Tenant-ID |
| Secrets | Not stored in Web DB; Tenant Settings secrets encrypted in API |
| Sanitisation | HtmlSanitizer + Markdig for tooltips/descriptions |
| HSTS | Enabled outside Development |
| Health | /health, /healthz, /liveness anonymous |
Permission claim shape (from API): {ResourceType}:{ResourceKey}:{AccessType}.
flowchart TD
A[Forwarded headers] --> B[TenantConfigurationMiddleware]
B --> C[Exception / status pages]
C --> D[HTTPS / static / cookie policy]
D --> E[Session + Authentication]
E --> F[TokenManagementMiddleware]
F --> G[ActivityBasedTokenRefresh]
G --> H[Permissions cache middleware]
H --> I[TemplateSelectionMiddleware]
I --> J[Authorization]
J --> K[Razor Pages / Controllers]
| Concern | Mechanism |
|---|---|
| Package | GovUK.Dfe.FlexForms.Api.Client (NuGet; local ProjectReference optional while developing) |
| Platform HTTP | /v1/host-config, /v1/tenant-config/resolve, /v1/tenant-config/tenants/{id} |
| Business clients | IApplicationsClient, ITemplatesClient, IUsersClient, IRolesClient, INotificationsClient, ITenantAdminClient, ITokensClient, … |
| Auth to API | Token exchange + X-Tenant-ID |
| Contracts | GovUK.Dfe.CoreLibs.Contracts.ExternalApplications.* (namespace historical; product is FlexForms) |
Web does not own SQL for applications/templates/users — the API does.
- .NET 10 SDK
- Running FlexForms API with TenantConfig populated (tenant, hostname, Web settings, principal for Web MI/SP if using app-only consume)
- Redis (typical) and Entra / DfE credentials in user secrets
Configure (user secrets or env):
PlatformBootstrap:ApiBaseUrlPlatformBootstrap:Scope,ClientId,ClientSecret(or DefaultAzureCredential)- Directory tenant id as required by your environment
See Properties/launchSettings.json:
| Profile | Typical URL | Notes |
|---|---|---|
Platform-https / Transfers-https |
https://localhost:7020 |
Needs TenantConfig hostname mapping for localhost if used |
Lsrp-https |
https://lsrp.localhost:7020 |
Hostname → LSRP tenant |
Visits-https |
https://rgvisits.localhost:7020 |
Often Entra-enabled tenant |
dotnet run --project src/GovUK.Dfe.FlexForms.Web --launch-profile Lsrp-httpsdotnet test GovUK.Dfe.FlexForms.Web.slnCypress specs live under src/Tests/GovUK.Dfe.FlexForms.CypressTests/ (optional Test auth).
| Route | Purpose |
|---|---|
/ |
Redirect to dashboard |
/applications/dashboard |
Application list |
/applications/{ref}/{taskId?}/{*pageId} |
Form engine |
/templates |
Template picker / preview |
/admin |
Admin hub |
/Notifications |
Notifications |
/Logout |
Sign out |
/Feedback/* |
Feedback |
/Cookies, /Privacy, /Terms |
Static |
/health* |
Probes |
sequenceDiagram
participant User
participant RF as RenderForm
participant ARS as ApplicationResponseService
participant Session
participant API as IApplicationsClient
User->>RF: POST answers
RF->>RF: Validate + conditional / collection updates
RF->>ARS: SaveApplicationResponseAsync
ARS->>Session: Merge form data
ARS->>ARS: JSON + Base64
ARS->>API: AddApplicationResponseAsync
ARS->>Session: Promote Created → InProgress when needed
docs/Form-Template-Designer-Manual.md— JSON template authoring- flexforms-api README — API, TenantConfig, roles, security
terraform/README.md— deployment
| Repo | Role |
|---|---|
| flexforms-api | API, TenantConfig, EA data |
| rsd-file-scanner-function / rsd-clamav-api | Antivirus pipeline |
| DfE.CoreLibs | Contracts, security, caching helpers |