Entity Auth

SDK (Web)

EntityAuthClient methods and usage

User roles

Read per-tenant roles for users via HTTP endpoints exposed by the dashboard app.

  • GET /api/user/roles?id=<userId>&workspaceTenantId=<tenant>{ roles: string[] }
  • GET /api/users/roles?workspaceTenantId=<tenant>&ids=<id1,id2,...>{ rolesByUserId: Record<string,string[]> }

Example:

const res = await fetch(
  `/api/user/roles?workspaceTenantId=${tenantId}&id=${userId}`,
  { headers: { Authorization: `Bearer ${accessToken}` } }
);
const { roles } = await res.json();

Configure tenant once via env (not forms)

From the dashboard → Setup tab, copy the .env snippet (tenant ID + API URL). Never ask users for workspaceTenantId in forms.

Initialize once, then construct with optional { baseURL }. All methods mirror /api/* routes and reuse the tenant ID set during init().

import { EntityAuthClient, init as initEA } from '@entityauth/auth-client';

initEA({
  workspaceTenantId: process.env.NEXT_PUBLIC_ENTITY_AUTH_WORKSPACE_TENANT_ID!,
  baseURL: process.env.NEXT_PUBLIC_ENTITY_AUTH_URL,
});

const ea = new EntityAuthClient({
  baseURL: process.env.NEXT_PUBLIC_ENTITY_AUTH_URL,
});

Auth

  • register({ email, password, defaultWorkspaceRole? })
  • login({ email, password }) → sets access token and returns { userId, sessionId }
  • refresh(headerRefreshToken?)
  • logout()

Default workspace role on registration

Pass defaultWorkspaceRole: "owner" | "member" to assign a workspace-level role to the new user automatically. This is useful for apps that want newly registered users to be able to create organizations immediately (requires owner).

import { EntityAuthClient, init as initEA } from '@entityauth/auth-client';

initEA({
  workspaceTenantId: process.env.NEXT_PUBLIC_ENTITY_AUTH_WORKSPACE_TENANT_ID!,
  baseURL: process.env.NEXT_PUBLIC_ENTITY_AUTH_URL,
});

const ea = new EntityAuthClient({ baseURL: process.env.NEXT_PUBLIC_ENTITY_AUTH_URL });

// Make the first user an owner in this workspace
await ea.register({
  email: 'first@acme.com',
  password: 'super-secure',
  defaultWorkspaceRole: 'owner',
});

If omitted, the user is created without a workspace owner/admin role; they can still sign in, but organization creation will be forbidden by policy.

Generic model helpers

  • Prefer the exported SDK helpers for entities and relations.
  • Active organization is derived from me().workspaceTenantId.
import { SDK } from '@entityauth/auth-client';

const me = await SDK.me();
const org = await SDK.createEntity({
  workspaceTenantId: me.workspaceTenantId!,
  kind: 'org',
  properties: { name: 'Acme', slug: 'acme', ownerId: me.id },
});

await SDK.linkRelation({
  workspaceTenantId: me.workspaceTenantId!,
  srcId: me.id,
  relation: 'member_of',
  dstId: org.id,
  attrs: { role: 'owner' },
});

Users

  • me()
  • Update properties via SDK.updateEntity:
await SDK.updateEntityEnforced({ id: userId, patch: { properties: { username: 'newusername' } }, actorId: userId });

// Universal list & upsert
const list = await SDK.listEntities({ workspaceTenantId: me.workspaceTenantId!, kind: 'user', filter: { status: 'active' }, limit: 25 });
await SDK.upsertEntity({ workspaceTenantId: me.workspaceTenantId!, kind: 'user', properties: { email: 'a@b.com' } });

Sessions

  • Managed server-side; use login/refresh/logout. No public list/revoke JS APIs.

Helpers

  • onTokenChange(listener) and getAccessToken()
  • getOpenAPI() to inspect the server schema
  • getConvexConfig() to read the Convex deployment URL
  • applyAccessToken(token) for external auth handoffs
  • SDK.refresh(headerRefreshToken?) for token maintenance without the class instance

Allowed origins & cross-origin

  • Localhost origins (http://localhost:3000, 3001, 5173, 4200) are pre-seeded for every workspace
  • Add production domains in the dashboard → Setup → Allowed Origins before deploying
  • The middleware sets Access-Control-Allow-Origin dynamically per workspace and supports credentials: 'include'

OpenAPI & GraphQL

const openapi = await ea.getOpenAPI();
const result = await ea.graphql<{ me: { id: string; email: string | null } }>(
  `query { me { id email } }`
);

if ("errors" in result) {
  console.error(result.errors);
} else {
  console.log(result.data.me);
}

Best practices

Use SDK Fetch Method

Always use EntityAuthClient.fetch for authenticated requests to automatically handle token refresh on 401 responses.

// Good: Auto-refreshes on 401
const response = await ea.fetch('/api/protected-endpoint');

// Avoid: Manual fetch without auto-refresh
fetch('/api/protected-endpoint', {
  headers: { Authorization: `Bearer ${token}` }
});

Proactive refresh when no token

The SDK automatically attempts a refresh once before the first authenticated request if no access token is present (using the ea_refresh cookie). This avoids an initial 401/auto-retry on page reloads.

Handle Token Changes

Listen for token updates to synchronize authentication state across your application.

ea.onTokenChange((newToken) => {
  if (newToken) {
    console.log('User authenticated');
  } else {
    console.log('User logged out');
  }
});

Avoid Token Persistence

Never store access tokens in localStorage or sessionStorage. The SDK manages tokens in memory for security.

Server-first refresh on reload

Perform refresh on the server using the ea_refresh cookie so protected pages don't appear logged out after a hard reload. Guard routes with middleware and/or refresh in server layouts. See Framework Integration.

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const refresh = request.cookies.get('ea_refresh');
  if (!refresh && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
  return NextResponse.next();
}

export const config = { matcher: ['/dashboard/:path*'] };

Token storage

Avoid persisting access tokens in localStorage; the SDK keeps them in memory and refreshes when needed.