Skip to main content

Overview

The jøsh database uses PostgreSQL via Prisma ORM. The schema is divided into two main domains:
  1. Legacy Models - Original jmash voting system (Photo, Vote, User, Shot)
  2. TPO Models - Time/Place/Occasion dating system (TpoUser, TpoDate, TpoMessage)

Schema Configuration

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("DIRECT_URL")
}

TPO (Dating) Models

TpoUser

Represents users in the dating system with comprehensive onboarding state.
id
String
default:"cuid()"
Unique identifier
createdAt
DateTime
default:"now()"
User creation timestamp
updatedAt
DateTime
Auto-updated on changes
phoneNumber
String
required
E.164 format phone number (unique)
status
TpoUserStatus
default:"ONBOARDING"
Current user state: ONBOARDING, PENDING_REVIEW, APPROVED, REJECTED, BANNED
onboardingStep
TpoOnboardingStep
default:"INTRO_SENT"
Current onboarding progress:
  • INTRO_SENT
  • AWAITING_ABOUT
  • AWAITING_PREFERENCES
  • AWAITING_CITY
  • AWAITING_PHOTOS
  • AWAITING_ID
  • COMPLETE
aboutMe
String?
Free-form text about the user (Q&A format)
onboardingQuestionIndex
Int
default:"0"
Current position in onboarding question sequence
aboutFollowupAsked
Boolean
default:"false"
Whether AI requested clarification on current answer
aboutFollowupCount
Int
default:"0"
Number of follow-up questions asked
preferences
String?
User’s dating preferences (merged with aboutMe)
preferencesFollowupAsked
Boolean
default:"false"
Legacy field for preferences follow-up
preferencesFollowupCount
Int
default:"0"
Legacy preferences follow-up counter
city
String?
User’s current city
structuredProfile
Json?
AI-generated structured profile with about/preferences sections
photoUrls
String[]
default:"[]"
Array of Supabase storage paths for user photos
idPhotoUrl
String?
Storage path for driver’s license photo
dlName
String?
Name extracted from driver’s license
dlAge
Int?
Age calculated from driver’s license DOB
dlHeight
String?
Height from driver’s license

Relationships

datesAsA  TpoDate[] @relation("DateUserA")
datesAsB  TpoDate[] @relation("DateUserB")

Example

const user = await db.tpoUser.create({
  data: {
    phoneNumber: "+12125551234",
    status: "ONBOARDING",
    onboardingStep: "AWAITING_ABOUT",
    onboardingQuestionIndex: 0,
  },
});

TpoDate

Represents a matched pair with scheduling state and message portal.
id
String
default:"cuid()"
Unique date identifier
createdAt
DateTime
default:"now()"
Date creation timestamp
status
TpoDateStatus
default:"ACTIVE"
ACTIVE or ENDED
endedAt
DateTime?
Timestamp when date was ended
portalEnabled
Boolean
default:"false"
Whether direct messaging between users is enabled
schedulingPhase
TpoSchedulingPhase
default:"PROPOSING_TO_A"
Current scheduling state:
  • PROPOSING_TO_A
  • WAITING_FOR_A_REPLY
  • PROPOSING_TO_B
  • WAITING_FOR_B_REPLY
  • WAITING_FOR_A_ALTERNATIVE
  • WAITING_FOR_B_ALTERNATIVE
  • AGREED
  • FAILED
  • ESCALATED
schedulingAttemptCount
Int
default:"0"
Number of scheduling attempts made
lastSchedulingMessageAt
DateTime?
Last scheduling message timestamp
schedulingEscalatedAt
DateTime?
When scheduling was escalated to manual intervention
schedulingFailedReason
String?
Reason for scheduling failure
proposedSlot
String?
Current proposed date/time (e.g., “Saturday, March 8th at 7pm”)
userAAvailable
Boolean?
User A’s availability for proposed slot
userBAvailable
Boolean?
User B’s availability for proposed slot
agreedTime
String?
Mutually agreed date/time
suggestedPlace
String?
AI-suggested venue for the date

Relationships

userAId  String
userA    TpoUser @relation("DateUserA", fields: [userAId], references: [id])

userBId  String
userB    TpoUser @relation("DateUserB", fields: [userBId], references: [id])

messages TpoMessage[]

Example

const date = await db.tpoDate.create({
  data: {
    userAId: "clx123...",
    userBId: "clx456...",
    status: "ACTIVE",
    schedulingPhase: "WAITING_FOR_A_REPLY",
    proposedSlot: "Friday, March 7th at 7pm",
  },
  include: {
    userA: true,
    userB: true,
  },
});

TpoMessage

Stores all messages in the scheduling and relay flows.
id
String
default:"cuid()"
Unique message identifier
createdAt
DateTime
default:"now()"
Message timestamp
dateId
String
required
Associated date record
fromPhone
String
required
Sender phone number (or “system” for bot messages)
toPhone
String
required
Recipient phone number (or “system” for incoming user messages)
body
String
required
Message content
blocked
Boolean
default:"false"
Whether message contained profanity/blocked content

Relationships

date  TpoDate @relation(fields: [dateId], references: [id])

Example

const message = await db.tpoMessage.create({
  data: {
    dateId: "clx789...",
    fromPhone: "system",
    toPhone: "+12125551234",
    body: "you've been matched!",
    blocked: false,
  },
});

Legacy (Voting) Models

Photo

Photos used in the original jmash voting system.
id
String
default:"cuid()"
Unique photo identifier
createdAt
DateTime
default:"now()"
Upload timestamp
person
Person
required
JOSH or JONATHAN
views
Int
default:"0"
Total view count
elo
Int
default:"1500"
Overall ELO rating
menElo
Int
default:"1500"
ELO rating from male voters
womenElo
Int
default:"1500"
ELO rating from female voters

