CommaaS · The laboratory
A personal deployment hub, which is mostly an excuse to try things properly
CommaaS started as a product and became something better: my own platform, where anything I build gets a real deployment instead of dying in a folder named projects.
It is a pet project, and it is meant to be fun. It is also the place where I test an architectural idea before it goes anywhere that matters, which means it is built to the same standard as the thing it is testing for. You are reading this on it.
- Apps live
- 2
- Tenants queued
- 5
- Lines of code
- ~12,800
- Migrations
- 17
- Shared packages
- 2
- Supabase projects
- 1
- Next.js 16 (App Router)
- TypeScript, strict
- npm workspaces monorepo
- Supabase (Postgres · Auth · Storage)
- Row-level security
- Vercel, one project per app
- Anthropic API
The shape: one project, one schema per app
Every app on the hub gets its own Postgres schema, its own Vercel project and its own subdomain, inside a single Supabase project and a single repository. Not a database each, which would be five bills and five backup stories. Not one shared schema either, which is how unrelated apps end up coupled through a table nobody owns.
The hub itself is an app like the others. It happens to own the schema that grants access to all of them.
Row-level security that asks two questions
Every policy checks that the row belongs to you AND that you have been granted the app. Owning the row is not enough: access to an app is revocable, per person, and revoking it closes the data with it.
A trap, written down
Postgres schemas have to be added to the API's exposed list AFTER they exist. Listing one that does not exist yet takes down the entire API with a single error code. It happened once, it cost an afternoon, and now the exposure lives in its own migration with the reason attached.
The contract as a package
Everything that must behave identically across apps lives in one package: the authenticated client, the guards that require an app or the owner, the session proxy, the shared cookie configuration, the model price table, and the two functions that bracket every paid call.
There is deliberately no service-role key in that package. Each app writes as the identity of the person using it, and row-level security decides what happens. A service-role key inside a tenant is treated as a review finding, not a shortcut.
Adding a tenant is a recipe, not a design session
An app folder, a migration with its schema and its double-question policies, a storage bucket if it holds files, the guard in its proxy, the spend bracket, a Vercel project, a DNS record, and three allowlists that always move together. Written down, in order, with the checklist that catches the one everyone forgets.
Permissions, budget and a ledger that knows what for
Access is granted per person and per app, with an optional budget, an optional expiry, and a revocation that leaves a trace. Before any paid call, the code asks whether this person, on this app, can still spend. After the call, it records what was actually spent, with the cost frozen at that moment so a later price change cannot rewrite history.
The ledger also records what the money was for, in the app's own terms. That is the difference between knowing that a month cost twelve dollars and knowing that one particular job application cost nine cents.
The recording function never throws. A ledger that can break the feature it is measuring is worse than no ledger.
The number that forced the architecture
The CV matcher cost $0.43 per run. The owner's verdict was that the whole thing had to stay under $0.10 or the tool was not realistically usable. Turning off extended thinking, turning off web search by default, and trimming the prompt to the job description itself brought a measured run to $0.0918, at the same model and the same quality.
The engine, and the one rule above all others
The original CommaaS product is a model that turns communication work into projected outcomes and a settlement. Its engine is a pure function: no input or output, no clock, no randomness. Nothing recomputes its formulas in SQL, in a React component or in an edge function.
The reason is auditability. Months after a settlement is issued, a client can ask how a number was produced, and the same parameter hash has to reproduce the same result exactly. Two implementations of one formula diverge, and the one that loses is the contract.
Invariants with a test each
The engine is deterministic. Piece order does not change the result. Decay weights sum to one. Calibration debiases sequence effects. A metric without real samples is not eligible for a bonus. Retention saturates instead of growing linearly. Each line is a row in a table in the repository, and each row names its test.
What is pivotable, and what is not
All the mechanics live in data, so changing them needs no migration and no deploy. The interface is explicitly pivotable too. The invariants above are not, and changing one is an architectural decision with a written trail.
One sign-in across every subdomain
A single gate, on the hub, serves every app. The session cookie is scoped to the parent domain so signing in on the hub is a session on the CV manager. Each app's proxy refreshes the token on every request and sends anyone without a session to the gate, remembering where they were going.
The proxy refreshes, it does not authorize. Authorization is row-level security, in the database, where it cannot be forgotten by a route.
What is on it
Two apps are live. The hub, with its launcher and a backstage for apps, people, grants, consumption and the audit log. And the CV App Manager: a repository of experience that matches against a job posting, produces a tailored CV and cover letter, and tracks the application afterwards, all under the ten-cent ceiling.
Those two are behind the invitation gate, so they are not pictured here. What is pictured is the queue: eight prototypes waiting to be ported in, each one a reason the hub exists at all.
A vacation destination engine being rebuilt on free open data after an expensive lesson. A paint-by-numbers studio with two independent color engines that have to agree. A string-art pattern maker. A document anonymizer built at a hackathon in Munich that works entirely in the browser. A party card game. A family archive. A paper photo-album planner. And the workshop behind a book about Egypt.


What transfers to other applications
The domain is specific. The patterns are not. These are the pieces I would carry into a different product on the first day.
The tenant pattern
Schema per tenant, policies that check both ownership and entitlement, a recipe for onboarding a new one. This is the cheap end of multi-tenancy for anyone whose product is several small applications rather than one big one.
Spend brackets around paid calls
Ask before, record after, never let the recorder break the feature, and store what it was for. It works for any metered dependency, not just language models.
Pure engines for anything audited
Pricing, commissions, scoring, payroll: if somebody can dispute a number months later, the thing that produces it should be a pure function with a version, and nothing else should be allowed to compute it.
A contract package instead of a convention
The things that must behave identically everywhere are code you import, not a paragraph in a wiki. What cannot be imported, one router line that a build tool refuses to resolve, is documented as the exception with the reason, so nobody deletes it as a duplicate.