FASSTAP / Worldline — Visual Architecture¶
Companion to 003-fasstap-configuration.md. Diagrams here render natively on GitHub via Mermaid.
1. System map — who talks to whom¶
flowchart LR
subgraph Device["Android Device (applicationId io.sagio.merchant)"]
UI["Settings UI<br/>SettingsScreen.kt"]
VM["SettingsViewModel"]
Repo["SettingsRepository<br/>EncryptedSharedPreferences"]
Mgr["FasstapManager"]
SDK["Soft Space MPOS SDK<br/>v3.6.5.0 (.aar)"]
UI --> VM --> Repo --> Mgr --> SDK
end
subgraph Sagio["Sagio Backend (Hono on Cloudflare)"]
Auth["/merchant-app/auth"]
Cfg["/merchant-app/fasspay-config"]
WLC["worldlineClient<br/>(future, server-only)"]
ConfigObj["config.fasspay<br/>env + baked-in defaults"]
Cfg --> ConfigObj
Auth --> ConfigObj
WLC --> ConfigObj
end
subgraph SoftSpace["Soft Space / Worldline Infrastructure"]
Att["Attestation Host<br/>mpos-uat.fasspay.com:9001/v2"]
Key["Keyloading Host<br/>mpos-uat.fasspay.com:9002/key/api/v3"]
REST["Worldline REST<br/>(settlement, reporting)"]
end
Repo -- "GET fasspay-config<br/>(Bearer token)" --> Cfg
Repo -- "POST auth<br/>(API key + secret)" --> Auth
SDK -- "attest + token refresh" --> Att
SDK -- "key loading" --> Key
WLC -- "X-SSO-ID header" --> REST
Key boundary: the Android device never talks to Worldline REST directly. The phone only authenticates to Soft Space via the SDK using libAccessKey / libSecretKey. Direct Worldline REST calls (settlement, reporting) go from our backend with X-SSO-ID.
2. First-launch bootstrap¶
sequenceDiagram
autonumber
participant U as User
participant App as Merchant App
participant BC as BuildConfig
participant API as Sagio API
participant Cfg as config.fasspay
participant Prefs as EncryptedSharedPrefs
participant SDK as Soft Space SDK
U->>App: Launch app
App->>BC: read SAGIO_API_KEY / SAGIO_API_SECRET
Note over BC: Injected at build time via<br/>local.properties or env vars
App->>API: POST /merchant-app/auth<br/>{ apiKey, apiSecret, deviceId }
API-->>App: { accessToken, refreshToken, merchantId }
App->>API: GET /merchant-app/fasspay-config<br/>Authorization: Bearer ...
API->>Cfg: read merged env + defaults
Cfg-->>API: 11 fields (host, cert, IDs, env)
API-->>App: { ...11 fields }
App->>Prefs: encrypt + persist MerchantConfig
App->>SDK: SSMPOSSDK.init(config)
SDK->>SDK: setLibAccessKey / SecretKey / UniqueID / DeveloperID +<br/>setAttestationHost / setKeyloadingHost / ...
SDK-->>App: ready (sdkVersion, cotsId)
If /fasspay-config rejects with "Fasstap configuration missing: uniqueId, developerId", the API operator is missing one of the four required env vars. The UI shows MISSING on the Worldline row.
3. Where each of the 11 SDK fields comes from¶
flowchart TD
classDef required fill:#fee,stroke:#c33,stroke-width:2px
classDef baked fill:#efe,stroke:#3a3
classDef envOverride fill:#eef,stroke:#33c
EnvVars["apps/api/.env.local<br/>or secrets manager"]:::required
Defaults["Baked into config/index.ts<br/>(from SDK demo v3.6.5.0)"]:::baked
EnvVars -- "ACCESS_KEY" --> AK[accessKey]:::required
EnvVars -- "SECRET_KEY" --> SK[secretKey]:::required
EnvVars -- "UNIQUE_ID" --> UID[uniqueId]:::required
EnvVars -- "DEVELOPER_ID" --> DID[developerId]:::required
EnvVars -- "ENVIRONMENT (default UAT)" --> ENV[environment]:::envOverride
Defaults -- "UAT host" --> AH[attestationHost<br/>mpos-uat.fasspay.com:9001/v2]:::baked
Defaults -- "UAT host" --> KH[keyloadingHost<br/>mpos-uat.fasspay.com:9002/key/api/v3]:::baked
Defaults -- "sha256 pin" --> ACP[attestationCertPinning]:::baked
Defaults -- "sha256 pin (same)" --> KCP[keyloadingCertPinning]:::baked
Defaults -- "SSPaymentCA PEM" --> CA[keyloadingCACert]:::baked
Defaults -- "Soft Space's project<br/>757874674469" --> GP[googlePlayProjectNumber]:::baked
AK & SK & UID & DID & ENV & AH & KH & ACP & KCP & CA & GP --> Resp["GET /merchant-app/fasspay-config<br/>response payload"]
Resp --> SDKBuilder["SSMPOSSDKConfiguration.Builder"]
SSO[ssoId<br/>FASSTAP_SSO_ID]:::envOverride -.->|backend-only,<br/>not in response| WLREST["Worldline REST<br/>X-SSO-ID header"]
Legend: - 🔴 Required — must be set as env var; endpoint rejects if blank. - 🟢 Baked-in default — works out of the box for UAT; override via env var if Soft Space rotates. - 🔵 Override — has a sensible default; env var only needed for non-UAT or non-default behavior.
4. Settings UI states¶
stateDiagram-v2
[*] --> Loading: App opens Settings
Loading --> Missing: BuildConfig keys empty<br/>OR API unreachable
Loading --> Ready: fasspay-config returned all required
Loading --> Partial: API returned but missing<br/>uniqueId / developerId
Missing --> Refreshing: tap "Refresh SDK Credentials"
Partial --> Refreshing: tap "Refresh SDK Credentials"
Ready --> Refreshing: tap "Refresh SDK Credentials"
Refreshing --> Ready: 200 + all required present
Refreshing --> Partial: 500 "missing: uniqueId, developerId"
Refreshing --> Missing: 401 / network error
state Ready {
[*] --> ShowEnv
ShowEnv: "Environment: UAT" displayed
}
state Missing {
[*] --> ShowError
ShowError: "Not loaded yet" + MISSING badge
}
The merchant only ever interacts with two controls: Refresh and (optionally) the Sagio API base URL field. They never type Worldline values.
5. Transaction flow (steady state)¶
sequenceDiagram
autonumber
participant Cust as Customer Card
participant Mer as Merchant App
participant SDK as Soft Space SDK
participant Att as Attestation Host
participant Key as Keyloading Host
participant MPOS as MPOS Host
Mer->>SDK: startTransaction(amount, ref)
SDK->>Att: ensure fresh attestation token
Att-->>SDK: token (refreshed every 5min)
SDK->>Key: load DUKPT working keys
Key-->>SDK: keys
SDK-->>Mer: state = WaitingForCard
Cust->>SDK: NFC tap
SDK-->>Mer: state = CardDetected → CardReadOk
SDK->>MPOS: authorize (encrypted PAN, amount)
MPOS-->>SDK: outcome (approved / declined)
SDK-->>Mer: state = Success(outcome) / Failed
Our app code in FasstapManager.kt:132-184 (startTransaction) only observes the MPOSTransaction.TransactionEvents callbacks — the attestation refresh, key loading, and authorization roundtrips are managed entirely inside the SDK.
6. Build & deploy topology¶
flowchart LR
subgraph Dev["Local Dev"]
LP["apps/sagio/local.properties<br/>SAGIO_API_KEY=...<br/>SAGIO_API_SECRET=..."]
EnvLocal["apps/api/.env.local<br/>FASSTAP_*"]
end
subgraph CI["CI / Release"]
Secrets["CI secrets store"]
GradleBuild["./gradlew assembleRelease"]
ApiDeploy["wrangler deploy"]
end
subgraph Runtime["Runtime"]
APK["Signed APK<br/>BuildConfig.SAGIO_API_KEY baked in"]
Worker["Cloudflare Worker<br/>process.env.FASSTAP_*"]
end
LP -. local build .-> APK
Secrets -- env vars --> GradleBuild --> APK
EnvLocal -. local dev .-> Worker
Secrets -- env vars --> ApiDeploy --> Worker
APK -- HTTPS --> Worker
Two independent credential pipelines:
- Merchant app side: Sagio API key/secret → Gradle local.properties or CI env → BuildConfig → baked into APK
- API side: FASSTAP_* env vars → process.env → config.fasspay → response payload
Neither path requires merchant-side data entry.
7. What lives where in the repo¶
| Concern | File |
|---|---|
| Baked-in UAT defaults | apps/api/src/config/index.ts (fasspay block) |
| Merchant-app config endpoint | apps/api/src/controllers/merchantAppController.ts → getFasstapConfig |
| Mint Sagio API key/secret | apps/api/src/scripts/mintMerchantKey.ts |
| Android applicationId (Soft Space-whitelisted) | apps/sagio/app/build.gradle.kts:16 |
BuildConfig wiring for Sagio API creds |
apps/sagio/app/build.gradle.kts:33-34 |
| Encrypted on-device persistence | apps/sagio/app/src/main/java/io/sagio/merchant/data/SettingsRepository.kt |
| SDK config builder call | apps/sagio/app/src/main/java/io/sagio/merchant/sdk/FasstapManager.kt (initialize) |
| Settings screen (status + refresh only) | apps/sagio/app/src/main/java/io/sagio/merchant/ui/screens/SettingsScreen.kt |
| SDK binaries | apps/sagio/app/libs/ssmobile-*.aar |
| TypeScript SDK wrapper (server-side) | packages/worldline-sdk/src/ |
Related¶
- 001 — Manual Tap Overview
- 002 — Security
- 003 — FASSTAP Configuration — the prose companion to this doc.