Platform Architecture
What was built, and why. This document is the authoritative description of the SynkronyXr platform shape: the topology, the identity model, the Azure landing zone, and the decisions that produced them.
Requirements that constrain this architecture are in Platform Requirements. How the architecture is realised in code and pipelines is in Platform Implementation.
1. Scope and product lines
SynkronyXr is an offline-first, edge-accelerated platform with a governed Azure system of record. Every product embeds a dedicated SynkronyXr engine instance.
Products are grouped into identity domains. An identity domain is the set of applications whose users are the same people. It is the unit that owns a CIAM tenant. Components inside a domain share one account. Separate domains never do.
| Identity domain | Code | Component | Code | Engine module | Scope |
|---|---|---|---|---|---|
| Event Route Optimiser | ero | Event Route Optimiser | ero | Clashfinder SynkronyXr | Multi-stage schedule overlap detection, set collision resolution, and offline alert planning |
| Synkronyx Life | life | Playlist List | pll | Playlist List SynkronyXr | Algorithmic artist discovery, line-up playlist generation, set audio previews, and playlist synchronisation |
| Synkronyx Life | life | Logbook Manager | lbm | Logbook SynkronyXr | Immutable event logger, attendee itinerary tracking, and offline audit trail syncing |
| Synkronyx Life | life | Logbook Synchroniser | lbs | Logbook SynkronyXr | Logbook replication between devices and downstream systems |
| Synkronyx Life | life | Life Data Stream | lds | Logbook SynkronyXr | Correlation and analytics across every Life component |
Synkronyx Life is a subscription bundle rather than a single application. A customer holds one Life account, and the subscription states which components they may use. Adding a component to an existing subscription never changes the account, so moving from a single component to the full bundle preserves everything the customer already has.
Every Life component is sellable standalone, and each one is an entry point to the next:
- Playlist List acquires the customer by synchronising playlists across their devices and services.
- Logbook Manager and Logbook Synchroniser add training and activity logging on top of the same account.
- Life Data Stream correlates the two. It answers questions that no single component can, such as which playlist accompanied the longest run.
That ladder is the reason the components share one identity domain. The value of Life Data Stream comes from data that Playlist List and the logbook components produced under the same account. Splitting them across tenants would break the correlation and force the customer to hold two identities to buy the upgrade.
Life Data Stream is the analytics and data component within Synkronyx Life. It is not the name of the bundle.
Event Route Optimiser is the first product line and the reference implementation. Its product-specific architecture is documented separately in the ERO documentation set.
Identity domain membership is defined here and in platform/infrastructure/azure/modules/ciam.bicep, which derives the tenant name. The products map in settings/synkronyxr.config.example.json is a documentation branding catalogue consumed only by the docs site build, so it carries no identity meaning and lists only the domains that publish a documentation site.
2. Platform topology
graph TD
subgraph UX["SynkronyXr User Experience"]
A["Astro PWA Client ERO"]
B["Astro PWA Client LBM"]
C["Future SynkronyXr Products"]
end
subgraph EDGE["Cloudflare Edge Platform"]
direction LR
D{"Edge Layer"}
E["Auth Middleware"]
F["API Workers"]
G["D1 SQLite Database"]
D -- "JWT Auth" --> E
D -- "API Gateway" --> F
F -- "CRUD" --> G
end
subgraph AZ["Azure Enterprise Platform System of Record"]
direction LR
H{"Azure Landing Zones"}
I["Microsoft Entra External ID CIAM"]
J["CAF Management Groups"]
K["Azure Subscriptions"]
H -- "Identity" --> I
H -- "Governance" --> J
H -- "Billing" --> K
end
A -- "OAuth2 OIDC" --> D
B -- "OAuth2 OIDC" --> D
C -- "OAuth2 OIDC" --> D
D -- "System Sync" --> H
style A fill:#2d3748,stroke:#fff,stroke-width:2px,color:#fff
style B fill:#2d3748,stroke:#fff,stroke-width:2px,color:#fff
style C fill:#2d3748,stroke:#fff,stroke-width:2px,color:#fff
Three layers, with a deliberate split of responsibility:
- Clients are Astro Progressive Web Applications that read and write a local datastore first and never block on the network.
- Cloudflare edge terminates authentication, serves the API, and holds edge data in D1 SQLite.
- Azure is the system of record for identity, governance, and billing. It is not on the request path for normal application traffic.
3. Identity model
Identity is partitioned by identity domain and tier. Each identity domain owns its own pair of Entra External ID tenants, so an account is valid across the components of one domain and is not shared between domains.
flowchart LR U[End User] --> X[Sign in] X --> C1["ERO CIAM tenant"] X --> C2["Synkronyx Life CIAM tenant"] C1 --> G[Google] C1 --> A[Apple] C1 --> F[Facebook] C1 --> E[Email OTP or password] C2 --> G C2 --> A C2 --> F C2 --> E C1 --> P1[ERO app] C2 --> P2[PLL app] C2 --> P3[LBM app] C2 --> P4[LBS app] C2 --> P5[LDS app]
Microsoft Entra External ID (CIAM) is the primary identity provider and federates to the major social providers. Public clients use OAuth 2.0 and OpenID Connect with PKCE. JSON Web Tokens are validated at the Cloudflare edge before any API access is granted.
Identity collects the minimum viable claim set. First sign-in requests only openid. Additional scopes are requested through progressive just-in-time consent when a feature actually needs them.
Which applications a customer may use is entitlement, not identity. Entitlement is held by the subscription and billing records. Discounts that span identity domains are settled by refund or code matched on billing address, never by linking accounts.
3.0 Why identity domains rather than one shared tenant
A CIAM tenant is a user population, so the boundary must follow the buyer, not the deployment unit.
- The free monthly active user allowance is granted per tenant. Separate domains receive separate allowances.
- Monthly active user billing splits by domain with no further analysis.
- A broken user flow or identity provider change affects one domain.
- A domain that is sold takes its user population with it.
- Data residency is fixed at tenant creation and cannot be changed afterwards.
One shared tenant would only be correct if a single buyer purchased a suite spanning domains and expected one sign-in across all of it. Synkronyx sells per domain, so that condition does not hold.
3.1 Tenant partitioning
Identity infrastructure is partitioned across two Entra External ID tenants so that non-production activity can never touch live attendee data. This table is the single source of truth for tenant assignment.
| Environment | Tenant tier | Tenant domain | Application registration | Audience |
|---|---|---|---|---|
| Development | Nonproduction | <nonproduction-tenant>.onmicrosoft.com | ero-development | Developer inner loop |
| Test | Nonproduction | <nonproduction-tenant>.onmicrosoft.com | ero-test | Automated testing |
| Support | Production | <production-tenant>.onmicrosoft.com | ero-support | Operational verification |
| Production | Production | <production-tenant>.onmicrosoft.com | ero-production | Festival attendees |
Workload subscriptions follow the same boundary. development and test run in sub-sknx-ero-nonproduction; production runs in sub-sknx-ero-production.
3.2 Application registration naming
Every application registration follows the <component>-<environment> taxonomy, where the component code comes from the product table in section 1. Registrations are per component and per environment, not per identity domain, because a CIAM user flow is attached to specific applications and a flow change must be provable on one environment before it reaches another.
Event Route Optimiser is the worked example:
ero-developmentin the nonproduction tenant, redirect URIhttps://localhost:4321/ero-testin the nonproduction tenant, redirect URIhttps://<test-host>/ero-supportin the production tenant, redirect URIhttps://<support-host>/ero-productionin the production tenant, redirect URIhttps://<production-host>/
Redirect URIs must carry the trailing slash. MSAL sends the browser origin with a trailing slash and Entra matches redirect URIs exactly.
3.3 Cost model
CIAM usage is billed to the production subscription of each product. Entra External ID bills the first 50,000 monthly active users at no charge, then $0.03 per monthly active user. At 100,000 monthly active users a single product therefore costs approximately $1,500 per month.
The free allowance is granted per tenant, so each identity domain receives its own.
3.4 Identity planes
Three planes exist. They never share an identity provider.
| Plane | Who signs in | Identity provider |
|---|---|---|
| Customer | Attendees and subscribers | The CIAM tenant of that identity domain |
| Workforce | Synkronyx staff, automation, and agents | Synkronyx corporate Entra tenant |
| Anonymous | Anyone | None |
Synkronyx-level applications are never customer-facing, so they never use CIAM. Documentation sites, the marketing site, and the status page are anonymous. Billing administration, the support console, and internal tooling are workforce. This is why no Synkronyx-level CIAM tenant exists and none is required.
3.5 Google Cloud project taxonomy
A Google Cloud project holds exactly one OAuth consent screen, and that screen is either Internal or External. One project therefore cannot serve both staff and the public. This constraint, not preference, sets the project boundary.
| Project | Trust direction | Consent screen |
|---|---|---|
sknx-auth-<tier> | Synkronyx is the relying party, asking a person for permission to reach their own Google data | Internal |
sknx-<domain>-auth-<tier> | Google is the identity provider, federating into the CIAM tenant of that identity domain | External |
The test is the direction of trust. If Synkronyx is requesting consent to reach someone’s data, it belongs in sknx-auth-<tier>. If Google is proving who someone is on the way into a product, it belongs in the identity domain project.
Rules:
- Project identifiers are immutable and are destroyed permanently on deletion. Create the correctly named project first, then delete the old one. Display names may be edited freely.
- Projects are created with an organisation parent. An unparented project sits outside organisation policy and cannot be governed, so
google.organizationIdmust be set in the runtime configuration before any project is created. - Project identifiers use the full tier word,
nonproductionandproduction. The abbreviated-nonprodstyle is reserved for Azure management groups, where it is a Cloud Adoption Framework convention. Every identity domain name fits inside the 30 character limit, so the abbreviation buys nothing. - Project identifiers are deterministic. They are never suffixed with a hash of the signed-in account. Account scoping was a workaround for identifier collisions in the global namespace under a personal account, and the organisation removes that need.
authprojects hold OAuth clients and consent screens only. Shared platform services belong in a separate project rather than accumulating in a project named for authentication.- Google OAuth clients follow the tenant, not the application, because the federation redirect URIs are tenant-scoped.
4. Azure landing zone
The platform follows the Microsoft Cloud Adoption Framework and uses a vending machine pattern to stamp out product landing zones with governance, security, and cost isolation already applied.
4.1 Core principles
- One legal and billing anchor. A single Microsoft Customer Agreement billing account (
Synkronyx Ltd) and one primary billing profile (bp-synkronyx-primary). - Product cost isolation. A dedicated invoice section per product line, so gross margin is attributable per product. Deployment discovers an existing section or provisions one, falling back to billing profile scope when no section is available.
- Governance at the management group tier. Policy and role-based access control are enforced on management groups, not individual resources.
- Full-name taxonomy. Environments, subscriptions, parameter files, and resource groups use unabbreviated names unless a provider length limit forbids it.
- Zero-touch automation. Parameter-driven Bicep deployments run from local build tasks or pipelines with no portal steps.
4.2 Hierarchy
Microsoft Entra ID / Tenant Boundary (Synkronyx Ltd)│├── Billing Account: Synkronyx Ltd (Legal MCA Contract and VAT Anchor)│ └── Billing Profile: bp-synkronyx-primary (One Monthly GBP Invoice)│ └── Invoice Section: Event Route Optimiser (per-product cost isolation)│└── synkronyx (CAF Root Management Group, existing) │ ├── synkronyx-platform │ ├── synkronyx-platform-prod │ └── synkronyx-platform-nonprod │ ├── synkronyx-shared-services │ ├── synkronyx-shared-identity (Entra External ID CIAM anchor, central Key Vault) │ ├── synkronyx-shared-networking (Global DNS routing) │ ├── synkronyx-shared-monitoring (Central Log Analytics) │ └── synkronyx-shared-automation (Azure DevOps billing and automation) │ └── synkronyx-landingzones (Application Landing Zones, product workloads) │ ├── Event Route Optimiser │ ├── synkronyx-ero-nonprod │ │ ├── synkronyx-ero-dev (policy scope) │ │ ├── synkronyx-ero-test (policy scope) │ │ └── sub-sknx-ero-nonproduction │ │ ├── rg-sknx-ero-development │ │ ├── rg-sknx-ero-test │ │ └── rg-sknx-ero-preproduction │ │ │ └── synkronyx-ero-prod │ └── sub-sknx-ero-production │ └── rg-sknx-ero-production │ └── Logbook Manager and future products ├── synkronyx-lbm-nonprod │ └── sub-sknx-lbm-nonproduction └── synkronyx-lbm-prod └── sub-sknx-lbm-productionThe vending machine references the existing synkronyx root and never recreates it. It creates the synkronyx-landingzones parent, nests product management groups beneath it, and binds each subscription to its management group at vend time through the alias additionalProperties.managementGroupId.
Management group tiers keep the established CAF abbreviation style (-nonprod and -prod). Subscriptions and resource groups keep full names.
4.3 Environment naming
Full, human-readable environment names are the default. Short tokens apply only where a resource type imposes a length limit.
| CIAM tier | Canonical environment | Short token |
|---|---|---|
| nonproduction | development | dev |
| nonproduction | test | test |
| production | support | supp |
| production | production | prod |
Shared identity resources are laid out by tier: rg-sknx-identity-nonproduction serves development and test, and rg-sknx-identity-production serves support and production.
4.4 Region placement
| Layer | Provider | Region | Boundary |
|---|---|---|---|
| Platform identity | Azure | uksouth | rg-sknx-identity-production in sub-sknx-platform |
| Development sandbox | Azure | uksouth | rg-sknx-ero-development |
| Integration testing | Azure | uksouth | rg-sknx-ero-test |
| Pre-production staging | Azure | uksouth | rg-sknx-ero-preproduction |
| Production workload | Azure | uksouth | rg-sknx-ero-production |
| Edge API and D1 SQLite | Cloudflare | Global anycast | Cloudflare edge network |
| Multi-cloud lakehouse | GCP | europe-west2 | synkronyx-ero-prod |
4.5 Domain allocation
Cloudflare is the authoritative registrar and DNS manager for all public traffic. Azure hosts no public DNS zones.
Synkronyx owns four domains. Each has one job, and the job determines where a thing publishes.
| Domain | Purpose | The test to apply |
|---|---|---|
synkronyx.com | Corporate identity, marketing, documentation | Something a person reads about Synkronyx |
synkronyx.app | Customer-facing product applications | Something a customer signs into and uses |
synkronyx.cloud | Platform and infrastructure services | Something an application calls, not a person |
synkronyx.co.uk | Defensive registration | Redirects to .com and hosts nothing |
Rules
- Production uses the clean host. Non-production prefixes the environment.
ero.synkronyx.appin production,test-ero.synkronyx.appin test,dev-ero.synkronyx.appin development. - A customer-facing application never publishes on
.cloud. Customers should never see an infrastructure domain in the address bar. - Documentation never publishes on
.appor.cloud. Documentation is read, not used. pages.devis deployment infrastructure, not a published address. It must not be linked from documentation, used in validation of a published environment, or given to a customer. The single exception is a pull request preview, which has no custom domain by definition.- Every published host is a proxied CNAME in Cloudflare, so certificates and edge routing are managed by custom domain bindings.
synkronyx.com
| Host | Serves | State |
|---|---|---|
www.synkronyx.com | Marketing site, with holding pages for Event Route Optimiser and Synkronyx Image Marketplace links | Built for Cloudflare Pages |
www.synkronyx.com/products/event-route-optimiser/ | Event Route Optimiser holding product page | Built for Cloudflare Pages |
www.synkronyx.com/products/vscode-image-extension/ | Synkronyx Image holding product page for Visual Studio Marketplace links | Built for Cloudflare Pages |
synkronyx.com | Redirect to www | Not configured |
docs.synkronyx.com | Core platform documentation | Live |
ero-docs.synkronyx.com | Event Route Optimiser documentation | Live |
test-docs.synkronyx.com, test-ero-docs.synkronyx.com | The same two sites, test | Live |
dev-docs.synkronyx.com, dev-ero-docs.synkronyx.com | The same two sites, development | Live |
synkronyx.app
| Host | Serves | State |
|---|---|---|
ero.synkronyx.app | Event Route Optimiser PWA | Not configured |
test-ero.synkronyx.app | Event Route Optimiser, test | Not configured |
dev-ero.synkronyx.app | Event Route Optimiser, development | Not configured |
Future products take a host on the same pattern, for example lbm.synkronyx.app.
synkronyx.cloud
| Host | Serves | State |
|---|---|---|
api.synkronyx.cloud | Edge API | Not configured |
test-api.synkronyx.cloud, dev-api.synkronyx.cloud | Edge API, non-production | Not configured |
Local development
Local work runs on https://localhost:4321 and does not use a public domain.
Known gaps
The allocation above is the target. Today only the documentation hosts exist. synkronyx.app and synkronyx.cloud carry no application records, and synkronyx.com has no www or apex record.
Two consequences follow. The Event Route Optimiser application is reachable only at its deployment address and has no customer-facing host. Marketplace listings have nowhere to link, because www.synkronyx.com does not exist.
Identity redirect URIs must move with the application host. Changing where the application publishes requires the matching Entra External ID application registration redirect URI to change in the same operation, otherwise sign-in breaks. See the registration taxonomy in section 3.2.
5. Architectural decision records
ADR-001: Offline-first client datastore
- Status: Accepted, implemented.
- Context: Festival grounds frequently have zero or degraded connectivity because of crowd density.
- Decision: Use Dexie.js over IndexedDB to hold schedules, stage geometry, and user preferences locally. Every read is served from the local cache.
- Consequences: Writes queue locally during network loss and flush automatically when connectivity returns.
ADR-002: Edge compute and micro-database
- Status: Accepted, implemented.
- Context: Line-up updates and preference sync need low latency globally.
- Decision: Cloudflare Workers bound to Cloudflare D1 SQLite.
- Consequences: Serverless routing at the edge with fast transactions and native CORS handling.
ADR-003: Subscription-scoped infrastructure as code
- Status: Accepted, implemented.
- Context: Environment isolation requires separate landing zones without manual resource group creation.
- Decision: Subscription-scoped Bicep deployments.
- Consequences: Pipelines create resource groups on demand with no portal intervention.
ADR-004: Dual-loop identity authentication
- Status: Accepted, implemented.
- Context: Production demands strict CIAM federation, while local development must run with no network.
- Decision: Split authentication into an inner-loop mock token path and an outer-loop Entra CIAM JWKS validation path using WebCrypto with
RSASSA-PKCS1-v1_5andSHA-256. - Consequences: Developers work offline while production validates real bearer tokens at the edge.
ADR-005: Spatial optimisation engine
- Status: Accepted, in progress.
- Context: Attendees need conflict resolution when chosen acts overlap across distant stages.
- Decision: Dijkstra shortest path combined with weighted interval scheduling to compute walking transit times, flag clashes, and recommend departure times.
- Consequences: Produces exact walking itineraries in the form act, transit, act.
ADR-006: Platform and product subscription boundaries
- Status: Accepted, implemented.
- Context: Governance requires corporate identity services to be decoupled from product workloads.
- Decision:
sub-sknx-platformholds central identity. ERO workloads live insub-sknx-ero-nonproductionandsub-sknx-ero-production. - Consequences: Removes cross-product blast radius and supports least-privilege role assignment for service connections.
ADR-007: Enterprise billing hierarchy
- Status: Accepted, implemented.
- Context: Accounting requires transparent cost attribution across cost centres and product lines under one agreement.
- Decision: Billing account
ba-synkronyxand billing profilebp-synkronyx-primarywith explicit invoice sectionsinv-sknx-platform,inv-sknx-ero, andinv-sknx-lbm. - Consequences: Granular financial tracking per section, and subscription alias vending linked directly to the product invoice section through
billingScope.
ADR-008: CAF management group governance and vending machine
- Status: Accepted.
- Context: Manually creating subscriptions, resource groups, and tenant configuration causes delay, drift, and inconsistent policy.
- Decision: Nest product workloads into the existing CAF hierarchy and adopt the vending machine pattern in Bicep. One tenant-scoped command creates the landing zone parent, vends subscriptions against the correct invoice section, and places each subscription in its management group at vend time. Provisioning a new product requires only a changed
productCodeparameter. - Consequences: Landing zone spin-up drops from days to minutes with guaranteed policy isolation and auditability.
ADR-009: Client applications are named by target platform
- Status: Accepted.
- Context: ERO carried two sibling directories,
appandweb, declaring the identical package name with identical scripts, andwebcontained no source at all. Local development, lifecycle validation, bootstrap, and Playwright targetedapp, while the preview workflow and the repository instructions targetedweb. The preview workflow therefore built an empty project and deployed it, so every ERO preview shipped without the application. A planned mobile client made the ambiguity urgent, becauseappversuswebexpresses no platform boundary. - Decision:
- Consolidate the client into
apps/event-route-optimiser/webholding the Astro PWA, retaining the real source and the strictertsconfig.json. - Name every client directory after the platform it targets.
webandmobileare permitted.appis prohibited because it identifies no platform. - Treat the PWA as the mobile experience. A
mobiledirectory is justified only by a capability the PWA cannot deliver, such as background notification delivery, and must not be created for parity alone. - Extract shared domain logic into
apps/event-route-optimiser/sharedbefore any second client exists. The Dexie schema, synchronisation engine, conflict resolution, now-and-next query, and routing engine must have exactly one implementation.
- Consolidate the client into
- Consequences: Restores a working preview deployment, removes the split between local tooling and CI, and gives a future mobile client an unambiguous home. Adding a second client now carries an explicit prerequisite to extract shared logic, which prevents divergent conflict resolution producing different results on different devices.
6. Brand and design baseline
- Corporate entity: Synkronyx Ltd. Slogan: “We are Synchronisation”.
- Engine brand mark: SynkronyXr, with capital
Xand lowercaser. - Primary typeface: Plus Jakarta Sans. Headings 600 to 800, body and labels 400 to 500.
- Design direction: minimalist, data-forward, high-contrast, mobile-first, dark-native, and accessible.
Use SynkronyXr in technical specifications, API documentation, and implementation contracts. Use Synkronyx wherever a person is addressed. Service contracts must name the SynkronyXr engine layer they sit on.
The sknx prefix is an internal identifier. It must never appear in a customer-facing name, URL, install command, or interface label.
6.1 Palette
The Event Route Optimiser palette is the Synkronyx baseline, not a product-specific style. It was designed to stay legible in direct sunlight, which makes it a good default everywhere.
The source of truth is tokens/synkronyx-palette.json in the brand repository, with tokens/synkronyx-palette.css generated from it. Products vendor those two files rather than restating hex values.
| Token | Hex | Role | Contrast on #09090b |
|---|---|---|---|
main-stage-black | #09090B | Primary background | surface |
backstage-grey | #18181B | Cards and raised surfaces | surface |
strobe-white | #FAFAFA | Body text | 19.06 |
route-cyan | #00E5FF | Primary actions, links, active state | 12.93 |
sunset-orange | #FF5C00 | Connectivity and transitional state | 6.43 |
clash-magenta | #FF007A | Conflict and error only | 5.24 |
gridline-grey | #333338 | Borders and separators | 1.58, decoration only |
Two brand colours are retained for marketing and data visualisation, and are deliberately not part of the product surface:
| Token | Hex | Role | Contrast on #09090b |
|---|---|---|---|
synkronyx-deep-blue | #0072B2 | Wordmark and marketing on light backgrounds | 3.84, fails AA for text |
synkronyx-sky-blue | #56B4E9 | Marketing accent, data visualisation | 8.62 |
Both come from the Okabe-Ito colour-blind-safe palette. That property is why they are kept for data visualisation, where distinguishing series matters more than surface contrast.
6.2 Rules
- Colour is never the sole signal for a state. Pair it with an icon, a label, or a shape.
clash-magentaandsunset-orangeconverge under protanopia, and they signal different things. clash-magentais reserved for conflict and error. Using it decoratively destroys the one signal a user scans for.gridline-greyis decoration only. At 1.58 it fails contrast for text by a wide margin, and must never be the sole boundary of an interactive control.synkronyx-deep-bluemust not carry text on a dark surface. At 3.84 it fails AA.- Body text meets WCAG AA. Every foreground token above except
gridline-greydoes so on both surfaces.
7. Implementation references
platform/infrastructure/azure/main.bicep(tenant scope)platform/infrastructure/azure/identity.bicep(subscription scope)platform/infrastructure/azure/modules/landing-zone.bicepplatform/infrastructure/azure/modules/identity.bicepplatform/automation/powershell/deploy/deploy-landingzone.ps1