Skip to content
version: 1.1.2-14

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 domainCodeComponentCodeEngine moduleScope
Event Route OptimisereroEvent Route OptimisereroClashfinder SynkronyXrMulti-stage schedule overlap detection, set collision resolution, and offline alert planning
Synkronyx LifelifePlaylist ListpllPlaylist List SynkronyXrAlgorithmic artist discovery, line-up playlist generation, set audio previews, and playlist synchronisation
Synkronyx LifelifeLogbook ManagerlbmLogbook SynkronyXrImmutable event logger, attendee itinerary tracking, and offline audit trail syncing
Synkronyx LifelifeLogbook SynchroniserlbsLogbook SynkronyXrLogbook replication between devices and downstream systems
Synkronyx LifelifeLife Data StreamldsLogbook SynkronyXrCorrelation 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:

  1. Playlist List acquires the customer by synchronising playlists across their devices and services.
  2. Logbook Manager and Logbook Synchroniser add training and activity logging on top of the same account.
  3. 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:

  1. Clients are Astro Progressive Web Applications that read and write a local datastore first and never block on the network.
  2. Cloudflare edge terminates authentication, serves the API, and holds edge data in D1 SQLite.
  3. 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.

  1. The free monthly active user allowance is granted per tenant. Separate domains receive separate allowances.
  2. Monthly active user billing splits by domain with no further analysis.
  3. A broken user flow or identity provider change affects one domain.
  4. A domain that is sold takes its user population with it.
  5. 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.

EnvironmentTenant tierTenant domainApplication registrationAudience
DevelopmentNonproduction<nonproduction-tenant>.onmicrosoft.comero-developmentDeveloper inner loop
TestNonproduction<nonproduction-tenant>.onmicrosoft.comero-testAutomated testing
SupportProduction<production-tenant>.onmicrosoft.comero-supportOperational verification
ProductionProduction<production-tenant>.onmicrosoft.comero-productionFestival 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-development in the nonproduction tenant, redirect URI https://localhost:4321/
  • ero-test in the nonproduction tenant, redirect URI https://<test-host>/
  • ero-support in the production tenant, redirect URI https://<support-host>/
  • ero-production in the production tenant, redirect URI https://<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.

PlaneWho signs inIdentity provider
CustomerAttendees and subscribersThe CIAM tenant of that identity domain
WorkforceSynkronyx staff, automation, and agentsSynkronyx corporate Entra tenant
AnonymousAnyoneNone

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.

ProjectTrust directionConsent screen
sknx-auth-<tier>Synkronyx is the relying party, asking a person for permission to reach their own Google dataInternal
sknx-<domain>-auth-<tier>Google is the identity provider, federating into the CIAM tenant of that identity domainExternal

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:

  1. 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.
  2. Projects are created with an organisation parent. An unparented project sits outside organisation policy and cannot be governed, so google.organizationId must be set in the runtime configuration before any project is created.
  3. Project identifiers use the full tier word, nonproduction and production. The abbreviated -nonprod style 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.
  4. 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.
  5. auth projects hold OAuth clients and consent screens only. Shared platform services belong in a separate project rather than accumulating in a project named for authentication.
  6. 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

  1. One legal and billing anchor. A single Microsoft Customer Agreement billing account (Synkronyx Ltd) and one primary billing profile (bp-synkronyx-primary).
  2. 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.
  3. Governance at the management group tier. Policy and role-based access control are enforced on management groups, not individual resources.
  4. Full-name taxonomy. Environments, subscriptions, parameter files, and resource groups use unabbreviated names unless a provider length limit forbids it.
  5. 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-production

The 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 tierCanonical environmentShort token
nonproductiondevelopmentdev
nonproductiontesttest
productionsupportsupp
productionproductionprod

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

LayerProviderRegionBoundary
Platform identityAzureuksouthrg-sknx-identity-production in sub-sknx-platform
Development sandboxAzureuksouthrg-sknx-ero-development
Integration testingAzureuksouthrg-sknx-ero-test
Pre-production stagingAzureuksouthrg-sknx-ero-preproduction
Production workloadAzureuksouthrg-sknx-ero-production
Edge API and D1 SQLiteCloudflareGlobal anycastCloudflare edge network
Multi-cloud lakehouseGCPeurope-west2synkronyx-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.

DomainPurposeThe test to apply
synkronyx.comCorporate identity, marketing, documentationSomething a person reads about Synkronyx
synkronyx.appCustomer-facing product applicationsSomething a customer signs into and uses
synkronyx.cloudPlatform and infrastructure servicesSomething an application calls, not a person
synkronyx.co.ukDefensive registrationRedirects to .com and hosts nothing

