feat(backend,web): record trial state server-side; wire trial CTA - #2740
Merged
Conversation
tyler-dane
force-pushed
the
claude/keyboard-education-billing-trial
branch
3 times, most recently
from
August 10, 2026 19:55
3523246 to
e6d085e
Compare
Base automatically changed from
claude/keyboard-education-onboarding-9cc41b
to
main
August 10, 2026 19:57
Adds durable, server-side trial/subscription state on the user record (optional field, no backfill needed) via a new billing module: POST /api/billing/trial/start (rate-limited) and GET /api/billing/status, idempotent so retries never push the trial window out further. The web trial CTA now calls this for real instead of a no-op stub. Placeholder terms only (14-day trial, single default plan) per keyboard-education/03-monetization-trial-checkout.md -- isolated in one constants module so the founder's pricing sweep is a small diff. Explicitly NOT done in this pass, so it isn't mistaken for a working checkout: no Stripe integration, no payment-method collection, no webhooks, and no read-only access enforcement wired into the actual event-mutation routes (the status API returns isReadOnly, but nothing currently checks it before allowing a write). See the brief for what remains.
tyler-dane
force-pushed
the
claude/keyboard-education-billing-trial
branch
from
August 10, 2026 19:58
e6d085e to
66d92c1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on #2736 (uses
PostOnboardingFlow/TrialCTA.tsxfrom that branch).billingmodule:POST /api/billing/trial/start(rate-limited, 5/min) andGET /api/billing/status.startTrialis idempotent — a retried or duplicate CTA click can never push the trial window out further.Schema_User(billing?: Schema_UserBilling) with no backfill needed — matches the existing incremental-field pattern on that schema (seelastSeenAt).keyboard-education/03-monetization-trial-checkout.md, isolated in oneBILLING_DEFAULTSconstants module so the founder's pricing sweep is a small diff, not a re-implementation.TrialCTAnow calls this for real (for authenticated users) instead of a no-op stub, with a loading state and error toast on failure.Explicitly NOT done in this pass
So this isn't mistaken for a working checkout:
GET /api/billing/statusreturnsisReadOnly, but nothing currently checks it before allowing a write. That's real scope for a follow-up, not a security claim this PR is making.express-rate-limitwasn't a dependency anywhere) — added it fresh for this route per the CodeQLjs/missing-rate-limitinggate on new authenticated routes.Test plan
bun run test:backend:fast— 243/243 passing (includes newderiveBillingStatusunit tests: none/trialing/expired/exact-boundary)bun test src/components/PostOnboardingFlow/— 4/4 passingbun run type-check— clean across core/backend/webbiome checkon all touched files — cleanSYNC_SERVICE_URL/SYNC_INTERNAL_AUTH_TOKENenv vars not present here (pre-existing environment gap, unrelated to this change). Routes were verified by reading the existingUserRoutespattern this mirrors exactly (CommonRoutesConfig,verifySession(), controller → service →mongoService.user), not by hitting them live.🤖 Generated with Claude Code