Skip to content

Migrate org-scoped backend routes to /:org URL prefix (follow-up to #3250) #3256

@tlgimenes

Description

@tlgimenes

Background

#3250 stopped persisting activeOrganizationId to the session row (it was leaking across browser tabs) and switched to a per-request x-org-id header. That fix works for normal MCP fetches via the SDK but exposed a class of bugs every time a callsite forgets the header:

  • The OAuth probe in isConnectionAuthenticated was a hand-rolled fetch without x-org-id → proxy returned 403, supportsOAuth came back false, and the OAuth popup never opened.
  • The save path POST /api/connections/:id/oauth-token had the same gap → token landed on connection.connection_token (fallback) instead of DownstreamTokenStorage, breaking buildCloneInfo ("No GitHub token found").
  • /api/vm-events and /org/:orgId/watch are SSE — EventSource can't set custom headers at all, so the header convention couldn't carry org context. We worked around it with a GET-only query-param fallback (?x-org-id=...).

Each of these was patched individually. The pattern is: the header convention is invisible from the URL, easy to forget, and structurally incompatible with EventSource.

Proposal

Move org context into the URL path for every org-scoped backend route, removing the need for the x-org-id header (and the SSE query-param workaround).

There's already precedent in the codebase:

  • /org/:organizationId/watch
  • /org/:organizationId/events/:type
  • /api/:org/models/*

After the migration:

  • /mcp/:connectionId/:org/mcp/:connectionId
  • /api/connections/:id/oauth-token/:org/api/connections/:id/oauth-token
  • /api/vm-events?.../:org/api/vm-events?... (no more x-org-id query param)
  • ...and the rest of the org-scoped surface for consistency.

authenticateRequest resolves the org from the path segment instead of the header; membership is verified the same way. Multi-tab safety comes from the URL itself — different tabs are different URLs.

Decisions to make

  1. Scope — narrow (just the routes that currently break or are fragile: proxy /mcp/*, /api/connections/:id/oauth-token*, the two SSE routes) or sweep (all org-scoped routes: /api/org-sso, /api/files, decopilot, KV, vm-events, etc.). Sweeping gives one consistent shape; narrow keeps the diff small.
  2. Slug vs id in the path — the frontend URL already uses the slug (/gimenes-local/...); reusing it reads nicer and matches what users see. authenticateRequest's membership query already supports either.
  3. Compatibility window — keep the old paths aliased and x-org-id header working during a deprecation window (matters for API-key consumers of /mcp/:connectionId and /api/connections/...)? Or hard cut?

Touched surface (rough)

Backend (Hono routes): apps/mesh/src/api/app.ts, apps/mesh/src/api/routes/proxy.ts, downstream-token.ts, vm-events.ts, plus the routes mounted under /api/....

SDK + frontend fetch helpers:

  • packages/mesh-sdk/src/hooks/use-mcp-client.ts — drop the x-org-id header, build org-scoped URL.
  • packages/mesh-sdk/src/lib/mcp-oauth.tsisConnectionAuthenticated and checkOAuthTokenStatus.
  • All oauth-token POST/DELETE callsites in apps/mesh/src/web/....
  • The three SSE consumers (vm-events-context.tsx, use-decopilot-events.ts, use-workflow-sse.ts).

Existing tests that assert the 403 cross-tenant guard (apps/mesh/src/api/routes/proxy.test.ts) need to be updated for the new path shape.

Out of scope

The interim fixes already in the codebase (per-callsite x-org-id headers, the GET-only query-param fallback in authenticateRequest) stay until this migration lands.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions