Aliquot
Getting Started

Environment

Required and optional env vars. The schema in apps/server/src/bootstrap/env.ts is the source of truth.

Last updated Oct 27, 2026

All env vars are validated by a Zod schema in apps/server/src/bootstrap/env.ts. The server fails to boot with a clear error if a required var is missing or malformed. Optional adapters boot to a no-op when their config is unset — you never need to fill all the keys.

Minimum to boot

These four are the floor. Without them the server exits early:

BETTER_AUTH_SECRET=<openssl rand -base64 32>
BETTER_AUTH_URL=http://localhost:3005
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/boilerplate?schema=public
CORS_ORIGINS=http://localhost:3004

Generate the auth secret with openssl rand -base64 32. CORS_ORIGINS is a comma-separated list of allowed origins for the browser.

Auth providers (optional)

GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=

Leave both empty to disable Google OAuth. Magic-link is configured through your email provider — see the notifications module.

Storage (optional)

STORAGE_PROVIDER=null            # null | local | s3 | uploadthing
# S3 / R2 / MinIO
STORAGE_S3_BUCKET=
STORAGE_S3_REGION=
STORAGE_S3_ACCESS_KEY_ID=
STORAGE_S3_SECRET_ACCESS_KEY=
STORAGE_S3_ENDPOINT=             # set for R2/MinIO; leave empty for AWS
STORAGE_S3_PUBLIC_BASE_URL=      # CDN base
# Local disk (dev)
STORAGE_LOCAL_ROOT=
STORAGE_LOCAL_PUBLIC_BASE_URL=
# UploadThing
UPLOADTHING_TOKEN=
UPLOADTHING_APP_ID=

