Skip to main content
Executor integrates with WorkOS for authentication, organization management, and user directory synchronization.

Overview

The WorkOS integration provides:
  • User authentication - OAuth 2.0 and AuthKit integration
  • Organization management - Multi-tenant workspace organization
  • Directory sync - Automatic user and membership synchronization
  • Webhook handlers - Real-time event processing

Configuration

Environment Variables

# WorkOS credentials (required)
WORKOS_CLIENT_ID=client_...
WORKOS_API_KEY=sk_test_...
WORKOS_WEBHOOK_SECRET=...

# Cookie encryption (required, 32+ characters)
WORKOS_COOKIE_PASSWORD=<openssl rand -base64 32>

# Optional: AuthKit domain for OAuth
WORKOS_AUTHKIT_ISSUER=https://your-authkit-domain.authkit.app
WORKOS_AUTHKIT_DOMAIN=https://your-authkit-domain.authkit.app

Convex Setup

WorkOS integration is initialized in executor/packages/database/convex/auth.ts:
import { AuthKit } from "@convex-dev/workos-authkit";
import { components, internal } from "./_generated/api";
import { workosEventHandlers } from "../src/auth/event_handlers";

const authKit = new AuthKit(components.workOSAuthKit, {
  authFunctions: internal.auth,
  additionalEventTypes: [
    "organization.created",
    "organization.updated",
    "organization.deleted",
    "organization_membership.created",
    "organization_membership.updated",
    "organization_membership.deleted",
  ],
});

// Register HTTP routes
authKit.registerRoutes(http);

// Register webhook handlers
const authKitEvents = authKit.events(workosEventHandlers);

Authentication

User Bootstrap

When a user signs in, Executor bootstraps their account and workspace:
export const bootstrapCurrentWorkosAccount = customMutation({
  args: {
    sessionId: v.optional(v.string()),
    profileName: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) return null;

    // Resolve user profile from WorkOS
    const profile = await getAuthKitUserProfile(ctx, identity.subject);
    
    // Create or update account
    const account = await upsertWorkosAccount(ctx, {
      workosUserId: identity.subject,
      email: profile.email,
      fullName: profile.fullName,
      // ...
    });

    // Link anonymous sessions
    await claimAnonymousSessionToWorkosAccount(ctx, {
      sessionId: args?.sessionId,
      targetAccountId: account._id,
    });

    // Ensure workspace access
    await getOrCreatePersonalWorkspace(ctx, account._id, profile);
    
    return account;
  },
});

Identity Resolution

Executor resolves user identity from multiple sources:
function resolveIdentityProfile({
  identity,
  authKitProfile,
}: {
  identity: UserIdentity;
  authKitProfile?: AuthKitProfile;
}) {
  return {
    email: identity.email || authKitProfile?.email,
    fullName: identity.name || authKitProfile?.firstName + " " + authKitProfile?.lastName,
    firstName: identity.givenName || authKitProfile?.firstName,
    lastName: identity.familyName || authKitProfile?.lastName,
    avatarUrl: identity.pictureUrl || authKitProfile?.profilePictureUrl,
    hintedWorkosOrgId: authKitProfile?.organizationId,
  };
}

Generated Names

Executor detects and refreshes auto-generated WorkOS names:
function isGeneratedWorkosLabel(value: string, workosUserId: string): boolean {
  const fallbackSuffix = workosUserId.trim().slice(-6);
  
  // Check for "User ABC123" pattern
  if (normalizedValue.toLowerCase() === `user ${fallbackSuffix}`.toLowerCase()) {
    return true;
  }
  
  return /^user\s+[a-z0-9]{6,}$/i.test(normalizedValue);
}

Organization Management

Organization Membership

Executor syncs organization memberships from WorkOS:
await activateOrganizationMembershipFromInviteHint(ctx, {
  organizationId: organization._id,
  accountId: account._id,
  email: account.email,
  now: Date.now(),
  fallbackRole: "member",
  billable: true,
});

Workspace Creation

Users without organization access get a personal workspace:
if (!hasWorkspaceMembership) {
  await getOrCreatePersonalWorkspace(ctx, account._id, {
    email: profile.email,
    firstName: profile.firstName,
    fullName: profile.fullName,
    workosUserId: identity.subject,
    now: Date.now(),
  });
}

Webhook Events

Executor handles WorkOS webhook events to keep user and organization data in sync.

Event Deduplication

Webhook events are fingerprinted to prevent duplicate processing:
function workosEventFingerprint(eventType: string, data: unknown): string {
  return `${eventType}:${hashFNV1a(stableSerialize(data))}`;
}

await ctx.db.insert("authWebhookReceipts", {
  provider: "workos",
  eventType: "user.created",
  fingerprint: workosEventFingerprint(eventType, data),
  receivedAt: Date.now(),
});

Supported Events

// user.created
workosEventHandlers["user.created"] = async (ctx, event) => {
  await upsertWorkosAccount(ctx, {
    workosUserId: event.data.id,
    email: event.data.email,
    fullName: [event.data.firstName, event.data.lastName]
      .filter(Boolean)
      .join(" ") || event.data.email,
    // ...
  });
};

// user.updated
workosEventHandlers["user.updated"] = async (ctx, event) => {
  const account = await getAccountByWorkosId(ctx, event.data.id);
  if (account) {
    await ctx.db.patch(account._id, {
      email: event.data.email,
      name: fullName,
      status: "active",
    });
  }
};

// user.deleted
workosEventHandlers["user.deleted"] = async (ctx, event) => {
  const account = await getAccountByWorkosId(ctx, event.data.id);
  if (account) {
    await ctx.db.patch(account._id, { status: "deleted" });
    // Deactivate all memberships
  }
};

Token Verification

Executor verifies WorkOS JWT tokens for MCP requests:
import { createRemoteJWKSet, jwtVerify } from "jose";

const jwks = createRemoteJWKSet(
  new URL("/oauth2/jwks", authorizationServer)
);

const { payload } = await jwtVerify(token, jwks, {
  issuer: authorizationServer,
});

if (payload.provider !== "anonymous") {
  return {
    provider: "workos",
    subject: payload.sub,
  };
}
WorkOS tokens use the client’s client ID as the audience, so Executor only validates issuer and signature.

Workspace Authorization

After authenticating with WorkOS, Executor checks workspace access:
const access = await getWorkspaceAccessForWorkosSubject({
  workspaceId: "wks_...",
  subject: "user_...",
});

context = {
  workspaceId,
  accountId: access.accountId,
  clientId: "mcp",
};

Credentials Management

WorkOS credentials can be stored in the WorkOS Vault for secure credential management:
import { WorkOSVaultReader } from "../src/runtime/workos_vault_reader";

const vaultReader = new WorkOSVaultReader({
  apiKey: process.env.WORKOS_API_KEY,
  organizationId: org.workosOrgId,
});

const credentials = await vaultReader.getCredentials(credentialId);

Testing

# Start with WorkOS in .env
bun run dev

# Visit local web app
open http://localhost:5312

# Sign in with WorkOS AuthKit
# Should create account and personal workspace

Source Files

  • executor/packages/database/convex/auth.ts - AuthKit initialization
  • executor/packages/database/src/auth/bootstrap.ts - Account bootstrap logic
  • executor/packages/database/src/auth/event_handlers.ts - Webhook event handlers
  • executor/packages/database/src/auth/identity.ts - Identity resolution
  • executor/packages/database/src/auth/accounts.ts - Account management
  • executor/packages/database/src/auth/memberships.ts - Organization membership
  • executor/packages/database/convex/http/mcp_auth.ts - MCP token verification

Build docs developers (and LLMs) love