Signup already captures a click ID per ad network -- gclid, fbclid,
li_fat_id, ttclid, rdt_cid, epik -- but only the UTM parameters reached
the subscription. UTMs say which campaign a customer came from; the click
ID identifies the individual click, which is what Google and Meta need to
match a subscription back to the ad that produced it.
Every key is spelled out at the call site, so what reaches Stripe is
readable in one place without following a constant.
Click IDs are text columns on purpose, since truncating them at 255 would
destroy the value. Stripe caps a metadata value at 500 characters and
rejects the request rather than truncating, so an unbounded column
reaching it would fail the whole checkout and the customer could not
subscribe at all. Values are cut to 500 before they are sent. The UTM
columns are varchar(255) and cannot reach it; this exists for the text
ones.
Every account arrives with the UTM parameters that brought it and the
onboarding answers it gave, but none of that reached Stripe. Revenue
lived in one system and the campaign that produced it in another, so
answering "which campaign is paying for itself" meant joining the two by
hand on email.
The account owner's five UTM parameters, persona, goals and referral
source now ride along as subscription metadata. Cashier's withMetadata()
puts them in subscription_data.metadata during Checkout, so they land on
the Stripe Subscription rather than the session: the session is gone in
24 hours, the subscription carries the attribution for as long as the
customer does, and it shows up on the subscription in the dashboard.
Stripe metadata holds strings, so the two enum-cast columns are unwrapped
to their backing value and goals -- a JSON column, the one list answer --
is joined with commas. Passing either through as-is fails the request.
array_filter drops the keys the user never filled in, since an absent key
reads the same in reporting and Stripe treats null as a delete.
The account may have no owner, so every read is null-safe and an account
without attribution simply sends no metadata.
* Make Stripe Checkout configurable via billing env knobs
Replace the hard-required $1 first-month coupon with env-driven trial days,
optional coupon, and allow_promotion_codes so SaaS can switch checkout modes
without a code change.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Fix no-effect ReflectionClass import in checkout test
CI treats bare global use statements as ErrorException and aborts
loading the suite before any assertions run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Document checkout env knobs in AGENTS.md instead of .ai/
Remove the Boost record-rule .ai/rules folder and keep durable billing
checkout guidance in AGENTS.md / CLAUDE.md project-specific rules.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Harden checkout env knobs from review findings
Default allow_promotion_codes to false, grant Stripe trial only to
first-time subscribers, clarify the coupon/promo XOR error, and cover
negative XOR cases plus StartSubscriptionCheckout wiring.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
The fixed amount_off coupon only nets $1 for a quantity of one, and the
offer is meant for genuinely new signups. A lapsed account that kept
several workspaces and re-subscribes through onboarding would otherwise
be charged N*price - amount_off (not $1), and a returning customer could
whittle down to one workspace to claim the discount again.
Apply the coupon only when the paid first month is enabled, the account
bills a single workspace, and it has never subscribed before; every other
checkout falls back to allowing promotion codes at the full price.
A $0 trial only runs a weak card authorization, so invalid cards slip
through and only fail once the real charge fires days later. Applying a
$11-off, duration=once Stripe coupon at checkout charges $1 for real on
the first invoice instead, which validates the card immediately, then
reverts to the full monthly price on the next invoice with no manual
swap needed.
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
(workspace/social/member) and the legacy plan tiers (single Workspace plan).
Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
(Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
(users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
price. 8-day trial so Stripe displays 7.
Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.
System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
(system feature, not the user's usage).
Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.