Selegic CRM Docs
ServerAuth & Sessions

Architecture

How sessions, API keys, and tenant resolution work in the CRM server.

Request Flow

has session cookie no session x-api-key header x-tenant-id header HTTP Request sessionMiddleware User resolved Anonymous tenantMiddleware API Key auth Tenant-only Organization resolved Route handler with context

Session Lifecycle

  1. Browser Request — Include session cookie (better-auth-session)
  2. sessionMiddleware — Calls auth.api.getSession() with request headers
  3. Context Population — Attaches session and user to HandlerContext
  4. 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:

  1. Header: x-api-key: <key>
  2. Verification: auth.api.verifyApiKey({ body: { key } })
  3. User Resolution: Keys have a referenceId linked to a user
  4. Organization: Optional x-tenant-id header, 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:

ScenarioHeadersBehavior
Session authx-tenant-idVerify user belongs to org
API keyx-api-keyVerify key + resolve org
Tenant-onlyx-tenant-id onlyAnonymous 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
});

On this page