All work
Work

Virtual Office Platform

A virtual office for small studios - an office floor with voice rooms, chat, projects and approvals in one TypeScript app.

Office floor view with team rooms, desks and a people panel
Role
Design and full-stack, personal project
Year
2026
Stack
  • Next.js (App Router)
  • React 19
  • TypeScript
  • Drizzle ORM
  • PostgreSQL
  • WebRTC
  • Server-Sent Events
  • Tailwind CSS v4
  • Radix UI
  • TanStack Query
  • Zod
  • Turborepo
  • Vitest
  • Playwright
What I did
  • Monorepo architecture: domain core, data, policy, auth, crypto and design-token packages behind one Next.js app
  • Office floor with team rooms, desks, presence and WebRTC voice, with knock and invite consent
  • Realtime over a single SSE stream fed by a server-side hub
  • Chat with encrypted message bodies, threads, reactions, pins and mentions
  • Projects and tasks: list and kanban views, drag-and-drop ranking, custom stages
  • Typed policy engine: every action maps to one rule function, checked on both page and API
  • Cookie session auth with signed tokens, verified against golden test vectors
  • AI agent register and usage metering with a monthly spend ceiling
  • Quality gate scripts: contrast, token sync, schema drift, RN-safe core

Overview

A small studio runs its day across too many tabs: who is in, who is talking to whom, what is due, who is on leave. This project puts it in one place. The home screen is an office floor with voice rooms; around it sit chat, projects and tasks, leave and approvals, a people directory, and a register of the AI agents the studio uses.

It is a showcase build: one Next.js app over a set of small packages for domain logic, data, policy, auth and encryption, with realtime updates on a single stream. The screens below use sample data and a placeholder brand.

The office floor

The home screen is a floor plan. Each team has a room with desks, and there are shared rooms - a conference room, a lounge, focus rooms, a theater. Clicking a seat walks you there; your presence dot, lunch and away states travel with you.

Rooms are voice rooms. Joining an occupied focus room sends a knock, pulling someone in sends an invite, and neither connects audio until the other side says yes. Signalling rides the same realtime stream as everything else; media is peer-to-peer WebRTC with a TURN fallback.

Main floor with team rooms and people panel

Projects and tasks

Every project has a task list and a board over the same data. Columns are per-project stages, cards are ranked with sortable string keys so a drag writes one row, and assignees, due dates and tags show on the card.

Projects carry status, owners, dates and progress, and sit in a personal sidebar order so the work you touch most stays on top.

Project kanban board
Project task list

Chat

Chat covers DMs, groups and one conversation per project, with threads, reactions, pins and mentions. Message bodies are encrypted at rest with a key the database never sees, so a row read straight from SQL is ciphertext.

Group conversation for a project

Roles and policy

Four roles and two tag modifiers decide what anyone can see or do. Instead of role checks scattered through handlers, every action in the product has a name - project.create, task.update, conversation.post - and each name maps to one rule function in a policy package.

API routes are wrapped with the action they perform, pages ask the same package whether to render, and the rules are plain functions with their own test suite.

People directory with roles

AI agents, on a budget

AI features are treated like staff: each agent has an owner, a procedure owner and a readiness checklist across ownership, documentation, training, evaluation and deployment. An agent that is live with an unfinished checklist is called out at the top of the page.

Every model call is metered by feature and by person, priced with peak and off-peak rates, and AI pauses automatically when the month hits its ceiling.

AI agent register with readiness gates
AI usage and cost dashboard

Built to stay green

Domain logic lives in a core package with no Node imports, checked by a script, so a future mobile client can share it. A drift check fails the build if the Drizzle schema and the database disagree, and one command runs lint, types, tests and the custom checks.