Database Schema
Arraf Auth requires four tables:| Table | Purpose |
|---|---|
| User | Stores user information (email, phone, name) |
| Session | Stores active sessions with tokens |
| Account | Links users to authentication providers |
| Verification | Stores OTP codes and verification tokens |
Prisma Adapter
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum VerificationType {
phone_otp
email_otp
email_verification
password_reset
phone_change
}
model User {
id String @id @default(cuid())
email String? @unique
phone String? @unique
name String?
emailVerified Boolean @default(false)
phoneVerified Boolean @default(false)
image String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
sessions Session[]
accounts Account[]
@@map("users")
}
model Session {
id String @id @default(cuid())
userId String
token String @unique
expiresAt DateTime
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([token])
@@map("sessions")
}
model Account {
id String @id @default(cuid())
userId String
providerId String
accountId String
accessToken String?
refreshToken String?
accessTokenExpiresAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([providerId, accountId])
@@index([userId])
@@map("accounts")
}
model Verification {
id String @id @default(cuid())
identifier String
token String
type VerificationType
expiresAt DateTime
attempts Int @default(0)
createdAt DateTime @default(now())
@@unique([identifier, type])
@@index([identifier])
@@map("verifications")
}
# Generate Prisma Client
npx prisma generate
# Create migration
npx prisma migrate dev --name init
# Apply migration to production
npx prisma migrate deploy
import { createAuth } from '@arraf-auth/core'
import { prismaAdapter } from '@arraf-auth/adapter-prisma'
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
const auth = createAuth({
secret: process.env.AUTH_SECRET!,
database: prismaAdapter(prisma)
})
export { auth, prisma }
Drizzle Adapter
import {
pgTable,
text,
boolean,
timestamp,
integer,
pgEnum,
uniqueIndex,
index
} from 'drizzle-orm/pg-core'
export const verificationTypeEnum = pgEnum('verification_type', [
'phone_otp',
'email_otp',
'email_verification',
'password_reset',
'phone_change'
])
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').unique(),
phone: text('phone').unique(),
name: text('name'),
emailVerified: boolean('email_verified').notNull().default(false),
phoneVerified: boolean('phone_verified').notNull().default(false),
image: text('image'),
createdAt: timestamp('created_at').notNull().defaultNow(),
updatedAt: timestamp('updated_at').notNull().defaultNow()
})
export const sessions = pgTable('sessions', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
token: text('token').notNull().unique(),
expiresAt: timestamp('expires_at').notNull(),
ipAddress: text('ip_address'),
userAgent: text('user_agent'),
createdAt: timestamp('created_at').notNull().defaultNow()
}, (t) => ({
userIdx: index('sessions_user_idx').on(t.userId),
tokenIdx: index('sessions_token_idx').on(t.token)
}))
export const accounts = pgTable('accounts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
providerId: text('provider_id').notNull(),
accountId: text('account_id').notNull(),
accessToken: text('access_token'),
refreshToken: text('refresh_token'),
accessTokenExpiresAt: timestamp('access_token_expires_at'),
createdAt: timestamp('created_at').notNull().defaultNow()
}, (t) => ({
providerAccountIdx: uniqueIndex('accounts_provider_account_idx').on(t.providerId, t.accountId),
userIdx: index('accounts_user_idx').on(t.userId)
}))
export const verifications = pgTable('verifications', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
identifier: text('identifier').notNull(),
token: text('token').notNull(),
type: verificationTypeEnum('type').notNull(),
expiresAt: timestamp('expires_at').notNull(),
attempts: integer('attempts').notNull().default(0),
createdAt: timestamp('created_at').notNull().defaultNow()
}, (t) => ({
identifierTypeIdx: uniqueIndex('verifications_identifier_type_idx').on(t.identifier, t.type),
identifierIdx: index('verifications_identifier_idx').on(t.identifier)
}))
import type { Config } from 'drizzle-kit'
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!
}
} satisfies Config
import { drizzle } from 'drizzle-orm/postgres-js'
import postgres from 'postgres'
import * as schema from './schema'
const connectionString = process.env.DATABASE_URL!
const client = postgres(connectionString)
export const db = drizzle(client, { schema })
Adapter Comparison
Database Operations
The adapter interface provides all necessary operations:Database Indexes
Critical indexes for performance:These indexes are automatically created when using Prisma or Drizzle with the provided schemas.
Verification Types
The verification system supports multiple use cases:phone-otp → phone_otp). This is handled automatically by the adapters (see prisma/index.ts:10-16 and drizzle/index.ts:26-32).
Cleanup Tasks
Schedule periodic cleanup of expired verifications:Multi-Database Support
Arraf Auth adapters work with:- PostgreSQL
- MySQL
- SQLite