Developer Docs

Wellness & Beauty

The Wellness vertical powers spas, salons, gyms, yoga studios, and beauty brands with three integrated systems: practitioner booking, tiered memberships, and client wellness profiles. Malet Owners configure these features via the Storefront Window layout system.

NOTE

These features live in the experiences subgraph โ€” the same cross-vertical Feature Plane that houses Entertainment & Experiences and Professional Services. The architecture is documented in docs/architecture/18-progressive-vertical-extraction.md.


Architecture: Two-Plane Design

The Wellness vertical uses the same Control + Feature plane separation as Entertainment and Professional:

  • Control Plane (malets subgraph): VerticalConfig feature flags decide which Wellness modules are enabled per Malet โ€” practitionerBooking, membershipEngine, productServiceBundles, clientProfiles.
  • Feature Plane (experiences subgraph): Self-contained MembershipModule and WellnessProfileModule. Each can be extracted to its own subgraph when a dedicated Wellness vertical Team forms.
  • Extensions (services subgraph): Practitioner-specific fields on existing Service and Booking entities.

Three new Storefront Window layout slot types are available: PRACTITIONER_GRID, MEMBERSHIP_PLANS, and BUNDLE_SHOWCASE.


Membership Engine

Create tiered membership plans offering session quotas, product discounts, and perks. Visitors subscribe and track usage in real-time.

Plan Tiers

Tier Typical Use
BRONZE Entry-level โ€” limited sessions, no product discount
SILVER Mid-tier โ€” moderate sessions, small product discount
GOLD Premium โ€” generous quotas, significant discounts
PLATINUM VIP โ€” unlimited sessions, maximum perks

TIP

The tier enum is named WellnessPlanTier (not MembershipTier) to avoid GraphQL Federation composition conflicts with the existing MembershipTier in the nodes subgraph.

Subscription Lifecycle

ACTIVE โ†’ PAUSED โ†’ ACTIVE
       โ†’ CANCELLED
       โ†’ EXPIRED

Subscriptions start as ACTIVE when a Visitor subscribes. They can be PAUSED for temporary holds (e.g., vacation), CANCELLED by the Visitor, or automatically EXPIRED when the billing period lapses without renewal.

Mutations

# Create a membership plan (Malet Owner)
mutation {
	createMembershipPlan(
		maletId: "m_serenity_spa"
		name: "Gold Wellness Membership"
		tier: GOLD
		sessionsPerMonth: 8
		monthlyPrice: 14900 # $149.00 (cents)
		productDiscountPercent: 20
	) {
		id
		name
		tier
		sessionsPerMonth
		productDiscountPercent
		isActive
	}
}

# Deactivate a plan (soft-delete โ€” no new subscriptions)
mutation {
	deactivateMembershipPlan(planId: "plan_1") {
		id
		isActive
	}
}

# Subscribe to a plan (Visitor)
mutation {
	subscribeMembership(maletId: "m_serenity_spa", planId: "plan_1") {
		id
		status
		sessionsRemaining
		currentPeriodStart
		currentPeriodEnd
	}
}

# Record a session usage (after booking)
mutation {
	recordMembershipUsage(subscriptionId: "sub_1") {
		id
		sessionsUsed
		sessionsRemaining
	}
}

# Pause membership (Visitor)
mutation {
	pauseMembership(subscriptionId: "sub_1") {
		id
		status
	}
}

# Resume membership
mutation {
	resumeMembership(subscriptionId: "sub_1") {
		id
		status
		sessionsRemaining
	}
}

# Cancel membership
mutation {
	cancelMembership(subscriptionId: "sub_1") {
		id
		status
	}
}

# Renew period โ€” reset usage counters (Malet Owner or cron)
mutation {
	renewMembershipPeriod(subscriptionId: "sub_1") {
		id
		sessionsUsed
		sessionsRemaining
		currentPeriodStart
		currentPeriodEnd
	}
}

Queries

# All active plans for a Malet (public โ€” storefront display)
query {
	membershipPlans(maletId: "m_serenity_spa") {
		id
		name
		tier
		sessionsPerMonth
		productDiscountPercent
		monthlyPrice
		features
	}
}

# Visitor checks their active membership
query {
	myMembership(maletId: "m_serenity_spa") {
		id
		planId
		status
		sessionsUsed
		sessionsRemaining
		currentPeriodEnd
	}
}

