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:

VariableRequiredDescription
DATABASE_URLyesPostgreSQL connection string. For Supabase, use the transaction pooler (port 6543) with ?pgbouncer=true&connection_limit=1
JWT_ACCESS_SECRETyesSecret for signing access tokens (32+ chars)
JWT_REFRESH_SECRETyesSecret for signing refresh tokens (32+ chars)
GOOGLE_CLIENT_IDyes*Google OAuth client ID (same key name in every env — values differ)
GOOGLE_CLIENT_SECRETyes*Google OAuth client secret (same key name in every env)
GOOGLE_CALLBACK_URLyes*Must match the authorized redirect URI
COOKIE_SECUREnotrue in production (cookies only over HTTPS)
COOKIE_DOMAINnoCookie domain; omit for localhost
COOKIE_SAMESITEnolax (default) or none (cross-site clients, requires COOKIE_SECURE=true)
CLIENT_SUCCESS_URLnoWhere users land after Google sign-in
CORS_ORIGINSnoComma-separated extra client origins allowed by CORS
PID_PROGRAM_IDyesSolana program id the wallet derives PDAs against
WEBAUTHN_RP_ID / WEBAUTHN_RP_NAME / WEBAUTHN_ORIGINSyesPasskey relying-party config
CLIENT_REDIRECT_ALLOWLISTnoComma-separated origins allowed for cross-origin SSO
SOLANA_NETWORKnodevnet (default) or mainnet-beta
ACCESS_TOKEN_TTLnoAccess token lifetime (default 15m)
REFRESH_TOKEN_TTLnoRefresh token lifetime (default 30d)
APP_TOKEN_TTLnoMachine (client-credentials) token lifetime (default 1h)
PORTnoHTTP 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.

VariableRequiredDescription
DOKU_MODEnosandbox (default) or production
DOKU_CLIENT_ID / DOKU_SECRET_KEYyes*DOKU merchant credentials (Checkout)
DOKU_PRIVATE_KEYnoRSA PEM for legacy Sub-Account calls (not needed for Checkout-only)
DOKU_WEBHOOK_URLnoPublic URL for DOKU payment notifications (/v1/fiat/webhook)
DOKU_CHECKOUT_NOTIFY_URLnoPer-payment notify override (defaults to DOKU_WEBHOOK_URL)
PID_FIAT_LEDGER_ENABLEDnoMaster switch (default true)
PID_FIAT_LEDGER_FROZENnoFreeze new issues/sends (default false)
PID_FIAT_LEDGER_TREASURY_PIDnoLabel for the global-fee treasury (env, never hardcoded)
PID_REDEMPTION_ENABLEDnoBank 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:3300

Deploy to Vercel + Supabase

  1. 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
  2. Vercel — API (apps/api/vercel.json is already configured for serverless). Add a project rooted at apps/api and set these env vars: DATABASE_URL (transaction pooler, port 6543, ?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.

  3. Vercel — docs (apps/web/vercel.json is already configured). Add a project rooted at apps/web; no env vars needed.

  4. 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 start

Legacy-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:

  1. 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;
  2. Deploy with pnpm --filter @peridotvault/pid-api db:deploy (same flow as the Supabase steps above).

  3. 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.

On this page