Backend package: Fastify HTTP API, Socket.IO, MySQL via mysql2. Runtime configuration and local development are described in the repository README.
The server owns all authoritative business rules and persistence. The browser and Laverna only send requests and socket events; validation on the wire is enforced here again even when the client also checks inputs.
| Domain | Scope |
|---|---|
| Authentication | Registration, login, token refresh and logout; email verification and password reset; JWT access and refresh cookies |
| Profiles | Profile data, tags, location, image upload (validation on type, size, aspect ratio) |
| Discovery | Browse and search with filters; parameterized SQL |
| Social | Likes, matches, blocks, reports; notifications |
| Realtime | Socket.IO path /api/socket.io; chat constrained to matches |
| Operations | SQL migrations at startup; seeds; script seed:profiles |
JSON request and response shapes for routes are defined alongside the client in @matcha/shared so both sides stay aligned.
Configuration and HTTP
- Environment variables are validated at startup (Zod); invalid config exits before listen.
- One allowed CORS origin; credentialed requests enabled.
- Request bodies use shared JSON schemas; extra sanitization on free text where needed.
Sign-in and JWT cookies
- Login with email or username + password (bcrypt). No tokens until email is verified.
- After login: access JWT (short-lived) + refresh JWT (long-lived), both in httpOnly cookies,
sameSite: lax,securein production. - Access cookie path is
/so the browser also sends it to Socket.IO on another port; refresh cookie stays under the API path. - Refresh rotation: each refresh exchanges the refresh JWT for a new one; old refresh rows are revoked (JTI tracked in the database).
- Reuse: presenting an already-revoked refresh token revokes all refresh tokens for that user (session invalidation across devices).
- Logout revokes the current refresh row; logout all revokes every refresh row for the user.
Calling authenticated APIs
- Access JWT:
Authorization: Bearer …or access cookie (same token). - Socket.IO: same access JWT via handshake or cookies (no secrets in frontend code).
Passwords and identifiers
- Passwords: bcrypt; strength checked with zxcvbn on register / reset / change.
- Email verification and password-reset tokens stored hashed (bcrypt compare).
- Public UUID-shaped ids in URLs instead of numeric database ids.
Blocks, matches, and limits
- Routes that target another user respect blocks.
- Match-only actions require membership in that match (e.g. chat).
- Rate limits: IP + route counters in MySQL, HTTP 429 when over budget — only in production (disabled locally).
Uploads, email, reputation
- Images: multipart size cap, MIME allowlist, aspect ratio check, max photos per user.
- Mail: nodemailer; dev may use test inboxes / log links; production needs SMTP in env.
- Fame (0–100): blended score from views, likes, matches, unlikes, reports, blocks.
- Reports: fixed reasons, optional description (sanitized, length-capped); admins can act on pending reports.
Entry point is src/index.ts: loads configuration, runs migrations, may trigger seeding, then starts Fastify and attaches Socket.IO to the same HTTP server instance. src/app.ts builds the Fastify app (plugins, routes, error handler) and creates the Socket.IO server with CORS aligned to the REST API.
server/
├── src/
│ ├── app.ts # Fastify + Socket.IO bootstrap
│ ├── index.ts # process entry, migrations, listen
│ ├── routes/ # route modules (auth, profiles, discovery, socials, notifications, …)
│ ├── middlewares/ # authGuard, blockGuard, matchGuard, rateLimit, password strength
│ ├── socket/ # socket auth, chat handlers, match helpers
│ ├── config/ # envSchema (Zod), constant.ts
│ ├── db/ # migrate.ts, seed.ts
│ └── utils/ # MySQL pool, sanitizers, UUID mapping, reply helpers
├── migration/ # versioned SQL files
└── scripts/ # seedProfiles, resetDb, …
Routes are grouped by domain under routes/ (for example authentication/, profiles/, discovery/, socials/). Shared cross-cutting behavior lives in middlewares/ and is composed per route as Fastify preHandler or preValidation hooks. Socket concerns stay under socket/ so connection lifecycle is separate from REST handler files.
Migrations apply automatically when the server starts. For a full reset during development, use the script from the server/ directory.
| Command | Description |
|---|---|
pnpm db:reset |
Drop and recreate database, apply migrations |
pnpm seed:profiles |
Run standalone profile seed |
Schema and seed content are defined in migration/ and src/db/seed.ts.