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:
Michilis
2026-06-25 07:12:59 +00:00
co-authored by Cursor
parent f0e2de2834
commit 613bd7be1d
75 changed files with 7702 additions and 5580 deletions
+82 -1
View File
@@ -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/`