Adapters
Use or implement adapters to connect Keyloom to your database. Prisma/Drizzle and SQL/NoSQL backends supported.
Adapters
Adapters implement a small contract to persist users, sessions, verification tokens, and OAuth accounts. Use a built-in adapter or create your own.
Use Prisma Adapter
import { PrismaAdapter } from "@keyloom/adapters";
import { PrismaClient } from "@prisma/client";
const db = new PrismaClient();
const adapter = PrismaAdapter(db);Pass adapter into your keyloom.config.ts or server builder.
Adapter Interface
See the Core overview for the full Adapter TypeScript interface. Any production adapter must implement:
- Users: create/get/update; unique email
- Accounts: link/get by provider
- Sessions: create/get/delete
- Tokens: create/use verification tokens (single-use, hashed at rest)
- Audit: append audit events
Testing & Contracts
The repo includes adapter contract tests under packages/adapters/_contracts. Use these to validate your custom adapter against the expected behavior (sessions, RBAC, refresh store, etc.).
Advanced: Other Adapters
Available packages:
@keyloom/adapters/prisma@keyloom/adapters/drizzle@keyloom/adapters/postgres@keyloom/adapters/mysql2@keyloom/adapters/mongo
Each package exposes a factory similar to PrismaAdapter(...). Consult package READMEs or upcoming per-adapter pages.
Memory Adapter (Dev/Test)
import { memoryAdapter } from "@keyloom/core";
const adapter = memoryAdapter();Useful for local prototyping and unit tests. Not for production.
Prerequisites
- Choose a database (Postgres, MySQL, SQLite, MongoDB, etc.)
- Install a compatible adapter package or plan a custom adapter
- Run migrations to create tables for users, accounts, sessions, tokens
Database schemas
model User {
id String @id @default(cuid())
email String? @unique
name String?
createdAt DateTime @default(now())
}
model Account {
id String @id @default(cuid())
provider String
providerAccountId String
userId String
user User @relation(fields: [userId], references: [id])
@@unique([userId, provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
userId String
expiresAt DateTime
user User @relation(fields: [userId], references: [id])
}
model VerificationToken {
id String @id @default(cuid())
identifier String
tokenHash String
expiresAt DateTime
}
model RefreshToken {
jti String @id
userId String
tokenHash String
expiresAt DateTime
parentJti String?
user User @relation(fields: [userId], references: [id])
}
model Membership {
userId String
orgId String
role String
user User @relation(fields: [userId], references: [id])
@@id([userId, orgId])
}export const users = pgTable("users", {
id: text("id").primaryKey(),
email: text("email").unique(),
name: text("name"),
});
export const accounts = pgTable("accounts", {
id: text("id").primaryKey(),
provider: text("provider").notNull(),
providerAccountId: text("provider_account_id").notNull(),
userId: text("user_id").notNull(),
});
export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
userId: text("user_id").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }),
});
export const verificationTokens = pgTable("verification_tokens", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
tokenHash: text("token_hash").notNull(),
expiresAt: timestamp("expires_at", { withTimezone: true }),
});
export const memberships = pgTable(
"memberships",
{
userId: text("user_id").notNull(),
orgId: text("org_id").notNull(),
role: text("role").notNull(),
},
(t) => ({ pk: primaryKey({ columns: [t.userId, t.orgId] }) })
);Adapter API (reference)
Prop
Type
Note: Real interfaces include additional fields. See @keyloom/*adapter*/readme for exact signatures.
Runnable: custom adapter skeleton
export function myAdapter(db: any) {
return {
async createUser(data) {
/* insert */ return { id: "...", ...data };
},
async getUser(id) {
/* select */ return null;
},
async getUserByEmail(email) {
/* select */ return null;
},
async updateUser(id, data) {
/* update */ return { id, ...data };
},
async linkAccount(userId, acc) {
/* upsert */
},
async getAccountByProvider(p, pid) {
/* select */ return null;
},
async createSession(s) {
/* insert */ return s;
},
async getSession(id) {
/* select */ return null;
},
async deleteSession(id) {
/* delete */
},
async createVerificationToken(t) {
/* insert */
},
async useVerificationToken(identifier, tokenHash) {
/* delete where match */ return true;
},
async getMembership(userId, orgId) {
/* select */ return { role: "member" };
},
async appendAudit(e) {
/* insert */
},
};
}Migrations & schema evolution
- Use additive migrations; avoid destructive changes without backfills
- Keep unique indexes on emails and provider accounts
- Add composite PKs where logical IDs exist (e.g., membership)
- For refresh tokens, store only hashes; rotate regularly and prune expired
Performance considerations
- Add indexes:
accounts(provider, providerAccountId),sessions(userId, expiresAt) - Prefer short-lived sessions for high-throughput systems; rely on JWT strategy when possible
- Use connection pooling; for serverless, prefer drivers that support Data Proxy/HTTP
- Batch writes for audit logs or use async outboxes
Troubleshooting
- Duplicate email errors: ensure
emailis nullable for social logins; enforce uniqueness only when present - Session not found after sign-in: verify cookie domain/path and session TTL alignment
- RBAC query slow: add index on
memberships(userId, orgId)
See also
- Installation: /docs/getting-started/installation
- Configuration: /docs/core/config
- RBAC: /docs/core/rbac
Next steps
- Pick an adapter package (Prisma/Drizzle) and run migrations
- Implement any custom fields needed and extend the adapter
How is this guide?