Skip to content

feat(orgs): trial mode — instance-admin managed trials + self-serve Stripe trial#436

Merged
angel-manuel merged 1 commit into
devfrom
feat/org-trial-mode
Jul 7, 2026
Merged

feat(orgs): trial mode — instance-admin managed trials + self-serve Stripe trial#436
angel-manuel merged 1 commit into
devfrom
feat/org-trial-mode

Conversation

@angel-manuel

Copy link
Copy Markdown
Contributor

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

  • New plan='trial' tier + orgs.trial_ends_at (migration 093, mirroring 052; the CHECK made adding a tier a one-liner). Lifecycle is derived from trial_ends_at — no status column, no background sweep.
  • Endpoints (session-only InstanceAdminAuth, act on any org by id):
    • POST /v1/orgs/{id}/trial — start/restart (defaults to TRIAL_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 to standard|free_unlimited (clears the window). This is how Reveni opts out.
  • Each write invalidates the billing-tier cache.

2. Self-serve trial (the "all users" requirement)

The "Not sure. Trial for free for a month" toggle on /billing/new-team drives Stripe's subscription_data[trial_period_days]: a card is collected but the first charge is deferred by the trial window (Stripe emits status='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_unlimited orgs are exempt from both mechanisms.

Plumbing

  • billing_tier: FreeUnlimitedCache generalized to cache (plan, trial_ends_at) and answer both is_free_unlimited and trial_status (30s TTL; fail-closed for free-unlimited, fail-open for trial).
  • /auth/me/identity gains a trial summary so every member sees the banner (the subscription endpoint is admin-only); that endpoint also gets a synthetic trialing/trial_expired branch.
  • Config knob TRIAL_DEFAULT_DURATION_DAYS (default 30) + .env.example.
  • Removes the now-orphaned org::get_plan (subsumed by get_billing).

Tests

crates/overslash-api/tests/trial.rs (12 tests) + billing_tier unit tests: repo set/extend/opt-out; endpoint auth + audit; subscription active/expired; me/identity summary + free_unlimited exemption; and the banner-only proof — an expired trial is neither blocked nor granted unlimited. All pass; free_unlimited regression suite still green. Ran /vet (clean) and drove the flow end-to-end on the real stack (screenshots below show the real POST /v1/orgs/{id}/trialtrialing subscription).

Screenshots

Self-serve toggle (/billing/new-team)
toggle

Active trial banner (all members) & expired banner
active
expired

Instance-admin panel (/org) — start / extend / opt-out
admin

Notes

  • Migration numbered 093 to clear origin/dev's 092.
  • Non-goals (unchanged): full paid-conversion/dunning flow, seat enforcement, a standalone instance-admin org console (the set/extend control lives on the org page for now).

🤖 Generated with Claude Code

…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>
@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
overslash Ready Ready Preview, Comment Jul 7, 2026 10:18am

Request Review

@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

@angel-manuel angel-manuel merged commit 630be78 into dev Jul 7, 2026
15 checks passed
@angel-manuel angel-manuel deleted the feat/org-trial-mode branch July 7, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant