feat: self-serve signup backend (Phase 1) #161

Merged
chihlasm merged 27 commits from feat/self-serve-signup-spec into main 2026-05-06 23:46:35 +00:00
Owner

Summary

Backend foundation for public self-serve signup. Reuses the existing subscriptions / plan_limits / feature_flags / account_invites / email_verification_tokens infrastructure and adds four small tables (oauth_identities, plan_billing, sales_leads, stripe_events) plus columns on users / accounts / account_invites. Replaces the trial auto-downgrade in deps.py with two non-mutating dependencies, adds a real BillingService wrapping Stripe, extends the existing webhook stub, wires Google + Microsoft OAuth callbacks, makes register auto-send the verification email, and adds bulk-create + soft-revoke invite routes.

Spec: docs/superpowers/specs/2026-05-05-self-serve-signup-onboarding-design.md
Plan: docs/superpowers/plans/2026-05-06-self-serve-signup-phase-1-backend.md

What's in the diff

Phase Tasks Output
A 1–8 9 alembic migrations (oauth_identities, password_hash nullable, role_at_signup + onboarding_step_completed, account wizard cols, invite revoked_at + email_sent_at, plan_billing, sales_leads, stripe_events). All apply cleanly on a fresh DB through to head c6cbfc534fad.
B 9–12 Subscription.has_pro_entitlement + complimentary status; trial auto-downgrade in deps.py removed; require_active_subscription + require_verified_email_after_grace mounted on Pro routers (trees, sessions, scripts, FlowPilot, etc.).
C 13–16 BillingService.start_trial wired into /auth/register; /billing/checkout-session + idempotent apply_subscription_event (writes through stripe_events for replay safety); webhook handler dispatches to BillingService.
D 17–19 Google + Microsoft OAuth callbacks with oauth_identities linking; OAuth-only login + password-reset guards (return use_oauth_provider with linked providers list, anti-enumeration on /password/forgot).
E 20 Auto-send verification email at register; case-insensitive invite-email-match enforcement.
F 21–23 Wire EmailService.send_account_invite_email into POST /accounts/me/invites; new POST /me/invites/bulk and DELETE /me/invites/{id} (soft-revoke, idempotent).
G 24 GET /billing/state aggregates Subscription + PlanLimits + PlanBilling + resolved feature flags.
H 25 c6cbfc534fad migration sets all live accounts to complimentary Pro; preserves canceled / past_due.

Test plan

  • pytest --override-ini="addopts="1167 passed (35 deselected). Up from 1116 baseline = 50 net-new tests across Phase 1.
  • alembic heads → single head c6cbfc534fad.
  • Dark-launch verified: every new endpoint is gated by env config, not a feature flag — Google/Microsoft callbacks return 503 when client IDs unset; checkout requires STRIPE_SECRET_KEY; webhook receiver no-ops without STRIPE_WEBHOOK_SECRET. SELF_SERVE_ENABLED flag added (defaults false) for Phase 2 to consume.
  • Post-deploy: apply migration c6cbfc534fad once on prod (forward-only; no data loss; preserves canceled/past_due rows).

Conftest / test changes worth flagging

  • test_user fixture now seeds a Pro/active Subscription after registration so Pro-guarded routers don't 402 every authenticated test. Same for test_admin.
  • Two existing tests adapted because they assumed the old free-plan default: test_subscription_limits.py (the two free-plan tests downgrade inline) and test_kb_accelerator.py::TestQuota::test_get_quota (the kb_setup fixture downgrades to free). Behavior unchanged from the user's perspective; tests just declare their plan explicitly.

Followups deferred (logged in .ai/HANDOFF.md)

  • OAuth callbacks don't call _store_refresh_token. Refresh tokens issued by Google/Microsoft callbacks aren't persisted to refresh_tokens. Decide before Phase 2 dark-launch whether to extract _store_refresh_token to a shared module and call it from _sign_in_or_register.
  • stripe_enabled was relaxed to bool(STRIPE_SECRET_KEY) (down from also requiring STRIPE_WEBHOOK_SECRET). The webhook handler independently checks the webhook secret before calling construct_event, so signature verification is still safe — but audit other callers if any rely on the old shape.
  • backend/app/core/stripe_handlers.py is a stub module no longer referenced after Task 16. Safe to delete in a follow-up.

