- Locks no longer fail open: an unreachable backend throws LockUnavailableError and withLock skips the run (or opts into running unlocked, as template seeding does). The lnbits payment poller keeps polling through an outage, made safe by only sending confirmation emails when a row actually transitioned. - Rate limiter INCR+PEXPIRE now runs as one Lua script, self-healing counters stranded without a TTL. - Per-email login lockout moves to a Redis-backed store shared across replicas (in-memory fallback preserved), case-insensitive on email. - Graceful shutdown on SIGTERM/SIGINT: stop periodic jobs, close the server, force-close lingering SSE sockets, close Redis. - Active PING probe backs the health flag; /health reports last ping. SSE payment streams also poll the DB as a pub/sub-gap fallback. - Scale compose gains requirepass, maxmemory 256mb with noeviction, and an authenticated healthcheck; .env.example documents passwords, TLS (rediss://), and DB-index selection. - First tests in the repo: vitest + ioredis-mock covering the lock, rate limiter, and login lockout stores (27 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
138 lines
5.3 KiB
Bash
138 lines
5.3 KiB
Bash
# Database Configuration
|
|
# Use 'sqlite' or 'postgres'
|
|
DB_TYPE=sqlite
|
|
|
|
# For SQLite (relative or absolute path)
|
|
DATABASE_URL=./data/spanglish.db
|
|
|
|
# For PostgreSQL
|
|
# DATABASE_URL=postgresql://user:password@localhost:5432/spanglish
|
|
|
|
# Max PostgreSQL connections per instance (default 10). When running multiple
|
|
# replicas, keep DB_POOL_MAX * replicas below the Postgres max_connections limit.
|
|
# DB_POOL_MAX=10
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Horizontal scaling (all optional)
|
|
# ---------------------------------------------------------------------------
|
|
# Leave everything below UNSET to run as a single instance with in-memory
|
|
# backends and local-disk uploads (zero-config, identical to the original
|
|
# behavior). Set them to run multiple API replicas behind a load balancer.
|
|
#
|
|
# Note: running more than one instance requires DB_TYPE=postgres. SQLite is a
|
|
# single local file and cannot be shared safely across instances.
|
|
|
|
# Redis connection URL. When set, the cache, rate limiter, login lockout,
|
|
# pub/sub (real-time payment events), distributed locks, and the email hourly
|
|
# cap are shared across all instances. When unset, each instance uses in-memory
|
|
# equivalents.
|
|
#
|
|
# In production always set a password (requirepass on the server) and put it in
|
|
# the URL. Use the rediss:// scheme for TLS (handled natively by the client),
|
|
# and an optional /N path to select a DB index when sharing a Redis instance
|
|
# with other applications.
|
|
# REDIS_URL=redis://localhost:6379
|
|
# REDIS_URL=redis://:your-redis-password@redis.internal:6379
|
|
# REDIS_URL=rediss://:your-redis-password@redis.example.com:6380/1
|
|
|
|
# Optional S3-compatible object storage for media uploads (e.g. Garage, MinIO,
|
|
# AWS S3). When S3_ENDPOINT and S3_BUCKET are set, uploads go to the bucket and
|
|
# are shared across instances. When unset, uploads are written to ./uploads on
|
|
# local disk (the default).
|
|
# S3_ENDPOINT=https://garage.example.com
|
|
# S3_REGION=garage
|
|
# S3_BUCKET=spanglish-media
|
|
# S3_ACCESS_KEY_ID=
|
|
# S3_SECRET_ACCESS_KEY=
|
|
# Public base URL used to build fileUrl for stored objects (CDN or web endpoint).
|
|
# If unset, a path-style URL against S3_ENDPOINT/S3_BUCKET is used.
|
|
# S3_PUBLIC_URL=https://media.example.com
|
|
# Use path-style addressing (true for Garage/MinIO). Defaults to true.
|
|
# S3_FORCE_PATH_STYLE=true
|
|
|
|
# JWT Secret (change in production!)
|
|
JWT_SECRET=your-super-secret-key-change-in-production
|
|
|
|
# Google OAuth (optional - for Google Sign-In)
|
|
# Get your Client ID from: https://console.cloud.google.com/apis/credentials
|
|
# Note: The same Client ID should be used in frontend/.env
|
|
GOOGLE_CLIENT_ID=
|
|
|
|
# Server Configuration
|
|
PORT=3001
|
|
API_URL=http://localhost:3001
|
|
FRONTEND_URL=http://localhost:3019
|
|
|
|
# Revalidation secret (shared with frontend for on-demand cache revalidation)
|
|
# Must match the REVALIDATE_SECRET in frontend/.env
|
|
REVALIDATE_SECRET=change-me-to-a-random-secret
|
|
|
|
# Payment Providers (optional)
|
|
STRIPE_SECRET_KEY=
|
|
STRIPE_WEBHOOK_SECRET=
|
|
MERCADOPAGO_ACCESS_TOKEN=
|
|
|
|
# LNbits Configuration (for Bitcoin Lightning payments)
|
|
# Get these from your LNbits instance
|
|
# URL should be like: https://lnbits.yourdomain.com or https://legend.lnbits.com
|
|
LNBITS_URL=
|
|
# Invoice/Read key from your LNbits wallet
|
|
LNBITS_API_KEY=
|
|
# Optional: webhook secret for additional verification
|
|
LNBITS_WEBHOOK_SECRET=
|
|
|
|
# Email Service
|
|
# Provider options: resend, smtp, console (console = log only, no actual email)
|
|
EMAIL_PROVIDER=console
|
|
EMAIL_FROM=
|
|
EMAIL_FROM_NAME=
|
|
|
|
# Resend Configuration (if EMAIL_PROVIDER=resend)
|
|
# Get your API key from https://resend.com
|
|
EMAIL_API_KEY=
|
|
# Alternative env name also supported:
|
|
# RESEND_API_KEY=
|
|
|
|
# SMTP Configuration (if EMAIL_PROVIDER=smtp)
|
|
# Works with any SMTP server: Gmail, SendGrid, Mailgun, Amazon SES, etc.
|
|
SMTP_HOST=mail.spango.lat
|
|
SMTP_PORT=465
|
|
SMTP_USER=
|
|
SMTP_PASS=
|
|
# Set to true for port 465 (implicit TLS), false for port 587 (STARTTLS)
|
|
SMTP_SECURE=true
|
|
# Set to false to allow self-signed certificates (not recommended for production)
|
|
SMTP_TLS_REJECT_UNAUTHORIZED=true
|
|
|
|
# Common SMTP examples:
|
|
# Gmail: SMTP_HOST=smtp.gmail.com, SMTP_PORT=587, use App Password for SMTP_PASS
|
|
# SendGrid: SMTP_HOST=smtp.sendgrid.net, SMTP_PORT=587, SMTP_USER=apikey, SMTP_PASS=your_api_key
|
|
# Mailgun: SMTP_HOST=smtp.mailgun.org, SMTP_PORT=587
|
|
# Amazon SES: SMTP_HOST=email-smtp.us-east-1.amazonaws.com, SMTP_PORT=587
|
|
|
|
# Email Queue Rate Limiting
|
|
# Maximum number of emails that can be sent per hour (default: 30)
|
|
# If the limit is reached, queued emails will pause and resume automatically
|
|
MAX_EMAILS_PER_HOUR=30
|
|
|
|
# Pending Booking Cleanup
|
|
# Pending bookings whose payment is still unpaid (not awaiting admin approval)
|
|
# are cancelled after this many minutes, freeing the seats (default: 30)
|
|
PENDING_BOOKING_TTL_MINUTES=30
|
|
# How often the cleanup job runs, in milliseconds (default: 300000 = 5 min)
|
|
PENDING_BOOKING_CLEANUP_INTERVAL_MS=300000
|
|
|
|
# Auto-Hold Sweep
|
|
# Bookings awaiting admin payment approval are put on hold (seat released) after
|
|
# this many hours with no confirmation (default: 72)
|
|
HOLD_THRESHOLD_HOURS=72
|
|
# How often the hold sweep job runs, in milliseconds (default: 900000 = 15 min)
|
|
HOLD_SWEEP_INTERVAL_MS=900000
|
|
|
|
# Ended-Event Payment Sweep
|
|
# Once an event is over, any unconfirmed payment (pending / pending_approval /
|
|
# on_hold) is auto-rejected and its ticket cancelled. No email is sent.
|
|
# How often this sweep runs, in milliseconds (default: 900000 = 15 min)
|
|
EVENT_END_SWEEP_INTERVAL_MS=900000
|
|
|