Skip to content

Snapshot for @system-core/core 0.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 RefreshToken instead of Session
  • User.status enums instead of isActive: 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

  1. Identify the user, token, invite, and reset models in your schema.
  2. Map those models into the UserRecord, SessionRecord, and token record shapes expected by AuthDeps.
  3. Implement the AuthDeps sections your app needs.
  4. Stub RBAC adapters when your app uses a simpler role model.
  5. Pass the adapter into createSystem({ auth }) or NestJS forRoot({ auth }).

Field Mapping

system-core fieldCommon real-world equivalent
UserRecord.isActiveuser.status === 'ACTIVE'
UserRecord.emailVerifieduser.emailVerifiedAt != null or hardcode true
UserRecord.firstName + UserRecord.lastNamederive from firstName / lastName, or split a single display name
SessionRecord.tokenHashrefreshToken.tokenHash
SessionRecord.expiresAtrefreshToken.expiresAt
SessionRecord.userIdrefreshToken.userId

Full Example: Status enum user model

The example below assumes:

  • User.status is 'ACTIVE' | 'INACTIVE' | 'SUSPENDED'
  • User.role is 'SUPER_ADMIN' | 'TENANT_ADMIN' | 'USER'
  • refresh tokens live in a RefreshToken table
  • 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.tokenHash column to SessionRecord.tokenHash.
  • Map your RefreshToken.expiresAt column to SessionRecord.expiresAt.
  • Use upsertByTokenHash() to store refresh-token rotation state.
  • Use deleteExpired() and countActiveByUser() 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 userRoles undefined 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 {}

system-core documentation for maintainers, integrators, and AI build agents.