Refactor monolithic modules and harden booking, email, and auth infrastructure.
Split oversized frontend API client, email service, and admin/booking pages into focused modules while preserving import surfaces, and add Redis-backed queues, stale booking cleanup, stronger auth, and scale deployment configs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -14,7 +14,7 @@ A full-stack web app for organizing and managing language exchange events (Asunc
|
||||
- **Backend**: Node.js + TypeScript, Hono, Drizzle ORM, SQLite (default) or PostgreSQL
|
||||
- **Auth**: JWT (via `jose`), **Argon2id** password hashing (with legacy bcrypt verification for older hashes)
|
||||
- **Email**: `nodemailer` (SMTP) with optional provider config
|
||||
- **Frontend**: Next.js 14 (App Router), Tailwind CSS, SWR, Heroicons
|
||||
- **Frontend**: Next.js 14 (App Router), Tailwind CSS, Heroicons
|
||||
|
||||
## Local development
|
||||
|
||||
@@ -86,6 +86,7 @@ Key settings (see `backend/.env.example` for the full list):
|
||||
- **URLs/ports**: `PORT`, `API_URL`, `FRONTEND_URL`
|
||||
- **Email**: `EMAIL_PROVIDER` (`console|smtp|resend`) and corresponding credentials
|
||||
- **Payments (optional)**: Stripe/MercadoPago/LNbits configuration
|
||||
- **Scaling (optional)**: `REDIS_URL`, `DB_POOL_MAX`, and `S3_*` (see "Horizontal scaling" below)
|
||||
|
||||
### Frontend (`frontend/.env`)
|
||||
|
||||
@@ -160,6 +161,86 @@ npm run db:migrate
|
||||
|
||||
Then install/enable the systemd services and nginx configs for your server.
|
||||
|
||||
## Horizontal scaling
|
||||
|
||||
The backend can run as a single instance with zero extra configuration (the
|
||||
default), or as multiple replicas behind a load balancer. Scaling support is
|
||||
fully optional and backward compatible: if you set none of the variables below,
|
||||
the app behaves exactly as before with in-memory state and local-disk uploads.
|
||||
|
||||
### Requirements for multiple instances
|
||||
|
||||
- **Use PostgreSQL.** Set `DB_TYPE=postgres`. SQLite is a single local file and
|
||||
cannot be shared safely across instances.
|
||||
- **Set `REDIS_URL`.** This makes the following subsystems shared across
|
||||
instances instead of per process:
|
||||
- distributed cache
|
||||
- rate limiting (shared sliding/fixed window)
|
||||
- pub/sub for real-time payment events, so an SSE client connected to one
|
||||
instance still receives an event when the LNbits webhook lands on another
|
||||
- distributed locks (so only one instance seeds email templates per boot and
|
||||
only one instance polls LNbits per pending ticket)
|
||||
- the email hourly cap (`MAX_EMAILS_PER_HOUR`) becomes a global cap
|
||||
- **Tune the DB pool.** `DB_POOL_MAX` is the max Postgres connections per
|
||||
instance (default 10). Keep `DB_POOL_MAX * replicas` below the Postgres
|
||||
`max_connections` setting (default 100). For example, 5 replicas at
|
||||
`DB_POOL_MAX=15` uses up to 75 connections.
|
||||
|
||||
If Redis is configured but becomes unreachable at runtime, each subsystem
|
||||
degrades gracefully (rate limiter fails open, cache misses fall through to the
|
||||
DB, locks proceed) and the API keeps serving rather than crashing.
|
||||
|
||||
### Uploads across instances
|
||||
|
||||
Media uploads default to local disk (`./uploads`). With more than one instance
|
||||
you must use shared storage so a file uploaded on one instance is readable on
|
||||
the others. Two options:
|
||||
|
||||
- **S3-compatible storage (recommended):** set `S3_ENDPOINT`, `S3_BUCKET`,
|
||||
`S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY` (and optionally `S3_PUBLIC_URL`,
|
||||
`S3_REGION`, `S3_FORCE_PATH_STYLE`). Works with Garage, MinIO, or AWS S3.
|
||||
- **Shared volume:** mount the same `./uploads` directory (e.g. NFS) into every
|
||||
instance.
|
||||
|
||||
### Real-time payment SSE behind a load balancer
|
||||
|
||||
The payment status stream (`/api/lnbits/stream/:ticketId`) is a long-lived SSE
|
||||
connection. With Redis pub/sub enabled, any instance can deliver the payment
|
||||
event regardless of which instance holds the socket, so sticky sessions are not
|
||||
strictly required. Enabling sticky sessions (IP hash) for the SSE path is still
|
||||
a reasonable optimization.
|
||||
|
||||
### Health and observability
|
||||
|
||||
`GET /health` always returns 200 and reports Redis connectivity and which
|
||||
backend each subsystem selected, for example:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"redis": { "enabled": true, "healthy": true },
|
||||
"backends": {
|
||||
"cache": "redis",
|
||||
"rateLimiter": "redis",
|
||||
"pubsub": "redis",
|
||||
"lock": "redis",
|
||||
"storage": "s3"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The same selection is logged once at startup.
|
||||
|
||||
### docker-compose example (N replicas + Redis)
|
||||
|
||||
A ready-to-edit snippet lives at `deploy/docker-compose.scale.yml`. It runs
|
||||
Postgres, Redis, and the API scaled to multiple replicas behind nginx. Bring it
|
||||
up with:
|
||||
|
||||
```bash
|
||||
docker compose -f deploy/docker-compose.scale.yml up --build --scale api=3
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Specs / notes**: `about/`
|
||||
|
||||
Reference in New Issue
Block a user