feat(orgs): trial mode — instance-admin managed trials + self-serve Stripe trial#436
Merged
Conversation
…tripe trial
Adds a time-boxed trial capability for orgs, with two mechanisms unified into
one dashboard banner:
- Instance-admin managed trials: new `plan='trial'` tier + `orgs.trial_ends_at`
(migration 093, mirrors 052). Instance admins start (POST /v1/orgs/{id}/trial,
defaulting to TRIAL_DEFAULT_DURATION_DAYS=30), bump (PATCH .../trial, extends
from the later of current-end/now), and opt out (PATCH .../plan →
standard|free_unlimited, clearing the window). Each invalidates the cache.
- Self-serve trials: the "Not sure. Trial for free for a month" toggle on the
create page drives Stripe's subscription_data[trial_period_days] — card on
file, first charge deferred, status='trialing'. No billing bypass.
Enforcement is banner-only (DECISIONS D25): an expired trial keeps serving API
calls and stays under normal rate limits (it is not free_unlimited). Expiry only
changes dashboard messaging.
- billing_tier: FreeUnlimitedCache generalized to cache (plan, trial_ends_at)
and answer both is_free_unlimited and trial_status (30s TTL, fail-open).
- /auth/me/identity gains a `trial` summary so every member sees the banner;
admin-only /v1/orgs/{id}/subscription gets a synthetic trial branch.
- Dashboard: TrialBanner in the layout (active/expired), instance-admin
start/extend/opt-out panel on the org page, self-serve toggle on new-team.
- Config knob TRIAL_DEFAULT_DURATION_DAYS (default 30) + .env.example.
- Tests: repo, endpoints (auth + audit), subscription active/expired,
me/identity summary + free_unlimited exemption, and a banner-only proof that
an expired trial is neither blocked nor granted unlimited. billing_tier unit
tests for the trial-status derivation.
Removes the now-orphaned org::get_plan (subsumed by get_billing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
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.
What
Adds a time-boxed trial mode for orgs (default 1 month, configurable and extendable by instance admins), plus a self-serve trial toggle on the org-create page. Two mechanisms, unified into one dashboard banner.
1. Instance-admin managed trials
plan='trial'tier +orgs.trial_ends_at(migration 093, mirroring052; the CHECK made adding a tier a one-liner). Lifecycle is derived fromtrial_ends_at— no status column, no background sweep.InstanceAdminAuth, act on any org by id):POST /v1/orgs/{id}/trial— start/restart (defaults toTRIAL_DEFAULT_DURATION_DAYS, 30).PATCH /v1/orgs/{id}/trial— bump; extends from the later of current-end/now so an expired trial still gets a fresh window.PATCH /v1/orgs/{id}/plan— opt out tostandard|free_unlimited(clears the window). This is how Reveni opts out.2. Self-serve trial (the "all users" requirement)
The "Not sure. Trial for free for a month" toggle on
/billing/new-teamdrives Stripe'ssubscription_data[trial_period_days]: a card is collected but the first charge is deferred by the trial window (Stripe emitsstatus='trialing'). Per the decision to require Stripe + card, don't bill for a month, this is not a free org — it closes the billing-bypass hole that a free self-serve org would open.Enforcement: banner-only (see DECISIONS D25)
An expired trial keeps serving API calls and stays under normal rate limits — it is not
free_unlimited, so no bypass, and there is no hard gate in the rate-limit middleware. Expiry only changes dashboard messaging.free_unlimitedorgs are exempt from both mechanisms.Plumbing
billing_tier:FreeUnlimitedCachegeneralized to cache(plan, trial_ends_at)and answer bothis_free_unlimitedandtrial_status(30s TTL; fail-closed for free-unlimited, fail-open for trial)./auth/me/identitygains atrialsummary so every member sees the banner (the subscription endpoint is admin-only); that endpoint also gets a synthetictrialing/trial_expiredbranch.TRIAL_DEFAULT_DURATION_DAYS(default 30) +.env.example.org::get_plan(subsumed byget_billing).Tests
crates/overslash-api/tests/trial.rs(12 tests) +billing_tierunit tests: repo set/extend/opt-out; endpoint auth + audit; subscription active/expired;me/identitysummary + free_unlimited exemption; and the banner-only proof — an expired trial is neither blocked nor granted unlimited. All pass;free_unlimitedregression suite still green. Ran/vet(clean) and drove the flow end-to-end on the real stack (screenshots below show the realPOST /v1/orgs/{id}/trial→trialingsubscription).Screenshots
Self-serve toggle (

/billing/new-team)Active trial banner (all members) & expired banner


Instance-admin panel (

/org) — start / extend / opt-outNotes
origin/dev's092.🤖 Generated with Claude Code