I create public functions for workers to consume, authenticated by a shared token you generate and store on Convex & Cloudflare:
// convex/fub/worker/api.ts
// consumable by a CF worker
export const getSyncProgress = query({
args: {
...secretArg,
progressId: v.string(),
},
returns: v.any(),
handler: async (ctx, args) => {
validateSecret(args.secret);
return await ctx.db.get(args.progressId as Id<"fubListSyncProgress">);
},
});// trigger a CF worker/queue/etc.
export const triggerFubSyncForTeams = internalAction({
args: {
teamIds: v.array(v.id("teams")),
},
handler: async (ctx, args) => {
const accountId = process.env.CF_ACCOUNT_ID!;
const queueId = process.env.CF_FUB_SYNC_QUEUE_ID!;
const client = new Cloudflare({
apiToken: process.env.CF_WORKERS_TOKEN!,
});
for (const teamId of args.teamIds) {
await client.queues.messages.push(queueId, {
account_id: accountId,
body: {
type: "sync-init" as const,
teamId,
requestedAt: Date.now(),
},
});
}
},
});And use it in the Worker:
// cloudflare/workers/sync/src/index.ts
import { ConvexClient } from "./lib/convex-client";
const convex = new ConvexClient(c.env);
const progress = await convex.query("fub/worker/api:getSyncProgress", {
progressId,
});// workers/src/lib/convex-client.ts
import { Env } from './env';
/**
* Convex HTTP client helper for calling public mutations/queries.
* Uses the shared secret for authentication.
*/
type ConvexFunctionArgs = Record<string, unknown>;
export class ConvexClient {
private baseUrl: string;
private secret: string;
constructor(env: Env) {
this.baseUrl = env.CONVEX_URL;
this.secret = env.CF_WORKER_SHARED_SECRET;
}
/**
* Call a Convex mutation via HTTP.
*/
async mutation<T = unknown>(path: string, args: ConvexFunctionArgs = {}): Promise<T> {
return this.call<T>('mutation', path, args);
}
/**
* Call a Convex query via HTTP.
*/
async query<T = unknown>(path: string, args: ConvexFunctionArgs = {}): Promise<T> {
return this.call<T>('query', path, args);
}
private async call<T>(type: 'query' | 'mutation', path: string, args: ConvexFunctionArgs): Promise<T> {
// Add secret to args for authentication
const argsWithSecret = {
...args,
secret: this.secret,
};
const url = `${this.baseUrl}/api/${type}`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
path,
args: argsWithSecret,
format: 'json',
}),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Convex ${type} ${path} failed: ${response.status} ${errorText}`);
}
const result = (await response.json()) as { status: string; value?: T; errorMessage?: string };
// Convex HTTP API returns { status: "success", value: T } or { status: "error", errorMessage: string }
if (result.status === 'error') {
throw new Error(`Convex ${type} ${path} error: ${result.errorMessage}`);
}
return result.value as T;
}
}