Rules

  1. Production uses the clean host. Non-production prefixes the environment. ero.synkronyx.app in production, test-ero.synkronyx.app in test, dev-ero.synkronyx.app in development.
  2. A customer-facing application never publishes on .cloud. Customers should never see an infrastructure domain in the address bar.
  3. Documentation never publishes on .app or .cloud. Documentation is read, not used.
  4. pages.dev is 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.
  5. Every published host is a proxied CNAME in Cloudflare, so certificates and edge routing are managed by custom domain bindings.

synkronyx.com

HostServesState
www.synkronyx.comMarketing site, with holding pages for Event Route Optimiser and Synkronyx Image Marketplace linksBuilt for Cloudflare Pages
www.synkronyx.com/products/event-route-optimiser/Event Route Optimiser holding product pageBuilt for Cloudflare Pages
www.synkronyx.com/products/vscode-image-extension/Synkronyx Image holding product page for Visual Studio Marketplace linksBuilt for Cloudflare Pages
synkronyx.comRedirect to wwwNot configured
docs.synkronyx.comCore platform documentationLive
ero-docs.synkronyx.comEvent Route Optimiser documentationLive
test-docs.synkronyx.com, test-ero-docs.synkronyx.comThe same two sites, testLive
dev-docs.synkronyx.com, dev-ero-docs.synkronyx.comThe same two sites, developmentLive

synkronyx.app

HostServesState
ero.synkronyx.appEvent Route Optimiser PWANot configured
test-ero.synkronyx.appEvent Route Optimiser, testNot configured
dev-ero.synkronyx.appEvent Route Optimiser, developmentNot configured

Future products take a host on the same pattern, for example lbm.synkronyx.app.

synkronyx.cloud

HostServesState
api.synkronyx.cloudEdge APINot configured
test-api.synkronyx.cloud, dev-api.synkronyx.cloudEdge API, non-productionNot 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_5 and SHA-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-platform holds central identity. ERO workloads live in sub-sknx-ero-nonproduction and sub-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-synkronyx and billing profile bp-synkronyx-primary with explicit invoice sections inv-sknx-platform, inv-sknx-ero, and inv-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 productCode parameter.
  • 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, app and web, declaring the identical package name with identical scripts, and web contained no source at all. Local development, lifecycle validation, bootstrap, and Playwright targeted app, while the preview workflow and the repository instructions targeted web. 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, because app versus web expresses no platform boundary.
  • Decision:
    1. Consolidate the client into apps/event-route-optimiser/web holding the Astro PWA, retaining the real source and the stricter tsconfig.json.
    2. Name every client directory after the platform it targets. web and mobile are permitted. app is prohibited because it identifies no platform.
    3. Treat the PWA as the mobile experience. A mobile directory is justified only by a capability the PWA cannot deliver, such as background notification delivery, and must not be created for parity alone.
    4. Extract shared domain logic into apps/event-route-optimiser/shared before any second client exists. The Dexie schema, synchronisation engine, conflict resolution, now-and-next query, and routing engine must have exactly one implementation.
  • 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 X and lowercase r.
  • 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.

TokenHexRoleContrast on #09090b
main-stage-black#09090BPrimary backgroundsurface
backstage-grey#18181BCards and raised surfacessurface
strobe-white#FAFAFABody text19.06
route-cyan#00E5FFPrimary actions, links, active state12.93
sunset-orange#FF5C00Connectivity and transitional state6.43
clash-magenta#FF007AConflict and error only5.24
gridline-grey#333338Borders and separators1.58, decoration only

Two brand colours are retained for marketing and data visualisation, and are deliberately not part of the product surface:

TokenHexRoleContrast on #09090b
synkronyx-deep-blue#0072B2Wordmark and marketing on light backgrounds3.84, fails AA for text
synkronyx-sky-blue#56B4E9Marketing accent, data visualisation8.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

  1. Colour is never the sole signal for a state. Pair it with an icon, a label, or a shape. clash-magenta and sunset-orange converge under protanopia, and they signal different things.
  2. clash-magenta is reserved for conflict and error. Using it decoratively destroys the one signal a user scans for.
  3. gridline-grey is 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.
  4. synkronyx-deep-blue must not carry text on a dark surface. At 3.84 it fails AA.
  5. Body text meets WCAG AA. Every foreground token above except gridline-grey does 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.bicep
  • platform/infrastructure/azure/modules/identity.bicep
  • platform/automation/powershell/deploy/deploy-landingzone.ps1