Skip to content

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 ​

ModelTablePurpose
UserusersCore user identity
OtpCodeotp_codesOTP verification codes
RefreshTokenrefresh_tokensSession refresh tokens
ReservedHandlereserved_handlesReserved @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 ​

FieldTypeNullableDefaultDescription
idString❌uuid()Primary key (UUID v4)
phoneString❌—Phone in E.164 format (+26878422613)
handleString✅nullUnique @handle (lowercase)
nameString✅nullDisplay name
emailString✅nullEmail address
avatarString✅nullAvatar URL
kycStatusKycStatus❌NONEKYC verification status
kycDataJson✅nullKYC data from YeboVerify
createdAtDateTime❌now()Account creation time
updatedAtDateTime❌autoLast update time

Constraints ​

  • phone — UNIQUE (one account per phone)
  • handle — UNIQUE (if set)

Relations ​

  • refreshTokens — One-to-many with RefreshToken
  • otpCodes — One-to-many with OtpCode

KycStatus Enum ​

prisma
enum KycStatus {
  NONE
  PENDING
  VERIFIED
  REJECTED
}
ValueDescription
NONENo KYC initiated
PENDINGKYC submitted, awaiting verification
VERIFIEDKYC approved
REJECTEDKYC 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 ​

FieldTypeNullableDefaultDescription
idString❌uuid()Primary key
phoneString❌—Phone number for OTP
codeString❌—6-digit OTP code
expiresAtDateTime❌—Expiration time
verifiedBoolean❌falseWhether OTP was used
createdAtDateTime❌now()Creation time
userIdString✅nullOptional link to existing user

Indexes ​

  • @@index([phone, code]) — Composite index for fast lookup

Relations ​

  • user — Many-to-one with User (optional)
    • onDelete: Cascade — OTP codes deleted when user deleted

Notes ​

  • OTPs are marked verified = true when used (prevents reuse)
  • Old unverified OTPs are invalidated when new OTP is sent
  • expiresAt checked 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 ​

FieldTypeNullableDefaultDescription
idString❌uuid()Primary key
tokenString❌—128-char hex token (unique)
userIdString❌—User who owns token
expiresAtDateTime❌—Expiration (7 days from creation)
createdAtDateTime❌now()Creation time
revokedAtDateTime✅nullWhen token was revoked

Constraints ​

  • token — UNIQUE

Indexes ​

  • @@index([userId]) — Find user's tokens

Relations ​

  • user — Many-to-one with User (required)
    • onDelete: Cascade — Tokens deleted when user deleted

Token States ​

StateCondition
ValidrevokedAt == null && expiresAt > now
ExpiredexpiresAt < now
RevokedrevokedAt != 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 ​

FieldTypeNullableDefaultDescription
idString❌uuid()Primary key
handleString❌—Reserved handle (lowercase)
reasonString❌—Why it's reserved
createdAtDateTime❌now()When reserved

Constraints ​

  • handle — UNIQUE

Common Reasons ​

ReasonUse Case
brandCompany/brand names
inappropriateOffensive words
adminSystem handles

Notes ​

  • This supplements the hardcoded RESERVED_WORDS array in handle.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

One chat. Everything done.