Skip to main content

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) — GrinGo users.id UUID

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:

  1. Looks up the user by privyDid. Creates the row if missing.
  2. Records the user's smart wallet + embedded EOA addresses in wallets.
  3. 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) or sk-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 POST sends a fresh Idempotency-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.bridgeEventId is 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.