Authentication
Three trust boundaries: user → API (Privy JWT), API → Bridge (API key), and Bridge → API (HMAC-signed webhook).
End-user auth (mobile → API)
Every non-webhook API route requires a Privy access token. The mobile app obtains this from Privy after login and attaches it as Authorization: Bearer <token> on every API request.
Verification
apps/api/src/auth/middleware.ts verifies the token using verifyAccessToken from @privy-io/node, fetching JWKS from:
https://auth.privy.io/api/v1/apps/{PRIVY_APP_ID}/jwks.json
On success the middleware sets on the Hono context:
c.set("privyDid", claims.user_id)— Privy user id (did:privy:...)c.set("userId", existing.id)— GrinGousers.idUUID
On any of (missing header, malformed token, invalid signature, expired) the middleware returns 401.
First-time provisioning
The first authenticated request from a new user is POST /auth/me, which idempotently:
- Looks up the user by
privyDid. Creates the row if missing. - Records the user's smart wallet + embedded EOA addresses in
wallets. - Returns
{ userId, walletAddress, kycStatus }.
Subsequent screens gate on kycStatus.
What's in a Privy JWT
Only user_id is trusted — everything else (email, wallet addresses) comes from the request body and is verified against Privy's server-side data before storage.
Server-side auth (API → Bridge)
The API authenticates to Bridge using an API key set at deploy time as a Cloudflare secret:
- Header:
Api-Key: sk-live-...(production) orsk-test-...(sandbox). - The Bridge client (
apps/api/src/bridge/client.ts) infers sandbox vs. prod from the key prefix and switches base URLs accordingly. - Every
POSTsends a freshIdempotency-Key: <uuid>header.
Webhook auth (Bridge → API)
Key properties:
- Signature scheme: RSA-SHA256 over
<timestamp>.<raw body>. - Replay window: 600 seconds (
apps/api/src/webhooks/bridge.ts). - Deduplication:
webhook_events.bridgeEventIdis unique; replayed events are silently 200'd.
On-chain auth
The Privy embedded wallet's signer is an EOA held by the user (managed by Privy). GrinGo has no key. Every USDC transfer is initiated by the mobile app calling Privy's SDK to prepare + sign + submit the tx — the API never sees or requests a signature.