What it is
Lumina is vertical SaaS for salons and barbershops — online booking, client records, staff scheduling and the financial side, with every business's data walled off from every other's. A client opens a salon's public booking page, picks a service and a time that's genuinely free, books, and the appointment lands on the owner's calendar with both parties emailed. That loop is what I'm building toward, and I treat it as the definition of done rather than as one feature among many.
It's also the direct sequel to Continuo, for an uncomfortable reason. I started Lumina in July 2025 and it stalled about 118 commits in, for exactly the reason Continuo did: I was producing code faster than I could verify any of it. Coming back a year later and actually measuring it, the build didn't compile, type-check had 686 errors, 101 of 114 test suites failed, coverage was 0%, and it had never once deployed. The seed factory had been broken for months — it computed required fields and then didn't return them — so nobody could run the app with data, so nothing had ever been checked end to end. That's how "86% complete" and 0% coverage managed to coexist for the better part of a year.
So the rebuild isn't a rewrite. It's a revisiting, with the support of Claude Code, to keep a consistent AI-companion throughout the project, and reduce the amount of micromanagement I was doing to push development forward.
It's the same product with a feedback loop bolted on: a small CI gate that actually passes, one branch per pull request, and a rule that written code isn't finished code. Where it stands against where it started:
| Before | Now | |
|---|---|---|
npm run build | fails | passes, 80/80 pages |
npm run db:seed | fails | works |
npm run type-check | 686 errors | 0 |
npm run lint | 180 errors, not run at build | 0, and it gates the build |
| Tests | 101 of 114 suites failing, 0% coverage | green in CI |
| Deployment | never happened | auto-deploys on green CI |
| Booking loop | never completed once | green end to end |
| Production CVEs (high/critical) | 16 | 2 |
The habit that came out of it is boring but has earned its keep: fix things by measuring them. A recent one looked like a timezone bug — a salon open nine to six was offering four-in-the-morning slots to anyone a few zones away. The real finding was that two services disagreed about what time it was, agreed with each other because both were wrong in the same direction, and the bug lived in the agreement. Fixing one of them turned a wrong answer into no answer at all. Same story with speed: "the booking page feels slow" became "one availability request runs 1,318 database queries and reads the same opening hours 180 times", which is a problem you can actually fix. It's now 391 queries.
Stack
- Frontend — Next.js 14 (App Router), React 18, TypeScript in strict mode, Tailwind CSS, Radix UI
- Backend — Next.js route handlers, NextAuth v5, Zod validation at every API boundary
- Data — PostgreSQL 15 with Prisma
- Email — Resend, for booking confirmations and reminders
- Testing — Jest for unit tests, Playwright for the end-to-end booking loop
- Hosting — Railway, deploying to staging automatically when CI is green
Design tokens live in exactly one file and the Tailwind config is generated from it, so there's no raw hex anywhere in the components. Multi-tenancy isn't a convention either — every query touching business data filters by tenant, and route access is checked against the session rather than trusted from the URL.
Where it's at
Live on staging, seeded, and the public booking loop completes end to end — I've taken a real booking through it myself, which after a year of it never once working is a better milestone than it sounds. Four of seven phases done or in progress. Still ahead: collapsing the design system, brand work, and an actual production launch. No customers yet, and I'm not pretending otherwise.
It's open source under AGPL-3.0 — free to self-host, forever. If there's ever revenue it comes from running it for people through Effuse Labs, not from locking up the code. Anyone running a modified copy as a service has to share their changes back, which is the point: it keeps the project open no matter who ends up hosting it, including me.
Going public was its own small lesson. A secret scan over the full history turned up an API key for the issue tracker I'd used during the first attempt, committed inside an IDE's config directory and left behind in history when that directory was deleted. Harmless in the end — the account was long gone — but rewriting history wasn't enough to remove it, because GitHub keeps the original commits of every pull request forever. The only clean fix was rebuilding the repository from scrubbed history, which is why Lumina now lives under the Effuse Labs organization and its history starts where it does. Worth knowing before you publish something, rather than after.
