YeboID Models — Full Prisma Schema
This document provides the complete database schema with every field, type, relation, and mapping.
Schema Location
File: api/prisma/schema.prisma
Database Configuration
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}Database: Neon PostgreSQL
ORM: Prisma 7.x
Client: @prisma/client
Models Overview
| Model | Table | Purpose |
|---|---|---|
User | users | Core user identity |
OtpCode | otp_codes | OTP verification codes |
RefreshToken | refresh_tokens | Session refresh tokens |
ReservedHandle | reserved_handles | Reserved @handles |
User Model
The core identity record for every YeboID user.
prisma
model User {
id String @id @default(uuid())
phone String @unique
handle String? @unique // @handle.yebo
name String?
email String?
avatar String?
// KYC
kycStatus KycStatus @default(NONE)
kycData Json?
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
refreshTokens RefreshToken[]
otpCodes OtpCode[]
@@map("users")
}Fields
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | String | ❌ | uuid() | Primary key (UUID v4) |
phone | String | ❌ | — | Phone in E.164 format (+26878422613) |
handle | String | ✅ | null | Unique @handle (lowercase) |
name | String | ✅ | null | Display name |
email | String | ✅ | null | Email address |
avatar | String | ✅ | null | Avatar URL |
kycStatus | KycStatus | ❌ | NONE | KYC verification status |
kycData | Json | ✅ | null | KYC data from YeboVerify |
createdAt | DateTime | ❌ | now() | Account creation time |
updatedAt | DateTime | ❌ | auto | Last update time |
Constraints
phone— UNIQUE (one account per phone)handle— UNIQUE (if set)
Relations
refreshTokens— One-to-many withRefreshTokenotpCodes— One-to-many withOtpCode
KycStatus Enum
prisma
enum KycStatus {
NONE
PENDING
VERIFIED
REJECTED
}| Value | Description |
|---|---|
NONE | No KYC initiated |
PENDING | KYC submitted, awaiting verification |
VERIFIED | KYC approved |
REJECTED | KYC failed or rejected |
OtpCode Model
Stores OTP codes for phone verification.
prisma
model OtpCode {
id String @id @default(uuid())
phone String
code String
expiresAt DateTime
verified Boolean @default(false)
createdAt DateTime @default(now())
// Optional link to user (for existing users)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([phone, code])
@@map("otp_codes")
}Fields
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | String | ❌ | uuid() | Primary key |
phone | String | ❌ | — | Phone number for OTP |
code | String | ❌ | — | 6-digit OTP code |
expiresAt | DateTime | ❌ | — | Expiration time |
verified | Boolean | ❌ | false | Whether OTP was used |
createdAt | DateTime | ❌ | now() | Creation time |
userId | String | ✅ | null | Optional link to existing user |
Indexes
@@index([phone, code])— Composite index for fast lookup
Relations
user— Many-to-one withUser(optional)onDelete: Cascade— OTP codes deleted when user deleted
Notes
- OTPs are marked
verified = truewhen used (prevents reuse) - Old unverified OTPs are invalidated when new OTP is sent
expiresAtchecked during verification
RefreshToken Model
Stores long-lived refresh tokens for session management.
prisma
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
revokedAt DateTime?
@@index([userId])
@@map("refresh_tokens")
}Fields
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | String | ❌ | uuid() | Primary key |
token | String | ❌ | — | 128-char hex token (unique) |
userId | String | ❌ | — | User who owns token |
expiresAt | DateTime | ❌ | — | Expiration (7 days from creation) |
createdAt | DateTime | ❌ | now() | Creation time |
revokedAt | DateTime | ✅ | null | When token was revoked |
Constraints
token— UNIQUE
Indexes
@@index([userId])— Find user's tokens
Relations
user— Many-to-one withUser(required)onDelete: Cascade— Tokens deleted when user deleted
Token States
| State | Condition |
|---|---|
| Valid | revokedAt == null && expiresAt > now |
| Expired | expiresAt < now |
| Revoked | revokedAt != null |
ReservedHandle Model
Stores handles that are reserved and cannot be claimed by users.
prisma
model ReservedHandle {
id String @id @default(uuid())
handle String @unique
reason String // "brand", "inappropriate", "admin"
createdAt DateTime @default(now())
@@map("reserved_handles")
}Fields
| Field | Type | Nullable | Default | Description |
|---|---|---|---|---|
id | String | ❌ | uuid() | Primary key |
handle | String | ❌ | — | Reserved handle (lowercase) |
reason | String | ❌ | — | Why it's reserved |
createdAt | DateTime | ❌ | now() | When reserved |
Constraints
handle— UNIQUE
Common Reasons
| Reason | Use Case |
|---|---|
brand | Company/brand names |
inappropriate | Offensive words |
admin | System handles |
Notes
- This supplements the hardcoded
RESERVED_WORDSarray inhandle.ts - Allows dynamic handle reservation without code changes
Full Schema
prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// ============================================
// USERS
// ============================================
model User {
id String @id @default(uuid())
phone String @unique
handle String? @unique // @handle.yebo
name String?
email String?
avatar String?
// KYC
kycStatus KycStatus @default(NONE)
kycData Json?
// Timestamps
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
refreshTokens RefreshToken[]
otpCodes OtpCode[]
@@map("users")
}
enum KycStatus {
NONE
PENDING
VERIFIED
REJECTED
}
// ============================================
// AUTHENTICATION
// ============================================
model OtpCode {
id String @id @default(uuid())
phone String
code String
expiresAt DateTime
verified Boolean @default(false)
createdAt DateTime @default(now())
// Optional link to user (for existing users)
userId String?
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([phone, code])
@@map("otp_codes")
}
model RefreshToken {
id String @id @default(uuid())
token String @unique
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
expiresAt DateTime
createdAt DateTime @default(now())
revokedAt DateTime?
@@index([userId])
@@map("refresh_tokens")
}
// ============================================
// HANDLES
// ============================================
model ReservedHandle {
id String @id @default(uuid())
handle String @unique
reason String // "brand", "inappropriate", "admin"
createdAt DateTime @default(now())
@@map("reserved_handles")
}Database Commands
bash
# Generate Prisma client
cd api && npm run db:generate
# Push schema to database (dev)
npm run db:push
# Run migrations (production)
npm run db:migrate