Skip to content
sign in

==> 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)

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.

accepted (revised: with SQLite there is no role to hide, so the process boundary is load-bearing)

  • 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; plus ctx.user and ctx.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.

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.

  • 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, ctx the 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.