Docs

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

text
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/token after 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

text
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

VariableRequiredDescription
DATABASE_URLYesPostgres connection
AUTH_SECRETYesSession + guest HMAC
LIVEKIT_URLYesSFU WebSocket URL
LIVEKIT_API_KEYYesLiveKit API key
LIVEKIT_API_SECRETYesLiveKit API secret (server-only)
NEXT_PUBLIC_APP_URLProd-ishAbsolute origin for invites / email links
ADMIN_EMAILSOptionalComma-separated bootstrap admins
RESEND_API_KEYOptionalAuth emails; app works without it
RESEND_FROM_EMAILOptionalDefaults to Resend test sender
ZARINPAL_MERCHANT_IDOptional*Paid checkout; mock used in non-prod when unset
ZARINPAL_SANDBOXOptionalDefaults to sandbox outside production
PAYMENT_PROVIDEROptionalauto | zarinpal | mock