Skip to content

Database Schema

PostgreSQL 16.9 + Drizzle ORM. 60 tables, 53 enums, 78 migrations.

All timestamps are timestamptz (UTC). IDs are uuid with gen_random_uuid(). Multi-tenant by teacherId.

Every service is classified by 3 orthogonal dimensions:

  • deliveryMode: live | async | hybrid
  • groupType: individual | group | open
  • structure: single | package | course | subscription

Core model: Service → Enrollment → Session

EnumValues
sessionStatusEnumhold, pending_confirmation, scheduled, in_progress, completed, pending_review, cancelled_by_student, cancelled_by_teacher, cancelled_system, no_show_student, no_show_teacher, rescheduled, expired
enrollmentStatusEnumpending_payment, active, completed, cancelled, expired, suspended
serviceStatusEnumdraft, active, paused, archived
participantStatusEnumconfirmed, cancelled, no_show, attended
EnumValues
deliveryModeEnumlive, async, hybrid
groupTypeEnumindividual, group, open
serviceStructureEnumsingle, package, course, subscription
serviceVisibilityEnumpublic, unlisted, private
pricingModelEnumcredits, fixed_price, free
billingIntervalEnumweekly, monthly, quarterly, yearly
scheduleItemTypeEnumlive_session, async_content, milestone
bookingModeEnumstudent_picks, fixed_schedule, teacher_assigns, none
EnumValues
creditActionEnumGRANT, RESERVE, CONSUME, RELEASE, ADJUST, EXPIRE
creditActorEnumsystem, teacher, student, stripe
EnumValues
oauthProviderEnumgoogle, microsoft, zoom, apple
integrationTypeEnumcalendar_read, calendar_write, drive, meet
calendarSyncStatusEnumsynced, pending, failed
EnumValues
reviewSourceEnumpinteach, preply, italki, verbling, google_business, trustpilot, other
reviewStatusEnumpending, approved, hidden, flagged
reviewRequestStatusEnumpending, completed, expired, dismissed
reviewRequestChannelEnumin_app, email, link
EnumValues
auditActorTypeEnumteacher, student, system, stripe, admin
erasureRequestedByEnumstudent, teacher
erasureRequestStatusEnumpending, processing, completed, rejected
EnumValues
waitlistStatusEnumwaiting, notified, enrolled, expired, cancelled
lifecycleEventTypeEnumfirst_contact, trial_requested, trial_completed, first_purchase, session_completed, milestone_reached, streak_achieved, session_cancelled, no_show, gap_detected, enrollment_expiring, churned, reactivated, enrollment_renewed
EnumValues
discountTypeEnumpercentage, fixed_amount
scheduledMessageStatusEnumpending, sent, cancelled
contentEntityTypeEnumtemplate, resource, category, tag
ColumnTypeConstraints
iduuidPK
emailvarchar(255)UNIQUE, NOT NULL
namevarchar(255)NOT NULL
slugvarchar(100)UNIQUE, NOT NULL
headlinevarchar(255)
biotext
avatarUrltext
timezonevarchar(100)NOT NULL
stripeAccountIdvarchar(255)
bufferMinutesintegerdefault 10
minNoticeHoursintegerdefault 6
defaultCurrencyvarchar(3)default ‘EUR’
completionModeenumdefault ‘AUTO_COMPLETE’
profileLayoutjsonb
onboardingCompletedbooleandefault false
ColumnTypeConstraints
iduuidPK
teacherIduuidFK→teachers CASCADE
emailvarchar(255)NOT NULL
namevarchar(255)NOT NULL
phonevarchar(50)
timezonevarchar(100)NOT NULL
levelvarchar(50)
goalstext
statusenumdefault ‘trial’
driveFolderIdvarchar(255)
preferredStartTimevarchar(5)HH:mm
preferredEndTimevarchar(5)HH:mm
preferredDaysjsonb (number[])ISO weekdays 1-7
referralSourcevarchar(50)‘direct’ / ‘directory’ / null
deletedAttimestamptzsoft-delete

UNIQUE on (teacherId, email). Soft-delete via deletedAt. referralSource tracks attribution: 'direct' = teacher’s own link (?ref=direct), 'directory' = PinTeach lead, null = manually created.

3-dimension unified service catalog.

ColumnTypeConstraints
iduuidPK
teacherIduuidFK→teachers
namevarchar(255)NOT NULL
slugvarchar(150)NOT NULL
deliveryModeenumNOT NULL
groupTypeenumNOT NULL
structureenumNOT NULL
sessionDurationMinutesinteger
sessionCountinteger
pricingModelenumNOT NULL
priceAmountintegercents
currencyvarchar(3)default ‘EUR’
isTrialbooleandefault false
statusenumdefault ‘draft’
visibilityenumdefault ‘public’
archivedAttimestamptzseparate from deletedAt
deletedAttimestamptzsoft-delete

UNIQUE on (teacherId, slug).

