Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Current State (Sep 2026)Launch Readiness

Launch Readiness

Final verification, 2026-09-06 evening. The core loop no longer gates launch: a brand-new user was driven from signup to a downloaded book in one real run. Launch is still blocked on the items in sections 5 to 8: no waitlist invite mechanism, open signup (and the Supabase project has email confirmation switched off, so addresses are never verified), waitlist tables readable with the public anon key, an admin API with no authentication, and dead tier enforcement.

Mechanisms updated 2026-09-06 after the fixes landed; see the final verification note below the title.

Verdict

MyStoryFlow still cannot invite a waitlist person today, because no invite mechanism exists: waitlist_entries and Supabase Auth are two disconnected systems, and the only admin action is flipping a status column that sends nothing. Signup itself is still open to anyone with the URL, so a “private beta” is private only by obscurity. What changed today is the dead end that used to sit right after signup: the book wizard is no longer nine steps ending in a mandatory Stripe paywall — it is two screens (who the book is about, then a template), and nothing in first-run asks for money. A confirmed new user now lands on /books/new and can create a book and start a chapter without ever seeing a payment screen (§“What changed on 2026-09-06” below). Nobody has ever paid: 0 rows in subscriptions, 0 profiles with a Stripe customer id — that remains true, and is now true by design rather than by a broken checkout. The good news, unchanged: the core user-content tables — profiles, stories, recordings, books, book_chapters, ai_conversations, export_jobs — all have RLS enabled with policies, and every API route audited here does a server-side ownership check, so a signed-in stranger cannot read another user’s stories. The exposure that remains is exactly what it was: waitlist_entries (276 email addresses) is readable and deletable by anyone holding the public anon key, and the admin app’s 32 API routes have no authentication at all. None of that was in scope for today’s fixes. Free-for-waitlist no longer needs the one-line change this section used to recommend — the paywall it targeted doesn’t exist anymore — but an invite mechanism and the admin-app auth gap are still the fastest honest path to 30 users.

Live numbers

TableRowsNote
waitlist_entries276154 pending, 122 verified
— never verified154verified_at is null
— never issued an OTP106otp_code is null and verified_at is null — the old details-step gate
— whose email has an auth account2the waitlist has produced essentially no users
auth.users6755 confirmed, 0 anonymous, 3 signed in within 30 days
profiles67all subscription_tier='starter', all subscription_status='trial', 0 with stripe_customer_id
stories25
books9
recordings18
ai_conversations78
trial_sessions1the trial path is effectively unused
subscriptions0table is never read or written by any code
export_jobs0

Waitlist signups are still arriving (latest 2026-09-06). The last backup was 2026-08-03; 16 signups have arrived since and are not in backups/. These counts are pre-fix and were not re-queried for this update — none of today’s commits write to waitlist_entries, auth.users, subscriptions, or export_jobs in a way that would move them, aside from the throwaway test rows created and deleted during the book-creation and export re-verification (§ the book-creation and recording-to-story pages).

Waitlist → account → first story

