D Dronggo 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.
  • Monthly messages — optional. Caps every visitor message across the workspace for the calendar month. Leave blank for no extra cap.
  • Max tokens per response — optional. Hard ceiling on LLM reply length. Min 100, max 8000.
  • Price (cents) — required. 0 = free / custom (skips Stripe).
  • Features — toggles: remove_branding (and future flags).
  • Active — defaults to true.

Resource limits (per-workspace caps)

The Resource limits card lets admins differentiate plan tiers beyond AI rate quotas. Every field accepts a positive integer, the literal 0, or blank:

  • Blank = unlimited. Every pre-1.3 plan was migrated to NULL on all six columns, so existing customers are never retroactively capped.
  • 0 = hard block. Useful for the Free tier ("no integrations on this plan").
  • Positive integer = absolute cap. Counting honours soft-deletes (a trashed agent does not count) and pending invitations DO count toward the member cap (otherwise a workspace could queue 100 invites and accept them all later).
FieldWhat it capsHow it's counted
agents_limitAgents per workspaceAgent::where('workspace_id', X) — every non-trashed agent.
sources_limitKnowledge sources across all agents in the workspaceSum of Source rows whose agent is owned by the workspace. The /app/agents Knowledge column renders this as used/limit (or just used when the plan is unlimited) — see [[agent-index-knowledge-column]].
workflows_limitWorkflows per workspaceWorkflow::where('workspace_id', X)
integrations_limitSlack/etc connections + outbound webhook subscriptionsIntegrationConnection + WebhookSubscription rows, summed.
members_limitSeats per workspaceAccepted workspace_users rows + non-expired pending invitations.
api_accessWhether workspaces on this plan can mint API tokensCheckbox. Defaults to ON for back-compat.

When a workspace hits a cap, the affected "Create" endpoint redirects back with a flash error message: "You've reached your plan's limit of N agents. Upgrade to add more." Frontends render the flash banner without needing per-resource code.

The enforcement lives in App\Services\Billing\PlanLimits. Tests under tests/Feature/PlanLimitsTest.php cover every controller + every "NULL = unlimited" back-compat path.

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.

Free trial plans

Toggle Free trial plan on a plan to turn it into a no-credit-card, time-limited trial, and set a Trial length in days (commonly 7, 14, or 30; blank defaults to 14). When a new user signs up on this plan, their workspace starts a trial that runs for that many days — no payment method is collected up front.

Pair it with Default plan for new signups to put every new account on a trial automatically. The quotas and feature flags you set on the trial plan are exactly what the customer gets during the trial, so you control how much of the product the trial unlocks.

When the trial period ends and the customer hasn't subscribed to a paid plan, the workspace is walled: every customer screen redirects to the billing page with an "upgrade to continue" prompt. Their agents, sources, and conversations are preserved — nothing is deleted — and access is restored the moment they pick a paid plan. The billing page, account settings, and sign-out stay reachable so the user is never trapped. A slim "X days left in your trial" banner appears across the app while the trial is active.

Trial state is computed from the workspace's plan-subscription row (status trialing with a current_period_end in the future), so there is no scheduler to run — a trial lapses the instant its end date passes.

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.