🤖 Generated with Claude Code

## Summary Backend foundation for public self-serve signup. Reuses the existing `subscriptions` / `plan_limits` / `feature_flags` / `account_invites` / `email_verification_tokens` infrastructure and adds four small tables (`oauth_identities`, `plan_billing`, `sales_leads`, `stripe_events`) plus columns on `users` / `accounts` / `account_invites`. Replaces the trial auto-downgrade in `deps.py` with two non-mutating dependencies, adds a real `BillingService` wrapping Stripe, extends the existing webhook stub, wires Google + Microsoft OAuth callbacks, makes register auto-send the verification email, and adds bulk-create + soft-revoke invite routes. Spec: [`docs/superpowers/specs/2026-05-05-self-serve-signup-onboarding-design.md`](docs/superpowers/specs/2026-05-05-self-serve-signup-onboarding-design.md) Plan: [`docs/superpowers/plans/2026-05-06-self-serve-signup-phase-1-backend.md`](docs/superpowers/plans/2026-05-06-self-serve-signup-phase-1-backend.md) ## What's in the diff | Phase | Tasks | Output | |---|---|---| | A | 1–8 | 9 alembic migrations (oauth_identities, password_hash nullable, role_at_signup + onboarding_step_completed, account wizard cols, invite revoked_at + email_sent_at, plan_billing, sales_leads, stripe_events). All apply cleanly on a fresh DB through to head `c6cbfc534fad`. | | B | 9–12 | `Subscription.has_pro_entitlement` + `complimentary` status; trial auto-downgrade in `deps.py` removed; `require_active_subscription` + `require_verified_email_after_grace` mounted on Pro routers (trees, sessions, scripts, FlowPilot, etc.). | | C | 13–16 | `BillingService.start_trial` wired into `/auth/register`; `/billing/checkout-session` + idempotent `apply_subscription_event` (writes through `stripe_events` for replay safety); webhook handler dispatches to BillingService. | | D | 17–19 | Google + Microsoft OAuth callbacks with `oauth_identities` linking; OAuth-only login + password-reset guards (return `use_oauth_provider` with linked providers list, anti-enumeration on `/password/forgot`). | | E | 20 | Auto-send verification email at register; case-insensitive invite-email-match enforcement. | | F | 21–23 | Wire `EmailService.send_account_invite_email` into `POST /accounts/me/invites`; new `POST /me/invites/bulk` and `DELETE /me/invites/{id}` (soft-revoke, idempotent). | | G | 24 | `GET /billing/state` aggregates Subscription + PlanLimits + PlanBilling + resolved feature flags. | | H | 25 | `c6cbfc534fad` migration sets all live accounts to complimentary Pro; preserves `canceled` / `past_due`. | ## Test plan - [x] `pytest --override-ini="addopts="` → **1167 passed** (35 deselected). Up from 1116 baseline = 50 net-new tests across Phase 1. - [x] `alembic heads` → single head `c6cbfc534fad`. - [x] Dark-launch verified: every new endpoint is gated by env config, not a feature flag — Google/Microsoft callbacks return 503 when client IDs unset; checkout requires `STRIPE_SECRET_KEY`; webhook receiver no-ops without `STRIPE_WEBHOOK_SECRET`. `SELF_SERVE_ENABLED` flag added (defaults `false`) for Phase 2 to consume. - [x] Post-deploy: apply migration `c6cbfc534fad` once on prod (forward-only; no data loss; preserves canceled/past_due rows). ## Conftest / test changes worth flagging - `test_user` fixture now seeds a Pro/active `Subscription` after registration so Pro-guarded routers don't 402 every authenticated test. Same for `test_admin`. - Two existing tests adapted because they assumed the old free-plan default: `test_subscription_limits.py` (the two free-plan tests downgrade inline) and `test_kb_accelerator.py::TestQuota::test_get_quota` (the `kb_setup` fixture downgrades to free). Behavior unchanged from the user's perspective; tests just declare their plan explicitly. ## Followups deferred (logged in `.ai/HANDOFF.md`) - **OAuth callbacks don't call `_store_refresh_token`.** Refresh tokens issued by Google/Microsoft callbacks aren't persisted to `refresh_tokens`. Decide before Phase 2 dark-launch whether to extract `_store_refresh_token` to a shared module and call it from `_sign_in_or_register`. - **`stripe_enabled` was relaxed** to `bool(STRIPE_SECRET_KEY)` (down from also requiring `STRIPE_WEBHOOK_SECRET`). The webhook handler independently checks the webhook secret before calling `construct_event`, so signature verification is still safe — but audit other callers if any rely on the old shape. - **`backend/app/core/stripe_handlers.py`** is a stub module no longer referenced after Task 16. Safe to delete in a follow-up. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
chihlasm added 27 commits 2026-05-06 23:19:17 +00:00
Adds docs/superpowers/specs/2026-05-05-self-serve-signup-onboarding-design.md.
Six-section design for opening ResolutionFlow to public self-serve registration
with a 14-day reverse trial on Pro, Stripe-backed billing, sales-assist
Enterprise lane, and a hybrid welcome wizard + dashboard onboarding.

