Skip to content

Core

Core is the backend of the Trident platform: a single Quarkus project, core, that contains the API every touchpoint shares. It knows who each member is, keeps their points in an append-only ledger, issues the one-time codes that move a guest between the app and partners, and records every gate scan. It deliberately does not hold bookings, rewards or payments: those belong to partners.

Repository: core (plus core/keycloak-extension, a separate jar deployed into Keycloak).

Language
Java 25
Framework
Quarkus 3.39Quarkus REST (RESTEasy Reactive)JacksonHibernate ValidatorQuarkus SchedulerSmallRye Health
Persistence
PostgreSQL 18Hibernate ORM + PanacheFlywaySchema owned by Flyway; Hibernate only validates it. Postgres 18 is required for uuidv7().
Security
Quarkus OIDC (Keycloak JWT)Bearer-token filtersSHA-256 hashing
Outbound HTTP
java.net.http.HttpClient5 s timeouts; HTTPS only (localhost excepted).
Build
MavenGraalVM / Mandrel native imageUBI 9 micro container
Runs on
DigitalOcean App PlatformDigitalOcean Managed Postgres
Tests
JUnit (Quarkus)REST AssuredSmallRye JWT (test tokens)Postgres Dev Services
flowchart TB
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef area fill:#E3F4F3,stroke:#13686C,color:#12191B
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B
  classDef staff fill:#FDE9E2,stroke:#E9724C,color:#12191B
  classDef store fill:#ffffff,stroke:#13686C,color:#12191B
  classDef job fill:#FFF4E8,stroke:#E9724C,color:#12191B

  app["Guest app"]:::ours
  admin["Admin console"]:::staff
  partners["Gate · Booking engine<br/>Sota Platforms"]:::ext
  kchook["Keycloak extension"]:::ext

  subgraph core["Core (Quarkus)"]
    oidc["<b>OIDC</b><br/>Keycloak JWT, azp=trident-app"]:::area
    filters["<b>Bearer-token filters</b><br/>one token per prefix,<br/>checked before routing"]:::area
    appapi["<b>App API</b><br/>/api/v1/app"]:::ours
    adminapi["<b>Admin API</b><br/>/api/v1/admin"]:::ours
    partnerapi["<b>Partner APIs</b><br/>/gate · /booking<br/>/points · /keycloak"]:::ours
    domain["<b>Domain</b> (active records)<br/>Customer · Qr · CheckIn · Handoff<br/>PointsTxn · PointsAccount · PointsEntry · PointsEvent<br/>Staff · StaffEvent"]:::area
    jobs["<b>Scheduled</b><br/>HandoffPurge · PointsCheck<br/>CustomerSync · StaffBootstrap"]:::job
    out["<b>Outbound HTTP</b><br/>KeycloakUsers<br/>SignOutNotifier"]:::area
  end

  db[("PostgreSQL 18")]:::store
  kc["Keycloak<br/>Admin API"]:::ext
  rev["Partners'<br/>/auth/revoke"]:::ext

  app --> oidc --> appapi
  admin --> filters
  partners --> filters
  kchook --> filters
  filters --> adminapi
  filters --> partnerapi
  appapi --> domain
  adminapi --> domain
  partnerapi --> domain
  jobs --> domain
  domain --> db
  appapi --> out
  partnerapi --> out
  out --> kc
  out --> rev
Prefix Caller Auth Endpoints
/api/v1/app Guest app Keycloak JWT, azp=trident-app POST /qrs · POST /handoffs · POST /widget-launches · GET /points · POST /logout
/api/v1/admin Admin console Worker Admin token + X-Finns-Actor-Sub GET /staff-access/{sub} · POST /staff-access/sign-ins · staff CRUD with If-Match · GET /customers · GET /customers/{sub} · POST /customers/{sub}/credits|debits (customers are addressed by Keycloak subject)
/api/v1/gate Gates Gate token POST /check-ins
/api/v1/booking Booking engine Booking token POST /handoffs/peek · POST /handoffs/redeem · POST /bookings
/api/v1/points Engagement partner Points token GET /customers/{extId} · POST /credits|debits · POST /transactions/{id}/refund · GET /events · POST /widget-launches/redeem · GET /reconciliation/{date}
/api/v1/keycloak Keycloak extension Webhook token POST /user-changes
/q/health/ready Platform none Readiness, including the database

