Credit allotment is now derived directly from BillingCycle::for($account)->creditAllotment()
instead of a cached Pennant feature, removing the dynamic cache-invalidation footgun
(forgetPlanFeatureCache) that had to be called from every subscription/workspace mutation.
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.
A past_due subscription made subscribed() return false (Cashier default),
so EnsureAccountReady redirected to /subscribe — which starts a brand-new
checkout and creates a second subscription. Past-due users already have a
subscription; they only need to update their payment method.
Enable Cashier::keepPastDueSubscriptionsActive() so past_due counts as
active and users keep navigating. Surface a past-due notice in the sidebar
footer linking to the Stripe billing portal (not /subscribe). Both trial
modes (subscription trial / generic trial) are unaffected.
Add a trypost config toggle to switch between card-required checkout trials and no-card signup trials, and wire signup, checkout, access gating, UI copy, and tests to both modes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Revert the no-card signup trial flow so access depends on a Stripe subscription trial started at checkout, preventing app access before a payment method is collected.
Co-authored-by: Cursor <cursoragent@cursor.com>
Use Account::postsCountCacheKey everywhere, avoid findOrFail when syncing
post deletes, dispatch SyncAccountUsage after DeleteWorkspace when enabled,
and add coverage for the new paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
Centralizes 'what date should the UI show as trial end?' on the model.
Returns null when not on trial, the subscription's trial date when on
trial-with-card, or the generic trial date for no-card users.
New signups land on a 7-day generic trial (Cashier trial_ends_at) without
a Stripe customer or subscription. Account is on Starter plan limits during
the trial. After 7 days, EnsureAccountReady redirects to /subscribe per the
existing flow.
- CreateUser sets account.trial_ends_at and plan_id = Starter
- EnsureAccountReady allows access when subscribed() OR onGenericTrial()
- Account::isOnTrial() includes generic trial check
Existing users unaffected: paying users have a subscription;
never-paid users continue redirecting to /subscribe.
Class names and method signatures already explain what these classes
do. Project guideline (CLAUDE.md): comments only when the WHY is
non-obvious — implementation details belong in commits, not noise on
top of every class. Kept the one comment that earns it: the (int)
cast in HasUsage::cachedPostCount, which documents the load-bearing
workaround for Laravel's Redis cache numeric optimisation.
Replaces the implicit Account::booted() observer with an explicit
Account::forgetPlanFeatureCache() method called from each plan_id
mutation site (StripeEventListener x3, BillingController x2). Self-hosted
installs naturally never reach any of these callsites — Stripe webhooks
do not fire and the billing controllers redirect to /calendar before any
plan mutation happens — so the Pennant flush is now guaranteed to be a
cloud-only operation.
Adds integration coverage proving the full chain webhook -> plan_id
update -> Pennant flush -> next Feature::value resolves against the new
plan limit.
- Billing settings page restructured around the design system: dropped
the 280px-label / 1fr-content split for stacked HeadingSmall sections;
current plan rendered as a hero card with display-font name, price,
amber sticker tile, and an inline primary "Change plan" CTA; payment
method consolidated into a single sticker row with a violet card-icon
tile and the manage CTA on the same line; empty payment-method state
handled. Invoices keep the sticker-row treatment.
- Upgrade dialog mirrors Subscribe.vue end-to-end: planTones backgrounds
per slug, ⭐ POPULAR / CURRENT badges absolute-positioned, ink-bordered
rounded-full CTA pill, sticker check icons, "Everything in {plan}"
copy, IconInfoCircle tooltip on credits, yearly/monthly toggle pill
with rotating amber save-months badge. While a request is in flight
every plan button is disabled and the active one shows IconLoader2
spinner.
- Account model gains `displayablePaymentMethod()` returning the
card array used by the UI. Resolves the customer-level default first,
falls back to the first attached payment method (Stripe Checkout trials
anchor the card to the subscription rather than the customer, so the
customer-level lookup returns null even when a card exists).
- i18n: added `billing.subscription.expires_on` and `no_payment_method`
in en/pt-BR/es.
- Sidebar reorganized: Workspace group (connections, hashtags, labels,
API keys, settings) and Account group (settings, usage, billing)
- Account group only visible to owner and hidden in self-hosted mode
- Onboarding simplified: role -> account (connect socials) -> completed
-> redirect to /subscribe. Removed Subscription setup step.
- Subscribe page redesigned with 4 plan cards, monthly/yearly toggle,
trial info, and per-plan features list
- Billing page redesigned following Sendkit layout (sections with
sidebar labels)
- Processing page uses usePoll with immediate watch for subscription
activation
- Cancel URL redirects directly to /subscribe
- Account settings page with name and billing_email (syncs with Stripe)
- Usage page with ring meters for all plan limits
- Settings layout tabs only for user pages (profile, password,
notifications). Workspace/API keys/billing are standalone pages.
- GoogleAuthButton extracted as reusable component
- WorkspaceRole TypeScript enum for type-safe role checks in frontend
- Trial period changed to 7 days
- Fixed onboarding loop when user confirms email
- All 1101 tests passing
- Create Account model as Cashier Billable entity (stripe, plan, subscription)
- Account owns workspaces and has an owner_id (User)
- User belongs to one Account via account_id
- Workspace belongs to Account via account_id, no longer has billing fields
- Remove Brand model entirely (workspaces serve as grouping)
- Rename brand_limit to workspace_limit in plans
- Workspace roles simplified: admin/member/viewer (owner via Account)
- Invites now belong to Account with workspaces JSON array
- Pennant features scope changed from Workspace to Account
- EnsureSubscribed middleware checks Account subscription
- All controllers updated: BillingController, OnboardingController,
WorkspaceInviteController, SocialController, StripeEventListener
- Frontend: extract GoogleAuthButton component, create WorkspaceRole
enum for type-safe role checks, fix all views for new architecture
- All 1101 tests passing