How We Built Workspace-Based Billing with Stripe in Voxr
From a static pricing page to a real subscription system: architecture, Stripe integration, workspace billing model, and product decisions behind Voxr subscriptions.
Introduction
Voxr started with a simple pricing page and static call-to-action buttons. There was no real subscription system behind it. The goal was to transform Voxr into a true subscription SaaS product with workspace-based billing, Stripe integration, usage limits, and upgrade flows.
This transition required both product decisions and technical implementation across the database, backend logic, Stripe integration, and application UI. The result is a billing system where subscriptions are tied to workspaces rather than individual users, with clear upgrade paths and enforced usage limits.
This article explains how the billing system was designed and implemented, from both an engineering and a product perspective.
Engineering perspective
The billing system was built across three main layers: the database, Stripe integration, and application routes and logic.
Database and billing model
The billing architecture is workspace-centric. Subscriptions are attached to a workspace instead of a user account. To support this model, new billing tables were added:
billing_customersbilling_subscriptions
A unique partial index was added to ensure that a workspace cannot have more than one active paid subscription at the same time. This prevents duplicate subscriptions and enforces a clean billing state.
Additional helpers, triggers, and functions were implemented for:
- plan resolution
- usage limits
- server-side usage counting
- plan-based feature checks
Usage limits such as number of members, feedback items, and forms are enforced both:
- at the application layer
- at the database layer
This double enforcement prevents bypassing limits through direct API calls or race conditions.
Stripe integration
Stripe was integrated using the Stripe SDK and includes the following components:
- Checkout session creation
- Webhook handling for subscription lifecycle events
- Customer portal for billing management
- Subscription synchronization from Stripe back into Supabase
- In-place subscription upgrade logic (Pro → Enterprise)
Webhooks handle events such as:
- checkout completion
- subscription creation
- subscription updates
- subscription cancellation
These events update the local billing tables to keep Supabase in sync with Stripe.
For upgrades from Pro to Enterprise, instead of creating a new subscription, the existing Stripe subscription is updated in place. This avoids duplicate subscriptions and maintains billing continuity.
App routes and UI
Several new routes and UI components were added:
- Pricing page with auth-aware CTAs
- Checkout route
- Claim route (to attach a subscription to a workspace)
- Billing portal route
- Billing tab in workspace settings
- Workspace selection or creation screen before checkout
- Localized billing copy in English and French
The checkout flow was modified so that a workspace must be selected or created before checkout starts. This ensures that every Stripe subscription is already associated with a workspace context.
App logic and safety
Several safety mechanisms were implemented:
- Auth redirect sanitization to allow only internal paths
- Plan limits enforced in server actions and database logic
- Billing-aware error handling in forms
- Owner checks before attach, upgrade, or manage billing operations
These checks prevent unauthorized billing operations and ensure that only workspace owners can manage subscriptions.
Integration issues encountered
Several integration issues appeared during implementation:
Webhook 404 errors
Stripe was sending events to /api/stripe/webhook and receiving 404 errors. The route existed and was included in the build manifest. The issue turned out to be local environment state, and the fix was restarting the local Next.js server and ensuring the correct process was running.
Stripe placeholder not replaced
After checkout, users were redirected to a URL containing {CHECKOUT_SESSION_ID} instead of the actual session ID.
The issue was caused by building the success URL with URLSearchParams, which encoded the placeholder before Stripe could replace it.
The fix was to build the success URL as a raw string. A fallback was also added to recover the latest unclaimed subscription if the placeholder was not replaced.
Incorrect feedback usage count
The billing usage count for feedback items was incorrect because the query used select("*", { count: "exact", head: true }) on a table with restricted columns.
The fix was to count using a visible column such as id instead of *.
Duplicate active subscription constraint error
Trying to subscribe a workspace that already had a paid subscription triggered a database constraint error.
The solution was:
- Same plan: block checkout and redirect to billing
- Higher plan: update the existing Stripe subscription instead of creating a new one
Checkout workspace picker lacked plan context
After adding workspace selection before checkout, the picker did not show whether a workspace was already on the selected plan.
The fix was to enrich workspace data with currentPlanKey and display appropriate messaging such as:
- current plan
- already on this plan
- upgrade from current plan
Product perspective
From a product perspective, the most important change was moving from a pricing page to a real subscription model tied to workspaces.
Workspace-based billing model
The subscription model was designed around workspaces instead of individual users. The rules are:
- Subscriptions are tied to a workspace, not a user
- Only the workspace owner manages billing
- The Free plan stays outside Stripe
- Pro and Enterprise are monthly recurring Stripe subscriptions
- Usage limits are enforced by plan
This model matches how teams actually use SaaS products. Teams pay for a shared workspace rather than individual accounts managing separate subscriptions.
Checkout and upgrade flow design
The user experience evolved in two phases.
Initial flow:
- User clicked Pro or Enterprise
- If logged out, the user was redirected to authentication
- After checkout, the user landed on a claim page to attach the subscription to a workspace or create a new one
Revised flow:
- The user must create or select a workspace before subscribing
/checkout/[plan]became the workspace selection step when no workspace ID is present- Checkout starts only after a workspace is chosen
- Upgrades from workspace settings skip this step because the workspace is already known
This revised flow reduces confusion and ensures that subscriptions are always clearly attached to a workspace.
Upgrade and plan behavior
The final purchase and upgrade behavior is:
- Free → Pro or Enterprise: new Stripe subscription
- Pro → Pro: blocked as duplicate and redirected to billing
- Pro → Enterprise: existing Stripe subscription updated in place
- Enterprise → Pro: not allowed through this flow
- Workspaces already on the selected plan are shown as such in the workspace picker with a billing link instead of a checkout button
This behavior prevents billing mistakes, duplicate subscriptions, and confusing upgrade paths.
Product impact
From a product perspective, this change transformed Voxr from a simple product with a pricing page into a real SaaS with:
- workspace-based billing
- controlled upgrade paths
- enforced usage limits
- billing management inside the product
- clear ownership and billing responsibility
- scalable pricing model for teams
The most important product shift was moving from “pricing page copy” to a real subscription workflow integrated into the product experience.
Conclusion
Implementing billing is not just about adding Stripe checkout. It requires decisions about subscription ownership, upgrade paths, usage limits, database constraints, and user experience flows.
In Voxr, the billing system was built around workspaces, with Stripe handling subscriptions and Supabase managing subscription state, usage limits, and permissions. The main technical work involved database modeling, webhook synchronization, checkout flows, and upgrade logic.
From a product perspective, the biggest transformation was turning Voxr into a real subscription SaaS with workspace-based billing and controlled upgrade behavior.
The most difficult parts were not the happy path, but the integration edges: webhook routing, Stripe placeholder substitution, restricted-column usage counts, and duplicate subscription handling. These edge cases are often where billing systems fail, and solving them was essential to making the billing system reliable.
Follow our journey on voxr.sh.