MichilisandClaude Fable 5 9b2668f498 Unify admin ticket creation into one modal with first-class payment status.
- Replace the three Attendees-tab modals (Manual Ticket / Add at Door /
  Invite Guest) with a single Add Ticket modal: Paid/Unpaid/Guest segmented
  control, shared fields, "Check in now" for all types, and a live
  "what happens" preview, backed by one POST /api/tickets/admin/add.
- Add tickets.payment_status (paid | unpaid | comp) with a backfill
  migration; keep it in sync on every payment-settlement path (mark-paid,
  admin approval, Lightning, free bookings, hold recovery).
- Show Paid/Unpaid/Comp badges in the attendee list, count only paid
  tickets toward revenue, let unpaid tickets be resolved via Mark Paid,
  and flag unpaid tickets with their balance due in the door scanner.
- Replace the per-page useStatsPrivacy hook with an admin-wide
  PrivacyContext + SensitiveValue mask, toggled from the admin layout.
- Add server-side pagination with page-size options to the users page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-26 05:25:00 +00:00
2026-01-29 14:13:11 -03:00

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

npm install
cp backend/.env.example backend/.env
cp frontend/.env.example frontend/.env

Initialize database (SQLite by default)

npm run db:migrate

Run

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:

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:

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:

DB_TYPE=postgres
DATABASE_URL=postgresql://user:password@localhost:5432/spanglish

Then run:

npm run db:migrate

Backups (export / import)

Create backups and restore if needed:

# 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:

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:

{
  "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:

docker compose -f deploy/docker-compose.scale.yml up --build --scale api=3

Documentation

  • Specs / notes: about/
  • Legal docs: frontend/legal/

License

MIT

S
Description
No description provided
Readme
6.9 MiB
2026-07-13 03:51:07 +00:00
Languages
TypeScript 93.7%
Go 5.9%
JavaScript 0.2%
CSS 0.1%