## ## ##### ####
## ## ## ## ## ##
## ## ## ## ## ##
## ## ## ## ## ##
### ##### ####
Virtual Office Dashboard — a self-hosted Google Workspace lobby
See who's in which Google Meet room, join or create rooms, and track presence — all from one dashboard.
- Google OAuth login — workspace members and external guests use separate flows
- Room management — workspace members create Meet-linked rooms via Google Calendar API
- Presence tracking — heartbeat-based with sendBeacon fallback on tab close
- Meet sync — on-demand sync pulls live participants from Google Meet REST API
- Guest support — any Google account can join rooms; unresolved participants shown as "Guest XXXX"
- Self-hosted — Postgres + Redis via Podman Compose, no managed cloud required
| Layer | Choice |
|---|---|
| Frontend | Next.js 14 (App Router) |
| Auth | NextAuth.js (Google Provider) |
| Database | PostgreSQL 16 + Prisma ORM |
| Cache | Redis 7 (ioredis) |
| Styling | Tailwind CSS + shadcn/ui |
| Infra | Podman Compose + Caddy |
| Monorepo | pnpm workspaces + Turborepo |
git clone <repo-url> && cd vod
make setup
# fill in .env and apps/web/.env.local with your credentials (see below)
make devThat's it. make dev starts Postgres + Redis, runs migrations, and launches the web app.
- Go to console.cloud.google.com → APIs & Services → Credentials
- Create OAuth 2.0 Client ID → Application type: Web application
- Add authorized redirect URI:
http://localhost:3000/api/auth/callback/google - Copy the Client ID and Client Secret
Go to APIs & Services → OAuth consent screen → set User type to External.
If your app is unverified by Google, add Gmail test users manually under "Test users" so they can log in during development.
Enable these APIs in your Google Cloud project:
- Google Calendar API
- Google Meet REST API
- People API
Copy the example env file:
cp .env.example .env
cp apps/web/.env.example apps/web/.env.local # create this if it doesn't existFill in apps/web/.env.local:
# NextAuth
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=your-random-secret-here # generate with: openssl rand -base64 32
# Google OAuth
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
# Workspace domain — users with this email domain are "Members" (can create rooms)
# All other Google accounts are "Guests" (can only join rooms)
ALLOWED_WORKSPACE_DOMAIN=yourcompany.com
NEXT_PUBLIC_ALLOWED_WORKSPACE_DOMAIN=yourcompany.com
# Database & Redis (matches podman-compose defaults)
DATABASE_URL=postgresql://appuser:localdev123@localhost:5432/virtualoffice
REDIS_URL=redis://localhost:6379Fill in .env (root, used by Podman Compose):
POSTGRES_DB=virtualoffice
POSTGRES_USER=appuser
POSTGRES_PASSWORD=localdev123
NODE_ENV=developmentmake setup # copy env files + install deps (run once)
# edit .env and apps/web/.env.local
make dev # start infra + migrate + launch web
make worker # (separate terminal) start background workerOpen http://localhost:3000.
The worker handles presence cleanup (every 2 min) and Meet reconciliation (every 5 min) — optional for local dev but needed in production.
make setup Copy env files, install deps
make dev Start infra + run migrations + start web (hot reload)
make worker Start background worker
make up Build and start full stack via Podman
make down Stop all Podman services
make logs Tail all service logs
make migrate Run pending DB migrations
make generate Regenerate Prisma client
make help Show all commands
make up # build + start all services (web, worker, postgres, redis, caddy)
make logs # tail logs
make down # stop everythingThe app will be available at https://office.localhost via Caddy (self-signed cert — trust it once in your browser).
| Role | Who | Permissions |
|---|---|---|
| Member | @yourcompany.com accounts |
Create rooms, close rooms, trigger Meet sync |
| Guest | Any Google account | Join existing rooms only |
On the sign-in page, members and guests use separate buttons. Both use the same OAuth callback URL — no extra redirect URI needed in Google Cloud Console.
The Sync button on the dashboard pulls live participants from Google Meet REST API using the room creator's stored OAuth token. Participants who haven't logged into VOD are shown as Guest XXXX and automatically registered when they eventually sign in.
Sync also runs automatically every 5 minutes via the background worker.
/
├── apps/
│ ├── web/ # Next.js 14 app (frontend + API routes)
│ │ ├── prisma/ # Schema + migrations
│ │ └── src/
│ │ ├── app/ # App Router pages + API routes
│ │ ├── components/ # UI components
│ │ ├── hooks/ # SWR hooks (rooms, heartbeat, beacon)
│ │ └── lib/ # Auth, DB, Redis, Google API, sync
│ └── worker/ # Background job runner
│ └── src/jobs/ # presence-cleanup, reconciler
├── packages/
│ ├── types/ # Shared TypeScript interfaces
│ ├── ui/ # Shared shadcn/ui components
│ └── config/ # Shared ESLint, Tailwind, TS configs
└── infra/
├── podman-compose.yml
├── podman-compose.dev.yml
├── caddy/Caddyfile
├── postgres/init.sql
└── redis/redis.conf
MIT
