product

Family Wall — handoff / start-here

Updated 2026-07-18. The pick-up point for the Family Wall. Read this first, then the INDEX sections + the linked design docs. The Family Wall went from a read-only display to a deep, monetizable family hub across ~18 PRs (#1392–#1409) in one session.

What it is now

A public, token-scoped full-screen display you run on any spare screen (old iPad, wall TV, laptop) — /family/wall/<token>. Managed at /dashboard/family/wall. No hardware to buy; that's the whole positioning (see the compare cluster). It shows routines, calendar, meals, shopping, pets, who's-home, notes, weather, activities — and is now interactive: tap-to-check-off, add-from-wall, JQ voice assistant + actions, intercom, voice alarm clocks, and sleep stories.

Trust model (unchanged, important): the wall page is unauthenticated — the token IS the capability. Every write-from-wall route re-scopes the target to the wall's own parent server-side; ids are only exposed on the projection when the relevant opt-in is on (leak-guard, asserted by the golden test in family-wall-core.test.ts).

Shipped this session (feature → migration → key code)

FeatureMigrationBackendFrontend
On-wall editing (shopping/dinner/notes) + backgrounds538 (allow_editing, background_image_url)shopping/meal/note-AddFromWall, listMyWallBackgroundssettings picker, wall ghost-outline quick-add
JQ voice actions + device identity + reminders539 (location_kind, owner_member_id, family_reminders)family-wall-actions.ts (pure detect), actOnWall, family-reminders.tswall JQ orb ask-who flow
Intercom (tap-to-talk) + quiet hours540 (allow_intercom, quiet_start/end, family_intercom_messages)family-intercom.ts + -core.ts/dashboard/family/intercom, wall inbox banner
Voice alarm clocks (Plus)541 (family_alarms)family-alarms.ts/dashboard/family/alarms, wall wake banner
Sleep stories on the wall (Plus)542 (show_sleep_stories)getSleepStoriesForWall (reuses sleep-story-gen.ts)wall 🌙 shelf + player + sleep timer
"Add me" self-membernone (reuses linked_user_id)POST /members/self, is_selffamily page "Add me" + You badge
Member inline-edit + card polishnonePATCH /members/:id (existing)family page ✏️ edit, CARD_ACTION
"Invite to the wall" join flownonereuses member create + JQ-bridge family inviteInviteToFamilyModal
New soundscapes (study/dinner/focus)none (enum)AMBIENT_TRACKSwall startMusic() generative engine
Settings redesign + curated backgroundsnonesectioned settings, /public/wall-backgrounds/* presets
Sample-screen generator + gallery + OGnonelib/family-wall-samples.ts, SampleWall, /family-wall/examples, opengraph-image.tsx
Compare / SEO clusternonelib/family-wall-compare.ts, /compare/family-wall[/<slug>]
Plus entitlement gatenone (reuses subscriptions)family-wall-plus.tssettings upsell banner + ✨Plus badges

Migrations 538–542 are all applied to prod. (MCP Supabase ≠ prod — the user applies file migrations to prod manually; every wall PR needed the migration applied before the backend deploy, or the new SELECT columns 500 the whole wall. This bit us repeatedly — always surface the SQL and gate the merge on it.)

Monetization — the gate is BUILT but OFF

family-wall-plus.ts reuses the shared subscriptions table (plan_key='family_wall_plus'). requireFamilyWallPlus() is a no-op until FAMILY_WALL_PLUS_ENFORCED=true, so nothing is paywalled today. Enforced at: createWallDisplay (2nd+ wall), askWall/actOnWall (Ask JQ), sendIntercom, updateWallDisplay (show_sleep_stories), alarm create/enable → HTTP 402. GET /api/family/wall/plus-status drives the settings upsell.

Stripe checkout — ✅ BUILT (mirrors Graphene+/write-cafe-pro). POST /api/payment/family-wall-plus/checkout ({ interval: 'monthly'|'annual' }) → Stripe Checkout; the webhook's priceToPlanKey maps STRIPE_PRICE_FAMILY_WALL_PLUS / _ANNUALfamily_wall_plus and upserts the subscriptions row on checkout.session.completed (same path every tier uses — cancel/update ride the existing customer.subscription.* handlers). GET /api/payment/family-wall-plus/config gates the CTA so it hides until the price envs exist. Settings CTA + ?plus=1 thank-you in wall/page.tsx.

Cost-point gates — ✅ BUILT. Cloned-voice wall messages (applyMessageAudiosayInUserVoice) now skip the paid clone render for non-Plus owners gracefully — the message still shows and reads aloud in free browser TTS, with a plus_required voice_warning, no 402, no ElevenLabs spend. Kids'-chat enablement (updateKidChat when enabled:true) is gated → 402 {upgrade, feature} (surfaced on the kids page error banner). Both no-op until enforcement is on.

To actually launch Plus — only ops steps remain (full runbook + grandfather SQL: FAMILY_WALL_PLUS_LAUNCH.md):

  1. Create the two Stripe prices (~$4.99/mo · $39/yr) and set STRIPE_PRICE_FAMILY_WALL_PLUS
    • STRIPE_PRICE_FAMILY_WALL_PLUS_ANNUAL on the Railway backend. (Checkout endpoints 503 until then; the config probe keeps the button hidden — no user-visible breakage.)
  2. Grandfather existing users generously (the runbook has one-user + all-wall-owners SQL).
  3. Set FAMILY_WALL_PLUS_ENFORCED=true.

Multi-tier cancel — ✅ FIXED. The self-serve cancel flow (subscription-cancellation.ts) is now plan_key-aware: every action (cancelSubscription / applyRetentionOffer / switchToAnnual / logKeptWithoutOffer / getCancellationConfig) takes an optional planKey, and the /api/payment/cancellation/* routes read plan_key from the body/query. A user holding one active sub still cancels with no plan_key (back-compat); a user holding two (e.g. Graphene+ and Family Wall Plus) must pass plan_key — omitting it returns HTTP 409 {plan_keys} instead of silently guessing. Interval detection + annual-switch are now generic per plan (planPriceIds). The CancellationModal takes a planKey prop (defaults graphene_plus); a future Family Wall Plus manage-screen passes planKey='family_wall_plus'.

See FAMILY_WALL_MONETIZATION.md for the free/paid split + pricing rationale.

  1. Stripe checkout + gate the two cost points ✅ built — to flip the gate, only ops remain: create the Stripe prices + set the envs, grandfather early users, then FAMILY_WALL_PLUS_ENFORCED=true (above).
  2. Bedtime stories in your voice (sleep-stories Phase 2) — re-narrate a story_episodes chapter in the parent's cloned voice (long-form narration.ts pipeline, not the short-clip sayInUserVoice). Cache per (story, voice); cap length. FAMILY_WALL_SLEEP_STORIES.md.
  3. Bundle Graphene+ into Plus — shared/derived entitlement so catalog access is coherent.
  4. Captured-but-unbuilt: view-only guest account scope + post-accept access change (FAMILY_WALL_JOIN_INVITE.md); the photo-frame layer (still COUNSEL-GATED for minors, FAMILY_PHOTO_FRAME.md); more compare competitors (one lib entry each); the intercom audio-clip recording (audio_url column reserved).

Landmines / working notes

  • Migration-before-deploy — see above. Non-negotiable for any wall PR that adds a column.
  • Wall-file merge coordination — many features touch family/wall/[token]/page.tsx, family-wall.ts, family-wall-core.ts. Land one wall PR (apply its migration, merge) before building the next same-file feature, or you stack conflicting PRs (repo squash-merge handles stacks painfully — see CLAUDE.md).
  • main is protected — squash-merge via PR only; branch first.
  • Generative audio is asset-free by design — soundscapes + alarm tones + JQ speech are all WebAudio/speechSynthesis (no hosted files/licensing). Keep new audio in that style.
  • Sample generator is seeded/deterministic — no Math.random/Date in it (stable SSR, no hydration mismatch).

Map of docs + memories

Docs: FAMILY_WALL_MONETIZATION.md, FAMILY_WALL_SLEEP_STORIES.md, FAMILY_WALL_JOIN_INVITE.md, FAMILY_WALL_JQ_ACTIONS.md, FAMILY_WALL_INTERCOM.md, FAMILY_PHOTO_FRAME.md (counsel-gated). INDEX.md has a section per feature with exact files/endpoints. Memories: project_family_frame, project_family_wall_monetization, project_family_wall_jq_actions, project_family_wall_intercom, project_family_self_member, project_family_wall_join_invite, project_sophia_licensing_gate.

FAMILY WALL HANDOFF — Docs | HiveJournal