Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesAnalyticsAnalytics — Data Model

Analytics — Data Model

Legacy analytics and TKN-24 basic projections are read-only over operational tables; TKN-24 adds no migration.

Tables read

TableUsed in
subscriptionsMRR, plan distribution, outstanding debt, at-risk members.
payment_transactionsCollected MTD, revenue trend.
membershipsMembers summary, growth, activation, at-risk, coach overview.
usersAt-risk sample, members activation (Clerk linkage).
invitationsMembers activation (invited bucket).
class_sessionsPopular classes, class utilization.
class_typesSame.
bookingsSame.
plansPlan distribution.
workouts, workout_assignments, workout_results, personal_recordsWorkouts summary.
organizationsTimezone normalization where required (currency, locale).
leads, organization_leadsOrg-scoped recorded creation history; current unarchived work queue.
lead_status_events, lead_activity_eventsv1 intake/status/contact evidence, not legacy reconstructed history.
conversations, conversation_messagesActor-backed one-to-one sends without acceptance by cutoff; present-tense unread inbound for current work.

Existing summary computed fields

MRR

For every active non-deleted subscription:

mrr_contribution = subscription.price_in_cents / INTERVAL_MONTHS[subscription.interval]

INTERVAL_MONTHS:

IntervalMonths
weekly0.2308
monthly1
quarterly3
yearly12

The constant lives in analytics.service.ts.

Member activation buckets

  • imported — users.clerk_id IS NULL AND no pending invitation.
  • invited — invitations.status = 'pending'.
  • activated — users.clerk_id IS NOT NULL.

activatedActive is the intersection of active membership ∧ Clerk-linked user, excluding import placeholders. This legacy discriminator is not the new attendance or cohort denominator.

Class utilization

bookings count / class_session.capacity averaged across sessions in the window. Sessions with capacity = null are excluded.

Trend buckets

Date-bucketed by date_trunc('month', x) for monthly trends. Labels are localized at render time on the client.

Basic report projections

Shared request/response contracts live in libs/shared/src/lib/schemas/analytics.schema.ts. Date windows use inclusive org-local dates, at most 366 days, converted to DST-correct midnight bounds and capped at now for observed data. Responses include timezone and generation time; cohort horizons are integers 1–12, default six.

ProjectionSource and definition
Studio active/new membersCurrent memberships.role='member'; active count requires active/non-deleted. New count uses MTD joined_at, excludes deleted rows and invitation shells.
Studio weekly classesclass_sessions → class_types → programs for org ownership; published/non-deleted sessions across the full local Sunday–Saturday week.
Attendance/rankingEnded published/non-deleted sessions in the start-date window. Non-deleted bookings with confirmed, attended, no_show form booked; attended/booked is the rate. Membership joins enforce org ownership, not current status/role, preserving historical visits of ended/deleted members and current staff.
CapacitySum each finite positive session capacity once, including empty sessions; capacityBooked / capacity × 100 is weighted. Unlimited/null and non-positive capacity are excluded from that ratio; unlimited sessions remain separately counted. Overbooking is not capped to 100%; undefined rates are null. Current externalSessionOccupancy is not history.
Revenue / Studio MTD receiptspayment_transactions under canonical receipts.sql.ts: completed/refunded actual receipts, negative refunds, no metadata.tokenOnly, settlement at COALESCE(completed_at, updated_at). Separate collected/refunded/net cents and actual methods per currency; no FX merging or MRR inference. Partial selected/current months have null MoM delta while exposing the prior full month’s net under the same method filter.
Member cohortsJoin-month memberships.joined_at denominator for trains member rows excluding invitation shells. Retained means not ended/deleted by observation end; deletion and end_date establish lifecycle end (no updated_at fallback). Shells nobody joined (joined_at null), cancelled rows with no end_date and departures that predate their import (source='import' AND end_date <= created_at) are left out. Empty/future cells are null and current observations partial.

Cohorts declare basis='membership_lifecycle': no suspension/reactivation or paid-subscription history is reconstructed. They are lifecycle estimates, not paid retention.

Indexes assumed

  • subscriptions(membership_id, status, deleted_at)
  • payment_transactions(organization_id, created_at, status)
  • memberships(organization_id, status, role)
  • class_sessions(organization_id, starts_at)
  • bookings(class_session_id, status)
  • workout_assignments(organization_id, scheduled_for)

Drops to these will produce visible latency regressions on the analytics tab.

CSV outputs

The shared GET /organizations/:orgId/analytics/export?tab=<tab> route retains these eight legacy CSV shapes and formatting:

TabRows
revenue-summary1 row (the summary object flattened; packsSold as packsSoldCount, packsSoldAmountInCents).
revenue-trendN month rows { bucket, label, totalInCents }.
plan-distributionN plan rows { planId, planName, interval, activeSubscriptions, monthlyRevenueInCents }.
members-summary1 row.
members-growthN month rows.
popular-classesN rows.
class-utilizationbyClassType[] rows.
workouts-summary1 row.

New gated report tabs use the same filtered projections as the web workspace:

TabProjection
attendance-reportAttendance class rows.
class-rankingRanked class attendance rows.
class-capacityClass capacity rows.
revenue-reportCurrency totals.
revenue-methodsCurrency/method rows.
revenue-monthlyCurrency/month rows with comparison metadata.
member-cohortsCohort observation cells.

analyticsReportCsv supplies stable headers even when empty, UTF-8 BOM, CRLF, quote/comma/newline escaping, and formula-safe text. Numeric negative receipts stay numeric; null cells are empty. Legacy CSV formatting is unchanged.