Skip to content

Product Overview

Every offering is modeled as a Service classified by 3 orthogonal dimensions:

DimensionOptionsExample
deliveryModelive, async, hybridVideo call vs self-paced
groupTypeindividual, group, open1:1 vs capped vs unlimited
structuresingle, package, course, subscriptionOne session vs N credits vs ordered sequence vs recurring

Core flow: Service → Enrollment → Session

sessionsTotal = total sessions purchased
sessionsScheduled = booked/reserved (not yet completed)
sessionsCompleted = completed
sessionsCancelled = cancelled (credit returned)
sessionsForfeited = forfeited (no-show, late cancel — credit consumed)
remaining = sessionsTotal - sessionsScheduled - sessionsCompleted
- sessionsCancelled - sessionsForfeited
Student visits /:teacher-slug
→ Auto-detect timezone
→ See service catalog
→ Select trial service → See available slots
→ Fill form (name, email, phone?, level, goals, notes)
IF trial is FREE:
→ Create Session(pending_confirmation)
→ Email teacher: "New trial request"
→ Teacher confirms or rejects
→ IF confirmed: Session(scheduled), create calendar event, email student
IF trial is PAID:
→ Create Session(hold) + holdExpiresAt = now + 15min
→ Create Stripe Checkout Session (on teacher's connected account)
→ Webhook: checkout.session.completed → Session(scheduled)

Flow B: Service Enrollment (Buy then book)

Section titled “Flow B: Service Enrollment (Buy then book)”
Student visits /:teacher-slug → See services tab
→ Select service → See pricing + availability preview
IF first purchase (no account):
→ Magic link auth → Accept legal documents
→ Redirect to Stripe Checkout
ELSE:
→ Check if legal docs need re-acceptance
→ Redirect to Stripe Checkout
Webhook: checkout.session.completed
→ Enrollment(active), credit GRANT
→ Student books sessions from portal

Flow C: Session Booking (Authenticated Student)

Section titled “Flow C: Session Booking (Authenticated Student)”
Student portal → /student/book
→ Select enrollment (package with remaining credits)
→ Select eligible service
→ See available slots (preferred hours highlighted)
→ Confirm booking
Backend:
→ Check remaining credits >= 1
→ Check policy engine (max active bookings, min notice)
→ Check slot available (race condition protection)
→ CreditLedger: RESERVE -1
→ Create Session(scheduled)
→ Create Google Calendar event
→ Trigger Drive auto-copy for linked resources
BullMQ job at: session.endsAt + gracePeriod
IF teacher.completionMode == AUTO_COMPLETE:
→ Session(completed), CreditLedger: CONSUME
IF teacher.completionMode == PENDING_REVIEW:
→ Session(pending_review), notify teacher
Teacher can override: COMPLETED / NO_SHOW_STUDENT / NO_SHOW_TEACHER / CANCELED
Student or teacher requests cancellation
PolicyEngine.evaluateCancellation():
→ Resolves policy: service-specific → teacher default → legal docs → allow all
→ Evaluates rules in order (first match wins)
→ Actions: full_refund (RELEASE), partial_penalty (CONSUME),
forfeit (CONSUME), block (not allowed), allow_free (RELEASE)
→ Session status updated
→ Credits adjusted via SessionCreditHandler
→ Calendar event cancelled
→ Waitlist: if slot freed → auto-notify next person
Service at capacity (active enrollments >= maxParticipants)
Student joins waitlist:
→ WaitlistEntry(waiting, position=N)
→ Partial UNIQUE ensures one active entry per student per service
When slot opens (enrollment cancelled / session cancelled):
→ WaitlistService.onSlotFreed() checks capacity
→ Auto-notifies next person: WaitlistEntry(notified), email sent
→ 48-hour offer window (expiresAt)
If student enrolls: WaitlistEntry(enrolled)
If offer expires: WaitlistEntry(expired), notify next person
→ BullMQ hourly cron job expires stale entries
Teacher can also manually notify specific entries.
  1. availability_schedules + availability_rules — Named weekly schedules with date-range activations
  2. availability_overrides — One-off time off or extra availability
  3. services.sessionDurationMinutes — Slot duration
  4. teacher.bufferMinutes — Gap between sessions
  5. teacher.minNoticeHours — Minimum advance booking
  6. Google Calendar Free/Busy — Busy blocks from selected calendars
  7. Existing sessions in DB (scheduled/hold statuses)
  8. viewer_timezone — Student’s IANA timezone for display
  9. preferredHours — Student’s preferred time range + days (optional)
1. Resolve which schedule applies for each date via activations
2. Load rules for active schedules + overrides
3. Generate candidate slots (teacher TZ → UTC)
4. Filter: remove slots where start < now + minNoticeHours
5. Fetch Google Calendar Free/Busy
6. Fetch existing DB sessions
7. Merge busy blocks (Google + DB + buffer)
8. Remove overlapping slots
9. Convert to viewer timezone
10. Mark isPreferred based on student's preferred hours
11. Return: [{ startUtc, endUtc, startLocal, endLocal, isPreferred }]
  • PinTeach: Students submit reviews via in-app prompts after sessions
  • External: Teachers import from Preply, Italki, Verbling, Google Business, Trustpilot

5 tabs: Overview (KPIs + recent), All (searchable, filterable), Requests (send solicitations), Import (external reviews), Settings (auto-request, auto-approve, display).

Embeddable review widgets with 4 layouts (grid/carousel/list/wall), configurable via /teacher/widgets.

  • material_folders: Hierarchical folder system (self-referential parentId)
  • lesson_templates: Reusable lesson blueprints with meet link, default content
  • teacher_resources: Resources (links, Drive files, YouTube, Vimeo) with provider + kind
  • Drive file picker for adding resources
  • Auto-copy: when Drive resources are linked to a session, DriveCopyService queues BullMQ jobs to copy files into student folders
  • Tracked in drive_file_copies table (pending → copying → copied → failed)
DecisionChoiceRationale
Service model3-dimension unifiedReplaces old split of 5+ entities
Payment modelBuy service first, book laterSimpler, no HOLD on purchase
CreditsReserve on book, consume on completeFlexibility for changes/no-shows
StripeConnect (teacher collects)We don’t handle funds
Student authMagic linkZero friction
Teacher authGoogle OAuthAlready uses Google
Auto-completeDefault ON, grace 30minLess clicks, with manual override
DB datesUTC + IANA timezoneZero DST errors
Google CalendarIntegration, not dependencyCore works without Google
Cancellation policiesPer-service JSONB rulesFlexible, template-based
ResourcesNormalized tables + junctionsNOT JSONB arrays
Soft-delete7 tables with deletedAtReversible, audit-friendly
Audit logAppend-only, fire-and-forgetNever blocks primary operations
WaitlistAuto-notify on slot freedReduces teacher manual work
Preferred hoursVisual only, no filteringStudents see all slots, preferred highlighted