ColumnTypeConstraints
iduuidPK
serviceIduuidFK→services
studentIduuidFK→students
teacherIduuidFK→teachers
statusenumdefault ‘pending_payment’
sessionsTotalinteger
sessionsScheduledintegerdefault 0
sessionsCompletedintegerdefault 0
sessionsCancelledintegerdefault 0
sessionsForfeitedintegerdefault 0
validFromtimestamptzNOT NULL
validUntiltimestamptz
discountCodeIduuidFK→discount_codes (nullable)
discountAmountintegercents saved (nullable)

Remaining credits: sessionsTotal - sessionsScheduled - sessionsCompleted - sessionsCancelled - sessionsForfeited

Optional discount: discountCodeId FK→discount_codes, discountAmount (cents saved).

Core session table with 3 satellite tables.

ColumnTypeConstraints
iduuidPK
enrollmentIduuidFK→enrollments
serviceIduuidFK→services
teacherIduuidFK→teachers
studentIduuidFK→students (nullable for groups)
startsAt / endsAttimestamptzNOT NULL
timezonevarchar(100)NOT NULL
statusenumdefault ‘scheduled’
isTrialSessionbooleandefault false
rescheduleCountintegerdefault 0

summary, teacherNotes, studentMood, rating, studentLevel, studentGoals, templateId

stripeCheckoutSessionId, stripePaymentIntentId, amountPaid, currency

googleCalendarEventId, googleMeetLink, meetingUrl, syncStatus (synced/pending/failed)

Immutable append-only credit operations.

ColumnTypeConstraints
iduuidPK
enrollmentIduuidFK→enrollments
sessionIduuidFK→class_sessions
studentIduuidFK→students
teacherIduuidFK→teachers
actionenumNOT NULL
amountintegerNOT NULL
idempotencyKeyvarchar(255)partial UNIQUE (WHERE NOT NULL)

Key format: {ACTION}:{enrollmentId}:{sessionId}:{studentId}

Provider-agnostic OAuth storage. UNIQUE on (teacherId, provider, providerAccountId).

Per-teacher integration config. UNIQUE on (teacherId, integrationType). JSONB config typed per integrationType.

Hierarchical folder system (self-referential parentId). Cascade soft-delete with deletionBatchId.

Reusable lesson blueprints. Soft-delete via deletedAt.

Resource library items. Provider: link/drive/youtube/vimeo. Kind: material/homework/reference.

TablePurpose
template_resourcestemplate ↔ resource (sortOrder)
session_resourcessession ↔ resource (sortOrder, source)
session_templatessession ↔ template (sortOrder)
template_tagstemplate ↔ tag (CASCADE)
template_categoriestemplate ↔ category (CASCADE)
resource_tagsresource ↔ tag (CASCADE)
session_tagssession ↔ tag (CASCADE)
service_tagsservice ↔ tag (CASCADE)
student_tagsstudent ↔ tag (CASCADE)
  • reviews — Student reviews + external imports (rating 1-5, source, status, isFeatured, teacherResponse)
  • review_requests — Review solicitation (channels: in_app/email/link, token-based)
  • review_settings — Per-teacher config (1:1)
  • availability_schedules — Named weekly sets
  • availability_rules — Recurring weekly time slots
  • availability_overrides — One-off time off or extra availability
  • schedule_activations — Date-range activations
  • legal_documents — Versioned legal docs (contract/policy/terms/privacy)
  • legal_document_acceptances — Student consent records
  • cancellation_policies — Per-service JSONB rules (one default per teacher)
  • waitlist_entries — Position-based queue with 48h offer window
TablePurpose
discount_codesTeacher promotional codes. Fields: code VARCHAR(50), discountType (percentage/fixed_amount), discountValue, currency, applicableServiceIds JSONB, maxUses, usesCount, maxUsesPerStudent, validFrom/Until, isActive, deletedAt. Soft-delete. UNIQUE on (teacherId, code) WHERE deleted_at IS NULL
discount_code_usesUsage tracking. FKs: discountCodeId CASCADE, studentId CASCADE, enrollmentId SET NULL. UNIQUE on (discountCodeId, studentId, enrollmentId)
TablePurpose
scheduled_messagesTeacher-scheduled contact reminders. Fields: teacherId, studentId, channel, body, scheduledFor TIMESTAMPTZ, status (pending/sent/cancelled), note, jobId (BullMQ), contactLogId FK→contact_log. PinTeach does NOT auto-send — creates contact_log + notification when due
  • audit_logs — Append-only audit trail (fire-and-forget)
  • data_erasure_requests — GDPR Art. 17 (30-day due date)
  • data_retention_settings — Per-teacher data retention config (1:1)
  • student_lifecycle_events — Event-sourced tracking (14 event types)
  • retention_settings — Per-teacher thresholds (1:1)
TablePurpose
content_analytics_cacheAggregation cache for content analytics

8 tables use deletedAt TIMESTAMPTZ (NULL = active, non-NULL = deleted):

students, services, lesson_templates, teacher_resources, material_folders, tags, class_categories, discount_codes

Use notDeleted(table) helper from @pinteach/db. For raw SQL: AND deleted_at IS NULL.

Exception: GDPR export intentionally includes soft-deleted records.

Driver: postgres.js via Drizzle ORM
URL: postgresql://pinteach:***@localhost:5433/pinteach
Schema: packages/db/src/schema/index.ts
Client: packages/db/src/index.ts (exports `db` instance)