Skip to content

refactor(api): move query-engine to a private internal tier - #461

Open
Makisuo wants to merge 1 commit into
mainfrom
refactor/query-engine-internal-tier
Open

refactor(api): move query-engine to a private internal tier#461
Makisuo wants to merge 1 commit into
mainfrom
refactor/query-engine-internal-tier

Conversation

@Makisuo

@Makisuo Makisuo commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

What

Moves the queryEngine group off the public /api/* surface onto its own HttpApi (MapleInternalApi) at /internal/query-engine, behind session-only auth, and deletes four endpoints that already had zero traffic.

Why

queryEngine was the largest and busiest thing left on the public surface — 62 endpoints, ~4,500 req/week, more than every other /api/* group combined — and /docs published all of them as a browsable public API reference.

It isn't public API. Using the maple.auth.method attribute stamped on every HTTP server span, across 30 days and all 62 endpoints production recorded exactly one API-key request (errors-by-type, 1 call). Everything else is a Clerk session.

That matters beyond hygiene: docs/http-api-migration.md already forbids a generic public query endpoint, because those contracts freeze our storage and dashboard internals into the public API. docs/api-v2.md had long declared the intended end state; this makes it true.

The design call

"Internal tier" reads as Effect RpcServer on /rpc. I deliberately didn't do that — apps/api/src/worker.ts documents that POST /mcp, the one existing RpcServer-backed route, hangs indefinitely on Workers when toWebHandler gets no middleware, attributed to a suspected RpcServer/HttpRouter scope-propagation bug. Putting the busiest surface in the product on that transport buys the same guarantee for a 62-endpoint rewrite plus a known Workers hang.

So the boundary is policy, not protocol. The typed contract, AtomHttpApi retention, the execute batcher and the v1 error envelope are all unchanged — the frontend behaves exactly as before. Being a separate HttpApi is what drops the group from /docs, which is generated from MapleApi.

Notable details

  • SessionAuthorization rejects on the maple_ak_ prefix, not by resolving the key. A refused key therefore costs no Postgres dial on the busiest path in the API — worth having, since the p95 there is the PG dial. The refusal is a typed 403 pointing at /v2; a bare 401 would read as "your key is broken" and send someone to rotate a key that's fine.

  • Four endpoints deleted after reaching zero traffic:

    • execute — superseded by the batcher in dc21b94. It has no single-request fallback at any batch size, so nothing called it.
    • service-dependencies / service-db-edges / service-platforms — superseded by serviceMapBundle in a3128de, which runs the same registry queries in-process.

    QueryEngineService.execute stays — the batch fan-out, serviceDetailOverview and six v2 telemetry handlers still call it in-process.

  • runWarehouseQuery now accepts either v1 client and provides both layers. It's shared with the session-replay adapters, which are still on /api; a blanket swap broke them. It narrows again when sessionReplays moves.

  • Handler moved from routes/v1/ to routes/internal/ so the directory stops lying.

Verification

Check Result
bun typecheck 40/40 tasks pass
apps/api tests 1757 passed
apps/web tests 1537 passed
packages/domain tests 489 passed
bun run knip exit 0
Route introspection MapleInternalApi = 58 endpoints, all under /internal/query-engine, zero stray; MapleApi = 118, zero query-engine

The last row is the one that matters — a wrong prefix or broken registration would leave typecheck green while 404ing every chart, so I introspected the built API directly rather than trusting the compiler.

Not verified — please exercise before merge

I did not run the browser pass. Outstanding at runtime:

  1. An API key against /internal/query-engine/execute-batch returns the typed 403 (and a session returns 200).
  2. The dashboard renders end to end — service detail, trace list, a multi-widget dashboard, logs.
  3. /docs no longer lists any query-engine operation.

Since sessions loaded before deploy keep calling /api/query-engine/* until reload, expect a burst of 404s in open tabs at cutover. If that's unacceptable, serve both prefixes from the same handler layer for one release and drop the old one next release — the plan allowed for it; this PR does the direct cutover.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

The queryEngine group was the largest and busiest thing left on the public
`/api/*` surface — 62 endpoints, ~4,500 requests/week — and `/docs` published
every one of them as browsable public API. It is not public API: across 30 days
and all 62 endpoints, production telemetry (`maple.auth.method`) recorded
exactly one API-key request. Everything else is a Clerk session.

Publishing it costs real freedom. `docs/http-api-migration.md` already forbids a
generic public query endpoint, because those contracts freeze Maple's storage
and dashboard internals into the public API.

The group now serves from its own `HttpApi` (`MapleInternalApi`) at
`/internal/query-engine`, behind a session-only `SessionAuthorization`. The
boundary is policy, not protocol: the typed contract, atom retention, the
execute batcher and the v1 error envelope are all unchanged, so the frontend
keeps working exactly as before. Being a separate `HttpApi` is what drops it
from `/docs`, which is generated from `MapleApi`.

`SessionAuthorization` refuses an API-key-shaped bearer on the `maple_ak_`
prefix rather than by resolving it, so a rejected key costs no Postgres dial on
what is the busiest path in the API. The refusal is a typed 403 pointing at
`/v2` — a bare 401 would read as "your key is broken" and send people to rotate
a key that is fine.

Also removes four endpoints that reached zero traffic days ago:

  - `execute`, superseded by the batcher in dc21b94. The batcher has no
    single-request fallback at any size, so nothing called it any more.
  - `service-dependencies`, `service-db-edges`, `service-platforms`, superseded
    by `serviceMapBundle` in a3128de, which runs the same registry queries
    in-process.

`QueryEngineService.execute` stays — the batch fan-out, the serviceDetailOverview
bundle and six v2 telemetry handlers still call it in-process.

`runWarehouseQuery` now accepts either v1 client and provides both layers: it is
shared with the session-replay adapters, which are still on `/api`. It narrows
again when sessionReplays moves.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant