Architecture overview
The core idea
GrinGo Pay is a stateless orchestration layer between three actors:
- The user (via the Expo mobile app), signing in with native Apple / Google OAuth.
- Infinia, which custodies the user's fiat balance, executes FX, and pushes payouts (PIX, FPS, SEPA).
- 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
PaymentProviderabstraction. Route handlers neverimportthe 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_eventsis unique on(provider, external_event_id)— a replayed webhook is a no-op. - Authoritative internal ledger. Balances are computed from our
transactionstable, 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-workersagainst a real Neon branch. External services (Infinia) are mocked at the HTTP boundary only.