the build
In progressmerQetia
A managed service agency in one app — clients order work, admins price and assign it, employees deliver it, and all three watch the same order move through the same lifecycle in real time.
- My role
- Solo — product, client, server, schema
- Timeline
- 2026 — ongoing
- Core stack
- React 19 · Vite · React Router

what's in it
Features
- The role is the layout
App.jsxwraps every route inProtectedRoute roles={[...]}plus one of three layouts, each owning its nav, command-palette index and notification bell. The same order is a project to a client, a job to an employee and a line item to an admin — no shared page bending into three shapes.- One lifecycle, checked at every hop
placed → assigned → accepted → review → completed, withrejectedas the escape hatch. Each transition is its own endpoint that reads the current status first, so an employee can only accept what was assigned to them and only the client confirms completion.- Real time on the same key as the API
- Socket.IO shares the HTTP server and authenticates on the handshake with the JWT the REST routes already use, then puts each user in a private room plus the broadcast rooms their role qualifies for — new orders reach every admin, an assignment reaches exactly one employee.
- Invoices that can't over-bill
- Numbers come from an atomic counter; full, advance and partial invoices are each capped at the order's *remaining* balance, so however the work is split across deposits and instalments the total can never exceed the price.
- Services carry their own questions
- A service is a price, an internal cost, a department and a list of questions. The answers ride on the order keyed by service id — the department routes the work, the internal cost feeds the P&L, and the answers feed the AI-written brief.
- Conversation where the work is
- Change requests and review submissions render as events rather than plain messages, attachments open in a lightbox, a staff-only thread carries @mentions scoped to the order's participants, and a completed order locks its thread read-only.
- Meetings are one embedded schema
meetingSchemais shared by orders, support tickets and team messages, so a call booked from any of the three behaves identically — a Calendar event with a Meet link, rendered inline, cancellable without being deleted so the history survives.- Private files stay private
- Order and chat attachments live in a non-public R2 bucket and stream through a route that first checks the caller is on that order, which is why images render through an
AuthedImagecomponent rather than a plain<img src>. Only avatars sit in the public bucket. - A P&L, not a revenue number
- Reports fold paid invoices against manual expenses and the internal cost of completed orders over any date range, then break out order volume by status and top services by revenue.
- Degrades instead of failing
- Without the AI keys the order brief is skipped, without mail credentials nothing is sent, without Google credentials meetings can't be booked — each optional service is absent, not fatal. Invoice PDFs render through Puppeteer against a pinned Chromium so the same template serves the download and the email.
a look around
What it looks like in use
The admin board groups by lifecycle stage rather than listing everything — new orders need a decision, active ones need watching, and the two never compete for the same row.
one order, three products
The lifecycle tracker is the transition log itself, so every hop carries the timestamp it actually happened at — including the loops back to In Progress.
The client sees projects and what needs paying, not the agency's machinery — same order record, different product.
An employee gets only the orders assigned to them, enforced on every REST route and socket message rather than by hiding a nav item.
Profit comes out of the same records the work runs on: paid invoices minus manual expenses and the internal cost attached to each completed service.
Three invoice shapes against one order — an advance, a partial and the balance — all sharing the template that renders to PDF.
Tickets get a readable tracking code, and a call can be scheduled inside the thread — cancelled meetings stay visible instead of disappearing from the record.
what it's made of
The stack
- Client
- React 19
- Vite
- React Router
- react-hook-form
- socket.io-client
- CSS design tokens
- Server
- Node
- Express 5
- Socket.IO
- JWT
- Helmet
- express-rate-limit
- express-mongo-sanitize
- Data
- MongoDB (Atlas)
- Mongoose 9
- Services
- Cloudflare R2
- Google Calendar + Meet
- Nodemailer
- OpenAI-compatible API
- Puppeteer