Errors everywhere are RFC 9457 problem details with a stable code (see Integration conventions).

Job Schedule Multi-instance safety What it does
HandoffPurge every 60 s Idempotent Deletes one-time codes an hour after they expire
PointsCheck 02:30 Bali (0 30 18 * * ? UTC) Advisory lock, one instance Recomputes every balance from the ledger; logs points.check_failed on any mismatch
CustomerSync.reconcile every 10 min (1 min after boot) Idempotent Re-reads Keycloak users to repair missed webhooks and deletions
StaffBootstrap at startup Advisory lock Seeds the first admins from FINNS_STAFF_BOOTSTRAP_ADMINS

A small Java jar (finns-core event listener, built against Keycloak 26.8) that tells core when a guest account changes, so core never has to poll for new members.

sequenceDiagram
  autonumber
  box rgb(246,241,233) Keycloak
    participant KC as Keycloak
    participant EXT as finns-core listener
  end
  box rgb(227,244,243) Core
    participant CORE as Core
  end

  KC->>EXT: User event (REGISTER, LOGIN, UPDATE_PROFILE,<br/>UPDATE_EMAIL, DELETE_ACCOUNT, IdP link…)<br/>or admin event on users/<id>
  Note over EXT: nowrap:Wait for Keycloak's<br/>transaction to commit
  EXT-)CORE: POST /api/v1/keycloak/user-changes {userId}
  alt Core can't reach Keycloak right now
    CORE-->>EXT: 503
    Note over EXT: nowrap:Retry after 1 s, 5 s, 30 s
  else
    CORE->>KC: GET /admin/realms/finns/users/{id} (finns-core-sync)
    KC-->>CORE: User (or 404 if deleted)
    Note over CORE: nowrap:Upsert customer, or mark it deleted.<br/>New customer → customer.created event
    CORE-->>EXT: 204
  end

The notice carries only the user id; core fetches the user itself, so a forged notice can’t inject data.

Core reads its settings from environment variables and refuses to start if a required one is missing, a token is shorter than 32 characters, or a partner URL isn’t HTTPS.

Variable Purpose
QUARKUS_DATASOURCE_* PostgreSQL connection
FINNS_AUTH_ISSUER Keycloak realm URL; the app’s tokens must come from here
FINNS_ADMIN_API_TOKEN Admin console’s token
FINNS_GATE_API_TOKEN Gates’ token
FINNS_BOOKING_API_TOKEN / FINNS_BOOKING_REVOKE_TOKEN / FINNS_BOOKING_URL Booking engine: inbound token, outbound revoke token, origin
FINNS_POINTS_API_TOKEN Engagement partner’s token
FINNS_WIDGET_URL / FINNS_WIDGET_REVOKE_TOKEN Engagement partner’s widget origin and revoke token
FINNS_KEYCLOAK_WEBHOOK_TOKEN / FINNS_KEYCLOAK_CLIENT_SECRET Keycloak extension’s token; finns-core-sync secret
FINNS_KEYCLOAK_RECONCILE_EVERY Reconciliation interval (default 10 min)
FINNS_CORS_ORIGINS Origins allowed to call /api/v1/app from a browser (the app’s web build)
FINNS_STAFF_BOOTSTRAP_ADMINS First admins’ emails

Tunables with defaults: QR lifetime 60 s, handoff and widget code lifetime 60 s, earn rates (finns.points.earn.*), reconciliation time zone Asia/Makassar.

  • Active-record domain. Entities are Panache active records with plain id fields, no JPA associations; each use case is one explicit, short transaction.
  • Concurrency by the database. Single-use codes are consumed by one conditional UPDATE; balances move with a conditional update that doubles as the member’s lock; staff changes take an advisory lock; staff edits use optimistic locking (If-Match / 412).
  • Outbox-style feed. points_event is written in the same transaction as the change and served in commit order below pg_snapshot_xmin, so readers never miss a late commit.
  • No broker, no cache. At current scale every call is a few indexed queries. Revoke callbacks are async fire-and-forget with one retry.
  • Logs are structured key=value lines without tokens or bodies. There are no metrics or traces yet.

Internal and confidential. Share partner sections only as partner builds (pnpm build:partner).