INSIDE ATLANTIS
Logical architecture, software and configuration

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.

ComponentVersionNotes
Keycloak (exchange and Poseidon ID)26.8.1quay.io/keycloak/keycloak:26.8, OpenJDK 21.0.12
Atlantis Keycloak extensionsbuilt from sourcedeploy/keycloak-extensions, compiled against the running Keycloak's jars
Auth0Essentials planUniversal Login, custom domain, Actions, Management API
Node.js (all Node services)22.23.4node:22-bookworm-slim
Express5.2.1HTTP framework for the services
jose6.2.8JWT, JWE, JWKS: request objects, client assertions, SD-JWT
@simplewebauthn/server13.3.2Agency passkeys
PostgreSQL16.15Keycloak storage (two schemas)
Caddy2.8.4HTTPS for eight domains, Let's Encrypt
Docker / Compose29.9.2 / 5.6.1All services in one Compose project
Ubuntu24.04.4 LTSBinaryLane VPS, Adelaide, 2 vCPU, 4 GB RAM, 60 GB disk
Atlantis WalletSwift 6, iOS 18 targetBuilt with Xcode 27.1, P-256 key in the Keychain
NFCPassportReader (wallet)2.3.3ICAO 9303 chip reading: PACE, BAC, secure messaging, active and chip authentication
Playwright (recordings, checks)1.56.1Scripted use cases and live TDIF checks
TypeScript / tsx5.9.3 / 4.23.12Service and tooling runtime

Memory on the server

Exchange (Keycloak)648 MB
Poseidon ID (Keycloak)570 MB
Each Node serviceabout 90 MB
PostgreSQL60 MB
Caddy18 MB
Whole server2.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 clientsagency-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-partiesApplies 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 objectsRequired. Signed by the agency (ES256) and encrypted to the realm's RSA-OAEP-256 key with A256GCM
Token mappersSHA-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 assuranceacr.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 providersatlantis-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 gatetdif-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 purgeEvent 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 themeatlantis-exchange: the provider chooser 'Who should confirm it's you?'
KeysFixed 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.

NbfMapperAdds nbf to ID and access tokens (AGDIS Sch 2 §1.8.3.1)
RpAuditIdMapperAdds rp_audit_id, equal to the exchange's code_id for the sign-in (Sch 1 §2.1.2)
AcrGateAuthenticatorPost-login gate that enforces an essential acr (Sch 2 §1.9.3)
AttributePurgeListenerDeletes attributes after the transaction (role of the IXP)
SignedRequestOidcProviderOverrides 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 domainlogin.id-atlantis.xyz (Universal Login with an Atlantis template)
Sign-inIdentifier first; passkeys and passwords on the Username-Password-Authentication connection
Post-login Actionatlantis-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 applicationRegular web app; private_key_jwt with the exchange's RS256 key as a client credential; ID tokens 300 seconds
Portal applicationAtlantis ID portal: account page, user management through the Management API
Identity verificationAtlantis 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
LimitsSigned 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.

FormatSD-JWT VC (dc+sd-jwt; legacy vc+sd-jwt also offered), holder-bound with cnf
Issuer identitydid:web:id-atlantis.xyz, key published at /.well-known/did.json; /.well-known/jwt-vc-issuer and /.well-known/openid-credential-issuer
IssuanceOpenID4VCI pre-authorised code with a 4-digit transaction code, three attempts, single-use offers by reference or by value
Claimsgiven name, family name, birthdate, over 18, email, Atlantis ID number, each selectively disclosable
PresentationOpenID4VP, DCQL for four claims, direct_post; unsigned request (redirect_uri) or signed request (x509_san_dns) for other wallets
VerificationTrusted-issuer list, did:web resolution, signature, disclosure hashes, KB-JWT nonce, audience and sd_hash, no unrequested claims
Trust failure demodid: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-inCode flow, PKCE S256, signed and encrypted request objects, private_key_jwt with a persistent ES256 key at /jwks
AccountsOwn account numbers (A-, C-, B-), persisted on the data volume; linking a further sign-in needs proof of record ownership
Step-upAgency 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
PasskeysOptional, RP ID = the agency's own host, user verification required, no exchange on return
LogoutRP-initiated logout and OIDC front-channel logout from the exchange

Hosting and operations

One VPS, one Compose project, eight HTTPS domains.

ServerBinaryLane, Adelaide: 2 vCPU, 4 GB, Ubuntu 24.04; key-only administrative access; only web ports open
TLSCaddy with Let's Encrypt; HTTP/1.1 and 2; Keycloak admin consoles blocked publicly
DNSVercel; each party has its own registrable .xyz domain, so browsers treat them as separate organisations
SecretsKept in a protected file on the server and never in source control
MonitoringServices post safe events to the hub; a collector polls Auth0 logs and Keycloak events; HMAC-chained log checked at /events/verify
Reset and checksA reset script returns every party to a clean start and checks each site; scripted live checks exercise the TDIF behaviour