52 commands, 3 doors: the app, REST and MCP share one write path
My ticket tracker has three ways in: the web app, a REST API and an MCP server. Every write from all three goes through the same 52 commands, and the Firestore rules deny everything else. It costs 150 to 400 ms a write where a direct client write takes about 50 ms. I'm Panth, and I lead the software team at Oizom. The tracker is ticket-tracker , my open-source board where coding agents work…
Oizom's ticket tracker has three ways to interact: a web app, a REST API, and an MCP server. All writes from these three sources go through the same 52 commands, with direct client writes taking around 50 milliseconds compared to 150 to 400 milliseconds for writes through the other paths. Panth, who leads the software team at Oizom, made this decision as the most costly to reverse.
The standard Firebase approach has clients writing Firestore directly, with security rules determining access, and triggers handling side effects. However, Panth chose a different path. Clients still read Firestore directly, but every write is a POST /api/{command}. The rules deny writes except for a person's own pointers.
Two main factors influenced this decision. Firstly, nearly every write requires a second document to keep in sync, such as key counters, activity logs, link endpoints, notifications, and webhooks. Secondly, Firebase rules cannot handle the necessary permissions. Writing such rules would be burdensome for maintenance.
The command is defined once in a shared package, specifying the name, scopes, permissions, errors, request schema, and response schema. The registry holds all 52 of these commands. The backend parses requests using the spec, while the frontend uses the same mapping for commands.
Three "doors" lead to these commands: /api/{command} for the app with a sign-in token and a request body, /v1 for a REST orchestrator or agent with a board token or OAuth route, and /mcp for Claude and other MCP clients. All three end in the same runner, which performs registry lookup, scope narrowing, request parsing, idempotency claim, handling, response parsing, incrementing the board's revision, and answering.
A token only narrows access based on the scopes intersected with the principal the token acts as. The scope check at the door only answers early, while the command decides what actions are allowed. A command that lists no scopes is restricted to app-only, meaning no token can reach it.
The chosen design requires all three doors to share one runner, minimizing cold starts and reducing costs. The runner makes one small Realtime Database write to update the board revision after a successful command. Optimistic updates hide the slower write in the app, while cold starts are covered by minimum instance settings in production.
In summary, for Oizom's ticket tracker, sharing one write path across the app, REST API, and MCP server made sense due to the shared logic, reduced rule duplication, and improved efficiency in handling writes.
Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.