Billing, credits, and seats
Project88's V3 billing model — org plans govern features in org canvases, personal plans cover off-org routes, and seats govern credits.
Project88's billing model (V3) separates two questions that earlier versions conflated:
- What features can I use? — answered by the plan of whichever org you're currently working in (or your personal plan, if you're on an off-org route).
- How many LLM credits can I burn? — answered by your seat in that org (which caps your usage allocation), with personal credits as the off-org fallback.
The V3 model
| Concept | Where it lives | Governs |
|---|---|---|
| Org plan | org_billing.plan_tier | Features in an org canvas |
| Personal plan | user_billing.personal_plan_tier | Features off-org |
| Org seat (assigned tier) | org_seats.assigned_tier | Credit allocation + usage policy |
| Personal credits | user_billing.credits_balance | Off-org LLM usage |
The effective feature tier is resolved by resolveEffectiveTier({ orgPlan, personalTier }):
- Inside an org canvas (
/<org-slug>/...) the org plan wins. If your org is onpro, every member of the org getsprofeatures inside that org — regardless of seat row state. - Off-org routes (settings, profile, personal pages) fall back to your personal plan.
This is a deliberate change from V2, where a missing seat row could silently lock paid-org members out of features the org had already paid for. In V3, the org plan is the single source of truth for org-canvas feature gating, and seats are no longer in that path.
Credits and deduction
Credits are billed per user against whichever wallet is in scope:
- Inside an org canvas, LLM usage is allocated against the user's
org-seat allowance (capped by
org_seats.assigned_tier). - Off-org, LLM usage deducts from the user's personal balance
(
user_billing.credits_balance).
The flow:
- The browser starts a chat completion via the
chat-proxyEdge Function. - The proxy streams the response back, including a final
usageevent. hosted.js(src/services/backends/hosted.js) catches the usage event and callslogUsage(), inserting a row intousage_logsand triggering credit deduction.- Supabase emits a real-time update on
user_billing(andorg_billing/org_seatswhen applicable) via Postgres channels —REPLICA IDENTITY FULLis set on both org tables so the realtime filtering carries every column. - The browser's billing store (
src/store/billing.jsx) subscribes via a single hoistedBillingProvider(mounted once at the app root) and updates balances in memory. - The
CreditsDisplaypill in the top bar animates digit-by-digit to the new balance.
The whole loop is sub-second — you see credits tick down as the response finishes streaming.
get_effective_billing RPC
get_effective_billing(p_org_id uuid) is the single read path the
client uses to populate BillingProvider. It bundles user_billing,
the caller's org_seats row, and org_billing into one atomic JSONB
response so the UI never renders against a partial snapshot.
usage_logs schema
From migration 002_managed_tables.sql:
id,org_idprovider,modelprompt_tokens,completion_tokenscreated_at
Totals are derived by summing the two token columns at read time. The
/status/* pages and any future usage dashboards read from this table.
Org seats and governance
Owners and admins control governance from Settings → Members
(OrgMembersPanel.jsx) via three RPCs:
update_org_member_role— promote / demote between owner, admin, memberremove_org_member— kick a member out of the orgtransfer_org_ownership— hand ownership to another member
When a member joins, an org_seats row is created with an
assigned_tier capped by org_billing.plan_tier. Removing a member
soft-deletes the seat; their personal plan and personal credits are
unaffected.
org_seats rows are visible only to the seat holder and to org
admins / owners (the SELECT policy splits self-read from admin-read-all),
so coworkers cannot see each other's credit caps.
Seat management — invites, role changes, tier ceilings — runs through
the manage-org-seats edge function, which enforces both org-admin
auth and the per-tier ceiling against org_billing.plan_tier.
Plans
| Plan | Notes |
|---|---|
free | Default. Cap on monthly credits. |
pro | Higher cap; richer tools and limits. |
enterprise | Custom — talk to us. |
Plan-driven feature checks all flow through planFeatures so every
gate reads the same source of truth.
Stripe wiring
Project88's Stripe integration runs through two edge functions:
stripe-checkout— creates a Checkout Session for a new subscription or redirects an existing customer to the Stripe Customer Portal. Org-admin auth required.stripe-webhook— receives subscription and invoice events. Signatures are verified; events are idempotent against thestripe_eventstable so retries are safe.
The client opens the portal via:
await api.billing.createPortalSession('<org-id>')This is how members on a paid org manage payment methods, view invoices, or cancel — no separate billing page is needed.
What you pay for
- Credits consumed on LLM calls — based on the model and token count. Project88 marks up a small percentage on top of the provider's wholesale rate to fund the platform.
- Project88 plan — flat monthly subscription that includes a credit allowance. Pro plans get more credits per month.
- Telnyx SMS — per-message rates billed directly to your Telnyx account.
- Composio toolkits — Composio's own pricing applies for some toolkits.
Status pages
src/pages/status/ houses PaymentRequired, BillingSuccess, and
related pages used during the upgrade and recovery flows. They're
intentionally stand-alone routes that bypass the canvas shell so they
work even when an org is over its limit.