21-Jun-2026 — A Python Reference Server: the Same API on Azure, Entra and a Hybrid Overlay


This is a companion to From Mock to Production. That article described the journey from a synthetic mock to a production-shaped TypeScript server on Fly, with Ramblers Head Office inheriting a five-function checklist. Since then the picture has changed in one important way. At the 16 June meeting, Ramblers were clear about the stack they work in: Python, Azure and Microsoft Entra ID, and would rather not take on the TypeScript server.

So rather than delete anything that has already been developed, the existing TypeScript implementation was retained and a parallel Python reference server was created that Head Office could adopt in their own stack. Both servers expose the same wire format and stay interchangeable from a consumer's point of view. The Python server is now the default production path; the TypeScript server stays as a reference and fallback.

There was very little to re-architect. The contract has been the primary definition since the 7 April #209 revision, the mock is a fixture consumers reach over HTTP rather than code Head Office maintain, and the Salesforce adapter was always Phase 4 work. So the job was mostly: hand the contract over as a first-class artifact, express Entra in it, and give Head Office a Python server wired to the same interface.

Two servers, one wire contract

From a consumer's point of view the wire format is identical whichever server answers. NGX Ramblers and MailMan do not need to know or care.

flowchart TB
    NGX@{ icon: "ngx:ramblers", label: "NGX Ramblers", pos: "b", h: 48 }
    MM@{ icon: "ngx:mailman", label: "MailMan", pos: "b", h: 48 }

    Wire(["@ramblers/sf-contract<br/>OpenAPI · JSON Schema · types"])

    subgraph ts["TypeScript server (reference)"]
        TSSRV@{ icon: "logos:fly-icon", label: "Express on Fly", pos: "b", h: 48 }
    end

    subgraph py["Python server (default)"]
        PYSRV["FastAPI on Azure Container Apps"]
        OVERLAY["Azure Table overlay"]
        PYSRV --> OVERLAY
    end

    NGX --> Wire
    MM --> Wire
    Wire --> ts
    Wire --> py

The repo is now language-agnostic: the TypeScript server lives in typescript/, the Python server in python/, both consuming @ramblers/sf-contract from a pinned tag.

The Python server

It is a FastAPI app, live at salesforce-server.ngx-ramblers.org.uk on Azure Container Apps. A push to main builds the image, pushes it to the registry and updates the Container App.

The Azure footprint is small, and every secret has one home. The Container App carries a managed identity that reads its secrets from Key Vault; nothing sensitive sits in the image, the repo or a plain environment variable.

flowchart TB
    subgraph azure["Azure"]
        GHA@{ icon: "logos:github-actions", label: "GitHub Actions deploy", pos: "b", h: 48 }
        ACR@{ icon: "logos:docker-icon", label: "Container Registry", pos: "b", h: 48 }
        APP["Azure Container Apps<br/>FastAPI · managed identity"]
        KV["Azure Key Vault<br/>overlay + Salesforce secrets"]
        TBL["Azure Table Storage<br/>consent overlay"]
        GHA -->|build + push| ACR
        ACR -->|deploy image| APP
        APP -->|secrets via managed identity| KV
        APP -->|extension + consent fields| TBL
    end
    SF["Salesforce org<br/>member records"]
    APP -->|native fields · Phase 4| SF

    style azure fill:#E8F0FB,stroke:#5B9BD5,stroke-width:2px,rx:12,ry:12,color:#404143

Inbound requests carry a Microsoft Entra ID token. A consumer fetches a token, calls the API with it, and the server validates it against the tenant JWKS:

flowchart LR
    CONSUMER@{ icon: "ngx:user", label: "Consumer (NGX or MailMan)", pos: "b", h: 48 }
    ENTRA["Microsoft Entra ID<br/>token issuer + JWKS"]
    APP["Azure Container Apps<br/>FastAPI"]

    CONSUMER -->|"1 get token"| ENTRA
    CONSUMER -->|"2 call with Bearer token"| APP
    APP -->|"3 validate against JWKS"| ENTRA

It runs in two modes:

Hybrid persistence: Salesforce is expensive about custom data

Salesforce charges heavily to store your own custom fields. So the attributes it will not hold cheaply - a preferred name, and the granular group, area and other marketing-consent flags - live in a cheap overlay store (Azure Table Storage), keyed by the Salesforce id. A HybridMemberProvider merges them on top of the Salesforce-native fields, so a consumer always sees one member in the exact contract shape and never knows where any field physically lives.

flowchart LR
    REQ([GET members]) --> HYB[HybridMemberProvider]
    HYB --> SRC[Salesforce-native fields]
    HYB --> OVL[Overlay: preferredName + consent flags]
    SRC --> MERGE[One unified member]
    OVL --> MERGE

Each attribute has exactly one home, set by a manifest. A field is either in Salesforce or in the overlay, never both. If Head Office ever want to promote an overlay field into Salesforce, they backfill it and drop it from the manifest; the API contract never changes, so no consumer breaks. The overlay holds consent data, so it is deployed in a UK region, encrypted at rest, with a delete path for erasure.

Entra authentication, as a visible plug-in

The server validates inbound Microsoft Entra ID access tokens (RS256) against the tenant JWKS, checking issuer and audience - the app-to-app (OAuth2 client-credentials) shape Head Office run. Switching it on is three settings - AUTH_MODE=entra, the tenant id and the audience - with no change to the routes. The single validation interface is one function.

The bearer scheme is declared in the OpenAPI document, so the interactive docs at /docs show an Authorize button, exactly like the mock. A consumer pastes an Entra token and tries the live endpoints. This was proven end to end against the live server with a real token from an Entra app registration: no token and a bad token return 401, a valid token returns 200.

The contract as a first-class artifact

@ramblers/sf-contract now ships a committed, static OpenAPI document (openapi.json and openapi.yaml) generated from the same builder the servers use, and attached to each release tag. A CI check fails if the committed document ever stops matching the builder. The security scheme can express Entra (a JWT bearer with the OpenID configuration URL) for the production servers while keeping an opaque bearer for the mock's local mode. This shipped as contract v0.5.0.

If a group adopts the reference server we provide, there is nothing to generate: the typed client and server are already built for them. Generating a client only matters in the other case - a group that decides to build its own implementation, outside what we supply, can generate a typed client in any language straight from that document, with no need to run Node or read TypeScript. See Generating a typed client from the OpenAPI document for the recommended tools and commands.

A portable conformance test drives that static document against the running server and proves the two agree on the wire - the same check any consumer, in any language, can run against its own server.

What Ramblers Head Office inherit

The structural work is done: routing, validation, the generated OpenAPI, the error envelope, Entra auth, and the hybrid overlay. Because the overlay owns the consent and extension fields, the Phase 4 surface is smaller than the TypeScript server's: three methods in the Salesforce source, where someone with Salesforce-side knowledge fills in the SOQL query, the single-member lookup and the consent writeback.

Head Office also get the pieces that make it deployable in their own infrastructure:

Status

Wire format, OpenAPI and the public API are unchanged. NGX Ramblers and MailMan are unaffected, and the mock continues to run at salesforce-mock.ngx-ramblers.org.uk.

If you're Ramblers Head Office: the Python server is standing and open at salesforce-server.ngx-ramblers.org.uk - the /docs page is the quickest way in. Your work is the three-method checklist in ramblers-salesforce-server#3, and the configuration reference in the repo lists every setting and secret you need to run it in your own Azure.