pex

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.