Self-hosting
Run the PeridotID API yourself
Requirements
- Node.js 20+
- pnpm
- PostgreSQL 16+ (Supabase works great)
Refresh-token state lives in the sessions table, so PostgreSQL is the only datastore — no Redis needed.
Environment variables
All configuration is loaded from .env in the API package. Copy
apps/api/.env.example to apps/api/.env and fill it in:
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | yes | PostgreSQL connection string. For Supabase, use the transaction pooler (port 6543) with ?pgbouncer=true&connection_limit=1 |
JWT_ACCESS_SECRET | yes | Secret for signing access tokens (32+ chars) |
JWT_REFRESH_SECRET | yes | Secret for signing refresh tokens (32+ chars) |
GOOGLE_CLIENT_ID | yes* | Google OAuth client ID (same key name in every env — values differ) |
GOOGLE_CLIENT_SECRET | yes* | Google OAuth client secret (same key name in every env) |
GOOGLE_CALLBACK_URL | yes* | Must match the authorized redirect URI |
COOKIE_SECURE | no | true in production (cookies only over HTTPS) |
COOKIE_DOMAIN | no | Cookie domain; omit for localhost |
COOKIE_SAMESITE | no | lax (default) or none (cross-site clients, requires COOKIE_SECURE=true) |
CLIENT_SUCCESS_URL | no | Where users land after Google sign-in |
CORS_ORIGINS | no | Comma-separated extra client origins allowed by CORS |
PID_PROGRAM_ID | yes | Solana program id the wallet derives PDAs against |
WEBAUTHN_RP_ID / WEBAUTHN_RP_NAME / WEBAUTHN_ORIGINS | yes | Passkey relying-party config |
CLIENT_REDIRECT_ALLOWLIST | no | Comma-separated origins allowed for cross-origin SSO |
SOLANA_NETWORK | no | devnet (default) or mainnet-beta |
ACCESS_TOKEN_TTL | no | Access token lifetime (default 15m) |
REFRESH_TOKEN_TTL | no | Refresh token lifetime (default 30d) |
APP_TOKEN_TTL | no | Machine (client-credentials) token lifetime (default 1h) |
PORT | no | HTTP port (default 3301) |
Fiat (DOKU Checkout + internal ledger)
Fiat is one namespace: DOKU Checkout is money-in only (no Sub-Account); every balance lives on the internal fiat ledger. See Fiat & payments.
| Variable | Required | Description |
|---|---|---|
DOKU_MODE | no | sandbox (default) or production |
DOKU_CLIENT_ID / DOKU_SECRET_KEY | yes* | DOKU merchant credentials (Checkout) |
DOKU_PRIVATE_KEY | no | RSA PEM for legacy Sub-Account calls (not needed for Checkout-only) |
DOKU_WEBHOOK_URL | no | Public URL for DOKU payment notifications (/v1/fiat/webhook) |
DOKU_CHECKOUT_NOTIFY_URL | no | Per-payment notify override (defaults to DOKU_WEBHOOK_URL) |
PID_FIAT_LEDGER_ENABLED | no | Master switch (default true) |
PID_FIAT_LEDGER_FROZEN | no | Freeze new issues/sends (default false) |
PID_FIAT_LEDGER_TREASURY_PID | no | Label for the global-fee treasury (env, never hardcoded) |
PID_REDEMPTION_ENABLED | no | Bank withdrawals (default false; disabled for now) |
* DOKU endpoints fail at call time until these are set. The global fee policy
is versioned in the DB (fiat_fee_policies) and can be published by an admin
(POST /v1/fiat/admin/fee-policy); per-app fees are configured per app in the
workspace.
* Google endpoints return 503 until these are set. Dev and prod run the
identical OAuth code path — only the values differ per environment. Create a
client per environment at the
Google Cloud console:
register GOOGLE_CALLBACK_URL (e.g. http://localhost:3301/v1/auth/google/callback)
on the dev client and the prod callback URL on the prod client.
Local development with Docker
docker compose up -d # Postgres
pnpm install
pnpm db:migrate # apply Prisma migrations
pnpm dev # API on http://localhost:3301, docs on http://localhost:3300Deploy to Vercel + Supabase
-
Supabase — create a project, then run migrations against it (use the session pooler, port
5432, for migrations):DATABASE_URL="postgresql://postgres.<ref>:<password>@aws-0-<region>.pooler.supabase.com:5432/postgres" \ pnpm --filter @peridotvault/pid-api db:deploy -
Vercel — API (
apps/api/vercel.jsonis already configured for serverless). Add a project rooted atapps/apiand set these env vars:DATABASE_URL(transaction pooler, port6543,?pgbouncer=true),JWT_ACCESS_SECRET,JWT_REFRESH_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_CALLBACK_URL,COOKIE_SECURE=true,COOKIE_SAMESITE,CLIENT_SUCCESS_URL,CORS_ORIGINS. -
Vercel — docs (
apps/web/vercel.jsonis already configured). Add a project rooted atapps/web; no env vars needed. -
Attach your custom domains and point DNS at Vercel.
Production build
pnpm build
pnpm --filter @peridotvault/pid-api db:deploy
pnpm --filter @peridotvault/pid-api startLegacy-database migration notes
Migrations are additive where possible, but the PID renames (identityId → pid,
PidAccount removal (1 identity = 1 wallet) rewrite identity-linked tables. For a fresh
database just run db:deploy + seed. For a database with existing rows from older
releases:
-
Email-duplicate audit — a non-null email is unique (one email = one PID). Resolve collisions before migrating (the uniqueness index fails otherwise):
SELECT lower(email) AS email, count(*), array_agg("pid") FROM identity_credentials WHERE email IS NOT NULL GROUP BY lower(email) HAVING count(*) > 1; -
Deploy with
pnpm --filter @peridotvault/pid-api db:deploy(same flow as the Supabase steps above). -
No backfill. Existing users get no wallet rows — they are created only by explicit user action (
POST /v1/account), so nothing is silently created for existing accounts.