Case study
KanbanHub
A team workspace with Trello-style boards, time tracking and live updates, five ways to sign in and per-device sessions, built and deployed end to end as a solo project.
- Role
- Solo project: product, design, frontend, backend and deployment
- Timeline
- Jun 2025 – Oct 2026
- Stack
- ReactZustandTailwind CSSNode.jsExpressSocket.IOMongoDBFirebase AuthWebAuthnZodPlaywright

- automated tests
- 215
- automated tests
- end-to-end browser flows
- 28
- end-to-end browser flows
- sign-in methods, incl. passkeys
- 5
- sign-in methods, incl. passkeys
- commits since June 2025
- 80
- commits since June 2025
The problem
Small teams scatter ownership, progress and time across chats, spreadsheets and boards nobody updates. KanbanHub keeps them in one place: every card has people, dates, a checklist and the hours spent on it, and the board updates live for everyone on it.
Boards can be private, shared with the workspace or opened through a public link, so access control is part of every feature: the API, the live connection, invite links and email notifications all check it.
What I built
- Trello-style boards with photo or colour backgrounds; lists you can colour, fold, sort, copy and move
- Cards with covers, labels, priorities, dates, checklists, attachments, watchers and comments
- Drag and drop written from scratch on pointer events, for mouse and touch, with edge auto-scroll and FLIP animations
- Time tracking: a timer per person, hours added by hand, estimates and totals per person
- Live updates and presence over Socket.IO, so changes appear without a refresh
- My tasks, a calendar with drag to reschedule, a team view with workload, a card filter and ⌘K search
- Sharing by email with roles, invite and public view links, and ownership transfer
- Notifications in the app, as desktop pop-ups and by email, instantly or as an hourly digest
- Email and password, Google, Microsoft, Apple and passkey sign-in, with a list of signed-in devices
- Light and dark themes; English, Ukrainian and Russian interface
Screens
Board
Lists with labels, priorities, dates, checklists and a live timer on the card in progress. Changes sync for everyone on the board.
Architecture
A React single-page app talks to an Express API over REST and Socket.IO. Both are deployed on Render under kanbanhub.app and api.kanbanhub.app; data lives in MongoDB Atlas.
01
Client
- React + Vite, Tailwind CSS
- Zustand state
- Own pointer-based drag and drop
- Socket.IO client
- Firebase Auth and SimpleWebAuthn
02
API
- Express REST API with Zod validation
- Socket.IO rooms and presence
- JWT in HttpOnly cookies, one session per device
- Helmet, CSRF origin checks, rate limits
- Background jobs: due reminders and email digests
03
Data and services
- MongoDB Atlas with transactions
- Firebase Admin for provider tokens
- Brevo transactional email
- Cloudinary for avatars and images
- Unsplash, Picsum and Wikimedia backgrounds
Engineering decisions
01
Realtime that respects permissions
Sockets authenticate with the same HttpOnly cookie as the REST API. Joining a board room requires ownership or membership, and every heartbeat re-checks the session, so a revoked session also loses live updates. People show as away after 45 seconds without a heartbeat.
02
A session per device
Each sign-in creates a session record named in the cookie, so the settings page lists every device and can sign out one or all the others. A signed-out device drops its live connection at once. Changing the password ends every other session and keeps the current one.
03
Time that is counted once
A person has one running timer: starting another one stops the first. A timer is finished with a conditional update, so two stop requests at the same moment log the time only once, and the card total is recalculated from the entries.
04
Safe under concurrent requests
Two signups for the same email, or two uses of one password-reset token or passkey challenge, can only succeed once. Removing a member is one MongoDB transaction: membership, their notifications, task assignments and open invitations are cleaned up together.
05
Links that expire when access changes
Public and invite links are stored as hashes with a version. Changing a board’s visibility or revoking a link bumps the version, so old links stop working immediately.
06
Sign-in bound to the provider account
Google, Microsoft and Apple sign-in is bound to the provider’s account ID, not the email, so another account claiming the same address cannot take over. Provider tokens must be fresh, unrevoked and for a verified email, and linking needs the current password.
07
Email that does not spam
Activity can arrive instantly, capped at 30 emails an hour, or as an hourly digest grouped by board. The digest waits five minutes to gather a burst of changes, and each user’s digest is claimed first so two servers never send it twice.
08
Drag and drop without a library
Cards and lists move with pointer events for both mouse and touch: a lifted ghost, a drop slot, auto-scroll near the edges and FLIP animations when cards land. On touch screens nothing opens the keyboard by itself.
Testing
152 backend integration tests run on Node’s built-in test runner with Supertest against an in-memory MongoDB, and 63 frontend unit tests cover card filters, sorting and display rules. 28 Playwright flows drive the real app in a browser, from sign-up to sharing a board. A few of the backend cases:
- logs time only once when a timer is stopped twice at the same moment
- never signs in a different provider account that claims the same email
- ends every other session and keeps this device signed in
- issues passkey sign-in challenges without an email and accepts each one only once
- invalidates a general invite link after room visibility changes
- allows a reset token to be claimed only once under concurrency
Want to talk about this project?
I’m happy to walk through the code, the trade-offs and what I would build next.