# Spanglish — Language Exchange Event Platform A full-stack web app for organizing and managing language exchange events (Asunción, Paraguay). ## Features - **Public site**: events, booking, contact, community, legal pages, bilingual (EN/ES) - **User dashboard**: profile, tickets, payments, sessions/security - **Admin** (`/admin`): events, tickets/check-in, users/roles, payments, email templates, media uploads - **API**: Swagger UI at `/api-docs`, OpenAPI JSON at `/openapi.json`, health check at `/health` ## Tech stack - **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, Heroicons ## Local development ### Prerequisites - Node.js 18+ - npm ### Setup ```bash npm install cp backend/.env.example backend/.env cp frontend/.env.example frontend/.env ``` ### Initialize database (SQLite by default) ```bash npm run db:migrate ``` ### Run ```bash npm run dev ``` Default URLs: - Frontend: `http://localhost:3002` - Backend API: `http://localhost:3001` - API docs: `http://localhost:3001/api-docs` ### First user becomes admin The first user to register becomes the **admin**. Register at `/register`. ## Useful scripts (workspace) Run these from the repo root: ```bash npm run dev npm run build npm run start npm run db:generate npm run db:migrate npm run db:studio npm run db:export # Backup database npm run db:import # Restore from backup ``` You can also run per workspace: ```bash npm run dev --workspace=backend npm run dev --workspace=frontend ``` ## Environment variables ### Backend (`backend/.env`) Key settings (see `backend/.env.example` for the full list): - **DB**: `DB_TYPE=sqlite|postgres`, `DATABASE_URL=./data/spanglish.db` (or Postgres URL) - **Auth**: `JWT_SECRET` (change in production) - **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`) Key settings (see `frontend/.env.example`): - **Server port**: `PORT=3002` - **API base URL**: `NEXT_PUBLIC_API_URL` (optional) - Leave empty to use same-origin `/api` (recommended when running behind nginx) - In local dev, Next.js rewrites `/api/*` and `/uploads/*` to the backend - **Social links (optional)**: `NEXT_PUBLIC_WHATSAPP`, `NEXT_PUBLIC_INSTAGRAM`, etc. ## Database ### SQLite (default) - DB file defaults to `backend/data/spanglish.db` (via `DATABASE_URL=./data/spanglish.db`) - Run migrations with `npm run db:migrate` ### PostgreSQL Set in `backend/.env`: ```env DB_TYPE=postgres DATABASE_URL=postgresql://user:password@localhost:5432/spanglish ``` Then run: ```bash npm run db:migrate ``` ### Backups (export / import) Create backups and restore if needed: ```bash # Export (creates timestamped file in backend/data/backups/) npm run db:export # Export to custom path npm run db:export -- -o ./my-backup.db # SQLite npm run db:export -- -o ./my-backup.sql # PostgreSQL # Import (stop the backend server first) npm run db:import -- ./data/backups/spanglish-2025-03-07-143022.db npm run db:import -- --yes ./data/backups/spanglish-2025-03-07.sql # Skip confirmation ``` **Note:** Stop the backend before importing so the database file is not locked. ## Production deployment (nginx + systemd) This repo includes example configs in `deploy/`: - **systemd**: `deploy/spanglish-backend.service`, `deploy/spanglish-frontend.service` - Backend runs on **3018**, frontend on **3019** by default (see the unit files) - Backend needs write access to `backend/data` and `backend/uploads` - **nginx**: - `deploy/spanglish_upstreams.conf` defines upstreams for ports 3018/3019 - `deploy/front-end_nginx.conf` proxies `/api` and `/uploads` to the backend and everything else to the frontend - `deploy/back-end_nginx.conf` is a dedicated API vhost example with CORS handling Typical production flow: ```bash npm ci npm run build 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/` - **Legal docs**: `frontend/legal/` ## License MIT