setup-guides

Family Wall Plus — launch runbook

How to turn on the Family Wall Plus freemium subscription. The gate + Stripe checkout + cost-point gates all shipped dark (PR #1411); this runbook is the three ops steps that flip it live. Mirrors the ABOUT_THAT_PAID_LAUNCH.md / EMBERKILN_PAID_LAUNCH.md flag-gated pattern.

The model

  • Free tier = one genuinely-useful kitchen wall — the acquisition wedge ("no hardware to buy" vs Skylight/Hearth). Plus (~$4.99/mo · $39/yr) unlocks the cost-bearing + power features: Ask JQ + voice actions, cloned-voice wall messages, intercom, extra (bedroom) walls, sleep stories, voice alarms, kids' chat.
  • Dark by default: the whole gate is behind FAMILY_WALL_PLUS_ENFORCED. While unset/false, requireFamilyWallPlus() is a no-op — nothing is paywalled and the wall behaves exactly as it does today. Flip to true only after the Stripe prices exist and early users are grandfathered.
  • Gates degrade, they don't hard-break. Extra walls / Ask JQ / intercom / sleep stories / alarms / kids'-chat-enable return 402 with an upgrade CTA. Cloned-voice wall messages degrade gracefully — the message still shows and reads aloud in free browser TTS; only the paid ElevenLabs clone render is skipped (plus_required warning, no 402, no spend).

One-time setup

1. Create two recurring Stripe prices

In the Stripe dashboard, one product ("Family Wall Plus") with two recurring prices:

  • Monthly — $4.99/month
  • Annual — $39/year

2. Set the price env vars (Railway backend)

STRIPE_PRICE_FAMILY_WALL_PLUS=price_...
STRIPE_PRICE_FAMILY_WALL_PLUS_ANNUAL=price_...

Until these are set:

  • POST /api/payment/family-wall-plus/checkout returns 503 (same as every other tier).
  • GET /api/payment/family-wall-plus/config returns { available: false }, so the settings Upgrade button stays hidden — no user-visible breakage.

The webhook already activates purchases via priceToPlanKey (maps both prices → family_wall_plus); no new code needed. STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET are already configured (shared with the other tiers).

3. Grandfather early users (do this BEFORE flipping the flag)

The free tier during the dark period let people use everything. Be generous — comp every pre-launch active family so the flag flip never yanks a feature someone was already relying on. No Stripe charge needed — insert active subscription rows directly.

Grandfather a single user:

insert into subscriptions (user_id, plan_key, status, current_period_end, cancel_at_period_end)
values ('<user-uuid>', 'family_wall_plus', 'active', now() + interval '10 years', false);

Grandfather everyone who has built a wall (the natural "already using it" cohort):

insert into subscriptions (user_id, plan_key, status, current_period_end, cancel_at_period_end)
select distinct d.parent_user_id, 'family_wall_plus', 'active', now() + interval '10 years', false
from family_wall_displays d
where not exists (
  select 1 from subscriptions s
  where s.user_id = d.parent_user_id and s.plan_key = 'family_wall_plus'
);

current_period_end far in the future + cancel_at_period_end = false = a comp that doesn't lapse. (Use a shorter interval + cancel_at_period_end = true for a time-boxed trial instead.) These rows have no stripe_subscription_id, which is fine — they're never touched by the webhook and isFamilyWallPlus() only checks status + period.

Run this against prod (Supabase MCP ≠ prod — see CLAUDE.md). Verify the row count matches the distinct wall-owner count before flipping the flag.

4. Flip the master switch

FAMILY_WALL_PLUS_ENFORCED=true

Redeploy the backend. Enforcement is now live: the settings page shows the upsell banner

  • ✨Plus badges for non-Plus users, the gated features return 402, and cloned-voice wall messages fall back to browser TTS for free users.

How activation works (no new code)

Checkout (POST /api/payment/family-wall-plus/checkout { interval: 'monthly'|'annual' }) → Stripe Checkout → the existing POST /api/payment/webhook upserts a subscriptions row with plan_key='family_wall_plus' on checkout.session.completed (mapped by priceToPlanKey). isFamilyWallPlus(userId) reads that row (active, keeps access through a cancelled-but-not-ended period). Cancel/update ride the existing customer.subscription.* webhook handlers.

Cancellation

Self-serve cancel is plan-aware (PR #1411): the CancellationModal should be mounted with planKey="family_wall_plus" on any Family Wall Plus manage screen so it acts on the right subscription for users who also hold another tier (e.g. Graphene+). The /api/payment/cancellation/* routes accept plan_key; omitting it when a user has >1 active sub returns 409 {plan_keys}. Stripe customer portal (/api/payment/customer-portal) also works.

Rollback

Set FAMILY_WALL_PLUS_ENFORCED=false (or unset) and redeploy — instantly un-paywalls everything, no data change. Grandfather rows are harmless while off.

Code map

  • Gate + flag + subscriber check: services/family-wall-plus.ts (requireFamilyWallPlus, isFamilyWallPlus, familyPlusEnforced).
  • Checkout + config + plan-key map: routes/payment.ts (/family-wall-plus/checkout, /family-wall-plus/config, priceToPlanKey).
  • Cost-point gates: services/family-wall.ts (applyMessageAudio — graceful clone-voice skip) + services/family-kid-chat.ts (updateKidChat — 402 on enable).
  • Status endpoint: GET /api/family/wall/plus-status (drives the upsell banner + badges).
  • Settings CTA + thank-you: dashboard/family/wall/page.tsx (startPlusCheckout, ?plus=1).
  • Plan-aware cancel: services/subscription-cancellation.ts.
  • Design + free/paid split rationale: docs/product/FAMILY_WALL_MONETIZATION.md; pick-up point: docs/product/FAMILY_WALL_HANDOFF.md.
FAMILY WALL PLUS LAUNCH — Docs | HiveJournal