Cornerstone Plus — launch runbook
Status: freemium gate + Stripe checkout BUILT, shipping OFF
(CORNERSTONE_PLUS_ENFORCED unset → nothing paywalled). Sibling of
FAMILY_WALL_PLUS_LAUNCH.md; same shape, same
subscriptions table (plan_key = 'cornerstone').
The model — own-the-key custody stays free; pay for the recurring-cost layer
- Free — the full encrypted vault (notes, photos, documents), the console, and the device mesh (each device keeps a local encrypted replica). This is the honest-custody core — you hold the key, we store only ciphertext — and it costs us nothing marginal, so it's free.
- Plus — the off-site encrypted backup (Glacier): a second encrypted copy we hold on a separate provider, so a house fire or a lost-every-device event isn't the end. It's the same ciphertext (we still can't read it) and it's a real recurring storage cost — the one thing it's honest to charge for. (Expanded storage quota + more mesh devices are the natural v2 gates.)
What's gated (service cornerstone-plus.ts, no-op until enforced): POST /api/cornerstone/backup/run
and POST /api/cornerstone/backup/restore → requireCornerstonePlus → 402 with upgrade:true.
GET /api/cornerstone/backup/status returns { plus, enforced } so the vault page shows the upsell.
One-time setup
1. Create two recurring Stripe prices
On the "Cornerstone Plus" product: a monthly and an annual price (pick the anchor — e.g. $8/mo · $79/yr — higher than Family Wall since it carries real per-family storage cost).
2. Set the price env vars (Railway backend)
STRIPE_PRICE_CORNERSTONE=price_... # monthly
STRIPE_PRICE_CORNERSTONE_ANNUAL=price_... # annual
Until set, /api/payment/cornerstone/config reports available:false and the vault page's Get-Plus
button self-hides — nothing breaks.
3. Grandfather early families (do this BEFORE flipping the flag)
Comp everyone who already built a vault, so the flip never yanks their off-site backup. No Stripe charge — insert active subscription rows directly.
Grandfather everyone with a vault item (the "already using it" cohort):
insert into subscriptions (user_id, plan_key, status, current_period_end, cancel_at_period_end)
select distinct v.owner_id, 'cornerstone', 'active', now() + interval '10 years', false
from cornerstone_vault v
where not exists (
select 1 from subscriptions s
where s.user_id = v.owner_id and s.plan_key = 'cornerstone'
);
current_period_end far out + cancel_at_period_end = false
= a comp that doesn't lapse; these rows have no stripe_subscription_id, which is fine (never
touched by the webhook; isCornerstonePlus() only checks status + period).
4. Flip the master switch
Set CORNERSTONE_PLUS_ENFORCED=true on the backend. Off-site backup + restore now require Plus;
the vault, console, and device mesh stay free for everyone.
5. Verify
A non-Plus test account hits the upsell when it taps "Back up off-site now"; a grandfathered (or subscribed) account backs up normally.
How activation works (no new code)
The same POST /api/payment/webhook that handles every other tier upserts a subscriptions row
with plan_key = 'cornerstone' on checkout.session.completed (price → plan_key map in
payment.ts). Cancellation flows through the shared, plan-key-aware path.
Rollback
Unset CORNERSTONE_PLUS_ENFORCED → the gate no-ops and everything is free again instantly.