Skip to main content

Architecture overview

The core idea

GrinGo Pay is a stateless orchestration layer between three actors:

  1. The user (via the Expo mobile app), signing in with native Apple / Google OAuth.
  2. Infinia, which custodies the user's fiat balance, executes FX, and pushes payouts (PIX, FPS, SEPA).
  3. The GrinGo API (Hono on Cloudflare Workers), which orchestrates the three of them.

GrinGo itself:

  • Verifies every incoming request via its own session JWT (issued after validating an Apple or Google ID token).
  • Creates and reads Infinia resources on the user's behalf.
  • Records an authoritative ledger of transactions in Postgres (Neon, via Cloudflare Hyperdrive).
  • Normalises Infinia webhooks into a shared internal event shape.
  • Never touches funds directly. Never signs a payout. Never holds a key. Infinia is the custodian.

System topology

Repo layout

See stack for tool-by-tool versions, data-model for the Postgres schema, custody-model for why Infinia holds user balances, payment-providers for how Infinia is wired behind an abstraction, privacy for the GDPR posture, and observability for the SLIs, SLOs, and alert framework.

Request lifecycle: a PIX payment

The load-bearing flow. Every other flow is a variant of it.

Six Infinia calls from us — the display quote is itself two FX quotes (USDC→BRL then GBP/EUR→USDC), then the leg-1 transfer, the fresh leg-2 quote, the leg-2 transfer, and the payout (3 quotes + 2 transfers + 1 payout) — three webhooks in, total ~20s from tap to merchant "paid". The two FX legs are internal Infinia transfers, so they stay within the point-of-sale latency budget. (The two-hop conversion is rolling out behind the USDC_ACCOUNTS_ENABLED flag.)

Key architectural choices

  • Stateless API. Every request is authenticated by a session JWT we sign ourselves; no server sessions to manage. See auth.
  • Native OAuth for identity. Apple + Google via each provider's ID token, verified against their JWKS. No auth vendor.
  • Infinia holds fiat. GrinGo never custodies. See custody-model for the trade-off analysis and the documented escape hatch back to a wallet-based model if we ever need it.
  • Thin PaymentProvider abstraction. Route handlers never import the Infinia client directly. See payment-providers.
  • Hyperdrive in production, HTTP driver in dev/tests. Same Drizzle schema, two connection factories (apps/api/src/db/index.ts).
  • Webhook idempotency at the API layer. webhook_events is unique on (provider, external_event_id) — a replayed webhook is a no-op.
  • Authoritative internal ledger. Balances are computed from our transactions table, reconciled nightly against Infinia. Vendor swap doesn't require a fresh source of truth.
  • Privacy by design. GDPR shapes the schema, not the other way round. Sensitive columns are encrypted at rest via pgcrypto; documents and selfies never enter our systems (Infinia HOSTED KYC absorbs the special-category exposure); every table has a retention policy. See privacy.
  • Observability defends the PoS promise. Golden-signal SLIs at every boundary, end-to-end PIX latency as the load-bearing SLO, page-worthy alerts with runbooks. See observability.
  • No mocks in tests where possible. API tests use @cloudflare/vitest-pool-workers against a real Neon branch. External services (Infinia) are mocked at the HTTP boundary only.