Skip to main content
For Convex, Ave’s id_token is validated by Convex itself. For a custom backend (Hono, Express, Elysia, etc.) backed by Postgres or SQLite, you validate the JWT on each request and map sub (Ave identity UUID) to your users table.

Verify id_token on the server

Use verifyAveIdToken from @ave-id/sdk/server — it wraps JWKS verification and checks aud against your registered clientId. Shortcut — parse the header and verify in one step:
import { verifyAveIdTokenFromAuthHeader } from "@ave-id/sdk/server";

const principal = await verifyAveIdTokenFromAuthHeader(
  request.headers.get("authorization"),
  { clientId: process.env.AVE_CLIENT_ID! }
);

if (!principal) {
  return new Response("Unauthorized", { status: 401 });
}

const aveIdentityId = principal.subject;
Or use verifyAveIdToken on the raw JWT string. Pass the same id_token your SPA would send in the Authorization header. Do not accept access_token_jwt as a substitute for identity unless you intentionally validate the API JWT audience — for app users, id_token is the right signal.

Drizzle upsert (Postgres or SQLite)

Example: one row per Ave identity, keyed by ave_identity_id.
import { eq } from "drizzle-orm";
import type { Database } from "./db"; // your Drizzle db
import * as schema from "./schema";

export async function getOrCreateUserFromAveIdToken(
  db: Database,
  claims: { sub: string; name?: string; email?: string; picture?: string }
) {
  const [row] = await db
    .insert(schema.users)
    .values({
      aveIdentityId: claims.sub,
      displayName: claims.name ?? "Unknown",
      email: claims.email ?? null,
      imageUrl: claims.picture ?? null,
    })
    .onConflictDoUpdate({
      target: schema.users.aveIdentityId,
      set: {
        displayName: claims.name ?? "Unknown",
        email: claims.email ?? null,
        imageUrl: claims.picture ?? null,
      },
    })
    .returning();

  return row;
}
SQLite / dialect differences: use your driver’s upsert form, or onConflictDoNothing then findFirst by aveIdentityId, or try/catch on unique constraint and findFirst on retry. Use a unique index on ave_identity_id. If you need a stable ID across Ave identities for one human user, consider the user_id scope and separate columns — see Convex custom auth for how sub vs user_id differ.

Scopes

Request openid profile email offline_access for SPAs that refresh tokens; the server only needs the Authorization: Bearer value carrying the id_token (or your own session cookie after establishing trust once).
Last modified on April 19, 2026