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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions