Architecture
What runs where, at which version, and the settings that make the TDIF profile work. For the moving picture, open the animated flows.
Logical architecture
Choose a flow to animate it. The same view is on the Animated flows page at full size.
Software and versions
As read from the running system.
| Component | Version | Notes |
|---|---|---|
| Keycloak (exchange and Poseidon ID) | 26.8.1 | quay.io/keycloak/keycloak:26.8, OpenJDK 21.0.12 |
| Atlantis Keycloak extensions | built from source | deploy/keycloak-extensions, compiled against the running Keycloak's jars |
| Auth0 | Essentials plan | Universal Login, custom domain, Actions, Management API |
| Node.js (all Node services) | 22.23.4 | node:22-bookworm-slim |
| Express | 5.2.1 | HTTP framework for the services |
| jose | 6.2.8 | JWT, JWE, JWKS: request objects, client assertions, SD-JWT |
| @simplewebauthn/server | 13.3.2 | Agency passkeys |
| PostgreSQL | 16.15 | Keycloak storage (two schemas) |
| Caddy | 2.8.4 | HTTPS for eight domains, Let's Encrypt |
| Docker / Compose | 29.9.2 / 5.6.1 | All services in one Compose project |
| Ubuntu | 24.04.4 LTS | BinaryLane VPS, Adelaide, 2 vCPU, 4 GB RAM, 60 GB disk |
| Atlantis Wallet | Swift 6, iOS 18 target | Built with Xcode 27.1, P-256 key in the Keychain |
| NFCPassportReader (wallet) | 2.3.3 | ICAO 9303 chip reading: PACE, BAC, secure messaging, active and chip authentication |
| Playwright (recordings, checks) | 1.56.1 | Scripted use cases and live TDIF checks |
| TypeScript / tsx | 5.9.3 / 4.23.12 | Service and tooling runtime |
Memory on the server
| Exchange (Keycloak) | 648 MB |
| Poseidon ID (Keycloak) | 570 MB |
| Each Node service | about 90 MB |
| PostgreSQL | 60 MB |
| Caddy | 18 MB |
| Whole server | 2.4 of 3.9 GB used |
Identity exchange (Keycloak realm atlantis)
Defined in deploy/keycloak/atlantis-realm.json, with secrets substituted from the server's environment at import.
| Relying-party clients | agency-a and agency-c: confidential, client authentication private_key_jwt (ES256) against the agency's /jwks, consent required, front-channel logout to /logout/frontchannel |
| Client policy tdif-relying-parties | Applies profile tdif-agdis-rp to clients marked tdif.profile=agdis-rp: secure-request-object (encryption required, nbf verified, 1-hour window), PKCE S256 enforcer, private_key_jwt only |
| Request objects | Required. Signed by the agency (ES256) and encrypted to the realm's RSA-OAEP-256 key with A256GCM |
| Token mappers | SHA-256 pairwise subject (sector = the agency's host, secret salt); acr from the provider's TDIF level; nbf = iat; rp_audit_id = the exchange's code_id |
| Levels of assurance | acr.loa.map holds the 13 levels of the AGDIS Data Standards (ip1:cl1 → 1 up to ip4:cl3 → 13). ID tokens last 300 seconds |
| Identity providers | atlantis-id (Auth0) and poseidon-id (Keycloak): code flow + PKCE, private_key_jwt (RS256), pass max_age, post-login flow tdif-post-broker-login. Poseidon receives signed request objects; Atlantis ID accounts are linked by Auth0's pairwise claim, without calling userinfo |
| Post-login gate | tdif-post-broker-login runs the essential-acr gate: if the agency required a level the provider didn't reach, the exchange refuses instead of issuing a token |
| Attribute purge | Event listener atlantis-attribute-purge: after an agency's token call, the brokered user's names, email, username and attributes are deleted; a pseudonymous record keeps the provider link, pairwise mapping and consent |
| Login theme | atlantis-exchange: the provider chooser 'Who should confirm it's you?' |
| Keys | Fixed RS256 signing key (its thumbprint registered at Auth0 for private_key_jwt); generated RSA-OAEP-256 encryption key; HS512 and AES internal keys |
Atlantis Keycloak extensions
Five small Java providers in deploy/keycloak-extensions, built on the server against Keycloak's own jars and mounted into the exchange. They use SPIs Keycloak calls internal, so they're pinned to 26.8.
| NbfMapper | Adds nbf to ID and access tokens (AGDIS Sch 2 §1.8.3.1) |
| RpAuditIdMapper | Adds rp_audit_id, equal to the exchange's code_id for the sign-in (Sch 1 §2.1.2) |
| AcrGateAuthenticator | Post-login gate that enforces an essential acr (Sch 2 §1.9.3) |
| AttributePurgeListener | Deletes attributes after the transaction (role of the IXP) |
| SignedRequestOidcProvider | Overrides the OIDC broker: signed request objects to Poseidon, and linking by a pairwise claim for Atlantis ID (Sch 2 §2, Sch 1 §1.2.3) |
Atlantis ID on Auth0
An Auth0 tenant on the Essentials plan, branded as Atlantis ID.
| Custom domain | login.id-atlantis.xyz (Universal Login with an Atlantis template) |
| Sign-in | Identifier first; passkeys and passwords on the Username-Password-Authentication connection |
| Post-login Action | atlantis-auth-methods: adds the sign-in methods, tdif_acr (the proofing level from app_metadata.tdif_ip with passkey → CL2, password → CL1; IP2 with a password is asserted as IP1 Plus / CL1) and https://id-atlantis.xyz/pairwise_sub (HMAC-SHA-256 of client and user id) |
| Exchange's application | Regular web app; private_key_jwt with the exchange's RS256 key as a client credential; ID tokens 300 seconds |
| Portal application | Atlantis ID portal: account page, user management through the Management API |
| Identity verification | Atlantis Wallet reads an Australian passport chip over NFC (PACE or BAC, then EF.SOD, DG1, DG14, DG15; never the photo). Atlantis ID checks the hashes, the Document Signer's signature, the chain to DFAT's CSCA and DFAT's CRL, and raises the identity to IP1 Plus. A Medicare card (simulated source verification) then gives IP2 |
| Limits | Signed request objects (JAR) need Enterprise with the Highly Regulated Identity add-on; Auth0 can't change its global sub |
Credential issuer and verifier
Atlantis ID issues; Agency B verifies. No exchange or identity provider takes part in a wallet sign-in.
| Format | SD-JWT VC (dc+sd-jwt; legacy vc+sd-jwt also offered), holder-bound with cnf |
| Issuer identity | did:web:id-atlantis.xyz, key published at /.well-known/did.json; /.well-known/jwt-vc-issuer and /.well-known/openid-credential-issuer |
| Issuance | OpenID4VCI pre-authorised code with a 4-digit transaction code, three attempts, single-use offers by reference or by value |
| Claims | given name, family name, birthdate, over 18, email, Atlantis ID number, each selectively disclosable |
| Presentation | OpenID4VP, DCQL for four claims, direct_post; unsigned request (redirect_uri) or signed request (x509_san_dns) for other wallets |
| Verification | Trusted-issuer list, did:web resolution, signature, disclosure hashes, KB-JWT nonce, audience and sd_hash, no unrequested claims |
| Trust failure demo | did:web:id-atlantis.xyz:untrusted issues the same credential; Agency B refuses it |
Agencies
Agencies A and C share one code module (services/oidc-agency.ts); Agency B is a separate verifier.
| Sign-in | Code flow, PKCE S256, signed and encrypted request objects, private_key_jwt with a persistent ES256 key at /jwks |
| Accounts | Own account numbers (A-, C-, B-), persisted on the data volume; linking a further sign-in needs proof of record ownership |
| Step-up | Agency A's payment details need an essential ip1:cl2; Agency C's full health record needs an essential ip2:cl2. Both use prompt=login and max_age=0 |
| Passkeys | Optional, RP ID = the agency's own host, user verification required, no exchange on return |
| Logout | RP-initiated logout and OIDC front-channel logout from the exchange |
Hosting and operations
One VPS, one Compose project, eight HTTPS domains.
| Server | BinaryLane, Adelaide: 2 vCPU, 4 GB, Ubuntu 24.04; key-only administrative access; only web ports open |
| TLS | Caddy with Let's Encrypt; HTTP/1.1 and 2; Keycloak admin consoles blocked publicly |
| DNS | Vercel; each party has its own registrable .xyz domain, so browsers treat them as separate organisations |
| Secrets | Kept in a protected file on the server and never in source control |
| Monitoring | Services post safe events to the hub; a collector polls Auth0 logs and Keycloak events; HMAC-chained log checked at /events/verify |
| Reset and checks | A reset script returns every party to a clean start and checks each site; scripted live checks exercise the TDIF behaviour |