Skip to content

Studio: plans, limits and Stripe billing - #12

Merged
visualfart merged 4 commits into
mainfrom
feat/studio-billing
Sep 29, 2026
Merged

visualfart merged 4 commits into
mainfrom
feat/studio-billing

Conversation

@visualfart

Copy link
Copy Markdown
Owner

What this is

Studio gains plans, limits and billing, per decision 0004. All of it applies only when the Worker's BILLING is on, which only the hosted Studio sets — a self-hosted Studio has no limits and no Billing page, and that's tested both ways.

Licensing. apps/studio moves to FSL-1.1-ALv2: source stays public, free to self-host for your own team, no competing hosted service, and each version becomes Apache-2.0 two years after release. Every other package stays Apache-2.0.

Plans and limits. Migration 0007_plans.sql adds the plan columns and a usage table. plans.ts holds the one limits table and enforces it at workspace creation, invites and invite accept, design-system import, Directions, screen publish, and version history. Over-limit answers 402 with code: "plan_limit", which the app turns into an upgrade dialog.

Fetch metering. A fetch by API key writes one Analytics Engine data point; an hourly cron rolls it into usage and sets or clears over_quota_since. Seven days over quota and edits answer 402 — fetches, reads and billing keep working.

Billing. Stripe's REST API through fetch, no SDK. Checkout, Customer Portal, a webhook whose signature is checked with Web Crypto (HMAC-SHA256, constant-time, 5-minute tolerance), and seat sync that follows the editor count with prorations. The webhook is the only thing that changes a plan.

Checked

  • 57 tests pass, including test/billing.worker.test.ts (signature checked, plan set from the subscription, seats synced, back to Free when it ends, the founding coupon until it runs out, self-hosted has none of it).
  • tsc -b clean; check-docs.ts clean.
  • Stripe is stubbed in tests. This has never run against real Stripe — see below.

Before this can take payments

  1. Apply 0007_plans.sql to the remote D1.
  2. Set the secrets on the production Worker: BILLING, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, the four STRIPE_PRICE_*, STRIPE_COUPON_FOUNDING, CF_ACCOUNT_ID, CF_ANALYTICS_TOKEN.
  3. Exercise the loop in Stripe test mode locally (stripe listen --forward-to localhost:8789/api/billing/webhook, card 4242) before pointing it at live.

Live products, prices (USD $8 / $80 Pro, $12 / $120 per editor Team), the founding coupon and the webhook destination are set up in the Stripe account.

Known trade-offs

  • The founding-coupon fallback retries without the coupon on any 4xx from Checkout, not only "coupon exhausted", so an unrelated bad request costs a second call before it surfaces.
  • No event-id dedupe on the webhook. Stripe can deliver twice; every handler is idempotent, so a repeat writes the same row.
  • invoice.payment_failed marks past_due by customer id. One customer with two subscribed workspaces would mark both.

🤖 Generated with Claude Code

visualfart and others added 4 commits September 29, 2026 10:32
… decision

Studio (apps/studio) moves from Apache-2.0 to FSL-1.1-ALv2: source stays public,
free to run for your own team or company, no competing hosted service, and each
version becomes Apache-2.0 two years after release. README, NOTICE, Studio's
landing page, the site's Studio page, docs and terms say so. Decision 0004
records the pricing and licensing plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plans and limits apply only when BILLING is on (the hosted Studio); a
self-hosted Studio has none. One table of limits per plan (plans.ts),
402 plan_limit at workspace create, editor invites and accepts, design
systems, Directions and published screens, and Free's history kept to
its last ten versions. Fetches by API key are counted in Workers
Analytics Engine, rolled up hourly by the cron into usage, and seven
days over quota locks editing (never fetching). Stripe through fetch:
checkout (founding coupon, Team seats), portal, and a signed webhook
that sets the plan; Team's seats follow editors joining and leaving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…team seats freed

The Billing page (Workspace → Billing, shown only where the Studio has
plans) gives the plan, seats, usage bars that turn amber at 80%, notices
for over quota, a paused workspace and a failed payment, a monthly or
yearly toggle, and upgrade or manage buttons for owners. Any 402 opens
a dialog that says what ran out and links to the plans. Team gains
Remove for owners and Withdraw for open invites, which hold a seat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d what 0004 built

The Studio README says how to turn billing on (Stripe products, prices,
coupon, webhook events, secrets, the Analytics Engine token) and that a
self-hosted Studio has no limits; the site's Studio page gives the plans
in plain words; the pricing decision ticks off workstreams 2 and 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@visualfart
visualfart merged commit fe71ed0 into main Sep 29, 2026
1 check passed
@visualfart
visualfart deleted the feat/studio-billing branch September 29, 2026 05:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant