A Google-Meet-class video meeting app with a unique, classy / gen-z vibe.
Marketing site + in-room app are built with Astro + Tailwind v4 (Vercel-inspired
design language, see DESIGN.md). The entire backend — signaling, presence, chat,
and serving the built frontend — is one Go binary.
┌─────────────────────────────┐ WebSocket /ws (signaling, chat, presence)
│ Browser (Astro + WebRTC) │◀──────────────────────────────────────────────┐
│ • marketing site (/) │ REST /api/rooms (create / lookup) │
│ • in-room app (/room/<id>) │◀───────────────────────────────────────────┐ │
└──────────────┬──────────────┘ peer media flows P2P (WebRTC mesh) │ │
│ video/audio ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │
▼ (direct peer-to-peer, never touches server) │ │
other participants ◀──────────────────────────────────────────────┘ │
│
┌──────────────────────────────────────────────────────────────────┐ │
│ Go backend (cmd/server) — serves dist/ + relays signaling ───────┘ │
└──────────────────────────────────────────────────────────────────────┘
Media is peer-to-peer (WebRTC mesh). The Go server only relays the SDP/ICE handshake + room events, so it stays light. For very large rooms this can later be swapped for a Pion SFU without touching the client protocol.
- ⚡ Instant rooms — one tap, shareable link
zmeet.online/<code>, no install, no sign-in. - 🎥 Camera / mic / screen share, device pickers, mirrored self-view.
- 🟢 Speaking detection, active-speaker rings, pin-to-spotlight, auto grid layout.
- 💬 In-room chat with unread badges.
- 📝 Live captions (Web Speech API, broadcast to the room) — the recap story for the marketing site.
- 🎉 Vibe reactions (floating emoji) + ✋ raise hand.
- 👥 People panel with live mic/cam/hand presence.
- ⏱️ No time limits, keyboard shortcuts (
dmic,ecam,cchat).
The backend reads backend/config.json (gitignored — copy from config.json.example):
cd backend && cp config.json.example config.json # then edit| Field | Purpose |
|---|---|
addr |
listen address (:8080) |
static_dir |
built frontend dir; "" = API/WS only (dev), ../dist = prod |
public_url |
origin the browser uses — dev http://localhost:4322, prod https://zmeet.online (used to build the OAuth redirect URI) |
log_format |
text (dev) or json (prod) |
log_level |
debug / info / warn / error |
database_url |
Postgres DSN; empty disables persistence + auth |
session_secret |
long random string that signs the session cookie |
google |
OAuth client_id + client_secret (empty = guest-only, no sign-in) |
admin_emails |
emails that get host powers in any room |
user_emails |
member allowlist (informational for now) |
A signed-in user whose email is in admin_emails joins any room as a host. In the
People panel they get per-participant controls: mute / unmute, stop / start
video, lock mic (prevent unmute), lock leave button, and remove (kick).
Hosts show a HOST badge.
Roles are assigned server-side from the session cookie + admin_emails, and host
commands are relayed only from verified admins — a regular user faking an admin
message is dropped. (Forcing a camera on is honoured directly here since it's an
invite-only environment; a public product would prompt the user first.)
When OAuth is configured, Zmeet is members-only: you must be signed in and
listed in admin_emails or user_emails to create or join a meeting. This is enforced
server-side — guests/unlisted accounts get 403 on POST /api/rooms and /ws.
A signed-in non-member sees an invite-only screen with a Request an invite
button (in the launcher and the room lobby). Clicking it records a row in the
invite_requests table. Review them and grant access by editing config.json:
docker exec -it zmeet-postgres psql -U zmeet -d zmeet \
-c "SELECT email, name, requests, updated_at FROM invite_requests ORDER BY updated_at DESC;"
# add the email to "user_emails" (or "admin_emails") in config.json, then restart the backenddocker run -d --name zmeet-postgres \
-e POSTGRES_USER=zmeet -e POSTGRES_PASSWORD=zmeet -e POSTGRES_DB=zmeet \
-p 5432:5432 postgres:16DSN: postgres://zmeet:zmeet@localhost:5432/zmeet?sslmode=disable. The users table
is created automatically on first boot.
- In Google Cloud Console → create an OAuth 2.0 Client ID (type: Web application).
- Add an Authorized redirect URI:
{public_url}/api/auth/google/callback(dev:http://localhost:4322/api/auth/google/callback). - Put the client id/secret into
config.json→google.
When configured, a "Sign in" button appears in the nav and the room uses the user's Google name + photo. Without it, everything still works as guests.
The Astro dev server proxies /api + /ws to the Go backend:
# 1) backend (config.json: static_dir "", public_url http://localhost:4322)
cd backend && go run ./cmd/server
# 2) frontend
npm install
npm run dev # http://localhost:4322Open two browser tabs, click New meeting in one, paste the link into the other.
(Camera/mic require localhost or HTTPS.)
Production is split: Go backend on the Oracle server (api.zmeet.online, systemd
zmeet behind nginx) + static frontend on Cloudflare Pages (zmeet.online).
make # list targets
make deploy # backend + frontend
make deploy-backend # cross-compile arm64, scp binary, restart service, health-check
make deploy-frontend # build with PUBLIC_API_BASE, wrangler pages deploy
make health # hit prod /healthz, /api/auth/me, and the web root
make logs-backend # tail server logsThe backend config.json (secrets) lives only on the server and is not pushed by
deploy-backend — edit it there (e.g. to add invite emails) and make restart-backend.
Override host/key/etc. inline, e.g. make deploy-backend SSH_HOST=1.2.3.4.
npm run build # → dist/
cd backend
go build -o zmeet-server ./cmd/server
# config.json: static_dir "../dist", public_url "https://zmeet.online"
./zmeet-server # serves everything on :8080Logging uses the stdlib log/slog. Each HTTP request is logged with method, path,
status, duration and client IP; room create / join / leave events are logged with
structured fields. /healthz and /_astro/* are logged at debug to keep info clean.
- WebRTC NAT traversal uses public STUN. Cross-network calls behind strict NATs need a
TURN server — add one to
ICE_SERVERSinsrc/scripts/room.ts(e.g. apion/turninstance). - Live captions use the browser Web Speech API (best in Chromium).
src/
components/ marketing + shared UI (Nav, Hero, Features, …, Icon, Logo)
layouts/ Layout.astro (fonts, meta, day/night theme)
pages/
index.astro marketing landing
room/[code].astro in-room app shell (served for any code by Go)
scripts/room.ts WebRTC mesh + signaling client
styles/global.css Tailwind v4 theme tokens + components
backend/
cmd/server/ entrypoint
internal/signal/ hub (rooms/clients), ws server, message protocol