Skip to content

Database Schema

Cloudflare D1 (SQLite) + Drizzle ORM (fue PostgreSQL hasta septiembre de 2026). 60 tables, 53 enums, 79 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

Enum Values
sessionStatusEnum hold, pending_confirmation, scheduled, in_progress, completed, pending_review, cancelled_by_student, cancelled_by_teacher, cancelled_system, no_show_student, no_show_teacher, rescheduled, expired
enrollmentStatusEnum pending_payment, active, completed, cancelled, expired, suspended
serviceStatusEnum draft, active, paused, archived
participantStatusEnum confirmed, cancelled, no_show, attended
Enum Values
deliveryModeEnum live, async, hybrid
groupTypeEnum individual, group, open
serviceStructureEnum single, package, course, subscription
serviceVisibilityEnum public, unlisted, private
pricingModelEnum credits, fixed_price, free
billingIntervalEnum weekly, monthly, quarterly, yearly
scheduleItemTypeEnum live_session, async_content, milestone
bookingModeEnum student_picks, fixed_schedule, teacher_assigns, none
Enum Values
creditActionEnum GRANT, RESERVE, CONSUME, RELEASE, ADJUST, EXPIRE
creditActorEnum system, teacher, student, stripe
Enum Values
oauthProviderEnum google, microsoft, zoom, apple
integrationTypeEnum calendar_read, calendar_write, drive, meet
calendarSyncStatusEnum synced, pending, failed
Enum Values
reviewSourceEnum pinteach, preply, italki, verbling, google_business, trustpilot, other
reviewStatusEnum pending, approved, hidden, flagged
reviewRequestStatusEnum pending, completed, expired, dismissed
reviewRequestChannelEnum in_app, email, link
Enum Values
auditActorTypeEnum teacher, student, system, stripe, admin
erasureRequestedByEnum student, teacher
erasureRequestStatusEnum pending, processing, completed, rejected
Enum Values
waitlistStatusEnum waiting, notified, enrolled, expired, cancelled
lifecycleEventTypeEnum first_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
Enum Values
discountTypeEnum percentage, fixed_amount
scheduledMessageStatusEnum pending, sent, cancelled
contentEntityTypeEnum template, resource, category, tag
Column Type Constraints
id uuid PK
email varchar(255) UNIQUE, NOT NULL
name varchar(255) NOT NULL
slug varchar(100) UNIQUE, NOT NULL
headline varchar(255)
bio text
avatarUrl text
timezone varchar(100) NOT NULL
stripeAccountId varchar(255)
bufferMinutes integer default 10
minNoticeHours integer default 6
defaultCurrency varchar(3) default ‘EUR’
completionMode enum default ‘AUTO_COMPLETE’
profileLayout jsonb
onboardingCompleted boolean default false
Column Type Constraints
id uuid PK
teacherId uuid FK→teachers CASCADE
email varchar(255) NOT NULL
name varchar(255) NOT NULL
phone varchar(50)
timezone varchar(100) NOT NULL
level varchar(50)
goals text
status enum default ‘trial’
driveFolderId varchar(255)
preferredStartTime varchar(5) HH:mm
preferredEndTime varchar(5) HH:mm
preferredDays jsonb (number[]) ISO weekdays 1-7
referralSource varchar(50) ‘direct’ / ‘directory’ / null
deletedAt timestamptz soft-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.

Column Type Constraints
id uuid PK
teacherId uuid FK→teachers
name varchar(255) NOT NULL
slug varchar(150) NOT NULL
deliveryMode enum NOT NULL
groupType enum NOT NULL
structure enum NOT NULL
sessionDurationMinutes integer
sessionCount integer
pricingModel enum NOT NULL
priceAmount integer cents
currency varchar(3) default ‘EUR’
isTrial boolean default false
status enum default ‘draft’
visibility enum default ‘public’
archivedAt timestamptz separate from deletedAt
deletedAt timestamptz soft-delete

UNIQUE on (teacherId, slug).

Column Type Constraints
id uuid PK
serviceId uuid FK→services
studentId uuid FK→students
teacherId uuid FK→teachers
status enum default ‘pending_payment’
sessionsTotal integer
sessionsScheduled integer default 0
sessionsCompleted integer default 0
sessionsCancelled integer default 0
sessionsForfeited integer default 0
validFrom timestamptz NOT NULL
validUntil timestamptz
discountCodeId uuid FK→discount_codes (nullable)
discountAmount integer cents 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.

Column Type Constraints
id uuid PK
enrollmentId uuid FK→enrollments
serviceId uuid FK→services
teacherId uuid FK→teachers
studentId uuid FK→students (nullable for groups)
startsAt / endsAt timestamptz NOT NULL
timezone varchar(100) NOT NULL
status enum default ‘scheduled’
isTrialSession boolean default false
rescheduleCount integer default 0

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

stripeCheckoutSessionId, stripePaymentIntentId, amountPaid, currency

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

Immutable append-only credit operations.

Column Type Constraints
id uuid PK
enrollmentId uuid FK→enrollments
sessionId uuid FK→class_sessions
studentId uuid FK→students
teacherId uuid FK→teachers
action enum NOT NULL
amount integer NOT NULL
idempotencyKey varchar(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.

Table Purpose
template_resources template ↔ resource (sortOrder)
session_resources session ↔ resource (sortOrder, source)
session_templates session ↔ template (sortOrder)
template_tags template ↔ tag (CASCADE)
template_categories template ↔ category (CASCADE)
resource_tags resource ↔ tag (CASCADE)
session_tags session ↔ tag (CASCADE)
service_tags service ↔ tag (CASCADE)
student_tags student ↔ 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
Table Purpose
discount_codes Teacher 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_uses Usage tracking. FKs: discountCodeId CASCADE, studentId CASCADE, enrollmentId SET NULL. UNIQUE on (discountCodeId, studentId, enrollmentId)
Table Purpose
scheduled_messages Teacher-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)
Table Purpose
content_analytics_cache Aggregation cache for content analytics

PinTeach no tiene tablas de leads ni de campañas. Las cinco que hubo (campaigns, leads, lead_score_events, campaign_enrollments, campaign_execution_logs) eran el resto del módulo borrado en 6c3547a y salieron del schema al retirar el stack Node (7-sep-2026): ninguna línea de aplicación las tocaba, solo el seed.

Campañas y leads se gestionan en SAASTRO Gen, igual que en PolizLoop. Lo que llega a PinTeach es lo ya ganado, por dos costuras:

  • gen_lead_events — filas que escribe POST /api/webhooks/gen, firmado con GEN_WEBHOOK_SECRET.
  • El embed del Marketing Hub de gen, que el profesor abre desde su panel.

Y en sentido contrario, PinTeach es proveedor de enriquecimiento de gen (POST /api/gen/enrich, firmado con ENRICHMENT_PROVIDER_SECRET) y le espeja su catálogo por Platform (PINTEACH_PLATFORM_SECRET). Las cinco costuras están declaradas en api.pinteach.app/_meta/connections.json.

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: Drizzle ORM sobre D1 (Worker, binding DB) o libsql (Node)
URL: binding DB en el Worker; file:.dev/pinteach.db en scripts de Node
Schema: packages/db/src/schema/index.ts
Client: packages/db/src/index.ts (exports `db` instance)