@intelligo-dev/auth
Multi-tenant authentication on Better-Auth: sign-up, workspaces, invitations and role-based access control.
Install
pnpm add @intelligo-dev/auth better-auth drizzle-orm react zodbetter-auth, drizzle-orm, react and zod are peers: the guards read the
Better-Auth instance and the database client the application already has,
and the verification and reset emails render with React.
What it owns
Sessions and accounts, organizations as workspaces, membership roles
(owner / admin / member), invitations, platform-admin impersonation, and
the guards every transport starts with: requireAuth, requireWorkspace,
requireRole, requirePlatformAdmin.
The team, workspace, profile and onboarding services live here too, behind ports the composition root binds — auth never imports billing.
Use
import { requireWorkspace } from "@intelligo-dev/auth";
const { user, workspace, membership } = await requireWorkspace();A guard that refuses throws an AuthGuardError; isAuthGuardError(error)
narrows it and error.code picks the status: unauthenticated (no session,
401), no_workspace (signed in, member of no workspace) or forbidden
(lacking the role, 403).
impersonateUser checks that the caller is a platform admin and that the
target is not one before it swaps the session. Products call
startImpersonation from @intelligo-dev/admin, which adds the audit event
and the mandatory reason.
hasSessionCookie(request) from @intelligo-dev/auth/edge is for an optional
optimistic redirect in a consumer’s proxy.ts. It reads cookie presence only
and is never authorization; the scaffold does not use it.
Platform admin is a row (users.role), not an environment variable. The
allowlist in PLATFORM_ADMIN_EMAILS is promoted into that column on first use,
so the plugin and the guard cannot disagree. The allowlist counts a verified
address only; on a machine that verifies none, intelligo admin grant <email>
writes the column directly, and intelligo admin revoke <email> takes it away.
Workspaces and memberships change through the team and workspace services,
which apply the plan’s limits and the deletion hook around Better-Auth’s
organization plugin; the plugin’s own mutating endpoints answer 404 over HTTP
(SERVICE_ONLY_ORGANIZATION_PATHS). An invitation can carry a product’s
own fields (inviteMember({ email, role, productFields }), a few short
strings — what the member is to the workspace, say); they are copied onto
the membership when it is accepted and read back with
memberProductFields({ workspaceId, userId }). Behind proxies, AUTH_IP_HEADERS
(headers to read the client address from, in order) and
AUTH_TRUSTED_PROXIES (CIDRs) let the auth rate limiter tell clients apart.
Deleting an account (createProfileService().deleteAccount) schedules it:
it is refused while the user is the only owner of a workspace other people
belong to, and the workspaces they own alone are deleted with it. Until the
grace period ends — ACCOUNT_DELETION_GRACE_DAYS, 30 by default — signing
in leads to a restore screen (getPendingDeletion, restoreAccount);
every other guard treats that session as signed out. After it,
purgeDeletedAccounts, called from the maintenance route, deletes the
account and erases the user from the memory audit.
Entry points
@intelligo-dev/auth@intelligo-dev/auth/client@intelligo-dev/auth/edge