Convex + WorkOS Real-Time Sync & Cloudflare Workers Plan

Created: December 25, 2025 Status: Completed Completed: December 25, 2025

Overview

This plan outlines the migration from our custom WorkOS → Convex sync implementation to the official @convex-dev/workos-authkit component, and the potential setup of Cloudflare Workers for do.dev.


Part 1: Official Convex + WorkOS AuthKit Component

The videos you saw are likely about the official Convex × WorkOS integration released in September 2025. This is:

Key Features

  1. Real-time user sync - WorkOS user changes automatically sync to Convex database via webhooks
  2. Convex Component architecture - Clean separation, maintains its own user table
  3. Event handlers - React to user.created, user.updated, user.deleted events
  4. WorkOS Actions - Block registration/authentication based on custom logic
  5. Session events - Optional session.created, session.revoked tracking

Current do-dev Implementation vs. Official Component

FeatureCurrent (Custom)Official Component
Webhook endpoint/api/webhooks/workos (Next.js)*.convex.site/workos/webhook (Convex HTTP)
User syncManual mutations (workosSync.ts)Automatic via component
Event handlingCustom logic in API routeTyped event handlers
User tableCustom users tableComponent's user table + optional custom
JWT validationauth.config.ts (manual)Handled by component
Actions (block auth)Not implementedBuilt-in support

Part 2: Migration Plan

Phase 1: Install Official Component

# Install the package
pnpm add @convex-dev/workos-authkit

# Set webhook secret in Convex
npx convex env set WORKOS_WEBHOOK_SECRET=<your-webhook-secret>

Phase 2: Configure Convex

2a. Update convex/convex.config.ts

// tools/convex/convex/convex.config.ts
import workOSAuthKit from "@convex-dev/workos-authkit/convex.config";
import { defineApp } from "convex/server";

const app = defineApp();
app.use(workOSAuthKit);
export default app;

2b. Create AuthKit Client (convex/authkit.ts)

// tools/convex/convex/authkit.ts
import { AuthKit, type AuthFunctions } from "@convex-dev/workos-authkit";
import { components, internal } from "./_generated/api";
import type { DataModel } from "./_generated/dataModel";

const authFunctions: AuthFunctions = internal.authkit;

export const authKit = new AuthKit<DataModel>(components.workOSAuthKit, {
  authFunctions,
});

// Export event handlers - this syncs to your custom users table
export const { authKitEvent } = authKit.events({
  "user.created": async (ctx, event) => {
    // Create user in your custom users table
    const userId = `usr_${crypto.randomUUID().slice(0, 8)}`;
    const custId = `cus_${crypto.randomUUID().slice(0, 8)}`;

    await ctx.db.insert("users", {
      email: event.data.email,
      name: `${event.data.firstName ?? ""} ${event.data.lastName ?? ""}`.trim() || null,
      image: event.data.profilePictureUrl ?? null,
      roles: ["user"],
      appId: "do-dev",
      verified: event.data.emailVerified,
      userId,
      custId,
    });
  },

  "user.updated": async (ctx, event) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_app_email", (q) =>
        q.eq("appId", "do-dev").eq("email", event.data.email)
      )
      .unique();

    if (!user) {
      console.warn(`User not found for update: ${event.data.email}`);
      return;
    }

    await ctx.db.patch(user._id, {
      name: `${event.data.firstName ?? ""} ${event.data.lastName ?? ""}`.trim() || null,
      image: event.data.profilePictureUrl ?? null,
    });
  },

  "user.deleted": async (ctx, event) => {
    const user = await ctx.db
      .query("users")
      .withIndex("by_app_email", (q) =>
        q.eq("appId", "do-dev").eq("email", event.data.email)
      )
      .unique();

    if (user) {
      await ctx.db.delete(user._id);
    }
  },
});

2c. Register HTTP Routes (convex/http.ts)

// tools/convex/convex/http.ts
import { httpRouter } from "convex/server";
import { authKit } from "./authkit";

const http = httpRouter();

// Register WorkOS webhook routes
// Endpoint: https://<deployment>.convex.site/workos/webhook
authKit.registerRoutes(http);

export default http;

Phase 3: Update WorkOS Dashboard

  1. Go to WorkOS Dashboard → Webhooks
  2. Create/update webhook with:
    • Endpoint URL: https://standing-bird-371.convex.site/workos/webhook
    • Events: user.created, user.updated, user.deleted
  3. Copy webhook secret → Set in Convex env

Phase 4: Update auth.config.ts