# Malet Owner views all subscribers
query {
	maletMembershipSubscriptions(maletId: "m_serenity_spa", status: ACTIVE) {
		id
		memberId
		planId
		sessionsUsed
		sessionsRemaining
	}
}

Session Usage Tracking

Usage is tracked atomically using MongoDB $inc operators:

  • sessionsUsed increments by 1
  • sessionsRemaining decrements by 1 (unless unlimited)
  • If sessionsRemaining reaches 0, further bookings are blocked with a BadRequestException
  • Plans with sessionsPerMonth: 0 grant unlimited sessions (sessionsRemaining is set to -1)

Client Wellness Profiles

Track client health data, preferences, and practitioner treatment notes across sessions. Each profile is scoped to a single Malet โ€” a Visitor visiting multiple Wellness Malets has separate profiles per Malet.

Skin Types

Type Description
OILY Excess sebum production
DRY Lacks moisture, may flake
COMBINATION Oily T-zone, dry cheeks
SENSITIVE Reacts to products, redness
NORMAL Balanced, minimal concerns

Mutations

# Complete intake form (Visitor) โ€” upserts on (maletId, clientId)
mutation {
	upsertWellnessProfile(
		maletId: "m_serenity_spa"
		allergies: ["latex", "eucalyptus"]
		skinType: SENSITIVE
		bodyFocus: ["lower back", "shoulders"]
		preferences: "Prefers firm pressure. Avoid scented oils."
	) {
		id
		allergies
		skinType
		bodyFocus
		intakeFormCompleted
	}
}

# Add treatment note (Practitioner)
mutation {
	addTreatmentNote(
		profileId: "wp_1"
		content: "Used hypoallergenic oil. Client reported reduced tension in lower back after 60min deep tissue."
		isPrivate: false
		bookingId: "bk_42"
	) {
		id
		treatmentNotes {
			practitionerId
			content
			isPrivate
			createdAt
		}
	}
}

# Add private note (only practitioner + Malet Owner can view)
mutation {
	addTreatmentNote(
		profileId: "wp_1"
		content: "Observe possible stress-related tension pattern. Recommend weekly sessions."
		isPrivate: true
	) {
		id
		treatmentNotes {
			content
			isPrivate
		}
	}
}

Queries

# Visitor checks their own profile
query {
	myWellnessProfile(maletId: "m_serenity_spa") {
		id
		allergies
		skinType
		bodyFocus
		preferences
		intakeFormCompleted
		treatmentNotes {
			content
			createdAt
		}
	}
}

# Malet Owner views all client profiles
query {
	maletWellnessProfiles(maletId: "m_serenity_spa") {
		id
		clientId
		allergies
		skinType
		intakeFormCompleted
		treatmentNotes {
			content
			practitionerId
			isPrivate
		}
	}
}

# Single profile with privacy filtering
query {
	wellnessProfile(id: "wp_1", isOwnerOrPractitioner: true) {
		id
		allergies
		treatmentNotes {
			content
			isPrivate
			practitionerId
		}
	}
}

Treatment Note Privacy

Each TreatmentNote has an isPrivate flag:

  • Public notes (isPrivate: false): Visible to the Visitor, practitioner, and Malet Owner โ€” useful for aftercare recommendations
  • Private notes (isPrivate: true): Visible only to the authoring practitioner and the Malet Owner โ€” useful for clinical observations the client shouldn't see

When a Visitor queries their own profile, private notes are automatically filtered out.


Practitioner Booking

Extended booking fields for wellness services on the existing services subgraph:

Service Fields

Field Type Description
practitionerIds [String] Organization member IDs who can perform this service
requiresIntakeForm Boolean Whether a wellness intake form is needed before booking

Booking Fields

Field Type Description
practitionerId String The specific practitioner assigned to this booking
treatmentNotes String Post-session practitioner notes (linked back to WellnessProfile)

Example: Create a Practitioner-Linked Service

mutation {
	createOneService(
		input: {
			service: {
				name: "60min Deep Tissue Massage"
				description: "Therapeutic deep tissue work targeting chronic tension"
				price: 9500 # $95.00 (cents)
				maletId: "m_serenity_spa"
				practitionerIds: ["staff_a", "staff_b", "staff_c"]
				requiresIntakeForm: true
			}
		}
	) {
		id
		name
		practitionerIds
		requiresIntakeForm
	}
}

