Skip to main content
All work

the build

Live

Kendro

A virtual co-working space — take a desk, say what you're working on, and work the block alongside your team, your friends, or whoever else is in the room.

My role
Solo — product, client, server, schema
Timeline
2025 — ongoing
Core stack
React 18 · Vite · Tailwind v4
Kendro running on a laptop

what's in it

Features

Sessions are the unit, rooms are optional
A Session can have room: null. That one nullable field is the difference between 'sign up and start working' and 'sign up, find a room, request to join, wait for a stranger to approve you' — and the grid, stats and presence all read the same collection either way.
A floor of desks, not a call
Every person is a nameplate carrying their name, their work topic and a clock since they sat down; video fills the desk only when the camera is on. Camera-off is the common case in co-working, so it gets a real state instead of a black rectangle.
Presence is one query
GET /sessions/live resolves everyone you share a team, room or friendship with and returns their open sessions with the connection labelled, so the dashboard can't drift from what the team page shows.
Chat channels are reconciled, not hooked
POST /chat/sync diffs your rooms and teams against your Stream channels and fixes the difference. Hooking eight mutation sites instead means any one failing quietly strands you in a channel for a room you left; the reconcile is idempotent and heals rooms that predate chat.
Durations are never trusted from the wire
actualDuration is derived server-side, has break time subtracted and is capped at twice the session's own target. A throttled sweep closes anything that stops sending a heartbeat, flags it abandoned, and abandoned rows are excluded from every aggregate.
The paywall is one line, in one place
createTeam checks user.plan. Nothing else does — so an invited member gets the whole team experience without an upgrade, and a pricing change touches a single function.
Team rooms are private by construction
getAllRooms only returns rooms whose team is null or one of yours, and requestToJoinRoom re-checks membership before admitting anyone. Visibility and access are enforced separately, so neither is the only thing between a stranger and a company room.
One hook owns "in session"
The dashboard, sidebar and session page all read useActiveSession, so they can't disagree — the dashboard used to offer 'take a desk' to someone already mid-session.
A record that buckets by your calendar
Every finished session fills a square on a contribution grid, bucketed by the user's local date, so a session closed at 11pm lands on the day it felt like.
Invites that degrade instead of failing
Team invites go out over Gmail SMTP; with the credentials unset — or the send failing — the invite is still created and the toast offers a shareable link, so a mail outage can't stop a team being built.

a look around

What it looks like in use

Two people at their desks with a task list and clock beside them. Desks lay out with auto-fill rather than viewport breakpoints, because the session panel takes 384px out of the column those breakpoints assume.

presence is the point

Hours and a year of squares first, then who else is at a desk right now — one /sessions/live call across teams, rooms and friends.

Breaks come off the clock, so "49 minutes worked" means work, not the timer being open.

A channel per room and per team, created from the database rather than by hand — joining a room adds its conversation, leaving takes it away.

Who's at a desk, hours logged this week per person, and the rooms only members can open. Creating a team is the paid part; joining one never is.

The stat row and the grid fold to one column; nothing is hidden behind a 'view on desktop' notice.

same room, one hand

The floor keeps its desk nameplates on a phone, with the call controls docked where a thumb already is.

what it's made of

The stack

Client
  • React 18
  • Vite
  • Tailwind v4
  • DaisyUI 5
  • TanStack Query
  • Zustand
  • React Router 7
Server
  • Node
  • Express 4
  • Mongoose
  • JWT
  • bcrypt
Data
  • MongoDB (Atlas)
Services
  • Stream (chat + video)
  • Nodemailer over Gmail
  • ImgBB