Skip to content

Cloud: Stripe card trial after sign-up, ended-trial wall, live connection check - #806

Merged
keysersoft merged 8 commits into
mainfrom
keysersoft/card-trial
Sep 30, 2026
Merged

keysersoft merged 8 commits into
mainfrom
keysersoft/card-trial

Conversation

@keysersoft

Copy link
Copy Markdown
Contributor

Three changes to the Cloud funnel (trial to paid is ~2%; of the users who get a first MCP call working, 37% pay).

Card trial

  • After sign-up and email verification, admins land on /start-trial: 7-day free trial with a card through Stripe (€0 today, converts unless cancelled). "Continue without payment details" is a small link in the corner and keeps today's no-card trial.
  • POST /api/license/checkout-link (cloud, ADMIN): asks the licence site for a one-time checkout intent with the user's own email and workspace; the card trial ends when the free trial would have.
  • Trial banner: "Add a payment method, no charge before ". Licence wall after the trial: plan picker, pay now.
  • The plan chosen on the pricing page (?plan=&period=) survives sign-up and verification.

Ended trials were shown "Start 7-Day Free Trial"

  • /api/license/status answered plan: null for an expired trial, so ~1,350 workspaces got a trial button the licence site never grants twice instead of "your trial has ended". It now reports the latest inactive licence (plan + status, no features). Gating unchanged.

Activation

  • Connect page: live check that turns green when the first request from an AI client arrives (GET /api/mcp-servers/:id/activity, org-scoped).
  • Dashboard: "Next step: connect your AI client" for workspaces with connectors and no calls.

Also: returning customers with a revoked/expired paid licence can buy again; LICENSE_API_URL overrides the licence site URL (already set by the cloud compose file).

Tested end to end locally in cloud mode against the website branch and Stripe test mode: sign-up → verify → /start-trial → Checkout (trial) → licence active in the app → trial end → first payment; skip path; expired-trial wall → pay-now checkout; live connection check.

Pairs with website PR (card trial on the licence site). Deploy the website first.

Backend: POST /api/license/checkout-link (Cloud only, ADMIN, throttled)
asks the licence site for a Stripe Checkout intent on behalf of the
signed-in admin. Email and workspace come from the session; a card trial
ends when the workspace's free trial would have, and after the trial the
link is a plain purchase. A consented Google Ads click id is passed as
ad metadata. Paying workspaces get 409 (plan changes go through the
billing portal); licence-site failures map to a generic 502.

Frontend: new /start-trial page with a plan picker (Starter, Team,
Business; monthly or yearly), shown once to cloud admins on a running
trial, before the welcome wizard, with a small "Continue without payment
details" link that keeps the no-card trial. The plan picked on the
pricing page is kept through email verification. The trial banner,
licence wall (after the trial) and licence settings open the checkout.
Self-hosted behaviour is unchanged.
/api/license/status answered plan null for a Cloud workspace whose trial had
run out, the same as for one that never had a licence, so the licence wall
offered 'Start 7-Day Free Trial' (which the licence site does not grant
twice) instead of 'your trial has ended' and the plans. It now reports the
latest inactive licence's plan and status, without features; gating is
unchanged. The trial banner hides for an ended trial, the activation page
reloads fully so the shell drops the trial banner, and LICENSE_API_URL (set
by the cloud compose file) overrides the licence site URL.
…nnect a client

Most workspaces that build a connector never send an MCP request, and the
connect page never told them whether what they did in Claude or ChatGPT
worked. It now polls the server's activity (new GET /api/mcp-servers/:id/
activity, org-scoped) and turns green the moment the first request lands
(product event first_call_seen). The dashboard sends workspaces with
connectors but no calls to that page.
const offerCard = cardTrialEligible({
isCloud: deploymentMode === 'cloud',
role: user?.role,
license: { plan, status: status ?? '', expiresAt, trialDaysLeft: daysLeft },
…lready registered' notice, trial without card made explicit

- Cloud login flagged every admin as needing licence setup (it read an
  instance-wide key cloud never writes), so the app showed 'Trial Activated!'
  at each sign-in, '0 days' to paying workspaces. Only a workspace with no
  licence at all needs setup; the client shows the card only for a trial
  started just now.
- Signing up with an address that already has an account (and its password)
  signs in as before, now with a notice saying so. Only the account's owner
  can see it, so it reveals nothing.
- The trial offer says plainly that the trial also works without a card.
- LICENSE_API_URL applies outside production only: in production a URL from
  the environment would let a self-hosted operator point licence checks at a
  server of their own.
…nge in cloud, card trial never lengthened

- EmailVerifiedGuard ran before AuthGuard('jwt'), found no req.user and let
  every request through: an unverified cloud account could call the whole
  API (and open a checkout). It now reads the session token itself. Audited
  on cloud first: none of the 427 unverified accounts has a connector, a
  call or a subscription.
- Cloud: PUT /api/users/me no longer changes the email address (it neither
  re-verified nor asked for the password, so a stolen session could move an
  account to another address). The app never did it; self-hosted unchanged.
- A card trial needs 48 hours of the free trial left; with less the plan is
  sold without a trial instead of stretching it to Stripe's minimum.
@keysersoft
keysersoft merged commit de5a43f into main Sep 30, 2026
13 checks passed
@keysersoft
keysersoft deleted the keysersoft/card-trial branch September 30, 2026 17:27
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants