Architecture
MeetCast splits responsibilities: Next.js owns auth, access control, and tokens; PostgreSQL stores durable data; LiveKit carries media and in-meeting signals.
Big picture
Browser
│
├── Next.js (UI, Server Actions, API routes)
│ ├── PostgreSQL (Neon + Drizzle)
│ └── issues short-lived LiveKit JWTs
│
└── LiveKit (audio, video, screen share, data channel)LiveKit is not the application database. When a meeting ends in Postgres, peers are not force-disconnected from LiveKit in the same instant — token expiry and client leave handle the rest.
Next.js
Handles:
- Registration, login, sessions, password reset, soft email verify
- Room create/edit, schedule windows, visibility, allowlists
- Plan limits and checkout/order fulfillment
- Admin panel actions and audit logs
POST /api/livekit/tokenafter authorization- Moderation APIs (mute, camera off, remove, role changes)
There is no middleware.ts in this repo. Access checks happen in Server Actions, route handlers, and page loaders.
PostgreSQL
Stores users, sessions, auth tokens, rooms, members, allowed emails, revoked guests, plans, orders, rate-limit events, and admin audit logs. Schema: src/db/schema.ts. Migrations: drizzle/.
LiveKit
The browser connects to LIVEKIT_URL with a JWT minted on the server. Chat, raise hand, and reactions use a LiveKit data channel topic (see src/lib/collaboration/protocol.ts) — there is no Socket.IO server.
Why these choices
Why LiveKit?
Building a reliable SFU, signaling, and reconnect story from scratch is a product of its own. MeetCast uses LiveKit for media and keeps authorization in Next.js.
Why PostgreSQL + Drizzle?
Rooms, memberships, plans, and orders need durable storage and migrations. Drizzle maps TypeScript schema to SQL under drizzle/. Neon’s HTTP driver is used — no multi-statement transactions — so some counters (capacity, rate limits) can soft-race under burst load.
Why no Redis?
Sessions and rate limits live in Postgres. That keeps local setup smaller. It is not a global edge rate limiter.
Why no raw WebRTC in app code?
The meeting UI uses LiveKit client SDKs. MeetCast never ships LIVEKIT_API_SECRET to the browser.
Project layout
src/
├── app/ # routes, Server Actions, API handlers
├── components/ # UI (meeting, rooms, billing, admin, docs…)
├── db/ # Drizzle schema + client
└── lib/ # auth, rooms, livekit, payments, security
drizzle/ # SQL migrations
docs/ # repo markdown (e.g. production-security)Environment variables
| Variable | Required | Description |
|---|---|---|
DATABASE_URL | Yes | Postgres connection |
AUTH_SECRET | Yes | Session + guest HMAC |
LIVEKIT_URL | Yes | SFU WebSocket URL |
LIVEKIT_API_KEY | Yes | LiveKit API key |
LIVEKIT_API_SECRET | Yes | LiveKit API secret (server-only) |
NEXT_PUBLIC_APP_URL | Prod-ish | Absolute origin for invites / email links |
ADMIN_EMAILS | Optional | Comma-separated bootstrap admins |
RESEND_API_KEY | Optional | Auth emails; app works without it |
RESEND_FROM_EMAIL | Optional | Defaults to Resend test sender |
ZARINPAL_MERCHANT_ID | Optional* | Paid checkout; mock used in non-prod when unset |
ZARINPAL_SANDBOX | Optional | Defaults to sandbox outside production |
PAYMENT_PROVIDER | Optional | auto | zarinpal | mock |