ServerAuth & Sessions
Architecture
How sessions, API keys, and tenant resolution work in the CRM server.
Request Flow
Session Lifecycle
- Browser Request — Include session cookie (
better-auth-session) - sessionMiddleware — Calls
auth.api.getSession()with request headers - Context Population — Attaches
sessionandusertoHandlerContext - Response — Session cookie maintained/updated by Better Auth
// crm/server/lib/middleware/session.ts
export const sessionMiddleware: MiddlewareHandler = async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
c.set("session", session?.session ?? null);
c.set("user", session?.user ?? null);
await next();
};Session Hook
The auth instance includes a database hook that sets the active organization on session creation:
databaseHooks: {
session: {
create: {
before: async (session) => {
const organization = await basePrisma.organization.findFirst({
where: { members: { some: { userId: session.userId } } },
});
return {
data: {
...session,
activeOrganizationId: organization.id,
activeOrganizationSlug: organization.slug,
},
};
},
},
},
},API Key Authentication
API keys are verified in tenantMiddleware and can identify both user and organization:
- Header:
x-api-key: <key> - Verification:
auth.api.verifyApiKey({ body: { key } }) - User Resolution: Keys have a
referenceIdlinked to a user - Organization: Optional
x-tenant-idheader, or auto-detected from user's memberships
// crm/server/lib/middleware/tenant.ts
if (apiKey) {
const data = await auth.api.verifyApiKey({ body: { key: apiKey } });
if (data.valid && data.key?.referenceId) {
// User resolved from API key
c.set("session", { userId: data.key.referenceId } as ...);
c.set("user", { id: data.key.referenceId } as ...);
}
}Tenant Resolution
The tenantMiddleware handles three scenarios:
| Scenario | Headers | Behavior |
|---|---|---|
| Session auth | x-tenant-id | Verify user belongs to org |
| API key | x-api-key | Verify key + resolve org |
| Tenant-only | x-tenant-id only | Anonymous access to tenant-scoped data |
Guarding Routes
Use requireAuth middleware to protect routes:
import { requireAuth } from "@/lib/middleware";
app.post("/api/resource", requireAuth, async (c) => {
const user = c.get("user");
const org = c.get("org");
// ...
});For granular checks in route handlers:
app.post("/api/resource", requireAuth, async (c) => {
const user = c.get("user");
const members = user?.members ?? [];
// Custom authorization logic
});