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
idStringuuid()Primary key (UUID v4)
phoneStringPhone in E.164 format (+26878422613)
handleStringnullUnique @handle (lowercase)
nameStringnullDisplay name
emailStringnullEmail address
avatarStringnullAvatar URL
kycStatusKycStatusNONEKYC verification status
kycDataJsonnullKYC data from YeboVerify
createdAtDateTimenow()Account creation time
updatedAtDateTimeautoLast update time

Constraints

  • phoneUNIQUE (one account per phone)
  • handleUNIQUE (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
idStringuuid()Primary key
phoneStringPhone number for OTP
codeString6-digit OTP code
expiresAtDateTimeExpiration time
verifiedBooleanfalseWhether OTP was used
createdAtDateTimenow()Creation time
userIdStringnullOptional 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
idStringuuid()Primary key
tokenString128-char hex token (unique)
userIdStringUser who owns token
expiresAtDateTimeExpiration (7 days from creation)
createdAtDateTimenow()Creation time
revokedAtDateTimenullWhen token was revoked

Constraints

  • tokenUNIQUE

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
idStringuuid()Primary key
handleStringReserved handle (lowercase)
reasonStringWhy it's reserved
createdAtDateTimenow()When reserved

Constraints

  • handleUNIQUE

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.