Relationships

photoTransform PhotoTransform?
leftVotes      Vote[]  @relation("LeftPhoto")
rightVotes     Vote[]  @relation("RightPhoto")
label          PhotoLabel?
ick            Ick[]

Vote

Records user votes between two photos.
id
String
default:"cuid()"
Unique vote identifier
userId
String
required
Voter’s user ID
person
Person
required
Which person’s photos were voted on
leftPhotoId
String
required
Left photo in comparison
rightPhotoId
String
required
Right photo in comparison
winnerSide
Side
required
LEFT or RIGHT
timeTaken
Float
required
Decision time in seconds

Relationships

user       User   @relation(fields: [userId], references: [id], onDelete: Cascade)
leftPhoto  Photo  @relation(name: "LeftPhoto", fields: [leftPhotoId], references: [id], onDelete: Cascade)
rightPhoto Photo  @relation(name: "RightPhoto", fields: [rightPhotoId], references: [id], onDelete: Cascade)

User

Voters in the jmash system.
id
String
default:"cuid()"
Unique user identifier
name
String
default:"Anonymous"
Display name
gender
Gender
default:"OTHER"
MALE, FEMALE, or OTHER
onboarded
Boolean
default:"false"
Whether user completed voting onboarding

Relationships

votes Vote[]

Shot

Represents dating profile “shots” for featured users.
person
Person
required
JOSH or JONATHAN
name
String
required
Display name
socialHandle
String
required
Social media handle
city
String
required
Current city
pickupLine
String
required
Opening line/bio
photoUrl
String
required
Profile photo URL

Utility Models

PhotoLabel

AI-generated labels for photos.
model PhotoLabel {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  photoId   String   @unique
  photo     Photo    @relation(fields: [photoId], references: [id], onDelete: Cascade)
  labels    Json
}

Ick

User reactions marking photos as “ick”.
model Ick {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  photoId   String
  photo     Photo    @relation(fields: [photoId], references: [id], onDelete: Cascade)
  userId    String
  
  @@unique([photoId, userId])
}

PhotoTransform

Crop/zoom metadata for photos.
model PhotoTransform {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  photoId   String   @unique
  photo     Photo    @relation(fields: [photoId], references: [id], onDelete: Cascade)
  x         Float
  y         Float
  scale     Float
}

BannedUser

Blocked users by ID or IP.
model BannedUser {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  userId    String   @default("")
  ipAddress String   @default("")
}

MutualLike

Test system for mutual matches.
model MutualLike {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  from      String
  to        String
  fromName  String
  toName    String
}

MutualTestUser

Test users for mutual matching.
model MutualTestUser {
  id          String   @id @default(cuid())
  createdAt   DateTime @default(now())
  phoneNumber String   @unique
}

Enums

TpoUserStatus

enum TpoUserStatus {
  ONBOARDING      // Currently completing intake
  PENDING_REVIEW  // Awaiting admin approval
  APPROVED        // Can be matched
  REJECTED        // Denied entry
  BANNED          // Blocked from platform
}

TpoOnboardingStep

enum TpoOnboardingStep {
  INTRO_SENT       // Welcome message sent
  AWAITING_ABOUT   // Collecting profile info
  AWAITING_PREFERENCES // Legacy step (now merged with ABOUT)
  AWAITING_CITY    // Legacy step (now part of ABOUT)
  AWAITING_PHOTOS  // Collecting 2+ photos
  AWAITING_ID      // Collecting driver's license
  COMPLETE         // Ready for review
}

TpoDateStatus

enum TpoDateStatus {
  ACTIVE  // Currently active
  ENDED   // Terminated by admin or system
}

TpoSchedulingPhase

enum TpoSchedulingPhase {
  PROPOSING_TO_A           // Bot proposing initial time to User A
  WAITING_FOR_A_REPLY      // Awaiting A's response to proposal
  PROPOSING_TO_B           // Forwarding A's time to User B
  WAITING_FOR_B_REPLY      // Awaiting B's response
  WAITING_FOR_A_ALTERNATIVE // A declined, waiting for counter-proposal
  WAITING_FOR_B_ALTERNATIVE // B declined, waiting for counter-proposal
  AGREED                   // Both confirmed, portal enabled
  FAILED                   // Could not reach agreement
  ESCALATED                // Requires manual intervention
}

Gender

enum Gender {
  MALE
  FEMALE
  OTHER
}

Side

enum Side {
  LEFT
  RIGHT
}

Person

enum Person {
  JOSH
  JONATHAN
}

Common Queries

Get user with all dates

const user = await db.tpoUser.findUnique({
  where: { phoneNumber: "+12125551234" },
  include: {
    datesAsA: true,
    datesAsB: true,
  },
});

Find active date for user

const activeDate = await db.tpoDate.findFirst({
  where: {
    status: "ACTIVE",
    OR: [
      { userA: { phoneNumber: senderPhone } },
      { userB: { phoneNumber: senderPhone } },
    ],
  },
  include: { userA: true, userB: true },
});

Get all pending review users

const pendingUsers = await db.tpoUser.findMany({
  where: { status: "PENDING_REVIEW" },
  orderBy: { createdAt: "desc" },
});

Get date messages in order

const messages = await db.tpoMessage.findMany({
  where: { dateId: "clx789..." },
  orderBy: { createdAt: "asc" },
});

Architecture Overview

System design and technology stack

API Endpoints

REST API reference for all endpoints

Build docs developers (and LLMs) love