Skip to content

Database Types

Learn how to properly separate database models from JWT token payloads.

The Problem

JWT tokens should only contain:

  • Small, non-sensitive data
  • Data needed for authorization
  • Public user information

Database records often contain:

  • Sensitive fields (passwords, API keys)
  • Large data (full documents, blobs)
  • Internal metadata (timestamps, IDs)

Mixing these leads to security vulnerabilities and performance issues.

The Solution: Separate Types

Token Payload (JWT)

typescript
import type { CustomTokenClaims } from '#nuxt-aegis'

export type AppTokenClaims = CustomTokenClaims<{
  role: string
  permissions: string[]
  organizationId: string
}>

Database Model

typescript
import type { AppTokenClaims } from './token'

export interface DatabaseUser extends AppTokenClaims {
  // Database-specific fields
  id: string
  createdAt: string
  lastLogin: string
  
  // Sensitive fields - NEVER in JWT!
  hashedPassword?: string
  apiKeys?: string[]
  
  // Large or internal data
  providers?: Provider[]
  metadata?: Record<string, unknown>
}

Real-World Example

typescript
// types/database.ts
import type { AppTokenClaims } from './token'

export interface Provider {
  name: string
  id: string
}

export interface DatabaseUser extends AppTokenClaims {
  id: string
  createdAt: string
  lastLogin: string
  providers?: Provider[]
  hashedPassword?: string  // Only for password auth
}

// Database operations
export function getUserById(id: string): DatabaseUser | null {
  // Returns full database record
}

// JWT claims mapping
export function userToTokenClaims(dbUser: DatabaseUser): AppTokenClaims {
  return {
    sub: dbUser.id,
    email: dbUser.email,
    name: dbUser.name,
    picture: dbUser.picture,
    role: dbUser.role,
    permissions: dbUser.permissions,
    organizationId: dbUser.organizationId,
    // Note: hashedPassword is deliberately excluded
  }
}

Password Authentication Example

typescript
// Unified persistence handler
onUserPersist: async (user, { provider }) => {
  if (provider === 'password') {
    const dbUser = await database.upsert({
      where: { email: user.email },
      update: { hashedPassword: user.hashedPassword },
      create: {
        email: user.email,
        hashedPassword: user.hashedPassword,
        role: 'user',
      },
    })
    
    // Return data to merge into user object (for JWT claims)
    return {
      userId: dbUser.id,
      role: dbUser.role,
      permissions: dbUser.permissions,
      organizationId: dbUser.organizationId,
    }
  }
  
  // Handle OAuth providers...
},

// Password provider handler
password: {
  async findUser(email) {
    const dbUser = await database.findByEmail(email)
    
    if (!dbUser || !dbUser.hashedPassword) {
      return null
    }
    
    // Return PasswordUser (includes hashedPassword for verification)
    return {
      id: dbUser.id,
      email: dbUser.email,
      hashedPassword: dbUser.hashedPassword,
      // Can include other fields needed for custom claims
      role: dbUser.role,
      permissions: dbUser.permissions,
    }
  },
}

Common Fields

FieldDatabaseJWT TokenNotes
id / subUse sub in JWT
emailSafe to include
nameSafe to include
roleNeeded for authorization
permissionsNeeded for authorization
hashedPasswordNEVER in JWT
apiKeyNEVER in JWT
createdAtInternal metadata
providersLarge/complex data

Best Practices

✓ Do

typescript
// Extend token payload for database model
interface DatabaseUser extends AppTokenClaims {
  id: string
  hashedPassword?: string
}

// Map database to JWT claims
function userToTokenClaims(db: DatabaseUser): AppTokenClaims {
  return {
    sub: db.id,
    email: db.email,
    // ... only safe fields
  }
}

✗ Don't

typescript
// DON'T put database-only fields in token type
type AppTokenClaims = CustomTokenClaims<{
  role: string
  hashedPassword: string  // ✗ Security risk!
}>

// DON'T use database type as JWT type
const user = getAuthUser<DatabaseUser>(event)  // ✗ Wrong!

Next Steps

Released under the MIT License.