==> read the decisions
ADR-0006: User services are handlers, and their authority is a parameter
Status: Accepted (revised: with SQLite there is no role to hide, so the process boundary is load-bearing; amended by ADR-0010 (2026-10-14): with Postgres the uid is also the database credential, through peer auth)
Context
Section titled “Context”Each user has their own backend code - the union of their projects. The question was whether that code is a process holding credentials or a module the platform runs.
Status
Section titled “Status”accepted (revised: with SQLite there is no role to hide, so the process boundary is load-bearing)
Decision
Section titled “Decision”- A user's backend is its own PROCESS, running as its own operating-system user, owning its own data directory.
- It receives a context:
ctx.db, bound to a file inside the authenticated user's own directory; plusctx.userandctx.project. - It never sees a connection string, never imports a driver, and never names the user. The binding comes from the verified token and never from an argument or a request field.
- User services are never exposed on the mesh.
api.reaches them over a socket only the platform can open.
Revision note
Section titled “Revision note”The first version had the user's code as a module inside the platform's runtime, and that was correct for PostgreSQL: the module held no credential, so it had nothing to reach another user with. With SQLite there is no credential to withhold -- access IS filesystem access -- so a module running under the platform's uid could open any path it liked, and the isolation would be a convention again. The process (and its uid) replaces the role as the thing the kernel enforces against.
This also buys something the earlier shape could not: a user's service can be written in any language, not only Node, which is what "custom services with custom functionality that only they need" implies.
Consequences
Section titled “Consequences”- A pwned or merely buggy module cannot reach another user's database, because no credential exists in its address space. The database boundary becomes a capability rather than a convention.
- Trust is structural: "api. already verified this" is a fact about reachability, not a header the service believes. A reachable service trusting such a header would be a forged-auth hole.
- This is only true while the module runs where the connection lives. A separate process needs its own connection string, which is why per-process services are rejected.
- The contract is symmetric with the frontend - custom element and CSS vars one way,
ctxthe other - which is what makes the MCP wrapper a different front door rather than a second implementation. - If a user's code ever genuinely needs its own process, the property is preserved with a local database proxy, never with a handed-over credential.