Snapshot for
@system-core/core0.12.x. Current docs live at /.
Custom Prisma Schemas / Bring Your Own Database
Use this when your Prisma client does not expose the exact model names expected by createPrismaDeps().
What AuthDeps Is
AuthDeps is the adapter contract behind the package-owned auth runtime. It lets you keep your own database schema and translate your tables into the records and operations that system-core expects.
This exists for projects like:
- Prisma schemas with
RefreshTokeninstead ofSession User.statusenums instead ofisActive: boolean- single-column roles instead of role/permission join tables
- existing products that cannot rename production tables to match the package schema
Use AuthDeps when:
- you need
createSystem({ auth: authDeps })without the package-owned Prisma schema - you want NestJS integration through
SystemCoreModule.forRoot({ auth: authDeps, http: nestjsAdapter }) - you want package-owned auth flows on top of your own Prisma models
Step-by-step
- Identify the user, token, invite, and reset models in your schema.
- Map those models into the
UserRecord,SessionRecord, and token record shapes expected byAuthDeps. - Implement the
AuthDepssections your app needs. - Stub RBAC adapters when your app uses a simpler role model.
- Pass the adapter into
createSystem({ auth })or NestJSforRoot({ auth }).
Field Mapping
| system-core field | Common real-world equivalent |
|---|---|
UserRecord.isActive | user.status === 'ACTIVE' |
UserRecord.emailVerified | user.emailVerifiedAt != null or hardcode true |
UserRecord.firstName + UserRecord.lastName | derive from firstName / lastName, or split a single display name |
SessionRecord.tokenHash | refreshToken.tokenHash |
SessionRecord.expiresAt | refreshToken.expiresAt |
SessionRecord.userId | refreshToken.userId |
Full Example: Status enum user model
The example below assumes:
User.statusis'ACTIVE' | 'INACTIVE' | 'SUSPENDED'User.roleis'SUPER_ADMIN' | 'TENANT_ADMIN' | 'USER'- refresh tokens live in a
RefreshTokentable - there are no dedicated RBAC join tables
ts
import { createSystem } from '@system-core/core'
import type {
AuthDeps,
PermissionRecord,
RolePermissionAssignment,
RoleRecord,
SessionRecord,
UserRecord
} from '@system-core/core'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const ROLE_LEVEL: Record<string, number> = {
SUPER_ADMIN: 100,
TENANT_ADMIN: 50,
USER: 10
}
const ROLE_PERMISSIONS: Record<string, string[]> = {
SUPER_ADMIN: ['*'],
TENANT_ADMIN: ['cms:publish', 'cms:read', 'users:read'],
USER: []
}
function toRoleRecord(name: string): RoleRecord {
return {
id: name,
name,
label: name.replaceAll('_', ' '),
level: ROLE_LEVEL[name] ?? 0,
isSystem: true,
isAssignable: name !== 'SUPER_ADMIN'
}
}
function toPermissionRecord(action: string): PermissionRecord {
return {
id: action,
action,
label: action
}
}
function toUserRecord(user: {
id: string
email: string
firstName: string | null
lastName: string | null
phone: string | null
role: string
status: 'ACTIVE' | 'INACTIVE' | 'SUSPENDED'
passwordHash: string | null
createdAt: Date
updatedAt: Date
}): UserRecord & { passwordHash?: string | null } {
return {
id: user.id,
email: user.email,
firstName: user.firstName,
lastName: user.lastName,
phone: user.phone,
role: user.role,
isActive: user.status === 'ACTIVE',
emailVerified: true,
passwordHash: user.passwordHash,
createdAt: user.createdAt,
updatedAt: user.updatedAt
}
}
function toSessionRecord(row: {
id: string
userId: string
tokenHash: string
expiresAt: Date
revokedAt: Date | null
ipAddress: string | null
userAgent: string | null
createdAt: Date
updatedAt: Date
}): SessionRecord {
return {
id: row.id,
userId: row.userId,
tokenHash: row.tokenHash,
expiresAt: row.expiresAt,
revokedAt: row.revokedAt,
ipAddress: row.ipAddress,
userAgent: row.userAgent,
createdAt: row.createdAt,
updatedAt: row.updatedAt
}
}
function unsupported(name: string): never {
throw new Error(`${name} is not implemented for this schema`)
}
const authDeps: AuthDeps = {
users: {
count: where => prisma.user.count({ where: where as any }),
async findById(id) {
const user = await prisma.user.findUnique({ where: { id } })
return user ? toUserRecord(user) : null
},
async findByEmail(email) {
const user = await prisma.user.findUnique({ where: { email } })
return user ? toUserRecord(user) : null
},
async list(query) {
const users = await prisma.user.findMany({
where: {
...(query.q
? {
OR: [
{ email: { contains: query.q, mode: 'insensitive' } },
{ firstName: { contains: query.q, mode: 'insensitive' } },
{ lastName: { contains: query.q, mode: 'insensitive' } }
]
}
: {}),
...(query.role ? { role: query.role } : {}),
...(query.isActive === undefined ? {} : { status: query.isActive ? 'ACTIVE' : { not: 'ACTIVE' } })
},
orderBy: { createdAt: 'desc' },
skip: query.skip,
take: query.take
})
return users.map(toUserRecord)
},
async create(data) {
const user = await prisma.user.create({ data: data as any })
return toUserRecord(user)
},
async update(id, data) {
const user = await prisma.user.update({ where: { id }, data: data as any })
return toUserRecord(user)
},
async delete(id) {
await prisma.user.delete({ where: { id } })
},
async countByRole(role, activeOnly) {
return prisma.user.count({
where: {
role,
...(activeOnly ? { status: 'ACTIVE' } : {})
}
})
}
},
roles: {
async findByName(name) {
return ROLE_LEVEL[name] ? toRoleRecord(name) : null
},
async list() {
return Object.keys(ROLE_LEVEL).map(toRoleRecord)
},
async create(data) {
return toRoleRecord(String(data.name))
},
async update(id) {
return toRoleRecord(id)
},
async delete() {}
},
permissions: {
async findByAction(action) {
const allActions = new Set(Object.values(ROLE_PERMISSIONS).flat())
if (!allActions.has(action) && action !== '*') return null
return toPermissionRecord(action)
},
async list() {
return Array.from(new Set(Object.values(ROLE_PERMISSIONS).flat())).map(toPermissionRecord)
},
async create(data) {
return toPermissionRecord(String(data.action))
},
async delete() {}
},
rolePermissions: {
async list(roleName) {
return (ROLE_PERMISSIONS[roleName] ?? []).map<RolePermissionAssignment>((action) => ({
id: `${roleName}:${action}`,
roleName,
permissionAction: action
}))
},
async create(data) {
return {
id: `${data.roleName}:${data.permissionAction}`,
roleName: String(data.roleName),
permissionAction: String(data.permissionAction)
}
},
async delete() {},
async deleteByRole() {}
},
userPermissions: {
async list() {
return []
},
async create() {
unsupported('userPermissions.create')
},
async delete() {},
async deleteByUser() {}
},
invites: {
async create(data) {
return prisma.invitation.create({ data: data as any }) as any
},
async findById(id) {
return prisma.invitation.findUnique({ where: { id } }) as any
},
async update(id, data) {
return prisma.invitation.update({ where: { id }, data: data as any }) as any
}
},
passwordResets: {
async create(data) {
return prisma.passwordResetToken.create({ data: data as any }) as any
},
async findById(id) {
return prisma.passwordResetToken.findUnique({ where: { id } }) as any
},
async update(id, data) {
return prisma.passwordResetToken.update({ where: { id }, data: data as any }) as any
}
},
emailVerifications: {
async create() {
unsupported('emailVerifications.create')
},
async findById() {
return null
},
async update() {
unsupported('emailVerifications.update')
}
},
sessions: {
async upsertByTokenHash(tokenHash, data) {
const row = await prisma.refreshToken.upsert({
where: { tokenHash },
create: { tokenHash, ...(data as any) },
update: data as any
})
return toSessionRecord(row)
},
async findByTokenHash(tokenHash) {
const row = await prisma.refreshToken.findUnique({ where: { tokenHash } })
return row ? toSessionRecord(row) : null
},
async findById(id) {
const row = await prisma.refreshToken.findUnique({ where: { id } })
return row ? toSessionRecord(row) : null
},
async listByUser(userId) {
const rows = await prisma.refreshToken.findMany({
where: { userId },
orderBy: { createdAt: 'desc' }
})
return rows.map(toSessionRecord)
},
async deleteByTokenHash(tokenHash) {
await prisma.refreshToken.delete({ where: { tokenHash } }).catch(() => {})
},
async deleteById(id) {
await prisma.refreshToken.delete({ where: { id } }).catch(() => {})
},
async deleteByUser(userId) {
const result = await prisma.refreshToken.deleteMany({ where: { userId } })
return result.count
},
async deleteExpired(before) {
const result = await prisma.refreshToken.deleteMany({
where: { expiresAt: { lt: before } }
})
return result.count
},
async countActiveByUser(userId, now) {
return prisma.refreshToken.count({
where: {
userId,
revokedAt: null,
expiresAt: { gt: now }
}
})
},
async deleteOldestByUser(userId, now) {
const oldest = await prisma.refreshToken.findFirst({
where: {
userId,
revokedAt: null,
expiresAt: { gt: now }
},
orderBy: { createdAt: 'asc' }
})
if (oldest) {
await prisma.refreshToken.delete({ where: { id: oldest.id } })
}
}
}
}
const system = await createSystem({
auth: authDeps
})Session Management
system-core expects SessionRecord, but many production apps store refresh tokens separately. That is fine.
- Map your
RefreshToken.tokenHashcolumn toSessionRecord.tokenHash. - Map your
RefreshToken.expiresAtcolumn toSessionRecord.expiresAt. - Use
upsertByTokenHash()to store refresh-token rotation state. - Use
deleteExpired()andcountActiveByUser()against your refresh-token table directly.
No RBAC Tables
If your schema uses one User.role column and no role-permission joins:
- implement
roles.list()from your enum or constant list - implement
rolePermissions.list()from a static permission map - return an empty list from
userPermissions.list()if you do not support per-user overrides - leave
userRolesundefined unless your app supports multi-role assignments
The stubbed roles, permissions, and rolePermissions sections in the example above are enough for many single-role applications.
NestJS Usage
ts
import { Module } from '@nestjs/common'
import { SystemCoreModule, nestjsAdapter } from '@system-core/core/integrations/nestjs'
@Module({
imports: [
SystemCoreModule.forRoot({
http: nestjsAdapter,
auth: authDeps
})
]
})
export class AppModule {}