STORAGE_PROVIDER=null (the default) skips registering an adapter so endpoints that need uploads return 503. The local provider builds public URLs from BETTER_AUTH_URL (the server's URL), not APP_URL — uploads are served by the API, not by Next.js.

Background jobs (optional)

REDIS_URL=redis://localhost:6379
JOBS_REDIS_URL=                  # defaults to REDIS_URL
JOBS_PREFIX=bull
BULL_BOARD_ENABLED=true          # mounts /admin/queues UI

Without Redis, BullMQ is registered but no worker connects. BULL_BOARD_ENABLED=true mounts a guarded Bull Board UI at /admin/queues.

Rate limiting

RATE_LIMIT_REDIS_ENABLED=true    # falls back to memory when REDIS_URL is unset
RATE_LIMIT_READ_PER_MIN=300
RATE_LIMIT_WRITE_PER_MIN=60
RATE_LIMIT_AUTH_IP_PER_MIN=30
RATE_LIMIT_AUTH_EMAIL_PER_15MIN=5

The four tiers (read, write, auth-ip, auth-email) each get their own Redis prefix (rl:read:, rl:write:, etc.) so counters never collide. The limiter handlers are memoized on the RateLimitDeps reference, so memory-mode behaves identically to Redis-mode (one shared store per tier).

AI chat (optional)

ANTHROPIC_API_KEY=               # empty = stub stream, no upstream calls
AI_MODEL=claude-haiku-4-5        # default model, always allowed
AI_ALLOWED_MODELS=               # extra models clients may request (comma-separated)
AI_MAX_OUTPUT_TOKENS=1024        # hard cap on generated tokens per completion
AI_STREAM_TIMEOUT_MS=120000     # hard wall-clock cap (ms) per chat stream; aborts upstream

POST /api/v1/ai/chat rejects any model outside AI_MODEL + AI_ALLOWED_MODELS with a 422, so a caller cannot pick a more expensive model.

Observability (optional)

SENTRY_DSN=                      # leave empty to disable
SENTRY_TRACES_SAMPLE_RATE=0.1
APP_VERSION=

OTEL_EXPORTER_OTLP_ENDPOINT=     # leave empty to disable trace export
OTEL_SERVICE_NAME=mern-saas-server

METRICS_ENABLED=true             # Prometheus /metrics endpoint

OTel must boot before Express/Prisma load. The first import in apps/server/src/index.ts is ./otel-init.js for that reason. Don't reorder it.

The Sentry server SDK is @sentry/bun (not @sentry/node). logger.error(...) does not auto-capture; use the captureError(err, ctx) helper for errors that should page someone.

Billing (provider-dependent)

Set PAYMENT_PROVIDER to one of stripe, mercado-pago, polar, then fill that provider's keys:

PAYMENT_PROVIDER=stripe
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# or
PAYMENT_PROVIDER=mercado-pago
MERCADO_PAGO_ACCESS_TOKEN=
MERCADO_PAGO_WEBHOOK_SECRET=
# or
PAYMENT_PROVIDER=polar
POLAR_ACCESS_TOKEN=
POLAR_WEBHOOK_SECRET=
POLAR_SERVER=sandbox   # or production

[!WARNING] POLAR_SERVER defaults to sandbox outside production. With NODE_ENV=production and PAYMENT_PROVIDER=polar it must be set explicitly (sandbox or production) — the server refuses to boot otherwise, so a forgotten variable can't silently send live traffic to the Polar sandbox.

See Billing for the strategy and webhook setup.

Usage metering (optional)

Disabled by default. Flip the flag once the downstream has registered its meters and a plan → cap map.

USAGE_METERING_ENABLED=false           # set to "true" to enforce quotas
USAGE_FREE_PRICE_ID=price_free_synthetic  # synthetic key for the Free tier; not a real Polar price

See Usage metering for the full reference.

Crypto payments (optional)

The crypto-payments module (Aliquot) boots without any of these. Until CRYPTO_MASTER_MNEMONIC is set, every /api/v1/crypto/* and /api/v1/public/* endpoint answers 503 CRYPTO_NOT_CONFIGURED; the rest of the server is unaffected.

CRYPTO_MASTER_MNEMONIC="<BIP-39 mnemonic>"   # custody; unset → crypto endpoints answer 503
CRYPTO_RPC_POLYGON_AMOY=https://polygon-amoy-bor-rpc.publicnode.com,https://polygon-amoy.drpc.org
CRYPTO_RPC_BSC_TESTNET=https://bsc-testnet-rpc.publicnode.com
CRYPTO_RPC_BASE_SEPOLIA=https://sepolia.base.org,https://base-sepolia-rpc.publicnode.com
# CRYPTO_RPC_POLYGON_MAINNET= / CRYPTO_RPC_BSC_MAINNET= / CRYPTO_RPC_BASE_MAINNET=
# CRYPTO_CONFIRMATIONS_POLYGON_AMOY=5  # dev-only override (defaults: Polygon 64, BSC 15, Base 30)
CRYPTO_PLATFORM_FEE_BPS=50             # default processing fee (0.50 %) for orgs without their own config
CRYPTO_PAYOUT_FEE_POLYGON=0.10         # flat token fee per payout, per network family
CRYPTO_PAYOUT_FEE_BSC=0.15
CRYPTO_PAYOUT_FEE_BASE=0.10
# CRYPTO_LOGS_MAX_RANGE_BSC_TESTNET=200      # max blocks per eth_getLogs call (default 200, all 6 networks)
# CRYPTO_NATIVE_PRICE_USD_POLYGON=0.50       # fixed native price for refund fee quotes (BSC 600, BASE 3000)
# CRYPTO_CHECKOUT_BASE_URL=https://pay.example.com  # hosted checkout origin; defaults to APP_URL
# TELEGRAM_BOT_TOKEN=                  # Telegram Mini-App initData verification
  • CRYPTO_MASTER_MNEMONIC is the custody wallet (HD path m/44'/60'/0'/0/i: index 0 is the hot wallet, invoice deposit addresses start at 1000). The API process derives addresses from the extended public key only; signing happens in the worker. It must be a valid BIP-39 English mnemonic.
  • CRYPTO_RPC_<NETWORK> takes comma-separated URLs (viem fallback, first = primary). A network without URLs is listed as disabled by GET /api/v1/crypto/networks and cannot take invoices. For local chains run bun run chain:dev and point these at the printed URLs.
  • NEXT_PUBLIC_CRYPTO_RPC_<NETWORK> (client, apps/client/.env.local, optional, one URL) is the RPC the hosted checkout's "Pay with wallet" button hands to the buyer's wallet when the wallet does not know the network yet (wallet_addEthereumChain). Unset, the checkout uses the network's public RPC. Set it only for local chains, e.g. NEXT_PUBLIC_CRYPTO_RPC_POLYGON_AMOY=http://127.0.0.1:18002, so a browser wallet sends to the same anvil chain the server scans. It is inlined at build time, so rebuild (or restart next dev) after changing it.
  • NEXT_PUBLIC_SITE_URL (client, optional) is the canonical origin of the public site: metadata, Open Graph, sitemap, robots, JSON-LD and llms.txt. Unset, it is https://aliquot.cc, the production domain. Set it for a staging deploy. It is inlined at build time.
  • CRYPTO_CHECKOUT_BASE_URL builds pay_url (<base>/pay/<invoiceId>), refund claim links (<base>/pay/<invoiceId>/refund/<token>) and payment-link URLs (<base>/l/<slug>).
  • CRYPTO_LOGS_MAX_RANGE_<NETWORK> caps the block range of each eth_getLogs call (default 200: sepolia.base.org refuses more). The scanner also halves any range a provider refuses for size and catches up a backlog in bounded steps, so you rarely need to touch it.
  • CRYPTO_NATIVE_PRICE_USD_<FAMILY> (POLYGON 0.50, BSC 600, BASE 3000) converts the gas of a refund into stablecoin units: the buyer receives gross − max(gas × max fee × price, 0.01), fixed when they claim.
  • TELEGRAM_BOT_TOKEN is the token of the bot that opens the checkout as a Telegram Mini-App (/pay/<invoiceId>?tg=1). POST /api/v1/public/telegram/verify checks the Mini-App initData signature with it (HMAC-SHA256, secret = HMAC("WebAppData", token)) and rejects an auth_date older than 24 h. Without it that endpoint answers 503 TELEGRAM_NOT_CONFIGURED; the checkout itself still works.
  • The worker (bun run worker) runs the money path: chain scanner (2 s), expiry (10 s), sweeps (15 s), payouts (3 s), refunds (5 s). It needs Redis; without it nothing is scanned or sent. Native gas sent to the hot wallet (index 0) is recorded automatically as operator funding (EQUITY:OPERATOR_FUNDING).

[!WARNING] With NODE_ENV=production the server refuses to boot with the public development mnemonic (the one committed in apps/server/.env and .env.example), and refuses CRYPTO_CONFIRMATIONS_* overrides for mainnets.

Adding a new variable

  1. Edit apps/server/src/bootstrap/env.ts and extend the Zod schema. Required vars must .min(1); optional ones use .optional() or .default(...).
  2. Add a documented entry in apps/server/.env.example.
  3. Read it from env.YOUR_VAR — never from process.env directly.

On this page