// tools/convex/convex/auth.config.ts
const clientId = process.env.WORKOS_CLIENT_ID;

export default {
  providers: [
    {
      type: "customJwt",
      issuer: `https://api.workos.com/`,
      algorithm: "RS256",
      applicationID: clientId,
      jwks: `https://api.workos.com/sso/jwks/${clientId}`,
    },
    {
      type: "customJwt",
      issuer: `https://api.workos.com/user_management/${clientId}`,
      algorithm: "RS256",
      jwks: `https://api.workos.com/sso/jwks/${clientId}`,
    },
  ],
};

Phase 5: Clean Up Old Code

After verifying the new setup works:

  1. Remove /api/webhooks/workos/route.ts (Next.js webhook handler)
  2. Remove /api/users/sync/route.ts (manual sync endpoint)
  3. Remove /api/admin/sync-all-users/route.ts (bulk sync)
  4. Remove workosSync.ts (custom Convex mutations)

Part 3: Cloudflare Workers for do.dev

Why Cloudflare Workers?

  1. Edge performance - Run code globally, close to users
  2. Webhook handling - Alternative to Convex HTTP or Next.js API routes
  3. Background jobs - Scheduled tasks, cron jobs
  4. API gateway - Rate limiting, auth, routing

Option A: Deploy Entire Next.js to Cloudflare Workers

Using OpenNext Cloudflare Adapter (@opennextjs/cloudflare):

# Install adapter
pnpm add @opennextjs/cloudflare

# Configure wrangler.jsonc
{
  "name": "do-dev",
  "main": ".open-next/worker.js",
  "compatibility_date": "2025-04-01",
  "compatibility_flags": ["nodejs_compat"]
}

Pros:

  • Full Next.js app on edge
  • Built-in KV, Durable Objects, D1 database access

Cons:

  • Major infrastructure change
  • Currently using Vercel (works well)
  • Convex still runs separately

Option B: Cloudflare Worker for Webhooks Only

Create a separate Worker to handle webhooks and forward to Convex:

// workers/webhook-handler/src/index.ts
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (url.pathname === "/workos/webhook" && request.method === "POST") {
      // Verify webhook signature
      const signature = request.headers.get("workos-signature");
      // ... verify with env.WORKOS_WEBHOOK_SECRET

      // Forward to Convex
      const convexResponse = await fetch(
        `${env.CONVEX_SITE_URL}/workos/webhook`,
        {
          method: "POST",
          headers: request.headers,
          body: await request.text(),
        }
      );

      return convexResponse;
    }

    return new Response("Not found", { status: 404 });
  },
};

Pros:

  • Adds layer of control/logging
  • Can add rate limiting, geo-blocking
  • Keeps main app on Vercel

Cons:

  • Extra hop (latency)
  • More infrastructure to manage
  • Not needed - Convex HTTP handles this natively
// workers/do-dev-jobs/src/index.ts
export default {
  // Cron triggers
  async scheduled(event: ScheduledEvent, env: Env) {
    switch (event.cron) {
      case "0 * * * *": // Hourly
        await syncMetrics(env);
        break;
      case "0 0 * * *": // Daily
        await generateReports(env);
        break;
    }
  },

  // HTTP triggers for on-demand jobs
  async fetch(request: Request, env: Env) {
    // Job queue processing, etc.
  },
};

Recommendations

Immediate (This Week)

  1. Migrate to @convex-dev/workos-authkit - This is the official, supported way
    • Install package
    • Configure Convex component
    • Update WorkOS webhook endpoint
    • Test thoroughly
    • Remove old custom code

Short-Term (Next Month)

  1. Set up Cloudflare Workers project for future use:
    packages/
    └── cloudflare-workers/
        ├── wrangler.toml
        └── src/
            └── index.ts

When to Use Cloudflare Workers

  • Background jobs/cron - Daily reports, cleanup tasks
  • External API proxy - Rate limiting, caching
  • Image processing - Resize, optimize
  • NOT for webhooks - Convex HTTP handles this better

Migration Checklist

  • Install @convex-dev/workos-authkit
  • Create convex/convex.config.ts with component
  • Create convex/authkit.ts with event handlers
  • Create/update convex/http.ts with routes
  • Update convex/auth.config.ts with JWT providers
  • Set WORKOS_WEBHOOK_SECRET in Convex env
  • Update WorkOS dashboard webhook endpoint
  • Archive old custom sync code
  • Deploy and test user creation flow
  • Deploy and test user update flow
  • Deploy and test user deletion flow
  • Update documentation

Resources

On this page