import cookie from '@fastify/cookie'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import websocket from '@fastify/websocket'; import Fastify from 'fastify'; import { ZodError } from 'zod'; import { sql } from './db'; import { env } from './env'; import { loadUser } from './lib/auth'; import { HttpError } from './lib/http'; import { redis } from './lib/redis'; import { authRoutes } from './routes/auth'; import { connectionRoutes } from './routes/connections'; import { meRoutes } from './routes/me'; import { realtimeRoutes } from './routes/realtime'; import { trackRoutes } from './routes/tracks'; import { userRoutes } from './routes/users'; import { webhookRoutes } from './routes/webhooks'; const app = Fastify({ logger: true, trustProxy: true }); await app.register(cors, { origin: [env.WEB_URL], credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], }); await app.register(cookie); // Rate limits are per-route (see `config.rateLimit`); the Redis store makes them global across instances. await app.register(rateLimit, { global: false, redis }); await app.register(websocket); app.decorateRequest('user', null); app.decorateRequest('sessionId', null); app.addHook('onRequest', loadUser); app.setErrorHandler((err, req, reply) => { if (err instanceof ZodError) { return reply.status(400).send({ error: err.issues[0]?.message ?? 'Invalid input', issues: err.issues }); } if (err instanceof HttpError) return reply.status(err.status).send({ error: err.message }); const status = (err as { statusCode?: number }).statusCode; if (status && status < 500) return reply.status(status).send({ error: (err as Error).message }); req.log.error(err); return reply.status(500).send({ error: 'Internal server error' }); }); app.get('/health', async () => { await Promise.all([sql`select 1`, redis.ping()]); return { ok: true }; }); await app.register(authRoutes); await app.register(meRoutes); await app.register(userRoutes); await app.register(trackRoutes); await app.register(connectionRoutes); await app.register(realtimeRoutes); // registered as its own plugin so its permissive body parsers stay scoped to webhook routes await app.register(webhookRoutes); for (const signal of ['SIGINT', 'SIGTERM']) { process.on(signal, async () => { await app.close(); await sql.end(); process.exit(0); }); } await app.listen({ port: env.PORT, host: '0.0.0.0' });