Files
0e31093a74 kv: harden in-process backend — atomic getdel, active expiry, multi-replica doc (#5)
Three RED-blessed non-blocking follow-ups on the KV-optional work.

1. Drop the non-atomic getdel() shim in lib/redis.ts. It shadowed BOTH backends'
   native atomic GETDEL with a get->del pipeline (other commands interleave
   between GET and DEL), reintroducing the one-time login-code double-use race
   that fetchAndDeleteLoginCodeData relies on GETDEL to prevent. ioredis maps
   getdel to the single-round-trip Redis GETDEL command; MemoryKV.getdel now
   reads+deletes in one un-yielded step (was `await this.get()` then delete — the
   await yielded the event loop, letting two racers both observe the value).

2. Bound MemoryKV growth. Lazy on-read eviction never fires for write-once-
   never-read keys (rl:<ip> rate-limit counters, one-shot tokens), so the map
   grew unbounded until the pod OOMKilled. Add sweepExpired() (one O(n) active-
   expire pass) run on an unref'd 60s timer, started by lib/redis.ts for the
   server singleton only (edge-gated; no test imports that module, so unit runs
   stay timer-free). MemoryKV stays pure (zero imports).

3. Document in .env.example that re-enabling multi-replica REQUIRES KV_URL
   (single-replica-by-construction otherwise: SQLite-on-RWO + in-process KV).
   The one-time non-silent in-process warning already exists in lib/kv/select.ts;
   no metrics surface exists, so log + doc is sufficient.

Tests: +3 cases (getdel atomicity under concurrent race; 100k-key sweep reclaims
to 0; sweep retains live/no-TTL keys). 28 pass, 3 skip (select.ts skips without
ioredis in a bare checkout), 0 fail.

Co-authored-by: Hanzo AI <ai@hanzo.ai>
2026-07-07 16:08:16 -07:00

105 lines
4.4 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
NEXTAUTH_SECRET=my-superstrong-secret
NEXTAUTH_URL=http://localhost:3000
NEXT_PUBLIC_BASE_URL=http://localhost:3000
NEXT_PUBLIC_MARKETING_URL=http://localhost:3000
NEXT_PUBLIC_APP_BASE_HOST=localhost
# These variables are from Vercel Storage Postgres
POSTGRES_PRISMA_URL=
POSTGRES_PRISMA_URL_NON_POOLING=
# This variable is from Vercel Storage Blob
BLOB_READ_WRITE_TOKEN=
# Hanzo IAM OAuth (required for production)
IAM_URL="https://hanzo.id"
IAM_CLIENT_ID=""
IAM_CLIENT_SECRET=""
# IAM_PROVIDER_NAME="Hanzo"
# Google client id and secret (deprecated — use Hanzo IAM above)
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# This variable is from Resend to send emails
RESEND_API_KEY=
# This variable is from Tinybird to publish and read event data
TINYBIRD_TOKEN=
# These variables are from Vercel and used for setting up custom domains
PROJECT_ID_VERCEL=
TEAM_ID_VERCEL=
AUTH_BEARER_TOKEN=
# Upstash QStash  required for queues and background jobs
# Get your QStash Token here: https://upstash.com/docs/qstash/overall/getstarted
QSTASH_TOKEN=
QSTASH_CURRENT_SIGNING_KEY=
QSTASH_NEXT_SIGNING_KEY=
# HANKO - required for signup with passkey
# Get your HANKO keys here: https://docs.hanko.io/passkey-api/reference/credentials/list-credentials
HANKO_API_KEY=add-your-hanko-api-key
NEXT_PUBLIC_HANKO_TENANT_ID=add-your-hanko-tenent-id
# Trigger v3
TRIGGER_SECRET_KEY=
TRIGGER_API_URL=https://api.trigger.dev
# [[STORAGE]]
# Defines the storage transport to use. Available options: vercel (default) | s3
NEXT_PUBLIC_UPLOAD_TRANSPORT="vercel"
# REQUIRED: Defines the distribution host (without protocol) to use for the storage transport. Either cloudfront, s3, or vercel blob host. Used in next.config.mjs configuration.
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_HOST="<BLOB_STORE_ID>.public.blob.vercel-storage.com" | "<DISTRIBUTION_ID>.cloudfront.net" | "<BUCKET_NAME>.s3.<REGION>.amazonaws.com"
# OPTIONAL: Defines the endpoint to use for the S3 storage transport. Relevant when using third-party S3-compatible providers.
NEXT_PRIVATE_UPLOAD_ENDPOINT=""
# Defines the region to use for the S3 storage transport. Defaults to us-east-1.
NEXT_PRIVATE_UPLOAD_REGION="us-east-1"
# REQUIRED: Defines the bucket to use for the S3 storage transport.
NEXT_PRIVATE_UPLOAD_BUCKET="YOUR_BUCKET_NAME"
# REQUIRED: Defines the access key ID to use for the S3 storage transport.
NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID=""
# REQUIRED: Defines the secret access key to use for the S3 storage transport.
NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY=""
# Defines the Cloudfront distribution domain to use for the S3 storage transport. Relevant when using Cloudfront.
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_DOMAIN=""
# Defines the public key ID to sign Cloudfront URLs. Relevant when using Cloudfront.
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_ID=""
# Defines the private key contents to sign Cloudfront URLs. Relevant when using Cloudfront. Should be a PEM-encoded RSA 2048 private key.
NEXT_PRIVATE_UPLOAD_DISTRIBUTION_KEY_CONTENTS=
# Encryption key for document passwords.
NEXT_PRIVATE_DOCUMENT_PASSWORD_KEY=my-superstrong-document-secret
# [[HANZO KV]] — OPTIONAL. Leave UNSET to run on the in-process backend
# (single-replica correct: cache, rate-limit, tus upload locks, export/download
# job stores and digest queues all work with no external datastore). SET it to
# an external Hanzo KV instance for multi-replica HA (shared state across pods).
# Accepts the Hanzo KV brand scheme kv:// (kvs:// for TLS) or a redis:// DSN.
# A malformed value fails CLOSED (the app throws rather than silently degrade).
#
# Re-enabling multi-replica (replicas > 1) REQUIRES KV_URL. dataroom is
# single-replica-by-construction otherwise: SQLite on a ReadWriteOnce volume plus
# the in-process KV. Without KV_URL each pod owns its OWN map, so sessions,
# rate-limit windows and tus upload locks split-brain across replicas — scaling
# out needs a shared DB AND KV_URL set.
# KV_URL=kv://:password@hanzo-kv:6379
KV_URL=
# [[TUS UPLOAD LOCKER]] — for bulk upload via tus.io, an exclusive locker
# prevents data corruption. It uses the Hanzo KV client above (KV_URL); with
# KV_URL unset the lock is in-process (correct for a single replica). The legacy
# Upstash REST locker below is unused and kept only for reference.
UPSTASH_REDIS_REST_LOCKER_URL=
UPSTASH_REDIS_REST_LOCKER_TOKEN=
# [[VERIFICATION]]
# Secret used to generate the checksum for the verification URL
NEXT_PRIVATE_VERIFICATION_SECRET=
# [[INCOMING WEBHOOKS]]
NEXT_PUBLIC_WEBHOOK_BASE_URL=
NEXT_PUBLIC_WEBHOOK_BASE_HOST=