Skip to content

Deployment

Trident runs on two clouds. Cloudflare hosts everything at the edge: the app’s web build, the admin console, and the partner reference implementations. DigitalOcean hosts the stateful core: the API, its database, and Keycloak. Only a test environment exists today.

flowchart TB
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B
  classDef staff fill:#FDE9E2,stroke:#E9724C,color:#12191B
  classDef person fill:#E9724C,stroke:#C55A37,color:#ffffff
  classDef store fill:#ffffff,stroke:#13686C,color:#12191B

  subgraph devices["Devices"]
    phone["<b>Guest phone</b><br/>FINNS app"]:::ours
    browser(["Guest browser"]):::person
    staffpc(["Staff browser"]):::person
    gatedev["<b>Gate reference</b><br/>Android app"]:::ext
  end

  subgraph cf["Cloudflare Workers"]
    appweb["<b>trident-app-web</b><br/>Flutter web, static"]:::ours
    bookw["<b>Booking reference</b><br/>Worker + D1"]:::ext
    gamw["<b>Engagement partner reference</b><br/>Worker + D1 + widget page"]:::ext
    adminw["<b>trident-admin</b><br/>SvelteKit SSR"]:::staff
  end

  subgraph do["DigitalOcean"]
    subgraph drop["Droplet keycloak-1"]
      caddy["<b>Caddy</b> TLS"]:::ext
      kc["<b>Keycloak 26.8</b><br/>+ finns-core extension"]:::ext
    end
    subgraph apl["App Platform"]
      core["<b>core</b><br/>Quarkus native container"]:::ours
    end
    subgraph pgc["Managed PostgreSQL 18"]
      kcdb[("keycloak db")]:::store
      coredb[("core db")]:::store
    end
  end

  jc["<b>JumpCloud</b><br/>staff SSO"]:::ext

  browser --> appweb
  browser --> bookw
  staffpc --> adminw
  phone --> gamw
  phone --> core
  phone --> caddy
  gatedev --> core
  adminw --> jc
  adminw --> core
  bookw --> core
  gamw --> core
  bookw --> caddy
  caddy --> kc
  kc --> kcdb
  kc <--> core
  core --> coredb
  • FINNS platform built and run by FINNS
  • Vendor or third party built or bought outside FINNS, to our contract
  • Staff tool used by FINNS staff, not guests
  • Person guest or staff

Operators reach the Keycloak droplet over Tailscale. This portal is a static Worker (trident-devportal) alongside the others.

Component Host Platform State
Core test-api.finnsbali.com DigitalOcean App Platform, app core Stateless; all state in Postgres
Keycloak test-auth.finnsbeachclub.com Droplet keycloak-1, Docker Compose, Caddy for TLS Its own database in the shared Postgres cluster
PostgreSQL 18 private, sslmode=require DigitalOcean Managed Postgres Core and Keycloak databases
Admin console test-admin.finnsbali.com Cloudflare Worker trident-admin None (sealed cookie sessions)
Guest app (web) trident-app-web.juna.workers.dev Cloudflare Worker, static assets None
Booking reference trident-poc-booking.juna.workers.dev Cloudflare Worker + D1 Sessions in D1
Engagement partner reference trident-poc-gamification.juna.workers.dev Cloudflare Worker + D1 Widget sessions in D1
Guest app (mobile) builds by hand, flavors prod and test iOS, Android Tokens in Keychain / Keystore
Gate reference Android app Android None

Every repository deploys itself from main; pull requests get a preview where the platform supports one.

flowchart LR
  classDef ours fill:#13686C,stroke:#0B4A4D,color:#ffffff
  classDef ext fill:#F3ECDF,stroke:#B9A88B,color:#12191B
  classDef step fill:#ffffff,stroke:#D9CFBE,color:#12191B

  subgraph coreci["core · GitHub Actions on Blacksmith"]
    direction LR
    c1["PR: JVM tests<br/>+ smoke tests"]:::step --> c2["main: GraalVM native build<br/>smoke tests on the binary"]:::step --> c3["Push image to GHCR<br/>:latest and :sha"]:::step --> c4["App Platform<br/>pulls and rolls out"]:::ours
  end

  subgraph cfci["admin · app web · booking · engagement"]
    direction LR
    w1["PR: build<br/>wrangler versions upload<br/>preview alias pr-N"]:::step
    w2["main: build, D1 migrations<br/>wrangler deploy"]:::step --> w3["Cloudflare<br/>Workers"]:::ours
  end

  subgraph manual["By hand"]
    direction LR
    m1["Flutter build<br/>APK / AAB / IPA"]:::step --> m2["Test devices"]:::ext
    k1["Keycloak extension jar<br/>deploy.sh over Tailscale"]:::step --> k2["keycloak-1"]:::ext
  end
Pipeline Trigger What it does
core PR JVM unit and integration tests, then smoke tests
core push to main Native image (Mandrel, container build) → smoke tests against the binary → ghcr.io/…:latest and :<sha> (UBI 9 micro base, non-root)
Workers (admin, app web, booking, engagement) PR Build and wrangler versions upload --preview-alias pr-<n>; production untouched
Workers push to main Typecheck, tests, D1 migrations (partners), wrangler deploy
Mobile apps manual flutter build per flavor; no mobile CI yet
Keycloak extension manual Build the jar, copy to keycloak-1, restart
  • Core reads everything from environment variables (encrypted in App Platform) and refuses to start if a required secret is missing or shorter than 32 characters, or a partner URL isn’t HTTPS.
  • Workers keep plain settings in wrangler.jsonc and secrets as Worker secrets (wrangler secret put), never in the repository.
  • Keycloak reads the extension’s settings from KC_SPI_EVENTS_LISTENER__FINNS_CORE__* variables.
  • Every integration has two secrets per direction pair: the token it presents to core, and the token core presents back. See Security model.
  • The connection pool is sized at 10 for the small managed Postgres plan; core is stateless and can scale out horizontally, and its scheduled jobs are safe to run on several instances (advisory locks or idempotent work).
  • Keycloak runs as a single droplet today, and its database shares the cluster with core.
  • The brief’s target is about 150,000 app users in year one. Neither the Postgres plan nor the single Keycloak node has been sized for that yet; see Roadmap.

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