G Growreplies docs

Platform admin

Plans & Stripe sync

Plans are the only piece of customer-facing data that admins create directly. The Plan CRUD page (/admin/plans) is paired with Stripe so you never touch the Stripe dashboard to provision Products and Prices — every save here syncs to Stripe automatically.

The plans table

/admin/plans lists every plan with its core attributes, the workspace count using it, and a sync status pill (green = in sync with Stripe, amber = pending, gray = local-only / free).

ColumnNotes
NameDisplay name. Editable.
SlugStable identifier. Locked after creation — workspaces.plan_id resolves by slug indirectly through the Plan table, and changing it would break invoices.
Monthly conversationsQuota.
PriceMonthly price. Changing it archives the old Stripe Price + creates a new one.
WorkspacesHow many workspaces are on this plan today.
Stripe IDsProduct + Price IDs after sync. Free / custom plans show "—".
ActiveToggle. Inactive plans aren't selectable on the customer side.

Creating a plan

New plan opens the form. Fields:

  • Name — required.
  • Monthly conversations — required. 0 = unlimited.
  • Price (cents) — required. 0 = free / custom (skips Stripe).
  • Features — toggles: remove_branding (and future flags).
  • Active — defaults to true.

On save, the server creates the local row, then triggers StripeProductSync::syncPlan(). If the price is > 0, a Stripe Product + Price are created and their IDs saved on the plan row. If Stripe is unreachable or misconfigured, the local row is kept and a flash error explains the failure — you can retry the sync without re-saving the form.

The Sync button

Each row has a Sync action that fires StripeProductSync::syncPlan() directly. Returns JSON with the result so the UI can show "Synced" / error inline without a page reload. Useful when:

  • You changed the Stripe key and want to re-bind everything.
  • A previous sync failed and you've fixed the underlying issue.
  • You want to verify a plan's Stripe state without touching the form.

Editing

Edits behave intuitively except for two subtleties:

  • Price changes rotate the Stripe Price. Stripe Prices are immutable, so we archive the old and create a new one. Existing subscriptions stay on the old Price (grandfathered); only new subscriptions use the new one.
  • Slug is locked. The form input is disabled in edit mode.

Deleting

Plans are never destructively deleted. The destroy action soft-deletes (is_active = false) and archives the Stripe Product. Reasons:

  • workspaces.plan_id is a real foreign key — deleting would orphan or cascade.
  • Historical invoices reference the plan; we need to be able to look it up forever.
  • Subscriptions in flight need a stable plan to attach to.

Reactivating a soft-deleted plan: edit it and toggle Active back on. The Stripe Product is unarchived and the plan is selectable again.

Free / custom plans

Plans with price_cents = 0 never sync to Stripe. They live only in Pitchbar — useful for the default Free plan and for hand-rolled enterprise deals where you want the quota and feature flags but invoice out-of-band.

Currency

Set globally via CASHIER_CURRENCY in the environment. Defaults to USD. Changing the currency mid-flight on a deployment with existing Prices is a manual migration — you'd archive every Stripe Price, change the env var, then sync each plan to mint new Prices in the new currency.