8.7 KiB
HANDOFF.md
Last updated: 2026-05-07 (PR #162 CI runner setup fixes applied; cutover ops still pending)
Active task: None mid-flight. Branch feat/self-serve-signup-phase-2 (HEAD 502c0a4) is ready to PR.
Where this session ended
Tasks 27–44 of Phase 2 implemented across 18 commits on feat/self-serve-signup-phase-2, branched off main (f918b76). Each task went through implementer + spec review + code-quality review per superpowers:subagent-driven-development. Cross-cutting review caught one redirect bug (fixed in-flight). After initial handoff, external code review surfaced four more real bugs — all fixed (commits 5e0c9d2, 3630dd5, 06200fa, 502c0a4):
- OAuth refresh tokens never stored (Phase 1 latent):
_store_refresh_tokenextracted to module-publicstore_refresh_token; both Google + Microsoft callbacks now persist the JTI before returning. Test covers callback →/auth/refreshround-trip. - OAuth callback never marked store authenticated (Phase 2 introduced):
setTokensnow setsisAuthenticated: true. Verified safe for the refresh-interceptor caller. - Stripe webhook idempotency could permanently drop failed events (Phase 1 latent):
apply_subscription_eventnow adds StripeEvent + runs handler + commits atomically; handler exception → rollback → Stripe retries. Inner_handle_*commits removed. - Missing
/account/billingand/account/billing/select-planpages (Phase 2 oversight): all the new billing CTAs (TrialPill, UpgradePrompt, NextStepCard, SetupChecklist) linked to non-existent routes. Built BillingPage (subscription summary + Manage-billing → portal session) and SelectPlanPage (plan cards + checkout flow).
Backend additions (Phase I, Tasks 27–31):
BillingService.open_customer_portal+GET /billing/portal-session(allowlisted for canceled/unverified users).PATCH /users/me/onboarding-step+POST /users/me/onboarding-dismiss-rest.POST /sales-leads(public, 5/hr/IP rate limit, fire-and-forget notification email, server-side PostHog event stub)./admin/plan-limitsGET/PUT round-tripsplan_billing(Stripe IDs, prices, public/archived flags) in one transaction;BillingService.invalidate_billing_cacheno-op stub for future cache.GET /config/public({self_serve_enabled, oauth_providers}); register-endpoint gate nowREQUIRE_INVITE_CODE and not SELF_SERVE_ENABLED and not invite_code.GET /accounts/invites/{code}/lookup(Phase 36).- OAuth callback extended to honor
account_invite_code+invited_emailfor invitee-via-OAuth path; rejects existing-email user withemail_already_registered_use_login. GET /plans/public(Phase 42).POST /beta-signupreturns 307 to${FRONTEND_URL}/register?from=beta.OnboardingStatusextended withemail_verified+shop_setup_done;UserResponseexposesonboarding_step_completed+onboarding_dismissed.
Frontend additions:
useBillingStore(Zustand, polled 60s viauseBillingPollmounted inAppLayout),useFeature/useFeatureLimit/useTrialBannerhooks,FeatureGate/UpgradePrompt/EmailVerificationGatecomponents.RegisterPageredesign: OAuth (Google/Microsoft) buttons + invite-code-conditional.OAuthCallbackPageat/auth/{google,microsoft}/callbackwith CSRF state validation.useAppConfighook (consumes/config/public, falls back toVITE_SELF_SERVE_ENABLED).AcceptInvitePageat/accept-invite?code=...(locked email, 3 sign-in options,/?welcome=teammateon success).EmailVerificationBannerrefactored to design-system tokens + grace-period hide;EmailVerificationWallpolished;VerifyEmailPageat/verify-email?token=...(single-fire guard).WelcomeRouter+WelcomeStep1/2/3at/welcome*. PSA tile UI + bulk invite emails per row.TrialPillin topbar (pristine/warning/urgent/expired/paid/complimentary/past_due/canceled).NextStepCard+SetupChecklist(replaces orphanedOnboardingChecklist); unified list, no SOLO/TEAM split, no Script Builder item.PricingPageat/pricing(B-style, 3 plan cards, hardcoded comparison v1).ContactSalesPageat/contact-sales.LandingPagegot "See pricing" CTA + replaced beta-signup form with<Link to="/register?from=beta">.
New env vars (Vite ARG/ENV in Dockerfile + .env.example): VITE_SELF_SERVE_ENABLED, VITE_GOOGLE_CLIENT_ID, VITE_MS_CLIENT_ID, VITE_OAUTH_REDIRECT_BASE, VITE_CALENDLY_URL. Backend env: SALES_LEAD_RECIPIENT_EMAIL (default sales@resolutionflow.com).
Resume point — DO THIS NEXT
- Review the branch and open a PR. 18 commits, all squashed-or-not at user discretion. Branch
feat/self-serve-signup-phase-2frommain. - Phase O — manual operational tasks (Tasks 45–47 from the plan):
- Task 45: Stripe live-mode setup (Products, Prices, Customer Portal, webhook endpoint + signing secret) and Railway prod env vars (
STRIPE_*,OAUTH_REDIRECT_BASE,GOOGLE_CLIENT_*,MS_CLIENT_*). Runpython -m scripts.sync_stripe_plan_idsto populateplan_billingrows with live Stripe IDs. - Task 46: Internal validation pass via
INTERNAL_TESTER_EMAILSallowlist (per-email bypass ofSELF_SERVE_ENABLED=false). Backend support for the allowlist itself is NOT implemented — needs a small addition toauth.py/config.pyif the validation pass requires it. Otherwise validate in test mode with the flag flipped temporarily. - Task 47: Flip
SELF_SERVE_ENABLED=true(backend) +VITE_SELF_SERVE_ENABLED=true(frontend rebuild). Watch PostHog funnel + Stripe webhook error rate.
- Task 45: Stripe live-mode setup (Products, Prices, Customer Portal, webhook endpoint + signing secret) and Railway prod env vars (
- Followups deferred during Phase 2 (logged below).
Followups deferred from Phase 2
- PostHog server-side capture infrastructure missing.
/sales-leadscalls_capture_posthog_eventwhich lazy-importsapp.core.analytics.posthog(no-op if module absent). Wire up the real PostHog server SDK before cutover — needsapp/core/analytics.pywith a configured client, pluspython-posthogdep. - Frontend
/usage/{field}endpoint missing.useFeatureLimitlazy-fetchesapiClient.get('/usage/' + field)— endpoint doesn't exist; hook silently falls back toused=0. Build the backend endpoint before any UI surface relies on real usage counts. - Pre-existing dual subscription stores (
authStore.subscriptionlegacy +billingStore.subscriptionnew).Subscription.statusunion infrontend/src/types/account.tsis missing'complimentary'. Phase 2 didn't introduce — but worth deprecating the old store before any UI readscomplimentaryfrom the wrong source. INTERNAL_TESTER_EMAILSper-email allowlist (Task 46): backend support not implemented. Add when Phase O Task 46 needs it.accept-invitenot gated byself_serve_enabled— by design (invites work in any mode). Confirm if pilot wants stricter gating.UserResponse.onboarding_step_completedvalidator suggested by reviewer (rejectcompletewithout data) — skipped, spec typesdata?as optional.- Welcome wizard behind
EmailVerificationGate: Day 7+ unverified users hit the wall on/welcome/*and have no path back into the wizard until they verify. Per spec — wall says "Resend verification" → user verifies → wall closes → wizard resumes. If product wants a different UX, allowlist/welcome/*in the gate. SelectPlanPageseat input doesn't validate against planmax_seats. Stripe Checkout will reject if exceeded; nicer UX would cap client-side.BillingPageManage-billing button always enabled — clicking with nostripe_customer_idfalls back to a toast. By design; could disable + tooltip if billing-state payload were extended with that flag.
Environment notes (carry-forward)
- Code-server LXC now standardizes on Python 3.12.
.python-versionpins 3.12.13 to match the Docker image; native backend work should usebackend/venvbuilt from that interpreter.pytest --versionandalembic --versionpass natively whenDEBUG=true SECRET_KEY=ci-test-secret-key-not-for-productionare supplied to avoid local.envvalidation failures. - Gitea CI now sets up Python 3.12 for backend/e2e and Node 20 for frontend/e2e explicitly; do not rely on runner ambient toolchains.
- Pytest:
docker exec resolutionflow_backend pytest tests/<file> -v --override-ini="addopts=". - Vitest:
docker exec -w /app resolutionflow_frontend npm test -- <path> --run. - TS build:
docker exec -w /app resolutionflow_frontend npx tsc -b. - Alembic:
docker exec -w /app resolutionflow_backend alembic .... Never--rev-id. - No
ghCLI — Gitea API via$GITEA_TOKENfor PR/issue work. - Single alembic head:
c6cbfc534fad(from Phase 1). Phase 2 added no migrations.