Corebase
Architecture of a multi-tenant platform in development — and how an owner check copied across 42 call sites became named rules that a linter won't let anyone scatter again.
- Client
- Internal tool
- Year
- 2026
- TypeScript
- Next.js
- PostgreSQL · Drizzle
- Multi-tenant
Context
An agent built for one client, like HanamiBot, proves the idea works. Offering it to many businesses is a different problem: tenants, accounts, permissions, configuration and an interface each business can run on its own. Corebase is the platform layer for that.
The problem
A custom deployment per client doesn't scale. The platform needs to keep each business's data isolated, let new verticals plug in without new apps, and stay small enough for a small team to maintain.
Architecture
- Actors. Every request is resolved to one actor: an agency admin, a client user with their workspace memberships, an API key, a share link or the system. Authorization rules are written once against that model.
- Authorization core. Guards such as
requireWorkspaceAccessandrequireStructureEditorare the only way a service reaches data. See the hard part. - Workspaces. The tenant boundary. Members have a role — viewer, editor or owner — and objects can carry grants that hide them or make them read-only.
- Page-first. The page is the universal container: bases, tables and views live inside pages, and permissions are inherited down the page tree.
- Real-time and files. Co-editing runs on a separate Yjs + Hocuspocus server; files go to MinIO.
- Integrations. API keys, webhooks, an MCP server and a custom n8n node — the node is how Hanami's workflows write into it.
Key decisions & trade-offs
Decision
Consolidate: Corebase as the only trunk
An earlier project, Synaptic, overlapped with Corebase. Instead of maintaining both, Corebase became the single trunk and Synaptic was frozen.
Trade-off · Useful code from the earlier project has to be ported by hand, and that project no longer gets fixes.
Decision
Page-first architecture, Notion-style
Pages are the primary unit of the product. New functionality arrives as something that lives inside a page instead of a new, separate section of the app — and permissions follow the page tree instead of being configured per screen.
Trade-off · A generic page model is harder to design up front than a set of fixed screens.
Decision
Tenant isolation in the application, not in database policies
Services derive the workspace from the record they load and check the actor against it before returning anything. One authorization model covers every kind of caller — including API keys and share links, which aren't database users.
Trade-off · The database won't stop a query that skips the guards, so the guards themselves need audits, lint rules and tests.
Decision
Mobile deferred until there's revenue
A React Native / Expo app would double the surface to build and maintain. It waits until the platform has revenue — an explicit business decision, not an oversight.
Trade-off · No native app for now; mobile users get the web app.
The hard part
One raw predicate was answering three different questions. An audit of permissions found isWorkspaceOwner at 42 call sites across the codebase, and the owner bypass — "agency admin or workspace owner" — written by hand in six places, two of them in the UI.
Why that was a risk.
- Each call site was really asking one of three things: is this person an end client subject to the portal's rules?, can this person operate the workspace?, or what access level does this object have for them? The same function answered all three, so reading the code didn't tell you which one was meant.
- A rule written on both sides of the client/server boundary drifts. The day it drifts, the UI promises a gate the server doesn't enforce.
- In a multi-tenant platform, a wrong check isn't a small bug: it's one business seeing another business's data.
The final design.
- Each question got a name in the authorization core:
isPortalSubject,isWorkspaceAdminandeffectiveObjectLevelFor. The six hand-written bypasses becameisWorkspaceAdmin. - A lint rule restricts the raw predicate to an allowlist. Adding a caller means editing that list and justifying it in the diff — the rule fails exactly when someone forgets, which is what the first refactor was missing.
- An HTTP smoke test covers the one gate no service test could reach. Its oracle is the rendered HTML, not the status code: the page streams, so the response is already
200when access is denied — a test asserting404would pass with the gate wide open. It was checked by sabotaging the gate on purpose.
Results
Corebase is in development, so there are no production metrics yet. What the refactor and the audit left in place:
- Server actions without an authorization guard
- 0 of 18
- From the permissions audit.
- Hand-written owner bypasses
- 6 → 1 named rule
- Replaced by isWorkspaceAdmin; new direct uses of the raw predicate fail lint.
- Service smoke checks after the refactor
- 906 · 0 failures
- Across 15 smoke suites, plus 19/19 on the new HTTP smoke test.
Stack
- TypeScript
- Next.js
- React
- PostgreSQL 17
- Drizzle ORM
- Yjs · Hocuspocus
- BlockNote
- MinIO
- Tailwind CSS
- MCP