Skip to main content
All work

the build

In progress

merQetia

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
merQetia running on a laptop

what's in it

Features

The role is the layout
App.jsx wraps every route in ProtectedRoute 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, with rejected as 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
meetingSchema is 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 AuthedImage component 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