Booking Flow

  1. Visitor selects service โ†’ sees available practitioners from practitionerIds
  2. Visitor picks a practitioner โ†’ availability checked against their schedule
  3. If requiresIntakeForm is true, the Visitor must complete their WellnessProfile first
  4. Booking is created with practitionerId set to the chosen practitioner
  5. After the session, the practitioner adds treatmentNotes to both the Booking and the client's WellnessProfile

TIP

Practitioners are Organization members โ€” their profiles, specialties, and availability are managed via the existing org โ†’ team โ†’ member hierarchy. No separate "Staff" entity is needed.


Workroom Steps

The Wellness vertical ships with 4 default Workroom steps for treatment lifecycle management:

Step Type Assignee Description
Health Intake Form FORM Client Health questionnaire: allergies, skin type, body focus, and preferences
Practitioner Assignment APPROVAL Malet Owner Match the right practitioner based on service and client needs
Treatment Notes FORM Malet Owner Record treatment notes, observations, and aftercare recommendations
Product Recommendations FORM Malet Owner Suggest follow-up products based on the treatment performed

These steps are automatically configured on new Wellness Malets via the vertical seed (teal/emerald branding).


Gamified Loyalty

Wellness Malets automatically gain access to the Gamified Loyalty system from the Entertainment vertical. Visitors earn points and badges for:

  • Completing bookings
  • Purchasing recommended products
  • Maintaining active memberships
  • Referring friends

Tier promotion (Bronze โ†’ Silver โ†’ Gold โ†’ VIP) is handled automatically by the existing loyalty engine. Configure loyalty via the gamifiedLoyalty feature flag.


Deferred Features

The following capabilities are planned for future phases using in-house, open-source solutions โ€” no third-party SaaS dependencies:

Feature Phase Approach
Product-Service Bundles Phase 2 New Bundle entity in products subgraph linking Product IDs + Service IDs with bundle pricing
Stripe Auto-Renewal Phase 2 Stripe Billing API on MembershipSubscription.stripeSubscriptionId
Subscription Boxes Phase 2 Monthly curated product shipments via scheduled Murchase creation
Post-Treatment Upsell Phase 2 relatedProducts: string[] on Service entity for product recommendation cards

TIP

The productServiceBundles feature flag is already present in the Control Plane โ€” it just needs a corresponding BundleModule in the Feature Plane when Phase 2 begins.


Frontend Implementation

Three Svelte 5 storefront widgets render the Wellness vertical in the Visitor-facing storefront:

Components

Widget File Description
WellnessProfile src/lib/components/wellness/WellnessProfile.svelte Displays client profile โ€” skin type, allergies, body focus areas, preferences, intake form status, and treatment notes timeline. Auth-gated.
MembershipPlans src/lib/components/wellness/MembershipPlans.svelte Tiered plan cards (Bronze/Silver/Gold/Platinum) with pricing, sessions/mo, product discounts, and features list. Active subscription card with session usage bar, billing period countdown, and low-session warnings.
PractitionerBooking src/lib/components/wellness/PractitionerBooking.svelte Service category cards (Massage, Facial, Hair, Wellness) linking to the Malet booking page.

File Map

File Purpose
src/lib/queries/wellness.ts GraphQL queries (GET_WELLNESS_PROFILE, GET_MEMBERSHIP_PLANS, GET_MY_MEMBERSHIP) + TypeScript interfaces
src/lib/utils/wellnessUtils.ts 13 utility functions โ€” skin type labels/icons, plan tier badges/colors, session tracking, subscription status, pricing formatters
src/lib/components/storefront/WidgetRegistry.svelte Widget registration: WELLNESS_PROFILE, MEMBERSHIP_PLANS, PRACTITIONER_BOOKING
tests/wellnessUtils.test.ts 21 unit tests covering all utility functions

Layout Slot Types

Slot Type Widget Auth Required
WELLNESS_PROFILE WellnessProfile โœ… Yes
MEMBERSHIP_PLANS MembershipPlans Public plans, auth for subscription
PRACTITIONER_BOOKING PractitionerBooking No

NOTE

For Visitor-facing documentation on wellness profiles, membership plans, and session tracking, see the Support Manual: Wellness Memberships & Profiles.