Reuses existing infrastructure (subscriptions, plan_limits, feature_flags,
plan_feature_defaults, account_feature_overrides, account_invites,
email_verification_tokens, /admin/plan-limits, /admin/feature-flags,
/accounts/me/transfer-ownership, /webhooks/stripe stub). New schema is
intentionally small: oauth_identities, plan_billing (sibling to plan_limits),
sales_leads, stripe_events, plus column additions for OAuth identity model
nullability, wizard step state, and pilot-account complimentary status.

Replaces deps.py:109 trial auto-downgrade with a non-mutating computed
expiry check enforced by a new require_active_subscription dep. Adds a
sibling require_verified_email_after_grace dep to enforce the 7-day email
verification grace at the API layer (frontend wall is UX over the same rule).

Defers promo codes from v1. No new combined /admin/plans surface — existing
admin endpoints handle plan/feature configuration with extended response
shape.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds two phase plans alongside the spec at
docs/superpowers/specs/2026-05-05-self-serve-signup-onboarding-design.md:

- Phase 1 (backend foundation, 26 tasks across 8 sub-phases A-H):
  schema migrations, subscription model + new guards, BillingService,
  Stripe webhook handler extension, OAuth callbacks, email verification
  auto-send + email-match enforcement, account-invite extensions,
  GET /billing/state, pilot user backfill. Step-by-step granularity
  with full code blocks per writing-plans skill.

- Phase 2 (frontend + cutover, 21 tasks across 7 sub-phases I-O):
  Phase-1-deferred endpoints, useBillingStore + hooks + gating
  components, register redesign + OAuth buttons + accept-invite,
  welcome wizard, dashboard redesign, pricing page + contact-sales,
  beta-signup deprecation, cutover. Higher-altitude — defines
  contracts, acceptance criteria, integration tests; leaves
  component-detail decisions to implementer.

Each phase ends in a mergeable PR. Cutover is gated behind
SELF_SERVE_ENABLED + VITE_SELF_SERVE_ENABLED. Execution deferred to
a future session.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Mounts on Pro routers (trees, sessions, scripts, FlowPilot, etc.) and
returns 402 with structured detail when an account's subscription is
missing or locked. Allowlist bypasses billing/account/auth flows so
users can recover from a lapsed subscription.

Conftest now seeds a default Pro/active Subscription on test_user and
test_admin (delete-then-insert because the register endpoint already
creates a free/active sub by default). Two existing tests adapted to
the new seeded plan; tenant-isolation tests seed Subscription rows for
the accounts they create directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The kb_setup fixture asserts free-plan quota numbers (lifetime_conversions_limit=3),
but Phase 1 conftest seeds test_user on Pro. Downgrade explicitly inside kb_setup
to preserve the original test intent without affecting other suites.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
docs(handoff): capture Phase 1 backend completion + followups
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
CI / frontend (pull_request) Successful in 6m0s
CI / backend (pull_request) Successful in 11m15s
CI / e2e (pull_request) Successful in 10m4s
fbb41e789c
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
chihlasm merged commit f918b766b0 into main 2026-05-06 23:46:35 +00:00
Sign in to join this conversation.