StepWhat existsStatusEvidence
Person joins the waitlistMarketing home renders HeroSectionCustom with the form in place of the CTAs when WAITLIST_ENABLED + WAITLIST_MODE='construction'apps/marketing-site/src/config/features.ts:19-24; src/app/page.tsx:24,296
Row is writtenPOST inserts into waitlist_entriesapps/marketing-site/src/app/api/waitlist/route.ts:37-50
Verification emailWorking tree issues the OTP at signup; previously a required phone/name details step gated it, which is why 106 of 276 never got a code⚠️ uncommittedroute.ts:3,67; resend-otp/route.ts:3,34,39
Returning unverified emailWorking tree lets an unverified email resume instead of 409ing⚠️ uncommittedapps/marketing-site/src/app/api/waitlist/route.ts:50
Orphaned duplicate formpackages/ui/src/components/waitlist/StoryFlowWaitlistForm.tsx is a copy of the old flow with no importers⚠️zero matches for the import across apps/**
Admin decides who gets inAdmin can only flip status (pending/verified/contacted/converted)⚠️apps/admin-app/src/app/dashboard/waitlist/page.tsx:338,436src/lib/waitlist-service.ts:286-321
Invite is sentNothing. No sendInvite, no invite_code, no signup link in any template. converted writes a column and a waitlist_events row and stopswaitlist-service.ts:286-321,361-365
Email templates that existsendOTPEmail, sendWelcomeEmail (“you’re #N on the waitlist”) — neither contains a signup linkapps/web-app/lib/email/waitlist.ts:9-83,85-194 (duplicated in marketing-site)
Sending domainRESEND_FROM_EMAIL and the hardcoded fallback are both on workbeehive.com, not mystoryflow.com. DNS/SPF/DKIM cannot be verified from here⚠️apps/marketing-site/src/lib/email/waitlist.ts:12-13
SignupOpen to anyone. invitationToken is optional and validated against family_invitations, not the waitlist; auth.signUp runs unconditionally. No OAuth⚠️apps/web-app/app/api/auth/signup/route.ts:9,21-50,53; UI at app/page.tsx:34,198-206,398-421
“Check your email” panelRenders after signupapp/page.tsx:344-372
Email confirmation landsreturnUrl honoured only against a trusted-domain allowlist, else /auth-redirect; failure → /auth/auth-code-error (page exists, copy is good)app/auth/callback/route.ts:4-51,18-25
First sessiononboarding_step='welcome' (the default for every new profile) no longer counts as “already in progress” — fixed 2026-09-06 (b05b8e3); a confirmed new user lands on /books/new, not caught in a loopapp/auth-redirect/page.tsx:134-147
DashboardGreeting + a single next-step card (“Start your book” → “Create your book”) → /books/newapp/(dashboard)/dashboard/page.tsx; app/components/dashboard/NextStepCard.tsx
First storyTwo screens (who the book is about, then a template), no payment step, book and first chapter both exist before money is mentioned. Unchanged while NEXT_PUBLIC_PAYWALL_MODE is off, which is the default and what an unset value meansapp/components/books/BookCreationWizard.tsx; wizard-steps/AboutBookStep.tsx, PickTemplateStep.tsx
Paywall, when switched onSetting NEXT_PUBLIC_PAYWALL_MODE=onboarding adds a third screen, “Choose your plan”, after the template pick. Three cards, one recommended, monthly/yearly in words, one button each into Stripe Checkout. POST /api/books refuses without a plan, so the screen cannot be skipped by URL✅ built, offlib/config/flags.ts; wizard-steps/ChoosePlanStep.tsx; app/api/billing/checkout/route.ts; app/api/books/route.ts
Sign in with an emailed codeSecond option under the password form: email → six big boxes → verifyOtp, same post-login routing. Hidden unless NEXT_PUBLIC_EMAIL_CODE_SIGNIN=true, because Supabase’s built-in mailer sends only a few per hour project-wide✅ built, offapp/(auth)/components/EmailCodeSignIn.tsx, EmailCodeInput.tsx; app/page.tsx
Password resetReal, both halvesapp/(auth)/forgot-password/page.tsx:33-35,60; app/(auth)/reset-password/page.tsx:67-69
Legal pages linked from signupPrivacy and Terms exist on the marketing site only; the web-app signup form links to neither. Not touched todayapps/marketing-site/src/app/(legal)/{privacy,terms}; zero matches in apps/web-app/app/page.tsx

What changed on 2026-09-06

  • The wizard’s mandatory paywall is gone, not just made skippable. b05b8e3 deleted SubscriptionStep, PrintOrderStep, GoalsStep, RemindersStep, and ReviewAndStartStep outright, along with the Stripe checkout call the wizard used to make. There is no paidStatuses check left to patch — the step it used to gate doesn’t exist.
  • The redirect trap at first login is fixed. onboarding_step='welcome' (the default value on every new profile) no longer counts as wizard progress, so a confirmed new user reaches /books/new instead of being routed to a dashboard that then sends them back into a loop. Fixed in b05b8e3.
  • The dashboard no longer fabricates stats. The prior four-tile, stat-heavy dashboard (page/word counts, “Quick Insights,” a duplicate continue-card, Elena promoted three times) is replaced by one next-step card, two ways to add a story, the book, and family — nothing invented. ContinueWhereYouLeftOff.tsx and ReEngagementNudge.tsx were deleted. Fixed in 75a4bcc.
  • Fake nav destinations are no longer linked. /billing, /orders, /publishing/kdp, and /family/shared are removed from lib/navigation-config.ts. The pages themselves still exist with their hardcoded fake data (e.g. /billing’s “Renews: March 15, 2025”) — they are unreachable from the UI now, not deleted. /export remains linked. Fixed in b05b8e3 (see “Prior blockers, re-verified” below).
  • Owner decision, stated for the record: the first-run paywall is removed by design. It now exists again as code behind a flag that defaults to off, so it can be switched on without another release once Stripe is configured — see the checklist at the end of this page.

Billing

Nobody has paid yet and nobody can: STRIPE_SECRET_KEY in .env.local is still the literal placeholder your_stripe_secret_key…, as are the publishable key and the webhook secret. What changed is that the paywall is now built — real when NEXT_PUBLIC_PAYWALL_MODE=onboarding, absent when it is not — so switching it on is a configuration job rather than a code one.

ConcernStateEvidence
Plan definitionsOne config: name, one plain line, both prices, and the STRIPE_PRICE_* env names. The plan step and the checkout route both read it, so the card and the charge cannot driftlib/billing/plans.ts
Price env namesAll six of STRIPE_PRICE_{STARTER,FAMILY,PREMIUM}_{MONTHLY,ANNUAL} are in apps/web-app/.env.local and now in .env.example. Still absent from root vercel.json’s env blocklib/billing/plans.ts; vercel.json
If a price var is missingThe new route answers 503 “That plan is not ready yet”. (The older create-checkout still falls back to the literal 'price_starter_monthly' and 500s)app/api/billing/checkout/route.ts; create-checkout/route.ts:15-25
subscriptions tableNow has writers. The webhook writes it on checkout.session.completed and on subscription created/updated/deleted, and the post-checkout verify endpoint writes it too, so a person who paid is not blocked while a webhook is in flightlib/billing/subscription-writer.ts; webhook/route.ts; app/api/billing/checkout/verify/route.ts
Stripe period fieldsAs of API version 2025-05-28.basil, current_period_end is on the subscription items, not the subscription. Read from the wrong place it is undefined and every subscription looks eternallib/billing/subscription-writer.ts
Server-side gatePOST /api/books answers 402 when the flag is onboarding and the person has no active subscriptions row. Keyed on subscriptions.user_id, which is a real column and the one the RLS policy useslib/billing/entitlements.ts; app/api/books/route.ts
Tier / usage enforcementQuery fixed. getUserSubscription filtered profiles.eq('user_id', …); profiles has no user_id column (PK is id), so it returned null for everyone. It now filters on id and reads both trial columnslib/subscription/subscription-service.ts
Trial column splitStill two columns. The reader now prefers trial_ends_at (the one the webhook writes, 67 rows) and falls back to trial_endlib/subscription/subscription-service.ts
Customer portalStill does not exist. A subscriber could not cancel or change a card
Gift purchase takes no moneyThe 6-step gift wizard’s card fields are never transmitted; the route writes purchase_status: 'completed' regardless. Anyone can take a gift for free. Not fixed — it needs a real Stripe callapp/campaigns/create/steps/PaymentStep.tsx:37-38; app/api/gifts/purchase/route.ts:56
Anonymous gift purchase does nothingEvery downstream step (family, campaign, all email) is gated on giver_user_id, which /purchase-gift never sets. A logged-out buyer’s recipient is never contacted. Not fixedapp/api/gifts/purchase/route.ts:91,143,228,260; app/purchase-gift/page.tsx:228
Queued gift email is never sentRows land in email_send_queue as pending; processEmailQueue has no caller and no cron drains it. Not fixedlib/email/email-automation-service.ts:225-236,336; vercel.json

Free for the waitlist

Nothing to do. NEXT_PUBLIC_PAYWALL_MODE is unset, which means off, which means the wizard is two screens and POST /api/books never checks for a subscription. All 67 existing profiles reach /books/new and create a book without a payment screen. Leaving the flag alone is the free-for-waitlist setting.

What’s left of the old “leave billing off” risk analysis still applies to the pages, not the wizard: /billing still renders the hardcoded “Renews: March 15, 2025” and /orders, /publishing/kdp, /family/shared still render invented data if reached directly by URL — they’re only unlinked from navigation now (see “What changed” above), not deleted. /export remains linked and is a real stub with a TODO, not fake data.

Safety for strangers

Advisors, 2026-09-06. Security: 3 ERROR categories, 6 WARN, 1 INFO — 10 categories over 258 findings. Performance: 3 WARN, 2 INFO over 1,277 findings (269 auth_rls_initplan, 529 multiple_permissive_policies, 381 unused indexes, 83 unindexed FKs, 15 duplicate indexes). Performance is noise at 30 users; ignore it for this launch.

Security findings that matter:

FindingCountNote
rls_disabled_in_public53 tableslist below
policy_exists_rls_disabled3family_groups, family_members, gifts — policies written, RLS switched off, so the policies are dead and the tables are fully open
rls_enabled_no_policy18includes book_chapter_blocks, book_chapter_stories, book_chapter_versions — reachable only by the service role
security_definer_view10includes waitlist_stats
anon_security_definer_function_executable44confirmed live: create_notification, debug_gifts_query, debug_service_role_access, test_gifts_rls are all anon-executable
function_search_path_mutable82
Postgres version1supabase-postgres-15.8.1.094 has outstanding security patches
Leaked-password protection1disabled in Auth settings

The core user-content tables are protected. profiles (4 policies), stories (3), recordings (2), books (13), book_chapters (4), ai_conversations (4), export_jobs (4) and subscriptions (2) all have relrowsecurity = true.

User-content tables with RLS off, from the 53:

waitlist_entries, waitlist_events, waitlist_settings, family_groups, family_members, family_book_permissions, gifts, audit_logs, anonymous_sessions, user_story_timeline, user_writing_patterns, user_ai_interactions, story_characters, story_elements, conversation_turns, conversation_summaries, conversation_contexts, conversation_analytics, voice_conversations, voice_conversation_turns, voice_anonymous_sessions, tools_pdf_flipbooks, experiment_participants.

The anon-key exposure, plainly. Every one of those tables carries SELECT, INSERT, UPDATE, DELETE, TRUNCATE grants for both the anon and authenticated roles (verified via information_schema.role_table_grants). With RLS off, PostgREST applies no further restriction. The anon key ships in every browser bundle by design. Therefore anyone who opens the site can read all 276 waitlist email addresses, names, phone numbers and IP addresses — and can delete or overwrite them with a single request to /rest/v1/waitlist_entries. The same is true of family_groups/family_members/gifts despite their policies, and of audit_logs. This is a live PII exposure today, before any invite is sent, and it grows with every new signup.

The API layer compensates for the tables that matter. Every audited route re-checks ownership server-side, so RLS is not the only line of defence for stories, books and recordings:

RouteOwnership checkEvidence
GET /api/stories/[storyId]Yes — owner, else book-family, else campaignapp/api/stories/[storyId]/route.ts:109,137-158
PATCH /api/stories/[storyId]Yes — same three-way checkapp/api/stories/[storyId]/route.ts:197,215-238
GET /api/recordingsYes — .eq('user_id', user.id)app/api/recordings/route.ts:21,49
GET /api/books/[id]Yes — owner or family groupapp/api/books/[id]/route.ts:15,31-36
GET /api/audio/[fileId]Yes — service-role client, but scoped .eq('user_id', user.id)app/api/audio/[fileId]/route.ts:27,57-73
GET /api/books/[id]/export/[jobId]/downloadYes — .eq('user_id', user.id) on the jobapp/api/books/[id]/export/[jobId]/download/route.ts:22,28-31
GET/POST /api/books/[id]/storiesYes — owner or family group, both verbsapp/api/books/[id]/stories/route.ts:15,30-34,72,104-108

Secrets. No 'use client' file references SUPABASE_SERVICE_ROLE_KEY, and no NEXT_PUBLIC_* variable carries it — 45 server-side files use it, none client-side. Live Backblaze B2 credentials remain in tracked files (CLAUDE.md, docs/blog-article-pipeline.md, .claude/skills/blog-publisher.md) and are therefore in git history; they need rotating regardless of what else is fixed.

Rate limiting: none. Zero matches for rateLimit|rate_limit|Ratelimit|upstash in apps/web-app/lib or apps/web-app/app. /api/transcribe and /api/stories/convert have no limiter. The trial endpoints are not the cost bomb they were: /api/trial/generate-story requires an authenticated user and verifies job ownership (app/api/trial/generate-story/route.ts:11-26), and although /api/trial/start:38 calls signInAnonymously() for anyone, auth.users holds 0 anonymous users and trial_sessions holds 1 row, so anonymous sign-in is not producing sessions in practice. The unmetered surface that does matter is the unauthenticated AI and upload routes in the blocker table below. At 30 invited users, a per-IP and per-user limiter on the AI and upload routes is sufficient; a global spend alarm on the OpenAI account is the cheaper backstop.

Sequencing. The owner decided on 2026-08-02 that features come before database hardening (Phase 4 in docs/IMPLEMENTATION_PLAN.md), and that decision stands here. This section states exposure as fact so it can be priced, not to reopen the ordering. One carve-out is worth making anyway, because it is small and self-contained: waitlist_entries is the one table where the exposed data is the launch asset itself, and revoking anon grants on the three waitlist_* tables does not touch app code.

Operations

ItemStatusDetail
Error monitoring❌ noneNo @sentry dependency, no instrumentation.ts, no sentry.*.config.ts. .env.example advertises NEXT_PUBLIC_SENTRY_DSN and SENTRY_AUTH_TOKEN that nothing reads
Security headers / CSPapps/web-app/next.config.ts has no headers() block. ignoreBuildErrors is correctly absent
Crons⚠️ conflictingRoot vercel.json declares /api/cron/weekly-prompts at 0 9 * * 1; apps/web-app/vercel.json declares the same path at 0 * * * * and per-app config wins
CRON_SECRETChecked at app/api/cron/weekly-prompts/route.ts:32-35; it is the only cron route in the repo
Export job reaper⬜ noneNo stuck/stale export_jobs cleanup anywhere. export_jobs is empty today, so nothing is stuck yet
Env declared vs needed⚠️Root vercel.json declares 18 vars but omits STRIPE_PRICE_* (6), STRIPE_WEBHOOK_SECRET, CRON_SECRET, RESEND_FROM_EMAIL, NEXT_PUBLIC_STRIPE_PRICE_*
RESEND_FROM_EMAIL⚠️Points at workbeehive.com, as does the hardcoded fallback. SPF/DKIM/DMARC for that domain cannot be verified from here — check in the Resend dashboard before any bulk send
Legal pages⚠️Exist at apps/marketing-site/src/app/(legal)/{privacy,terms}; not linked from the web-app signup form
Account deletioncomponents/settings/DataSection.tsx:95 DELETEs /api/user/delete-account; the route does not exist anywhere in the repo
User data exportapp/api/user/export-data/route.ts:15-24 — authenticated, user.id !== userId rejected, writes user_data_exports
Waitlist backup⚠️ stalescripts/backup/export-waitlist.js exists; latest artefacts in gitignored backups/ are dated 2026-08-03; 16 signups since
Test suite74 of 105 Jest suites fail, 58 of them failing to load modules deleted in the campaign→book migration. No signal before a deploy
CINo .github/; the only gate is a bypassable pre-push hook

Prior blockers, re-verified

IdOpen?Evidence on 2026-09-06
A1 RLS off on 53 tablesOpenStill exactly 53; core user tables now covered, waitlist_* and family_* are not
A2 Policies with RLS offOpenfamily_groups, family_members, gifts — advisor policy_exists_rls_disabled
A5 anon-executable SECURITY DEFINEROpencreate_notification, debug_gifts_query, debug_service_role_access, test_gifts_rls all return has_function_privilege('anon', …) = true
A6 SECURITY DEFINER viewsOpen10, including waitlist_stats
A7 avatars bucket listableOpenThe only storage bucket, marked public, 4 storage policies
B1 Admin API unauthenticatedOpen32 of 32 routes under apps/admin-app/src/app/api contain no getUser/requireAdmin/getSession call; no middleware.ts in the app
B2 Gift purchase takes no paymentOpenapp/api/gifts/purchase/route.ts:23,50,56 — service-role client, purchase_status: 'completed', no Stripe call
B3 Three unauthenticated file endpointsOpenupload/backblaze, upload-url, upload/delete — zero getUser calls in each
B4 No rate limitingOpenZero limiter matches in web-app
B5 Premium tier used as admin checkOpenapp/api/admin/email/templates/route.ts:24,116profile.subscription_tier !== 'premium'
B7 Cross-tenant trial conversionPartly fixedAnonymous callers now rejected (trial/convert/route.ts:11-14), but the trial_sessions lookup at :18-21 still has no user_id filter, so any authenticated user with a session id can re-parent someone else’s trial
D1 Account deletion impossibleOpenRoute absent; UI still calls it
D2 GDPR erase/access are stubsOpen3 TODOs each in packages/analytics/src/api/privacy/{erase,access}.ts
D3 No Stripe customer portalOpenZero matches for billingPortal
D5 No entitlement enforcementOpenRoot cause identified: the profiles.user_id bug below makes it structurally impossible
P1-5 processEmailQueue unwiredOpenlib/email/email-automation-service.ts:336 — only callers are its own tests
P1-8 Fabricated family invitationsOpenapp/api/families/[familyId]/invitations/route.ts:72 inv_${Date.now()}, no insert, email is console.log at :86,97. The working /api/family/invitations path still exists
P1-9 Book share calls a missing edge functionOpenapp/books/[id]/share/page.tsx:191,201 invoke send-book-invitation; no supabase/functions directory anywhere
P1-12 Mock data rendered on fetch failureHalf fixedConversations fixed (app/conversations/page.tsx:81 “No mock fallback”); notifications still openapp/notifications/page.tsx:102,121
P1-13 Silent upload data loss✅ fixed 2026-09-06 (275d9fe)uploadRecording (lib/storage/backblaze.ts:157-286) now throws a BackblazeUploadError instead of returning a mock URL; a mock URL survives only when B2 env is entirely absent, flagged mock: true. The 13 legacy production rows already sitting on a mock URL are unaffected — this is a code fix, not a data cleanup
P1-14 Fake screens in navigationPartly fixed 2026-09-06 (b05b8e3)/billing, /orders, /publishing/kdp, /family/shared removed from lib/navigation-config.ts — see comment at :42-46. The pages themselves still exist with the same fake data (app/billing/page.tsx:28 still hardcodes “Renews: March 15, 2025”), just unreachable from the UI now. /export remains linked and is a real stub, not fake data
P1-15 profiles.eq('user_id')Openlib/subscription/subscription-service.ts:46-47. Confirmed against the live schema: profiles has 36 columns and none is user_id
E1 Committed B2 credentialsOpenKey id still present in CLAUDE.md, docs/blog-article-pipeline.md, .claude/skills/blog-publisher.md
F1 Unearned compliance claimsOpenapps/docs-app/content/security.mdx:19-20 still claims SOC 2 Type II and ISO 27001

30-user private beta checklist

Ordered. Sizes: S ≈ under an hour, M ≈ half a day, L ≈ multiple days.

Must land before the first invite

  1. S — Revoke anon/authenticated grants on waitlist_entries, waitlist_events, waitlist_settings. No app code reads them through the anon key; the marketing route uses the service role. This closes the 276-address leak without touching the RLS sequencing decision.
  2. S — Back up the waitlist. Run scripts/backup/export-waitlist.js; the current artefact is 34 days and 16 signups stale, and step 1 should not be the first thing you do to a table you have no fresh copy of.
  3. S — Take the paywall out of first-run. Done 2026-09-06 (b05b8e3). The wizard step was deleted, not bypassed.
  4. Partly done 2026-09-06 (b05b8e3) — hide the fake surfaces. /billing, /orders, /publishing/kdp, /family/shared are removed from lib/navigation-config.ts. Still open: the pages themselves still exist and still render fake data if reached directly, and /export is still linked (it’s a real stub, not fake data, so lower priority).
  5. M — Build the invite. The smallest honest version: an admin action that generates a single-use token, stores it against the waitlist_entries row, emails a signup link, and makes invitationToken required at app/api/auth/signup/route.ts:53 when a beta flag is on. Without the required half, “private beta” means “anyone with the URL”.
  6. S — Put the admin app behind auth, or take it off the internet. 32 unauthenticated service-role routes, one of which grants admin on an unauthenticated PATCH. If a real guard is too slow, restrict the deployment to an allowlisted IP or take it down for the beta.
  7. S — Ship account deletion, or remove the button. The UI promises deletion and calls a route that does not exist. A stub route that soft-deletes and emails you is acceptable; a button that lies is not.
  8. S — Point RESEND_FROM_EMAIL at a domain you control for this product, and confirm SPF/DKIM in the Resend dashboard before sending to 276 addresses at once. Invites from workbeehive.com will land in spam and burn the list.

Can follow within the first week

  1. S — Fix profiles.eq('user_id').eq('id') at subscription-service.ts:47, and reconcile trial_end vs trial_ends_at. Nothing is enforced until this is right, so it is not urgent while everything is free — but it is a one-character fix that unblocks all tier work.
  2. S — Add error monitoring. With 3 active users a month, no monitoring meant nobody noticed; with 30 it means you learn about breakage from an email. Sentry, or a console.error drain from Vercel logs.
  3. M — Re-send verification to the 106 entries that never got an OTP, using the working-tree resend path, after step 8. These are people who tried to join and were silently dropped.
  4. M — Rate-limit the unauthenticated AI and upload routes, and set a hard monthly spend cap on the OpenAI account as a backstop.
  5. S — Add authentication to the three upload endpoints (upload/backblaze, upload-url, upload/delete); upload-url is a read-SSRF and upload/delete can destroy an unrelated customer recording.
  6. S — Strip the SOC 2 / ISO 27001 claims from apps/docs-app/content/security.mdx:19-20 before the docs site has an audience.
  7. S — Rotate the B2 credentials committed to CLAUDE.md and two other tracked files.
  8. M — Link Privacy and Terms from the signup form, and drop the mock-notification fallback at app/notifications/page.tsx:102.
  9. M — Reconcile the two cron schedules and add an export_jobs reaper before exports carry real load.

Stripe and sign-in setup checklist for the owner

Everything below is dashboard work. No code change is needed for any of it.

1. Stripe: create the products and prices

In the Stripe Dashboard → Product catalogue, create three products, each with two recurring prices (USD):

ProductMonthlyYearly
Just me$19 / month$180 / year
My family$39 / month$360 / year
Everything$59 / month$540 / year

Copy each price id (price_…) into apps/web-app/.env.local and into Vercel’s environment variables:

STRIPE_PRICE_STARTER_MONTHLY, STRIPE_PRICE_STARTER_ANNUAL, STRIPE_PRICE_FAMILY_MONTHLY, STRIPE_PRICE_FAMILY_ANNUAL, STRIPE_PRICE_PREMIUM_MONTHLY, STRIPE_PRICE_PREMIUM_ANNUAL

Also set STRIPE_SECRET_KEY and NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY. All eight are placeholders today.

2. Stripe: the webhook

Developers → Webhooks → Add endpoint: https://<your-domain>/api/subscriptions/webhook

Subscribe to: checkout.session.completed, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_succeeded, invoice.payment_failed. Paste the signing secret into STRIPE_WEBHOOK_SECRET.

To test locally: stripe listen --forward-to localhost:3000/api/subscriptions/webhook and use the whsec_… it prints as STRIPE_WEBHOOK_SECRET.

3. The two flags

FlagValuesWhat it does
NEXT_PUBLIC_PAYWALL_MODEoff (default; also what an unset or misspelled value means)Wizard is two screens, book is created, nobody is asked for money.
onboardingAdds “Choose your plan” after the template pick, and POST /api/books returns 402 without an active subscription.
NEXT_PUBLIC_EMAIL_CODE_SIGNINfalse / unset (default)Only the password form on the sign-in screen.
trueAdds “Email me a sign-in code” underneath it.

Both are NEXT_PUBLIC_, so they are baked in at build time — changing one needs a redeploy, not a restart. Do not set NEXT_PUBLIC_PAYWALL_MODE=onboarding before step 1 is done, or signup dead-ends at the plan step.

4. Supabase: send auth email through Resend

Dashboard → Authentication → Emails → SMTP Settings → enable custom SMTP:

  • Host smtp.resend.com, port 465
  • Username resend, password = your Resend API key
  • Sender = an address on a domain verified in Resend (today RESEND_FROM_EMAIL is on workbeehive.com, not mystoryflow.com — verify the domain you actually want to send from)

Then Authentication → Emails → Templates → Magic Link: the body must contain {{ .Token }}, which is the six-digit code. The default template only has {{ .ConfirmationURL }}, and with only that, the code screen has no code to show.

Finally, Authentication → Rate Limits: raise “Emails per hour” from the built-in default (a handful per hour for the whole project — fine for one tester, not for 30 people). The limit is the reason NEXT_PUBLIC_EMAIL_CODE_SIGNIN ships off.

Leave email auto-confirm as it is. The code path signs people in directly and does not depend on it.