Responsibility boundary
Authentication identifies. Your product authorizes.
The callback answers only whether an email may authenticate now. Invitations, teams, roles, billing access, and domain permissions stay in application-owned data and route logic.
Membership
Your application decides which normalized email addresses are currently approved.
Roles
Keep roles and permissions in your domain tables; the auth package returns identity only.
Revocation
Session creation, resolution, and authenticated passkey operations recheck current membership.
Isolation
Every application still owns a separate D1 database, secret, cookies, passkeys, and recovery policy.
Configuration
Supply a live membership check
The package normalizes the email before calling authorizeEmail. Check an indexed, authoritative membership source and return only active membership—not a cached role decision.
import { createMultiUserAuth } from
"@santi020k/auth-cloudflare";
app.all("/api/auth/*", (context) => {
const auth = createMultiUserAuth({
appName: "Example team workspace",
applicationOrigin: context.env.APPLICATION_ORIGIN,
authServerURL: context.env.AUTH_SERVER_URL,
cookiePrefix: "example-team",
database: context.env.AUTH_DB,
secret: context.env.AUTH_SECRET,
authorizeEmail: (email) =>
isActiveMember(context.env.AUTH_DB, email),
sendVerificationOTP: ({ email, otp }) =>
sendLoginCode(context.env, email, otp),
waitUntil: (task) => {
context.executionCtx.waitUntil(task);
},
});
return auth.handler(context.req.raw);
});import { createApplicationAuthClient } from
"@santi020k/auth-cloudflare/client";
export const authClient = createApplicationAuthClient({
authServerURL: env.PUBLIC_AUTH_SERVER_URL,
});import {
createAuthMiddleware,
type AuthEnv,
} from "@santi020k/auth-cloudflare/hono";
interface AppEnv extends AuthEnv {
Bindings: Bindings;
}
app.use("/account/*",
createAuthMiddleware<AppEnv>((context) =>
createAuthForContext(context),
),
);
app.get("/account/profile", (context) =>
context.json(context.var.authSession),
);Lifecycle
Check membership at every meaningful boundary
- Request a code.Approved and rejected addresses receive the same success-shaped response. Only approved addresses receive mail. Better Auth may persist a hashed, expiring verification record as part of that uniform flow.
- Create or update identity.The database hook blocks user writes when the callback no longer approves the normalized email.
- Create or resolve a session.The callback runs again and authenticated passkey operations are guarded. A removed member cannot create a new session, mutate passkeys, or receive a resolved identity even if an unexpired session record remains stored.
- Apply product permissions.Use the returned email and user ID to load current roles from application-owned domain data.
Migration
No authentication schema migration is required
The canonical user and session tables already support multiple identities. Add a consumer-owned membership table only if your product does not have an authoritative member source, and introduce it through that application's normal additive migration process.
Moving from createOwnerAuth
- Keep the same database, secret, cookie prefix, origins, delivery callback, and schema.
- Add the current owner to the authoritative membership source before changing the factory.
- Replace the owner factory, browser client, and Hono middleware with the generic names shown above.
- Verify owner access, a second approved account, a rejected account, and immediate membership revocation.