Connect any auth (generic)
No managed connector required. If you can run code when a user signs up or signs in — a signup handler, a database trigger, a CDC stream — you can connect to Apex. Your stable user id (DB primary key, UUID, whatever you already use) becomes the internal_user_id.
From your signup / login handler
async function emitApexIdentity(user: {
id: string;
email: string;
name?: string;
plan?: string;
}, isSignup: boolean) {
await fetch("https://app.apex.inc/api/v1/events", {
method: "POST",
headers: {
"content-type": "application/json",
"x-api-key": process.env.APEX_API_KEY!, // apex_sk_...
},
body: JSON.stringify({
events: [
{
type: isSignup ? "user_signed_up" : "user_identified",
email: user.email,
data: { userId: user.id, name: user.name, plan: user.plan },
},
],
}),
}).catch(() => {}); // best-effort — never fail the user's action
}
Call it right after you create the account (signup) and after a successful login (returning users).
Or use the SDK
import { sendServerEvent } from "@apex-inc/sdk";
await sendServerEvent(
{ apiKey: process.env.APEX_API_KEY! },
{ type: "user_identified", email: user.email, data: { userId: user.id } },
{ idempotencyKey: `identify-${user.id}` },
);
Batch from a DB trigger / CDC
If you stream row changes (Debezium, Postgres LISTEN/NOTIFY, a nightly job), batch up to 100 identify events per request:
import { sendServerEvents } from "@apex-inc/sdk";
await sendServerEvents(
{ apiKey: process.env.APEX_API_KEY! },
changedUsers.map((u) => ({
type: "user_identified",
email: u.email,
data: { userId: u.id, plan: u.plan },
})),
);
Tip
The only rule that matters: send your stable id as data.userId. That's
what makes reconciliation deterministic. Email is great as a secondary
identifier, but ids don't change and aren't shared.
See Backfill existing users to bring in the base you had before Apex.