From 84ad401ccd62a4ae3f8574f05b05bef678bdacd7 Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Sat, 13 Jun 2026 13:30:04 -0700
Subject: [PATCH 001/145] Fix onboarding UI jank: no Loading flash, no
canonicalize double-hop (#1007)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Two flashes visible in the onboarding flow:
1. create-org flickered 'Create your organization' → 'Loading' → back. The
header was gated on the pending-invitations atom being initial OR waiting,
and that atom is keyed on auth — so the create-org submit (which invalidates
the auth keys) put it into 'waiting' and flipped the whole page to 'Loading'.
Almost no new user has a pending invitation, so we now show NO loading
placeholder at all: the create form renders immediately and stays put, and
invitations (when they exist) just appear above it once the fetch resolves.
Removed the InvitationsSkeleton entirely.
2. The connect-MCP step flashed the app sidebar. 'Continue to app' / 'Skip'
navigated to the BARE /{-$orgSlug}, which mounts the shell at / and then
OrgSlugGate fires a SECOND navigation to canonicalize / → /. That
double hop is the window where the shell paints over the still-mounted
onboarding page. Now it navigates straight to / (the page already has
the org slug), so the shell mounts once.
Both pre-existing; the new auth-routing flow test (#1006) made them visible.
---
apps/cloud/src/web/pages/create-org.tsx | 45 ++++++-------------------
apps/cloud/src/web/pages/setup-mcp.tsx | 10 ++++--
2 files changed, 18 insertions(+), 37 deletions(-)
diff --git a/apps/cloud/src/web/pages/create-org.tsx b/apps/cloud/src/web/pages/create-org.tsx
index c96696c822..711b8f28da 100644
--- a/apps/cloud/src/web/pages/create-org.tsx
+++ b/apps/cloud/src/web/pages/create-org.tsx
@@ -6,7 +6,6 @@ import * as Exit from "effect/Exit";
import { authWriteKeys } from "@executor-js/react/api/reactivity-keys";
import { trackEvent } from "@executor-js/react/api/analytics";
import { Button } from "@executor-js/react/components/button";
-import { Skeleton } from "@executor-js/react/components/skeleton";
import { AUTH_PATHS } from "../../auth/api";
import { acceptInvitation, pendingInvitationsAtom, useAuth } from "../auth";
@@ -84,8 +83,11 @@ export const CreateOrgPage = () => {
},
});
- const isLoading =
- AsyncResult.isInitial(invitationsResult) || AsyncResult.isWaiting(invitationsResult);
+ // Almost no new user has a pending invitation, so we never show a loading
+ // placeholder for them — the create form renders immediately and stays put.
+ // Invitations (when they exist) just appear above it once the fetch resolves;
+ // a refetch (the create-org submit invalidates the auth-keyed atom) keeps the
+ // last value, so nothing flickers.
const invitations = AsyncResult.match(invitationsResult, {
onInitial: () => [] as readonly PendingInvitation[],
onFailure: () => [] as readonly PendingInvitation[],
@@ -103,13 +105,9 @@ export const CreateOrgPage = () => {
Step 1 of 2
- {isLoading
- ? "Loading"
- : count === 0
- ? "Create your organization"
- : "You've been invited"}
+ {count > 0 ? "You've been invited" : "Create your organization"}
- {!isLoading && count === 0 && (
+ {count === 0 && (
Organizations group your sources, secrets, and teammates. You can invite others once
it's set up.
@@ -117,9 +115,7 @@ export const CreateOrgPage = () => {
)}
- {isLoading && }
-
- {!isLoading && sole && (
+ {sole && (
{
/>
)}
- {!isLoading && count > 1 && (
+ {count > 1 && (
{
/>
)}
- {!isLoading && (count === 0 || sole || count > 1) && (
-
- )}
+
{/* oxlint-disable-next-line react/forbid-elements */}
@@ -161,25 +155,6 @@ export const CreateOrgPage = () => {
// ---------------------------------------------------------------------------
-const InvitationsSkeleton = () => (
-
-
-
-
-);
-
-const InvitationRowSkeleton = () => (
-
-);
-
-// ---------------------------------------------------------------------------
-
const InviterAttribution = ({ invitation }: { invitation: PendingInvitation }) => {
const inviterLabel = invitation.inviter
? invitation.inviter.name && invitation.inviter.name.length > 0
diff --git a/apps/cloud/src/web/pages/setup-mcp.tsx b/apps/cloud/src/web/pages/setup-mcp.tsx
index 9a2912e79f..c3c592eeb3 100644
--- a/apps/cloud/src/web/pages/setup-mcp.tsx
+++ b/apps/cloud/src/web/pages/setup-mcp.tsx
@@ -23,6 +23,12 @@ export const SetupMcpPage = () => {
const auth = useAuth();
const organizationSlug =
auth.status === "authenticated" ? (auth.organization?.slug ?? null) : null;
+ // Land DIRECTLY on the org's canonical URL. Navigating to the bare
+ // `/{-$orgSlug}` would mount the shell at `/`, then OrgSlugGate would fire a
+ // SECOND navigation to canonicalize `/` → `/` — that double hop is the
+ // window where the shell paints over this still-mounted onboarding page.
+ const goToApp = () =>
+ navigate({ to: "/{-$orgSlug}", params: { orgSlug: organizationSlug ?? undefined } });
const [origin, setOrigin] = useState(null);
const [advancedOpen, setAdvancedOpen] = useState(false);
const [elicitationMode, setElicitationMode] = useState("model");
@@ -150,7 +156,7 @@ export const SetupMcpPage = () => {
type="button"
onClick={() => {
trackEvent("setup_mcp_skipped");
- void navigate({ to: "/{-$orgSlug}" });
+ void goToApp();
}}
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
>
@@ -160,7 +166,7 @@ export const SetupMcpPage = () => {
size="sm"
onClick={() => {
trackEvent("setup_mcp_completed");
- void navigate({ to: "/{-$orgSlug}" });
+ void goToApp();
}}
>
Continue to app
From b998f8d17dcc9006d18fd877ad473d13ad32cd3c Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Sat, 13 Jun 2026 13:30:16 -0700
Subject: [PATCH 002/145] Org slugs: mint at insert, drop lazy minting, make
the column NOT NULL (#1006)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The org-slug column was nullable and the runtime self-healed it: every
resolveOrganization (and the SSR gate) lazily minted a slug for any row that
lacked one. That left a permanent nullable state and a heal-on-read cost.
Move slug generation to the single moment a row is inserted — upsertOrganization
mints the slug at INSERT (ON CONFLICT DO NOTHING absorbs the id/slug races,
retrying with a fresh candidate) and keeps the existing slug on conflict. The
three insert sites (createOrganization, acceptInvitation, the WorkOS-fallback
upsert in resolveOrganization) all get a slug for free; resolveOrganization and
the SSR gate's organizationDisplay no longer heal. The schema column is now
NOT NULL (migration 0004), backfilled once for legacy rows.
Deploy order matters and is documented in the backfill script: backfill →
deploy this code → apply 0004. The old code inserts a NULL slug then heals, so
NOT NULL cannot land while it still serves.
Also adds e2e/cloud/auth-routing-flow.test.ts: one browser session walking the
whole arc — signed out → /login → hosted-AuthKit sign-in → org-less onboarding
→ create org → canonical / dashboard — through the real dev-server SSR
gate. The hops are covered piecewise elsewhere; this is the narrative regression
guard and the only test pinning the bare-/ → // dashboard canonicalization
a fresh login lands on.
---
apps/cloud/drizzle/0004_nervous_quasar.sql | 1 +
apps/cloud/drizzle/meta/0004_snapshot.json | 1242 +++++++++++++++++
apps/cloud/drizzle/meta/_journal.json | 7 +
apps/cloud/scripts/backfill-org-slugs.ts | 19 +-
.../api/protected-api-key-auth.node.test.ts | 7 +-
apps/cloud/src/auth/handlers.ts | 10 +-
.../src/auth/org-selector-auth.node.test.ts | 8 +-
apps/cloud/src/auth/organization.ts | 14 +-
apps/cloud/src/auth/ssr-gate.ts | 18 +-
apps/cloud/src/auth/user-store.ts | 74 +-
apps/cloud/src/db/db.test.ts | 35 +
apps/cloud/src/db/schema.ts | 8 +-
e2e/cloud/auth-routing-flow.test.ts | 74 +
13 files changed, 1429 insertions(+), 88 deletions(-)
create mode 100644 apps/cloud/drizzle/0004_nervous_quasar.sql
create mode 100644 apps/cloud/drizzle/meta/0004_snapshot.json
create mode 100644 e2e/cloud/auth-routing-flow.test.ts
diff --git a/apps/cloud/drizzle/0004_nervous_quasar.sql b/apps/cloud/drizzle/0004_nervous_quasar.sql
new file mode 100644
index 0000000000..3cb5dd2477
--- /dev/null
+++ b/apps/cloud/drizzle/0004_nervous_quasar.sql
@@ -0,0 +1 @@
+ALTER TABLE "organizations" ALTER COLUMN "slug" SET NOT NULL;
\ No newline at end of file
diff --git a/apps/cloud/drizzle/meta/0004_snapshot.json b/apps/cloud/drizzle/meta/0004_snapshot.json
new file mode 100644
index 0000000000..9db834cf1b
--- /dev/null
+++ b/apps/cloud/drizzle/meta/0004_snapshot.json
@@ -0,0 +1,1242 @@
+{
+ "id": "1187b7aa-fdd6-412a-bcfb-20d6ea8f8644",
+ "prevId": "80591016-4fb7-47d8-ab60-9a5c8eaea9c8",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.accounts": {
+ "name": "accounts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.memberships": {
+ "name": "memberships",
+ "schema": "",
+ "columns": {
+ "account_id": {
+ "name": "account_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "organization_id": {
+ "name": "organization_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "memberships_account_id_accounts_id_fk": {
+ "name": "memberships_account_id_accounts_id_fk",
+ "tableFrom": "memberships",
+ "tableTo": "accounts",
+ "columnsFrom": ["account_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "memberships_organization_id_organizations_id_fk": {
+ "name": "memberships_organization_id_organizations_id_fk",
+ "tableFrom": "memberships",
+ "tableTo": "organizations",
+ "columnsFrom": ["organization_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "memberships_account_id_organization_id_pk": {
+ "name": "memberships_account_id_organization_id_pk",
+ "columns": ["account_id", "organization_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.organizations": {
+ "name": "organizations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "organizations_slug_unique": {
+ "name": "organizations_slug_unique",
+ "columns": [
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.blob": {
+ "name": "blob",
+ "schema": "",
+ "columns": {
+ "namespace": {
+ "name": "namespace",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "id": {
+ "name": "id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "blob_id_uidx": {
+ "name": "blob_id_uidx",
+ "columns": [
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.connection": {
+ "name": "connection",
+ "schema": "",
+ "columns": {
+ "integration": {
+ "name": "integration",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template": {
+ "name": "template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider": {
+ "name": "provider",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "item_ids": {
+ "name": "item_ids",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "identity_label": {
+ "name": "identity_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client": {
+ "name": "oauth_client",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_client_owner": {
+ "name": "oauth_client_owner",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "refresh_item_id": {
+ "name": "refresh_item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oauth_scope": {
+ "name": "oauth_scope",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_state": {
+ "name": "provider_state",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "connection_uidx": {
+ "name": "connection_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "integration",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.definition": {
+ "name": "definition",
+ "schema": "",
+ "columns": {
+ "integration": {
+ "name": "integration",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection": {
+ "name": "connection",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "schema": {
+ "name": "schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "definition_uidx": {
+ "name": "definition_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "integration",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "connection",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.integration": {
+ "name": "integration",
+ "schema": "",
+ "columns": {
+ "slug": {
+ "name": "slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "config": {
+ "name": "config",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "can_remove": {
+ "name": "can_remove",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "can_refresh": {
+ "name": "can_refresh",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "integration_uidx": {
+ "name": "integration_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_client": {
+ "name": "oauth_client",
+ "schema": "",
+ "columns": {
+ "slug": {
+ "name": "slug",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "authorization_url": {
+ "name": "authorization_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_url": {
+ "name": "token_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "grant": {
+ "name": "grant",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret_item_id": {
+ "name": "client_secret_item_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource": {
+ "name": "resource",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin_kind": {
+ "name": "origin_kind",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "origin_integration": {
+ "name": "origin_integration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_client_uidx": {
+ "name": "oauth_client_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "slug",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oauth_session": {
+ "name": "oauth_session",
+ "schema": "",
+ "columns": {
+ "state": {
+ "name": "state",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_slug": {
+ "name": "client_slug",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "integration": {
+ "name": "integration",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "template": {
+ "name": "template",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "redirect_url": {
+ "name": "redirect_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pkce_verifier": {
+ "name": "pkce_verifier",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "identity_label": {
+ "name": "identity_label",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "payload": {
+ "name": "payload",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oauth_session_uidx": {
+ "name": "oauth_session_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.plugin_storage": {
+ "name": "plugin_storage",
+ "schema": "",
+ "columns": {
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "collection": {
+ "name": "collection",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "plugin_storage_uidx": {
+ "name": "plugin_storage_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "plugin_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "collection",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.private_executor_cloud_settings": {
+ "name": "private_executor_cloud_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'1.0.0'"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tool": {
+ "name": "tool",
+ "schema": "",
+ "columns": {
+ "integration": {
+ "name": "integration",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection": {
+ "name": "connection",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plugin_id": {
+ "name": "plugin_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "input_schema": {
+ "name": "input_schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "output_schema": {
+ "name": "output_schema",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "annotations": {
+ "name": "annotations",
+ "type": "json",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "tool_uidx": {
+ "name": "tool_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "integration",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "connection",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tool_policy": {
+ "name": "tool_policy",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "pattern": {
+ "name": "pattern",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position": {
+ "name": "position",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "row_id": {
+ "name": "row_id",
+ "type": "varchar(255)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "tenant": {
+ "name": "tenant",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "owner": {
+ "name": "owner",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "tool_policy_uidx": {
+ "name": "tool_policy_uidx",
+ "columns": [
+ {
+ "expression": "tenant",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "owner",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {},
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/apps/cloud/drizzle/meta/_journal.json b/apps/cloud/drizzle/meta/_journal.json
index 1119feba80..7d2353b9d8 100644
--- a/apps/cloud/drizzle/meta/_journal.json
+++ b/apps/cloud/drizzle/meta/_journal.json
@@ -29,6 +29,13 @@
"when": 1781283644753,
"tag": "0003_friendly_monster_badoon",
"breakpoints": true
+ },
+ {
+ "idx": 4,
+ "version": "7",
+ "when": 1781375837041,
+ "tag": "0004_nervous_quasar",
+ "breakpoints": true
}
]
}
diff --git a/apps/cloud/scripts/backfill-org-slugs.ts b/apps/cloud/scripts/backfill-org-slugs.ts
index e0583caff2..383acb2b3f 100644
--- a/apps/cloud/scripts/backfill-org-slugs.ts
+++ b/apps/cloud/scripts/backfill-org-slugs.ts
@@ -1,16 +1,19 @@
/* oxlint-disable executor/no-try-catch-or-throw -- boundary: out-of-band migration script over a raw postgres connection */
// ---------------------------------------------------------------------------
// One-off data backfill: mint URL slugs for organizations that predate the
-// slug column. Run OUT-OF-BAND against the database after applying the
-// schema migration that adds `organizations.slug` (0002):
+// slug column (added by migration 0003, nullable).
//
// bun run db:backfill-org-slugs:prod # op run --env-file=.env.production
// bun run db:backfill-org-slugs:dev # against the local PGlite dev db
//
-// Idempotent — already-slugged rows are skipped, so re-running is safe. The
-// runtime also lazily mints slugs on first resolution (see
-// auth/organization.ts), so this backfill is belt-and-braces for bulk
-// pre-warming rather than a deploy gate.
+// Idempotent — already-slugged rows are skipped, so re-running is safe.
+//
+// Ordering matters: the runtime now mints a slug at the moment a row is
+// inserted and NO LONGER self-heals null slugs, and migration 0004 makes the
+// column NOT NULL. So this backfill is the SOLE way pre-existing null-slug rows
+// get a slug, and it MUST run BEFORE 0004 is applied (a null row would fail the
+// NOT NULL alter). The correct sequence is: backfill (this script) → deploy the
+// mint-at-insert code → apply 0004.
// Pass --dry-run to print the plan without writing.
// ---------------------------------------------------------------------------
@@ -47,8 +50,8 @@ for (const row of rows) {
planned.add(slug);
console.log(`${row.id} ${JSON.stringify(row.name)} -> ${slug}`);
if (dryRun) continue;
- // Guard WHERE slug IS NULL: a concurrently-deployed runtime may have lazily
- // minted this org's slug between our read and this write.
+ // Guard WHERE slug IS NULL keeps re-runs idempotent (and tolerates a row that
+ // gained a slug between our read and this write).
await sql`UPDATE organizations SET slug = ${slug} WHERE id = ${row.id} AND slug IS NULL`;
updated += 1;
}
diff --git a/apps/cloud/src/api/protected-api-key-auth.node.test.ts b/apps/cloud/src/api/protected-api-key-auth.node.test.ts
index c13ae96ee8..15094617cb 100644
--- a/apps/cloud/src/api/protected-api-key-auth.node.test.ts
+++ b/apps/cloud/src/api/protected-api-key-auth.node.test.ts
@@ -50,7 +50,7 @@ const stubUsers = Layer.succeed(UserStoreService)({
getAccount: async (id: string) => ({ id, createdAt }),
upsertOrganization: async (org: { id: string; name: string }) => ({
...org,
- slug: null,
+ slug: `org-slug-${org.id}`,
createdAt,
}),
getOrganization: async (id: string) => ({
@@ -65,11 +65,6 @@ const stubUsers = Layer.succeed(UserStoreService)({
slug,
createdAt,
}),
- ensureOrganizationSlug: async (org: { id: string; name: string; slug: string | null }) => ({
- ...org,
- slug: org.slug ?? `org-slug-${org.id}`,
- createdAt,
- }),
}),
),
});
diff --git a/apps/cloud/src/auth/handlers.ts b/apps/cloud/src/auth/handlers.ts
index 5f6fb6ffad..bc67b37c50 100644
--- a/apps/cloud/src/auth/handlers.ts
+++ b/apps/cloud/src/auth/handlers.ts
@@ -342,10 +342,10 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
const org = yield* workos.createOrganization(name);
yield* workos.createMembership(org.id, session.accountId, "admin");
+ // `upsertOrganization` mints the slug at insert — no separate heal step.
const mirrored = yield* users.use((s) =>
s.upsertOrganization({ id: org.id, name: org.name }),
);
- const slugged = yield* users.use((s) => s.ensureOrganizationSlug(mirrored));
// Try to attach the new org to the current session. This can fail
// (or silently return a session still scoped to the old org) when
@@ -372,7 +372,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
}
(yield* SessionCookies).set("wos-session", refreshed, RESPONSE_COOKIE_OPTIONS);
- return { id: org.id, name: org.name, slug: slugged.slug };
+ return { id: org.id, name: org.name, slug: mirrored.slug };
}),
)
.handle("pendingInvitations", () =>
@@ -441,12 +441,12 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
return yield* new WorkOSError();
}
- // Mirror the org locally so domain tables can FK against it.
+ // Mirror the org locally so domain tables can FK against it; the
+ // upsert mints the slug at insert — no separate heal step.
const org = yield* workos.getOrganization(invitation.organizationId);
const mirrored = yield* users.use((s) =>
s.upsertOrganization({ id: org.id, name: org.name }),
);
- const slugged = yield* users.use((s) => s.ensureOrganizationSlug(mirrored));
// Attach the just-accepted org to the current session. Same shape
// as createOrganization: refresh + verify; if we can't pin the
@@ -468,7 +468,7 @@ export const CloudSessionAuthHandlers = HttpApiBuilder.group(
}
(yield* SessionCookies).set("wos-session", refreshed, RESPONSE_COOKIE_OPTIONS);
- return { id: org.id, name: org.name, slug: slugged.slug };
+ return { id: org.id, name: org.name, slug: mirrored.slug };
}),
)
.handle("getMcpPaused", ({ params }) =>
diff --git a/apps/cloud/src/auth/org-selector-auth.node.test.ts b/apps/cloud/src/auth/org-selector-auth.node.test.ts
index ef27f68f77..8199bd63b2 100644
--- a/apps/cloud/src/auth/org-selector-auth.node.test.ts
+++ b/apps/cloud/src/auth/org-selector-auth.node.test.ts
@@ -60,9 +60,10 @@ const stubUsers = Layer.succeed(UserStoreService)({
fn({
ensureAccount: async (id: string) => ({ id, createdAt }),
getAccount: async (id: string) => ({ id, createdAt }),
+ // Slug is minted at insert now — the stub returns a slugged row.
upsertOrganization: async (org: { id: string; name: string }) => ({
...org,
- slug: null,
+ slug: org.id,
createdAt,
}),
getOrganization: async (id: string) => ({ id, name: `Org ${id}`, slug: id, createdAt }),
@@ -74,11 +75,6 @@ const stubUsers = Layer.succeed(UserStoreService)({
slug,
createdAt,
}),
- ensureOrganizationSlug: async (org: { id: string; name: string; slug: string | null }) => ({
- ...org,
- slug: org.slug ?? org.id,
- createdAt,
- }),
}),
),
});
diff --git a/apps/cloud/src/auth/organization.ts b/apps/cloud/src/auth/organization.ts
index f734453f59..c1242c82b3 100644
--- a/apps/cloud/src/auth/organization.ts
+++ b/apps/cloud/src/auth/organization.ts
@@ -29,22 +29,20 @@ import { WorkOSClient } from "./workos";
// unknown org is read. All other callers just do `getOrganization` and get
// a self-healing lookup for free.
//
-// URL slugs are OURS (WorkOS orgs have none): resolution also lazily mints a
-// slug for any org that predates them, so every resolved org is routable at
-// `//…` without a separate backfill dependency.
+// URL slugs are OURS (WorkOS orgs have none) and are minted at the moment a
+// row is inserted — `upsertOrganization` is the single mint point, so the
+// mirror-on-first-read below produces a slugged, routable org without any
+// read-path healing.
export const resolveOrganization = (organizationId: string) =>
Effect.gen(function* () {
const users = yield* UserStoreService;
const existing = yield* users.use((s) => s.getOrganization(organizationId));
- if (existing) return yield* users.use((s) => s.ensureOrganizationSlug(existing));
+ if (existing) return existing;
const workos = yield* WorkOSClient;
const fresh = yield* workos.getOrganization(organizationId);
- const mirrored = yield* users.use((s) =>
- s.upsertOrganization({ id: fresh.id, name: fresh.name }),
- );
- return yield* users.use((s) => s.ensureOrganizationSlug(mirrored));
+ return yield* users.use((s) => s.upsertOrganization({ id: fresh.id, name: fresh.name }));
});
// ---------------------------------------------------------------------------
diff --git a/apps/cloud/src/auth/ssr-gate.ts b/apps/cloud/src/auth/ssr-gate.ts
index 011d484a8e..c329ac8cbd 100644
--- a/apps/cloud/src/auth/ssr-gate.ts
+++ b/apps/cloud/src/auth/ssr-gate.ts
@@ -139,22 +139,18 @@ const resolveAuthHint = async (
};
// The sealed session carries the org ID but not its name/slug; the local
-// mirror has both (the slug minted lazily for orgs that predate slugs). Only
-// consulted when minting (absent/mismatched hint) — never on the steady-state
-// path — and over per-request layers, because a connection cached in the
-// shared runtime would be reused across requests, which Cloudflare forbids. A
-// miss or failure reads as empty strings — display-only, corrected by the
-// client's /account/me write.
+// mirror has both (every org row is born with a slug — see
+// `upsertOrganization`). Only consulted when minting the hint (absent/mismatched
+// cookie) — never on the steady-state path — and over per-request layers,
+// because a connection cached in the shared runtime would be reused across
+// requests, which Cloudflare forbids. A miss or failure reads as empty strings
+// — display-only, corrected by the client's /account/me write.
const organizationDisplay = async (
organizationId: string,
): Promise<{ name: string; slug: string }> => {
const exit = await getRuntime().runPromiseExit(
Effect.flatMap(UserStoreService.asEffect(), (users) =>
- users.use(async (store) => {
- const org = await store.getOrganization(organizationId);
- if (!org) return null;
- return store.ensureOrganizationSlug(org);
- }),
+ users.use((store) => store.getOrganization(organizationId)),
).pipe(Effect.provide(Layer.provide(makeUserStoreLayer(), makeDbLayer()))),
);
return Exit.isSuccess(exit)
diff --git a/apps/cloud/src/auth/user-store.ts b/apps/cloud/src/auth/user-store.ts
index 5d08d9517b..76929bc06c 100644
--- a/apps/cloud/src/auth/user-store.ts
+++ b/apps/cloud/src/auth/user-store.ts
@@ -17,9 +17,6 @@ import type { DrizzleDb } from "../db/db";
export type Account = typeof accounts.$inferSelect;
export type Organization = typeof organizations.$inferSelect;
-/** An organization row whose slug has been minted (see `ensureOrganizationSlug`). */
-export type SluggedOrganization = Organization & { readonly slug: string };
-
export const makeUserStore = (db: DrizzleDb) => {
const getOrganization = async (id: string) => {
const rows = await db.select().from(organizations).where(eq(organizations.id, id));
@@ -34,38 +31,45 @@ export const makeUserStore = (db: DrizzleDb) => {
return rows.length > 0;
};
- // The unique index can reject a candidate when two requests race to mint
- // slugs (for the same org or colliding candidates); surface that as null
- // and let the caller re-read or retry.
- const trySetSlug = async (organizationId: string, slug: string) => {
- // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a unique-index violation from a concurrent mint is an expected race, not a failure
- try {
+ // Insert a brand-new org row carrying a freshly-minted slug. `ON CONFLICT DO
+ // NOTHING` (no target) absorbs BOTH unique violations without throwing: an
+ // id collision (the org was mirrored concurrently) and a slug collision (the
+ // candidate was claimed by a different org). Returns the inserted row, or
+ // null when either conflict swallowed the insert — the caller decides whether
+ // to re-read (id race) or retry with a new candidate (slug race).
+ const tryInsertOrg = async (id: string, name: string, slug: string) => {
+ const [row] = await db
+ .insert(organizations)
+ .values({ id, name, slug })
+ .onConflictDoNothing()
+ .returning();
+ return row ?? null;
+ };
+
+ // Every new org row is born with a slug — there is no nullable window and no
+ // self-healing. Existing rows keep their slug (stable across renames, so org
+ // URLs survive) and only refresh their name.
+ const upsertOrganization = async (org: { id: string; name: string }) => {
+ const existing = await getOrganization(org.id);
+ if (existing) {
const [updated] = await db
.update(organizations)
- .set({ slug })
- .where(eq(organizations.id, organizationId))
+ .set({ name: org.name })
+ .where(eq(organizations.id, org.id))
.returning();
- return updated ?? null;
- } catch {
- return null;
+ return updated ?? existing;
}
- };
-
- // Mint and persist a slug for an org that predates slugs (or was mirrored
- // before its name was known). Slugs are stable once set — renames do NOT
- // regenerate them, so org URLs survive.
- const ensureOrganizationSlug = async (org: Organization): Promise => {
- if (org.slug) return org as SluggedOrganization;
- for (let attempt = 0; attempt < 3; attempt++) {
+ for (let attempt = 0; attempt < 4; attempt++) {
const slug = await generateOrgSlug(org.name, slugTaken);
- const updated = await trySetSlug(org.id, slug);
- if (updated?.slug) return updated as SluggedOrganization;
- // Lost a race: either another request minted this org's slug (re-read
- // and return), or the candidate got claimed by a different org (retry).
+ const inserted = await tryInsertOrg(org.id, org.name, slug);
+ if (inserted) return inserted;
+ // The insert was swallowed by a conflict. If the id now exists, a
+ // concurrent request mirrored it — return that row. Otherwise the slug
+ // candidate collided; loop and mint a fresh one.
const fresh = await getOrganization(org.id);
- if (fresh?.slug) return fresh as SluggedOrganization;
+ if (fresh) return fresh;
}
- // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: slug minting exhausted retries; surfacing loudly beats silently unslugged orgs
+ // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: slug minting exhausted retries; surfacing loudly beats a silently unslugged org
throw new Error(`unable to mint a slug for organization ${org.id}`);
};
@@ -84,17 +88,7 @@ export const makeUserStore = (db: DrizzleDb) => {
// --- Organizations ---
- upsertOrganization: async (org: { id: string; name: string }) => {
- const [result] = await db
- .insert(organizations)
- .values(org)
- .onConflictDoUpdate({
- target: organizations.id,
- set: { name: org.name },
- })
- .returning();
- return result!;
- },
+ upsertOrganization,
getOrganization,
@@ -102,7 +96,5 @@ export const makeUserStore = (db: DrizzleDb) => {
const rows = await db.select().from(organizations).where(eq(organizations.slug, slug));
return rows[0] ?? null;
},
-
- ensureOrganizationSlug,
};
};
diff --git a/apps/cloud/src/db/db.test.ts b/apps/cloud/src/db/db.test.ts
index b437b4bf76..889daab6dc 100644
--- a/apps/cloud/src/db/db.test.ts
+++ b/apps/cloud/src/db/db.test.ts
@@ -22,6 +22,7 @@
import { describe, it, expect } from "@effect/vitest";
import { Effect, Layer } from "effect";
+import { isValidOrgSlug } from "@executor-js/api";
import { DbService } from "./db";
import { makeUserStore } from "../auth/user-store";
@@ -118,3 +119,37 @@ describe("DbService", () => {
expect(result?.name).toBe("Acme");
}, 15_000);
});
+
+describe("upsertOrganization · slug is minted at insert", () => {
+ const upsert = (org: { id: string; name: string }) =>
+ program(
+ Effect.gen(function* () {
+ const { db } = yield* DbService;
+ return yield* Effect.promise(() => makeUserStore(db).upsertOrganization(org));
+ }),
+ );
+
+ it("mints a valid slug on insert", async () => {
+ const org = await upsert({ id: `org_${crypto.randomUUID()}`, name: "Slug Mint Co" });
+ expect(org.slug, "a new org row is born with a slug").toBeTruthy();
+ expect(isValidOrgSlug(org.slug), "the minted slug fits the URL grammar").toBe(true);
+ });
+
+ it("keeps the slug stable across renames (conflict updates name only)", async () => {
+ const id = `org_${crypto.randomUUID()}`;
+ const created = await upsert({ id, name: "Original Name" });
+ const renamed = await upsert({ id, name: "Renamed Org" });
+ expect(renamed.slug, "the slug survives a rename").toBe(created.slug);
+ expect(renamed.name, "the name is refreshed on conflict").toBe("Renamed Org");
+ });
+
+ it("discriminates same-name collisions into distinct slugs", async () => {
+ // Same name → same slug base; the second insert collides on the unique
+ // index and gets a discriminated slug.
+ const name = `Collide ${crypto.randomUUID().slice(0, 8)}`;
+ const a = await upsert({ id: `org_${crypto.randomUUID()}`, name });
+ const b = await upsert({ id: `org_${crypto.randomUUID()}`, name });
+ expect(a.slug).not.toBe(b.slug);
+ expect(isValidOrgSlug(a.slug) && isValidOrgSlug(b.slug), "both slugs are valid").toBe(true);
+ });
+});
diff --git a/apps/cloud/src/db/schema.ts b/apps/cloud/src/db/schema.ts
index 7779a65412..fb1bd79987 100644
--- a/apps/cloud/src/db/schema.ts
+++ b/apps/cloud/src/db/schema.ts
@@ -22,15 +22,17 @@ export const accounts = pgTable("accounts", {
/**
* Organization (billing entity, scoping root). The `id` is the WorkOS
* organization ID. The `slug` is OURS, not WorkOS's (their org object has no
- * slug): minted at create time, lazily backfilled for orgs first seen via the
- * mirror's self-heal path, and stable across renames so org URLs don't break.
+ * slug): minted at the moment a row is inserted (the single mint point is
+ * `upsertOrganization`) and stable across renames so org URLs don't break.
+ * NOT NULL — there is no nullable window: legacy rows were backfilled once and
+ * every insert since carries a slug.
*/
export const organizations = pgTable(
"organizations",
{
id: text("id").primaryKey(),
name: text("name").notNull(),
- slug: text("slug"),
+ slug: text("slug").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => ({
diff --git a/e2e/cloud/auth-routing-flow.test.ts b/e2e/cloud/auth-routing-flow.test.ts
new file mode 100644
index 0000000000..0080ecc758
--- /dev/null
+++ b/e2e/cloud/auth-routing-flow.test.ts
@@ -0,0 +1,74 @@
+// Cloud-only (browser): the whole auth-routing arc in ONE session, end to end
+// through the real dev server (the e2e cloud target boots `vite dev` with the
+// SSR gate active, so this is the same routing that runs in prod):
+//
+// signed out → /login
+// click Sign in → WorkOS (emulator) callback → org-less → /create-org
+// create the first org → canonical dashboard at /
+//
+// The individual hops are also covered piecewise (cloud/unauthenticated-skeleton,
+// cloud/session-gate, scenarios/org-slug-routing) — those pin edge cases this
+// narrative doesn't (invalid-cookie clearing, token refresh, returnTo
+// open-redirect, unknown-slug 404). This one is the regression guard for the
+// happy path as a single continuous flow, and is the only test that pins the
+// bare-`/` → `//` dashboard canonicalization a fresh login lands on.
+//
+// The headless helpers sign in via a `login_hint` query param, but the
+// emulator's hosted AuthKit also serves a real form ("continue as a new user"),
+// so the browser flow just fills it — genuinely end-to-end through the UI.
+import { randomUUID } from "node:crypto";
+
+import { expect } from "@effect/vitest";
+import { Effect } from "effect";
+
+import { scenario } from "../src/scenario";
+import { Browser, Target } from "../src/services";
+
+scenario(
+ "Auth routing · signed out → login → onboarding → dashboard, one browser session",
+ {},
+ Effect.gen(function* () {
+ const browser = yield* Browser;
+ yield* Target;
+
+ // A brand-new user the emulator provisions on first login_hint — no org, so
+ // the post-callback document is gated to onboarding.
+ const email = `flow-${randomUUID().slice(0, 8)}@e2e.test`;
+
+ // No cookies → a clean, signed-out browser.
+ const anonymous = { label: "anonymous" };
+
+ yield* browser.session(anonymous, async ({ page, step }) => {
+ await step("Signed out, the root redirects to /login", async () => {
+ await page.goto("/", { waitUntil: "commit" });
+ await page.getByText("Sign in to manage your tools and sources").waitFor();
+ });
+ expect(new URL(page.url()).pathname, "the signed-out root lands on /login").toBe("/login");
+
+ await step("Sign in through the UI → org-less session lands on onboarding", async () => {
+ await page.getByRole("link", { name: "Sign in" }).click();
+ // The emulator's hosted AuthKit form: provision a brand-new user.
+ await page.getByPlaceholder("new-user@example.com").fill(email);
+ await page.getByRole("button", { name: /Continue/ }).click();
+ // callback mints the session → SSR gate sees no org → /create-org.
+ await page.waitForURL((url) => url.pathname === "/create-org", { timeout: 30_000 });
+ await page.getByPlaceholder("Northwind Labs").waitFor({ timeout: 30_000 });
+ });
+
+ await step("Create the first org → canonical dashboard at /", async () => {
+ await page.getByPlaceholder("Northwind Labs").fill("Flow Test Org");
+ await page.getByRole("button", { name: "Create organization" }).click();
+ await page.getByText("Connect your MCP client").waitFor({ timeout: 30_000 });
+ await page.getByRole("button", { name: "Continue to app" }).click();
+ // The bare landing canonicalizes onto the new org's slug.
+ await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), { timeout: 30_000 });
+ await page.getByText("Integrations").first().waitFor({ timeout: 30_000 });
+ });
+
+ expect(
+ /^\/[a-z0-9-]+\/?$/.test(new URL(page.url()).pathname),
+ "the signed-in user ends on their org-slug dashboard",
+ ).toBe(true);
+ });
+ }),
+);
From 64b3544c297f122fb915ab281f2ac84c766ddcfd Mon Sep 17 00:00:00 2001
From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com>
Date: Sat, 13 Jun 2026 13:38:02 -0700
Subject: [PATCH 003/145] Self-host MCP OAuth: connect-card path, approval
screen, real-agent e2e + framework dwells (#1002)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Self-host: serve the org-scoped MCP path the Connect card prints
The console Connect-an-agent card prints `//mcp` —
a convention the multi-tenant cloud worker routes by stripping the org segment
at the edge. Self-host only served the bare `/mcp`, so a connecting MCP client
completed OAuth and then 404'd on reconnect. Self-host is single-tenant (the
session pins the one org), so the segment carries no routing meaning: both
front-ends (the prod Bun server in serve.ts and the vite dev middleware) now
strip a single leading segment from `//mcp` and the matching OAuth
discovery path, mirroring cloud's edge rewrite but accepting any segment.
Coverage gap this slipped through: every MCP e2e built `${baseUrl}/mcp`
itself and connected to that, so none exercised the URL the card actually
hands the user. New scenario selfhost/mcp-connect-card-url scrapes the URL out
of the rendered card and asserts the server serves it (was 404, now 401) — it
fails on the pre-fix build. Also adds selfhost/mcp-oauth-consent, which records
the MCP OAuth browser flow and documents that it currently auto-consents (no
approval screen).
Shared pure helper (stripMcpOrgSegment) with unit tests; verified green on
both the selfhost (vite) and selfhost-docker (production image) targets.
* Self-host: require an MCP OAuth approval screen
Connecting MCP clients were auto-consented — a signed-in user's client got a
token with no prompt. Better Auth's MCP authorize only shows consent when the
request carries prompt=consent (clients don't send it) AND a consentPage is
configured. So: configure oidcConfig.consentPage -> the SPA route
/mcp-consent, and force the screen by injecting prompt=consent on every
/api/auth/mcp/authorize in the shared Better Auth handler (force-mcp-consent,
unit-tested). The handler also looks the registered client_name up from the
DCR row and appends it to the consent redirect so the screen reads
"Connect ?" rather than an opaque id (self-declared — a label, not a
trust signal). The /mcp-consent page (rebuilt from the earlier
test/selfhost-e2e-org-setup work) shows the client, scopes, an explicit note
that the grant is limited to the MCP server (not a web-app login, no other API
calls), and Allow/Deny posting to /api/auth/oauth2/consent.
Also fixes a dev-only routing bug the new route exposed: the vite middleware
matched MCP paths with rawUrl.startsWith("/mcp"), which swallowed
/mcp-consent (and its source module) and misrouted them to the API handler.
Now matches on the exact pathname.
The mcp-oauth-consent e2e asserts the approval screen appears with the client
name, Allow grants a code, and gates the connection. Green on selfhost (vite)
and selfhost-docker (prod image); unit + lint + typecheck clean.
* e2e: real OpenCode binary through self-host MCP OAuth consent, spliced
Drives the actual opencode binary through the full MCP OAuth handshake against
self-host (its own DCR/PKCE/scopes/token store), with the browser hop driven by
a real Playwright session that lands on the forced /mcp-consent approval screen
and clicks Allow — the consent redirect goes to OpenCode's own localhost
callback, so OpenCode receives the code and finishes the grant for real
(asserts a stored access token + "connected").
The agent PTY (terminal.cast) and the browser (session.mp4) run concurrently
via Effect.all and mark focus terminal→browser→terminal, so scripts/film.ts
cuts them into one film.mp4: the agent asks → we approve in the browser → the
agent is connected. Green on selfhost (vite) and selfhost-docker (prod image);
skips where the opencode binary isn't installed.
This is the real-client + spliced-recording coverage that previously lived only
on the unmerged test/selfhost-e2e-org-setup branch (record-flow-oauth.ts).
* e2e: framework-managed human dwells, gated by E2E_FILM
A scenario author shouldn't hand-code waitForTimeout to make a recording
readable — pacing is the recording's concern, not the scenario's. A dwell is a
property of a focus transition: tabbing between tools, a developer lingers a
beat to take in where they landed. So the framework owns it:
- timeline.ts gains beat()/enterFocus() + isFilming(). beat() is a no-op unless
the run is being filmed (E2E_FILM=1, also implied by the desk's E2E_DESK).
- The browser step() enters focus (lingering on the outgoing window) and beats
at the end so each named step's result holds on screen.
- The Cli surface holds the terminal's final frame at end of drive.
So fast/CI runs pay nothing, and a filmed run paces itself into a watchable
'moving between apps' splice. The OpenCode consent scenario drops all its
hand-rolled dwells and just names two browser steps + enterFocus at the
terminal transitions — verified: 2.9s film unfilmed, 7.2s paced under
E2E_FILM=1, green both ways.
* docs(e2e): add the OpenCode self-host MCP OAuth consent recording
The paced film (E2E_FILM=1) of selfhost/mcp-opencode-consent — the real
opencode binary asking to connect, the browser approval screen, and connected
— embedded in the PR so reviewers see the flow, not just read about it.
* Self-host: drive mcp-oauth end-to-end test through the consent screen
The forced MCP approval screen makes /authorize redirect to /mcp-consent with a
consent_code instead of straight to the client callback. Update the opaque-bearer
end-to-end test to approve on the consent screen (POST /api/auth/oauth2/consent)
and follow the returned redirectURI to the code, matching what a real client's
browser does.
---
.changeset/selfhost-mcp-connect-url.md | 10 ++
.changeset/selfhost-mcp-consent.md | 10 ++
apps/host-selfhost/src/auth/better-auth.ts | 13 +-
.../src/auth/force-mcp-consent.test.ts | 67 +++++++++
.../src/auth/force-mcp-consent.ts | 59 ++++++++
apps/host-selfhost/src/auth/index.ts | 34 ++++-
apps/host-selfhost/src/mcp/mcp-oauth.test.ts | 28 +++-
apps/host-selfhost/src/mcp/org-path.test.ts | 30 ++++
apps/host-selfhost/src/mcp/org-path.ts | 36 +++++
apps/host-selfhost/src/serve.ts | 32 +++-
apps/host-selfhost/vite.config.ts | 29 +++-
apps/host-selfhost/web/mcp-consent.tsx | 139 ++++++++++++++++++
apps/host-selfhost/web/routes/__root.tsx | 16 ++
e2e/recordings/mcp-opencode-consent.gif | Bin 0 -> 123858 bytes
e2e/selfhost/mcp-connect-card-url.test.ts | 72 +++++++++
e2e/selfhost/mcp-oauth-consent.test.ts | 120 +++++++++++++++
e2e/selfhost/mcp-opencode-consent.test.ts | 139 ++++++++++++++++++
e2e/src/surfaces/browser.ts | 11 +-
e2e/src/surfaces/cli.ts | 12 +-
e2e/src/timeline.ts | 41 ++++++
20 files changed, 879 insertions(+), 19 deletions(-)
create mode 100644 .changeset/selfhost-mcp-connect-url.md
create mode 100644 .changeset/selfhost-mcp-consent.md
create mode 100644 apps/host-selfhost/src/auth/force-mcp-consent.test.ts
create mode 100644 apps/host-selfhost/src/auth/force-mcp-consent.ts
create mode 100644 apps/host-selfhost/src/mcp/org-path.test.ts
create mode 100644 apps/host-selfhost/src/mcp/org-path.ts
create mode 100644 apps/host-selfhost/web/mcp-consent.tsx
create mode 100644 e2e/recordings/mcp-opencode-consent.gif
create mode 100644 e2e/selfhost/mcp-connect-card-url.test.ts
create mode 100644 e2e/selfhost/mcp-oauth-consent.test.ts
create mode 100644 e2e/selfhost/mcp-opencode-consent.test.ts
diff --git a/.changeset/selfhost-mcp-connect-url.md b/.changeset/selfhost-mcp-connect-url.md
new file mode 100644
index 0000000000..4fae997f3c
--- /dev/null
+++ b/.changeset/selfhost-mcp-connect-url.md
@@ -0,0 +1,10 @@
+---
+"executor": patch
+---
+
+Fix the self-hosted "Connect an agent" MCP URL. The card printed an
+organization-scoped path (`//mcp`) that the
+single-tenant self-host server didn't serve, so connecting an MCP client
+authorized successfully but then failed to reach the tools with an HTTP 404.
+The self-host server now accepts the organization-scoped path and routes it to
+its MCP endpoint.
diff --git a/.changeset/selfhost-mcp-consent.md b/.changeset/selfhost-mcp-consent.md
new file mode 100644
index 0000000000..a2b9e19a7f
--- /dev/null
+++ b/.changeset/selfhost-mcp-consent.md
@@ -0,0 +1,10 @@
+---
+"executor": patch
+---
+
+Self-hosted MCP connections now require explicit approval. When an MCP client
+connects, the browser stops on an approval screen showing the connecting
+client's name, what it can access, and that the grant is limited to the MCP
+server (not a web-app login, and it can't make other API calls on your behalf);
+a token is granted only after you Approve. Previously a signed-in user's client
+was authorized automatically with no prompt.
diff --git a/apps/host-selfhost/src/auth/better-auth.ts b/apps/host-selfhost/src/auth/better-auth.ts
index 07acc4df70..8adfdb5a3b 100644
--- a/apps/host-selfhost/src/auth/better-auth.ts
+++ b/apps/host-selfhost/src/auth/better-auth.ts
@@ -93,7 +93,18 @@ const makeAuthOptions = (url: string, getOrganizationId: () => string, gate?: Si
admin(),
apiKey({ enableSessionForAPIKeys: true, rateLimit: { enabled: false } }),
bearer(),
- mcp({ loginPage: "/login" }),
+ // `consentPage` makes the MCP authorize flow redirect to a human approval
+ // screen instead of auto-issuing a code — but ONLY when the request
+ // carries `prompt=consent`. MCP clients don't send that, so the self-host
+ // serving layer injects it on every authorize (see resolveAuthProviders'
+ // force-mcp-consent shim); together they force an approval step for every
+ // connecting client. The page itself is the SPA route `/mcp-consent`.
+ // `loginPage` in oidcConfig is required by the type but the mcp() plugin
+ // overrides it with the top-level one; `consentPage` is what we're after.
+ mcp({
+ loginPage: "/login",
+ oidcConfig: { loginPage: "/login", consentPage: "/mcp-consent" },
+ }),
],
databaseHooks: {
session: {
diff --git a/apps/host-selfhost/src/auth/force-mcp-consent.test.ts b/apps/host-selfhost/src/auth/force-mcp-consent.test.ts
new file mode 100644
index 0000000000..1febcf3992
--- /dev/null
+++ b/apps/host-selfhost/src/auth/force-mcp-consent.test.ts
@@ -0,0 +1,67 @@
+import { describe, expect, it } from "@effect/vitest";
+
+import {
+ consentRedirectClientId,
+ promptWithConsent,
+ withClientName,
+ withForcedMcpConsent,
+} from "./force-mcp-consent";
+
+describe("promptWithConsent", () => {
+ it("adds consent to an empty prompt", () => {
+ expect(promptWithConsent(null)).toBe("consent");
+ expect(promptWithConsent("")).toBe("consent");
+ });
+ it("preserves other prompt values without duplicating consent", () => {
+ expect(promptWithConsent("login")).toBe("login consent");
+ expect(promptWithConsent("consent")).toBe("consent");
+ expect(promptWithConsent("login consent")).toBe("login consent");
+ });
+});
+
+describe("withForcedMcpConsent", () => {
+ const authorize = (qs: string) => new Request(`https://host.example/api/auth/mcp/authorize${qs}`);
+
+ it("injects prompt=consent on MCP authorize", () => {
+ const out = withForcedMcpConsent(authorize("?client_id=abc&response_type=code"));
+ expect(new URL(out.url).searchParams.get("prompt")).toBe("consent");
+ });
+
+ it("merges with an existing prompt", () => {
+ const out = withForcedMcpConsent(authorize("?client_id=abc&prompt=login"));
+ expect(new URL(out.url).searchParams.get("prompt")).toBe("login consent");
+ });
+
+ it("leaves an already-consent request unchanged (same instance)", () => {
+ const req = authorize("?client_id=abc&prompt=consent");
+ expect(withForcedMcpConsent(req)).toBe(req);
+ });
+
+ it("never touches non-authorize or non-GET requests", () => {
+ const other = new Request("https://host.example/api/auth/mcp/token", { method: "POST" });
+ expect(withForcedMcpConsent(other)).toBe(other);
+ const consent = new Request("https://host.example/api/auth/oauth2/consent", { method: "POST" });
+ expect(withForcedMcpConsent(consent)).toBe(consent);
+ });
+});
+
+describe("consentRedirectClientId", () => {
+ it("returns the client id of a consent redirect lacking a name", () => {
+ expect(consentRedirectClientId("/mcp-consent?consent_code=c&client_id=abc&scope=openid")).toBe(
+ "abc",
+ );
+ });
+ it("returns null when the name is already present, or it isn't a consent redirect", () => {
+ expect(consentRedirectClientId("/mcp-consent?client_id=abc&client_name=Codex")).toBeNull();
+ expect(consentRedirectClientId("/login?client_id=abc")).toBeNull();
+ expect(consentRedirectClientId(null)).toBeNull();
+ });
+});
+
+describe("withClientName", () => {
+ it("appends client_name to a consent redirect (path+query only)", () => {
+ expect(withClientName("/mcp-consent?consent_code=c&client_id=abc", "Claude Code")).toBe(
+ "/mcp-consent?consent_code=c&client_id=abc&client_name=Claude+Code",
+ );
+ });
+});
diff --git a/apps/host-selfhost/src/auth/force-mcp-consent.ts b/apps/host-selfhost/src/auth/force-mcp-consent.ts
new file mode 100644
index 0000000000..56afe8a470
--- /dev/null
+++ b/apps/host-selfhost/src/auth/force-mcp-consent.ts
@@ -0,0 +1,59 @@
+// Force a human approval screen on every MCP OAuth connection.
+//
+// Better Auth's MCP authorize endpoint only shows the consent page when the
+// request carries `prompt=consent` (it otherwise auto-issues an authorization
+// code — see better-auth/plugins/mcp/authorize). MCP clients don't send that,
+// so a connecting client would be granted a token with no human approval. This
+// wraps Better Auth's web handler and adds `consent` to the `prompt` of every
+// `GET /api/auth/mcp/authorize`, so — paired with `oidcConfig.consentPage` —
+// every connect is gated on the `/oauth/consent` approval screen.
+//
+// Pure + Effect-free; the wrapper is a plain Request -> Request transform so it
+// composes with whatever serves the Better Auth handler (prod + vite dev both
+// mount the same handler).
+
+const AUTHORIZE_PATH = "/api/auth/mcp/authorize";
+const CONSENT_PAGE = "/mcp-consent";
+
+/** Merge `consent` into a possibly-empty space-separated `prompt` value. */
+export const promptWithConsent = (prompt: string | null): string => {
+ const set = new Set((prompt ?? "").split(/\s+/).filter((value) => value.length > 0));
+ set.add("consent");
+ return Array.from(set).join(" ");
+};
+
+/**
+ * Return the MCP-authorize request with `prompt=consent` ensured, or the
+ * original request unchanged when it isn't an MCP authorize call.
+ */
+export const withForcedMcpConsent = (request: Request): Request => {
+ if (request.method !== "GET") return request;
+ const url = new URL(request.url);
+ if (url.pathname !== AUTHORIZE_PATH) return request;
+ const prompt = url.searchParams.get("prompt");
+ if (prompt && prompt.split(/\s+/).includes("consent")) return request;
+ url.searchParams.set("prompt", promptWithConsent(prompt));
+ return new Request(url, request);
+};
+
+/**
+ * If `location` is Better Auth's redirect to the consent page carrying a
+ * `client_id` but no `client_name`, return that client id (so the caller can
+ * look up the registered name and enrich the redirect). Otherwise null.
+ * Better Auth's authorize only puts the opaque `client_id` on the consent
+ * redirect; the registered name makes the approval screen legible.
+ */
+export const consentRedirectClientId = (location: string | null): string | null => {
+ if (!location) return null;
+ const url = new URL(location, "http://host.internal");
+ if (url.pathname !== CONSENT_PAGE) return null;
+ if (url.searchParams.get("client_name")) return null;
+ return url.searchParams.get("client_id");
+};
+
+/** Append `client_name` to a consent-page redirect URL (path + query only). */
+export const withClientName = (location: string, clientName: string): string => {
+ const url = new URL(location, "http://host.internal");
+ url.searchParams.set("client_name", clientName);
+ return `${url.pathname}${url.search}`;
+};
diff --git a/apps/host-selfhost/src/auth/index.ts b/apps/host-selfhost/src/auth/index.ts
index 63c2e81ae4..211982740d 100644
--- a/apps/host-selfhost/src/auth/index.ts
+++ b/apps/host-selfhost/src/auth/index.ts
@@ -5,6 +5,7 @@ import { IdentityProvider } from "@executor-js/api/server";
import type { SelfHostDbHandle } from "../db/self-host-db";
import { BetterAuth, buildBetterAuth, type BetterAuthHandle } from "./better-auth";
import { betterAuthIdentityLayer } from "./identity";
+import { consentRedirectClientId, withClientName, withForcedMcpConsent } from "./force-mcp-consent";
export { BetterAuth, buildBetterAuth, type BetterAuthHandle } from "./better-auth";
export { betterAuthIdentityLayer } from "./identity";
@@ -37,9 +38,40 @@ export const resolveAuthProviders = async (
): Promise => {
const betterAuth = await buildBetterAuth(dbHandle.url, dbHandle.client);
const betterAuthLayer = Layer.succeed(BetterAuth)(betterAuth);
+
+ // The consent redirect from Better Auth's authorize only carries the opaque
+ // client_id; look the registered client_name up (its adapter sees the
+ // just-written DCR row) so the approval screen reads "Connect Codex?" not a
+ // random id. Self-declared at open DCR — a label, not a trust signal.
+ const lookupClientName = async (clientId: string): Promise => {
+ const ctx = await betterAuth.auth.$context;
+ const app = await ctx.adapter.findOne<{ name?: string | null }>({
+ model: "oauthApplication",
+ where: [{ field: "clientId", value: clientId }],
+ });
+ return app?.name ?? null;
+ };
+
+ // Force the MCP approval screen: inject `prompt=consent` on every MCP
+ // authorize so a connecting client is gated on /mcp-consent rather than
+ // silently granted a token (see ./force-mcp-consent), and enrich the
+ // resulting consent redirect with the registered client name.
+ const authHandler = async (request: Request): Promise => {
+ const response = await betterAuth.handler(withForcedMcpConsent(request));
+ if (response.status !== 302) return response;
+ const clientId = consentRedirectClientId(response.headers.get("location"));
+ if (!clientId) return response;
+ const name = await lookupClientName(clientId);
+ if (!name) return response;
+ // Preserve the rest of the response — notably the signed consent cookie.
+ const headers = new Headers(response.headers);
+ headers.set("location", withClientName(response.headers.get("location")!, name));
+ return new Response(null, { status: 302, headers });
+ };
+
return {
identityLayer: betterAuthIdentityLayer.pipe(Layer.provide(betterAuthLayer)),
- authHandler: betterAuth.handler,
+ authHandler,
betterAuth,
};
};
diff --git a/apps/host-selfhost/src/mcp/mcp-oauth.test.ts b/apps/host-selfhost/src/mcp/mcp-oauth.test.ts
index 3ef26cffa3..5b0c10d23f 100644
--- a/apps/host-selfhost/src/mcp/mcp-oauth.test.ts
+++ b/apps/host-selfhost/src/mcp/mcp-oauth.test.ts
@@ -96,7 +96,11 @@ test("MCP OAuth opaque-bearer flow authenticates /mcp end-to-end", async () => {
expect([200, 201]).toContain(reg.status);
const clientId = String((await json(reg)).client_id);
- // 2. PKCE authorize with the signed-in session cookie -> 302 to redirect_uri?code=…
+ // 2. PKCE authorize with the signed-in session cookie. Self-host forces an
+ // approval screen (prompt=consent injected by the serving layer), so authorize
+ // does NOT redirect straight to the client callback — it redirects to the
+ // relative /mcp-consent page carrying a consent_code. A real client's browser
+ // lands there and the user clicks Allow; we drive that POST directly.
const verifier = b64url(crypto.getRandomValues(new Uint8Array(32)));
const challengeBytes = new Uint8Array(
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(verifier)),
@@ -114,9 +118,25 @@ test("MCP OAuth opaque-bearer flow authenticates /mcp end-to-end", async () => {
const authorize = await handler(
new Request(authorizeUrl, { headers: { cookie }, redirect: "manual" }),
);
- expect([302, 200]).toContain(authorize.status);
- const location = authorize.headers.get("location") ?? "";
- const code = new URL(location).searchParams.get("code") ?? "";
+ expect(authorize.status).toBe(302);
+ // Resolve against BASE — the consent redirect is a relative app path.
+ const consentRedirect = new URL(authorize.headers.get("location") ?? "", BASE);
+ expect(consentRedirect.pathname).toBe("/mcp-consent");
+ const consentCode = consentRedirect.searchParams.get("consent_code") ?? "";
+ expect(consentCode).not.toBe("");
+
+ // 2b. Approve on the consent screen — the same POST the /mcp-consent page makes.
+ // It returns { redirectURI } back to the client's callback carrying the code.
+ const consent = await handler(
+ new Request(`${BASE}/api/auth/oauth2/consent`, {
+ method: "POST",
+ headers: { "content-type": "application/json", cookie },
+ body: JSON.stringify({ accept: true, consent_code: consentCode }),
+ }),
+ );
+ expect(consent.status).toBe(200);
+ const redirectURI = String((await json(consent)).redirectURI ?? "");
+ const code = new URL(redirectURI).searchParams.get("code") ?? "";
expect(code).not.toBe("");
// 3. Token exchange.
diff --git a/apps/host-selfhost/src/mcp/org-path.test.ts b/apps/host-selfhost/src/mcp/org-path.test.ts
new file mode 100644
index 0000000000..631971e62b
--- /dev/null
+++ b/apps/host-selfhost/src/mcp/org-path.test.ts
@@ -0,0 +1,30 @@
+import { describe, expect, it } from "@effect/vitest";
+
+import { stripMcpOrgSegment } from "./org-path";
+
+describe("stripMcpOrgSegment", () => {
+ it("strips a single org segment before /mcp", () => {
+ expect(stripMcpOrgSegment("/iI9idP7BZcWpg9wW8cit3xE4r4dFSnHj/mcp")).toBe("/mcp");
+ expect(stripMcpOrgSegment("/org_123/mcp")).toBe("/mcp");
+ });
+
+ it("strips the org segment from the protected-resource discovery path", () => {
+ expect(stripMcpOrgSegment("/.well-known/oauth-protected-resource/abc123/mcp")).toBe(
+ "/.well-known/oauth-protected-resource/mcp",
+ );
+ });
+
+ it("leaves the bare paths untouched", () => {
+ expect(stripMcpOrgSegment("/mcp")).toBeNull();
+ expect(stripMcpOrgSegment("/.well-known/oauth-protected-resource/mcp")).toBeNull();
+ expect(stripMcpOrgSegment("/.well-known/oauth-authorization-server")).toBeNull();
+ });
+
+ it("never claims OAuth endpoints or unrelated paths", () => {
+ expect(stripMcpOrgSegment("/api/auth/mcp/authorize")).toBeNull();
+ expect(stripMcpOrgSegment("/api/auth/mcp/register")).toBeNull();
+ expect(stripMcpOrgSegment("/integrations")).toBeNull();
+ expect(stripMcpOrgSegment("/")).toBeNull();
+ expect(stripMcpOrgSegment("/a/b/mcp")).toBeNull(); // deeper than one segment
+ });
+});
diff --git a/apps/host-selfhost/src/mcp/org-path.ts b/apps/host-selfhost/src/mcp/org-path.ts
new file mode 100644
index 0000000000..d457cd597d
--- /dev/null
+++ b/apps/host-selfhost/src/mcp/org-path.ts
@@ -0,0 +1,36 @@
+// Self-host serves MCP at the bare `/mcp` path (and bare OAuth discovery docs).
+// The console "Connect an agent" card, however, prints
+// `//mcp` — a convention the multi-tenant cloud worker
+// routes (it strips the org segment at the edge, carrying the org in a header).
+// Self-host is single-tenant: the session already pins the one org, so the org
+// segment in the URL carries no routing meaning. Rather than special-case the
+// card per host, both self-host front-ends (the prod Bun server and the vite
+// dev middleware) strip a single leading segment so the card's URL reaches the
+// real route — mirroring cloud's edge rewrite, but accepting ANY segment (a
+// Better Auth org id is not the `org_…` shape cloud keys on) and setting no
+// header.
+//
+// Pure + Effect-free on purpose: the vite config imports it too.
+
+const PRM_PREFIX = "/.well-known/oauth-protected-resource";
+
+/**
+ * Given a request pathname, return the bare MCP pathname it should route to
+ * when it carries a single leading org segment, or `null` when no rewrite
+ * applies (already bare, not an MCP path, or an OAuth endpoint like
+ * `/api/auth/mcp/authorize`).
+ *
+ * //mcp -> /mcp
+ * /.well-known/oauth-protected-resource//mcp -> /.well-known/oauth-protected-resource/mcp
+ */
+export const stripMcpOrgSegment = (pathname: string): string | null => {
+ if (pathname.startsWith(`${PRM_PREFIX}/`)) {
+ const rest = pathname
+ .slice(PRM_PREFIX.length + 1)
+ .split("/")
+ .filter((segment) => segment.length > 0);
+ return rest.length === 2 && rest[1] === "mcp" ? `${PRM_PREFIX}/mcp` : null;
+ }
+ const segments = pathname.split("/").filter((segment) => segment.length > 0);
+ return segments.length === 2 && segments[1] === "mcp" ? "/mcp" : null;
+};
diff --git a/apps/host-selfhost/src/serve.ts b/apps/host-selfhost/src/serve.ts
index 2d8b1d271e..1041a2be40 100644
--- a/apps/host-selfhost/src/serve.ts
+++ b/apps/host-selfhost/src/serve.ts
@@ -15,15 +15,39 @@
import { fileURLToPath } from "node:url";
-import { HttpRouter, HttpStaticServer } from "effect/unstable/http";
+import {
+ HttpMiddleware,
+ HttpRouter,
+ HttpServerRequest,
+ HttpStaticServer,
+} from "effect/unstable/http";
import { BunFileSystem, BunHttpServer, BunPath, BunRuntime } from "@effect/platform-bun";
-import { Layer } from "effect";
+import { Effect, Layer } from "effect";
import { makeSelfHostApp } from "./app";
import { loadConfig } from "./config";
+import { stripMcpOrgSegment } from "./mcp/org-path";
const distDir = fileURLToPath(new URL("../dist/", import.meta.url));
+// Rewrite `//mcp` (and its OAuth discovery path) to the bare path before
+// routing, so the "Connect an agent" card's org-pinned URL reaches the real
+// `/mcp` route — see ./mcp/org-path. A no-op for every other request.
+const mcpOrgPathRewrite = HttpMiddleware.make((httpApp) =>
+ Effect.gen(function* () {
+ const request = yield* HttpServerRequest.HttpServerRequest;
+ const url = new URL(request.url, "http://host.internal");
+ const rewritten = stripMcpOrgSegment(url.pathname);
+ if (rewritten === null) return yield* httpApp;
+ return yield* httpApp.pipe(
+ Effect.provideService(
+ HttpServerRequest.HttpServerRequest,
+ request.modify({ url: `${rewritten}${url.search}` }),
+ ),
+ );
+ }),
+);
+
export const startServer = async (): Promise => {
const config = loadConfig();
const { AppLayer } = await makeSelfHostApp();
@@ -35,7 +59,9 @@ export const startServer = async (): Promise => {
Layer.provide(BunPath.layer),
);
- const ServerLive = HttpRouter.serve(Layer.mergeAll(AppLayer, StaticLive)).pipe(
+ const ServerLive = HttpRouter.serve(Layer.mergeAll(AppLayer, StaticLive), {
+ middleware: mcpOrgPathRewrite,
+ }).pipe(
Layer.provide(
BunHttpServer.layer({ hostname: config.host, port: config.port, idleTimeout: 0 }),
),
diff --git a/apps/host-selfhost/vite.config.ts b/apps/host-selfhost/vite.config.ts
index d0f08b4e24..1199fb6075 100644
--- a/apps/host-selfhost/vite.config.ts
+++ b/apps/host-selfhost/vite.config.ts
@@ -8,6 +8,7 @@ import { tanstackRouter } from "@tanstack/router-plugin/vite";
import executorVitePlugin from "@executor-js/vite-plugin";
import { routes } from "./tsr.routes";
+import { stripMcpOrgSegment } from "./src/mcp/org-path";
// Self-host web SPA. Mirrors @executor-js/app's vite plugin bundle, but points
// the TanStack router codegen at THIS app's routes (web/routes) so we get the
@@ -53,16 +54,32 @@ function executorApiPlugin(): Plugin {
if (path.includes("/src/") || path.endsWith("/executor.config.ts")) handlerPromise = null;
});
server.middlewares.use(async (req, res, next) => {
- const rawUrl = req.url ?? "/";
+ let rawUrl = req.url ?? "/";
+ // The "Connect an agent" card prints `//mcp`; self-host
+ // serves the bare `/mcp`, so rewrite it here (prod does the same in
+ // serve.ts) — otherwise this org-pinned path isn't recognized as an MCP
+ // path and falls through to the SPA as a 404. Mirrors ./src/mcp/org-path.
+ const devOrigin = `http://${req.headers.host ?? `localhost:${DEV_PORT}`}`;
+ const pathname = stripMcpOrgSegment(new URL(rawUrl, devOrigin).pathname) ?? "";
+ if (pathname !== "") {
+ const original = new URL(rawUrl, devOrigin);
+ rawUrl = `${pathname}${original.search}`;
+ }
+ // Match on PATHNAME, not a raw-URL prefix: `/mcp` must NOT swallow the
+ // SPA route `/mcp-consent` (nor its source module `/mcp-consent.tsx`),
+ // or the dev server misroutes them to the API handler and they 404.
+ const path = new URL(rawUrl, devOrigin).pathname;
const handled =
- rawUrl === "/api" ||
- rawUrl.startsWith("/api/") ||
- rawUrl.startsWith("/mcp") ||
- rawUrl.startsWith("/docs") ||
+ path === "/api" ||
+ path.startsWith("/api/") ||
+ path === "/mcp" ||
+ path.startsWith("/mcp/") ||
+ path === "/docs" ||
+ path.startsWith("/docs/") ||
// RFC 9728 / RFC 8414 OAuth discovery the MCP client fetches before
// auth. Served by the Effect router in prod; without this the SPA
// index.html fallback answers 200-with-HTML and breaks discovery.
- rawUrl.startsWith("/.well-known/");
+ path.startsWith("/.well-known/");
if (!handled) return next();
// oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: Vite dev middleware must convert handler failures into HTTP 500 responses
diff --git a/apps/host-selfhost/web/mcp-consent.tsx b/apps/host-selfhost/web/mcp-consent.tsx
new file mode 100644
index 0000000000..33ec4baa42
--- /dev/null
+++ b/apps/host-selfhost/web/mcp-consent.tsx
@@ -0,0 +1,139 @@
+import { useState } from "react";
+
+import { Button } from "@executor-js/react/components/button";
+
+// ---------------------------------------------------------------------------
+// MCP OAuth consent. Better Auth's mcp() plugin redirects /authorize here
+// (consentPage) with ?consent_code&client_id&scope once the user is signed in;
+// the self-host serving layer forces it for every client (see
+// src/auth/force-mcp-consent) and enriches it with the registered client_name
+// (src/auth/index). We show the requesting client + scopes and require an
+// explicit Allow/Deny — without this the server would auto-consent, so any
+// authorize link clicked by a logged-in user would silently mint a token for
+// whoever crafted it. On a decision we POST /api/auth/oauth2/consent and follow
+// the returned redirectURI (back to the client's callback with a code, or with
+// an access_denied error). Rendered directly by routes/__root.tsx (a static
+// import, not a lazy route — so it can't hit Vite's dynamic-import dep flake),
+// chromeless inside the auth gate. No throws (result drives state).
+// ---------------------------------------------------------------------------
+
+const SCOPE_LABELS: Record = {
+ openid: "Confirm your identity",
+ profile: "Read your basic profile",
+ email: "Read your email address",
+ offline_access: "Stay connected (refresh access without re-approving)",
+};
+
+export const McpConsentPage = () => {
+ const params = new URLSearchParams(globalThis.window?.location.search ?? "");
+ const consentCode = params.get("consent_code") ?? "";
+ const clientId = params.get("client_id") ?? "";
+ // The name the client gave at registration (e.g. "Codex"). Self-declared, so
+ // it's shown as a label, not a trust signal — hence the id + warning below.
+ const clientName = params.get("client_name")?.trim() || "";
+ const scopes = (params.get("scope") ?? "").split(/[+\s]+/).filter(Boolean);
+ const [busy, setBusy] = useState<"allow" | "deny" | null>(null);
+ const [error, setError] = useState(null);
+
+ const decide = async (accept: boolean) => {
+ setBusy(accept ? "allow" : "deny");
+ setError(null);
+ const res = await fetch(`${globalThis.location.origin}/api/auth/oauth2/consent`, {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify({ accept, consent_code: consentCode }),
+ });
+ if (!res.ok) {
+ setBusy(null);
+ setError(
+ "Could not record your decision. The request may have expired — try connecting again.",
+ );
+ return;
+ }
+ const body = (await res.json()) as { redirectURI?: string };
+ if (body.redirectURI) {
+ globalThis.location.href = body.redirectURI;
+ return;
+ }
+ setBusy(null);
+ setError("No redirect was returned by the server.");
+ };
+
+ return (
+
+
+
+
+ {clientName ? `Connect ${clientName}?` : "Connect an agent?"}
+
+
+ {clientName ? `${clientName} is` : "An MCP client is"} asking to connect to your
+ Executor instance.
+
+
+
+
+
+ Requesting client
+
+ {clientName &&
{clientName}
}
+
+ {clientId || "(unknown)"}
+
+
+ It will be able to
+
+
+ {(scopes.length ? scopes : ["openid"]).map((s) => (
+
+ •
+ {SCOPE_LABELS[s] ?? s}
+
+ ))}
+
+
+
+ {/* Scope of the grant: this token is an MCP credential only. */}
+
+ This access is limited to the{" "}
+ MCP server : the client connects as an
+ MCP client and uses your tools through this instance's{" "}
+ /mcp endpoint. It is not a web-app login and can't
+ make other API calls on your behalf.
+
+
+
+ Only allow this if you just started connecting an agent yourself.
+
+
+ {error &&
{error}
}
+
+
+ void decide(false)}
+ >
+ {busy === "deny" ? "Denying…" : "Deny"}
+
+ void decide(true)}
+ >
+ {busy === "allow" ? "Allowing…" : "Allow"}
+
+
+
+
+ );
+};
diff --git a/apps/host-selfhost/web/routes/__root.tsx b/apps/host-selfhost/web/routes/__root.tsx
index 5ca654b969..c20d4bb930 100644
--- a/apps/host-selfhost/web/routes/__root.tsx
+++ b/apps/host-selfhost/web/routes/__root.tsx
@@ -11,6 +11,7 @@ import { Shell, defaultShellNavItems } from "@executor-js/react/multiplayer/shel
import { plugins as clientPlugins } from "virtual:executor/plugins-client";
import { authClient } from "../auth-client";
+import { McpConsentPage } from "../mcp-consent";
import { LoginPage } from "../login";
import { SetupPage } from "../setup";
import { fetchNeedsSetup } from "../setup-status";
@@ -107,6 +108,21 @@ function RootComponent() {
);
}
+ // The MCP OAuth approval screen: chromeless (no shell) but inside the auth
+ // gate — the user is already signed in (Better Auth's authorize requires a
+ // session before redirecting here). Rendered directly (static import), not as
+ // a lazy route, so it can't hit Vite's dynamic-import dep flake.
+ if (pathname === "/mcp-consent") {
+ return (
+
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/e2e/recordings/mcp-opencode-consent.gif b/e2e/recordings/mcp-opencode-consent.gif
new file mode 100644
index 0000000000000000000000000000000000000000..a5f041e4d84cd9e00518b4ba21fddf789905f471
GIT binary patch
literal 123858
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 cNJ8dPQKlqJ
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