Stripe Billing System: Technical Documentation#
Table of Contents#
- Stripe Billing System: Technical Documentation
- Table of Contents
- Core Code File Index
- 1. Core Concept: Dual-Subscription Model
- 2. Data Model and Key Identifiers
- 3. Core Workflows and Technical Implementation
- 4. Q&A and Key Business Rules
- 5. Frontend and Helper Logic
- 7. Rate Limit System Integration
- 8. Database Architecture and Statistics System
- 9. Agent Workflow Integration
- 10. Complete Limiting Decision Tree and Business Logic
This document provides a comprehensive overview of Dosu's billing architecture, built on a dual-subscription model using Stripe. It aims to help developers understand the system's logic, data flow, and core workflows.
Core Code File Index#
Frontend#
- Stripe Router: frontend/packages/api/src/routers/stripe.ts
- Constants: frontend/packages/core/src/constants/stripe.ts
- Helper Functions: frontend/packages/core/src/utils/bill-helper.ts
- UI Components:
Backend#
- Billing Checks: backend/core/stripe/billing_check.py
check_if_org_reached_limit(): line 137check_if_user_reached_limit(): line 175
- Strategy Decision: backend/core/stripe/billing_strategy.py
determine_billing_strategy(): line 33
- Stripe Client: backend/core/stripe/client.py
- Constants: backend/core/stripe/constants.py
ANONYMOUS_USER_RATE_LIMIT_COUNT = 10: line 1ORG_RATE_LIMIT_COUNT = 100: line 3
- Agent Workflow Rate Limit Check: backend/agent/workflows/steps/rate_limit_check.py
RateLimitCheck: line 58
Database#
- SQL Migration: supabase/migrations/20250424014552_check_rate_limit.sql
get_user_interaction_count_by_agent_activity(): line 3get_org_interaction_count(): line 26
- Query Definitions: supabase/queries/billing_event.sql
CreateBillingEvent: line 1GetUserInteractionCountByAgentActivity: line 7GetOrgInteractionCount: line 10
1. Core Concept: Dual-Subscription Model#
Each customer (organization) in our system is managed through two distinct, coexisting Stripe subscriptions. This model separates billing for fixed recurring costs (seats) from variable usage-based costs (metered features).
-
Seat Subscription (
metadata.type: 'seat'):- Manages the number of user seats purchased by an organization.
- Contains a single price item representing the per-seat plan (e.g., Free, Team monthly, Team yearly).
- The
quantityof this item determines the number of seats. - Handles billing for plan upgrades and seat changes.
-
Metered Usage Subscription (
metadata.type: 'meter'):- Tracks and bills consumption of various features (e.g., Dosu AI requests).
- Contains one or more price items corresponding to different metered operations.
- These prices are usually tiered (e.g., first 100 requests free, next 500 charged $X).
- This subscription itself does not expire, but usage counts reset at the start of each billing period (monthly/annual).
This separation provides flexibility to add, remove, or change metered metrics without affecting the core seat-based plan.
Integrate Rate Limit System#
The dual-subscription model integrates seamlessly with our intelligent limiting system:
- Free user limit checks: by inspecting organization usage in the
billing_eventtable - Paid user checks: by verifying the active status of the seat subscription
- Limit configuration sources: read
anonymous_limit=10andfree_limit=100from Stripe product metadata
2. Data Model and Key Identifiers#
Our system relies on Stripe lookup_key to programmatically identify prices, ensuring our code is decoupled from hard-coded price IDs.
Price Lookup Keys#
The mapping between plans and their corresponding seat/meter prices is managed through constants (see frontend/packages/core/src/constants/stripe.ts).
-
Seat Prices (defined in frontend/packages/core/src/constants/stripe.ts):
LOOKUP_KEY_COMMUNITY_MONTHLY_SEAT: default Free plan seat. (line 8)LOOKUP_KEY_TEAM_MONTHLY_SEAT: Team plan, monthly. (line 6)LOOKUP_KEY_TEAM_YEARLY_SEAT: Team plan, yearly.LOOKUP_KEY_ENTERPRISE_YEARLY_FIXED: Enterprise plan, yearly fixed price.
-
Metered Prices:
- The mapping (
METER_LOOKUP_KEY_PER_PLAN) connects each seat plan to its corresponding metered usage price. (frontend/packages/core/src/constants/stripe.ts:68) LOOKUP_KEY_COMMUNITY_MONTHLY_DOSU_REQUEST_METER: meter for the Free plan.LOOKUP_KEY_TEAM_MONTHLY_DOSU_REQUEST_METER: meter for the Team monthly plan.LOOKUP_KEY_ENTERPRISE_MONTHLY_DOSU_REQUEST_METER: meter for the Enterprise plan.
- The mapping (
This structure is critical for the updateSubscription logic, which automatically finds the correct metered price based on the selected new seat price.
Stripe Product Configuration#
Product metadata configuration:
- Product IDs:
- Test environment:
prod_QFYhqUmEX5GCoE - Live environment:
prod_QcqSgZqNW6Maj7
- Test environment:
- Limit configuration:
anonymous_limit: "10"(anonymous user limit)free_limit: "100"(free user limit)product_key: "dosu_subscription"
3. Core Workflows and Technical Implementation#
This chapter details the main user flows and the backend logic supporting them, implemented in stripe.ts.
3.1 New User Onboarding#
- Trigger: when a new organization is created.
- Flow: call the
initCustomerfunction (stripe.ts:1160).- Create a Stripe
Customerobject using the organization owner's email. - Create a seat subscription and attach it to the customer. Include one item: the
LOOKUP_KEY_COMMUNITY_MONTHLY_SEATprice with quantity 1. - Create a metered usage subscription and attach it. Include the
LOOKUP_KEY_COMMUNITY_MONTHLY_DOSU_REQUEST_METERprice item.
- Create a Stripe
- Result: every new user starts on the Free Community plan with the dual-subscription setup, ready for future usage tracking and seamless upgrades.
- References:
initCustomerfunction: stripe.ts:1160- E2E tests:
billing-initialization.cy.ts
3.3 Plan Change (Upgrade)#
Users can upgrade from Community to Team, or from Team monthly to Team yearly.
- Trigger: the user selects a higher-tier plan in the UI.
- Flow: call the
updateSubscriptiontRPC endpoint withnewSeatPriceId(stripe.ts:163).- The system identifies the active seat and metered subscriptions (see
isSeatSubscriptionActive,isMeterSubscriptionActivein frontend/packages/core/src/utils/bill-helper.ts). - Importantly, it checks whether a downgrade has already been scheduled (
subscription.schedule). If yes, it throws an error to prevent conflicting changes. (see check logic in stripe.ts) - It derives the target metered price ID from the new seat price ID using the
METER_LOOKUP_KEY_PER_PLANmapping (frontend/packages/core/src/constants/stripe.ts). - It updates the seat subscription with the new seat price item.
- It updates the metered subscription with the new metered price item.
- For both updates,
proration_behavioris set toalways_invoice, meaning the user is immediately charged the prorated difference.
- The system identifies the active seat and metered subscriptions (see
- Result: both subscriptions are atomically updated to the new plan tier, and the user is billed immediately.
3.4 Plan Downgrade (Plan Cancellation)#
Users cannot manually downgrade in a single step. Instead, they "cancel" the subscription, which schedules a downgrade to the Free Community plan at the end of the current billing period.
- Trigger: the user clicks "Cancel Subscription".
- Flow: call the
scheduleDowngradeendpoint (stripe.ts:414).- Identify the active seat and metered subscriptions.
- Determine the target Community plan prices (
LOOKUP_KEY_COMMUNITY_MONTHLY_SEATand its corresponding metered price). - For both subscriptions, create a
SubscriptionSchedule. This schedule has two phases:- Phase 1: the current active plan (e.g., Team yearly) runs until
current_period_end. - Phase 2: the new downgraded Community plan starts from
current_period_end.
- Phase 1: the current active plan (e.g., Team yearly) runs until
- The schedule's
end_behavioris set torelease, so the schedule is deleted after the transition, leaving a standard subscription.
- Result: the user's plan remains unchanged for the rest of the billing period. At the end of the period, both subscriptions automatically revert to the Free Community plan.
3.5 Reactivate Scheduled Downgrade#
Users can revoke a scheduled cancellation before it takes effect.
- Trigger: the user clicks "Renew Subscription" or a similar reactivation button.
- Flow: call the
cancelScheduledChangeendpoint (stripe.ts:662).- Find the active seat and metered subscriptions.
- For each subscription, if a
scheduleexists, callstripe.subscriptionSchedules.release(scheduleId). - This action immediately removes the scheduled downgrade.
- Result: the user's subscription is no longer scheduled for cancellation and will renew normally at the end of the period.
3.6 Automatic Seat Management#
Important Change: the system has transitioned from manual seat management to automatic seat management.
-
Trigger: automatically triggered when users join or leave an organization.
-
Implementation Mechanism:
- Database Trigger: a trigger is set on the
user_orgtable (supabase/migrations/20250603070626_add_auto_seat_trigger.sql)INSERToperations trigger aUSER_JOINEDwebhookDELETEoperations trigger aUSER_LEFTwebhook
- Webhook Handling: the trigger sends an HTTP request to
/api/webhook/auto-seat-management - Automatic Billing: the system automatically adjusts the seat quantity and updates the Stripe subscription accordingly
- Database Trigger: a trigger is set on the
-
User Interface:
- The frontend only displays the current seat count and does not provide a manual modification entry
- The SeatsBlock component is only used to display seat information and pricing details
-
Result: the number of seats is always kept in sync with the actual number of active users in the organization, with no manual intervention required.
4. Q&A and Key Business Rules#
This section summarizes key business logic, validated by code implementations.
-
How many subscriptions does a user have?
- Exactly two: one for seats and one for metered usage.
-
Do subscriptions expire?
- No. They are continuous. Plan changes are implemented by swapping price items within them rather than canceling and creating new subscriptions.
-
Can users upgrade?
- Yes. Community -> Team, Team monthly -> Team yearly. This is an immediate prorated change.
-
Can users manually downgrade?
- No. They can only schedule a cancellation, which reverts them to the Free Community plan at the end of the current billing period.
-
Can users upgrade while a downgrade is scheduled?
- No. The
updateSubscriptionlogic explicitly checks for an active schedule and will fail. Users must first usecancelScheduledChangeto reactivate their plan.
- No. The
-
When do users pay for upgrades or added seats?
- Immediately.
proration_behavioris set toalways_invoice.
- Immediately.
-
What happens when seats are removed?
- The seat quantity is updated immediately. The unused portion is credited to the customer's account for future invoices, with no refund.
-
What is the minimum/default number of seats?
- One. All new users start with one seat, and the quantity cannot be reduced to zero.
-
How are seats handled during plan upgrades?
- The seat quantity is retained. If a user has 5 seats on Team monthly and upgrades to Team yearly, they will have 5 seats on Team yearly. Stripe calculates the immediate prorated charge based on the new price, seat count, and remaining period.
-
What happens to seats after a plan downgrade takes effect?
- The seat subscription item is replaced with the
LOOKUP_KEY_COMMUNITY_MONTHLY_SEATprice, and the quantity is reset to 1. The metered subscription reverts to the corresponding Community metered price.
- The seat subscription item is replaced with the
-
How is the seat quantity automatically managed?
- Through database triggers for automatic seat adjustments. When users join or leave an organization, the
handle_auto_seat_adjustment()function automatically triggers a webhook call to/api/webhook/auto-seat-managementto adjust seat quantities.
- Through database triggers for automatic seat adjustments. When users join or leave an organization, the
-
Can users still manage seats manually?
- No. In the current implementation, seat management is fully automated, and the frontend
SeatsBlockcomponent only displays the current seat count without providing manual increase/decrease functionality.
- No. In the current implementation, seat management is fully automated, and the frontend
-
How does automatic seat adjustment work?
- Database layer:
INSERT/DELETEtriggers on theuser_orgtable - API layer: triggers call the
/api/webhook/auto-seat-managementendpoint - Stripe layer: automatically updates the seat quantity in the subscription
- Implementation location: supabase/migrations/20250603070626_add_auto_seat_trigger.sql
- Database layer:
5. Frontend and Helper Logic#
- Frontend:
- frontend/app/.../PlanBlock.tsx - displays plan information and upgrade options
- frontend/app/.../SeatsBlock.tsx - manages seat quantity
- Uses the
useSubscriptionDatahook and calls the frontend/packages/api/src/routers/stripe.ts tRPC router
- Utilities: foundational utility functions in frontend/packages/core/src/utils/bill-helper.ts such as
isSeatSubscriptionActive,isMeterSubscriptionActive, andisPaidSubscriptionActive. They allow the backend to reliably distinguish between the two subscription types and determine the customer's current plan level based on the seat subscription'slookup_key.
7. Rate Limit System Integration#
Intelligent Limiting Strategy#
Our billing system is deeply integrated with an intelligent Rate Limit system to implement scenario-based dynamic limiting strategies:
7.1 Strategy Decision Mechanism#
The BillingStrategy module (backend/core/stripe/billing_strategy.py:33) automatically determines the usage scenario:
# Location: backend/core/stripe/billing_strategy.py:33
def determine_billing_strategy(space, thread):
"""
PUBLIC strategy trigger conditions:
- Public spaces in the Dosu App
- Conversations in GitHub public repositories
PRIVATE strategy trigger conditions:
- Private repositories/spaces
- Platforms that do not support public mode (e.g., Slack)
"""
7.2 Limit Enforcement Logic#
PUBLIC strategy (public spaces):
- Check user-level limits
- Anonymous users: 10 times/month (
ANONYMOUS_USER_RATE_LIMIT_COUNT- backend/core/stripe/constants.py:1) - Registered users: 100 times/month (
ORG_RATE_LIMIT_COUNT- backend/core/stripe/constants.py:3) - Statistics: calculated per username based on the
agent_activitytable
PRIVATE strategy (private spaces):
- Check organization-level limits
- Entire organization: 100 times/month (
ORG_RATE_LIMIT_COUNT- backend/core/stripe/constants.py:3) - Statistics: accumulated per organization based on the
billing_eventtable
7.3 Paid User Privileges#
- Pro/Enterprise plans: unlimited interactions
- Especially important: paid users also enjoy free unlimited usage in public repositories/spaces
7.4 Limit Configuration Sources#
Read from Stripe product metadata:
{
"anonymous_limit": "10",
"free_limit": "100",
"product_key": "dosu_subscription"
}
8. Database Architecture and Statistics System#
8.1 Dual Statistics Mechanism#
User-level statistics (get_user_interaction_count_by_agent_activity - ../../../../../supabase/migrations/20250424014552_check_rate_limit.sql:3):
-- Count agent activities triggered by the user that completed successfully
SELECT count(distinct message.id)
FROM agent_activity
INNER JOIN message ON message.id = trigger_message
WHERE stop_reason = 'WORKFLOW_COMPLETED'
AND message.author = username
AND message.created_at >= date_trunc('month', CURRENT_DATE);
Organization-level statistics (get_org_interaction_count - ../../../../../supabase/migrations/20250424014552_check_rate_limit.sql:26):
-- Count all billing_event records for the organization
SELECT COALESCE(SUM(be.meter_count), 0)
FROM billing_event be
WHERE be.org_id = org_id
AND be.created_at >= date_trunc('month', CURRENT_DATE);
8.2 Table Structures#
agent_activitytable: records agent activities triggered by users, used for user-level statistics- Table definition location: Supabase database
- Main fields:
trigger_message,stop_reason,execution_id
billing_eventtable: records the organization's billing events, used for organization-level statistics- Table definition location: Supabase database
- Main fields:
org_id,meter_count,event_type,event_id
messagetable: a bridge table connecting users and activities- Main fields:
id,author,thread_id,created_at
- Main fields:
8.3 Statistical Accuracy Assurance#
- The two statistical systems are completely independent, ensuring precise control of user-level and organization-level limits
- Use PostgreSQL's
date_trunc('month', CURRENT_DATE)function to ensure accurate monthly statistics - The
WORKFLOW_COMPLETEDstatus ensures that only successfully completed interactions are counted
9. Agent Workflow Integration#
9.1 Workflow Architecture#
The Rate Limit check serves as the first step in the Agent workflow (implemented in backend/agent/workflows/steps/rate_limit_check.py:58):
User initiates a request
↓
Agent workflow starts → RateLimitCheck step [rate_limit_check.py:58](../../../../../backend/agent/workflows/steps/rate_limit_check.py#L58)
↓
Obtain Space and Thread information
↓
determine_billing_strategy() [billing_strategy.py:33](../../../../../backend/core/stripe/billing_strategy.py#L33)
↓
PUBLIC strategy? PRIVATE strategy?
↓ ↓
check_if_user_reached_limit check_if_org_reached_limit
[billing_check.py:175] [billing_check.py:137]
↓ ↓
Count by username via Accumulate by org_id via
agent_activity billing_event
↓ ↓
Anonymous: 10 times/month Organization shared: 100 times/month
Registered: 100 times/month
9.2 Limit Check Implementation#
Core logic in billing_check.py (../../../../../backend/core/stripe/billing_check.py):
def check_if_user_reached_limit(username, time_period='month'):
"""User-level limit check"""
# Location: backend/core/stripe/billing_check.py:175
# Query the user profile; if it does not exist, treat as an anonymous user
# Anonymous users use ANONYMOUS_USER_RATE_LIMIT_COUNT (10)
# Registered users use ORG_RATE_LIMIT_COUNT (100)
def check_if_org_reached_limit(org_id, time_period='month'):
"""Organization-level limit check"""
# Location: backend/core/stripe/billing_check.py:137
# Check paid subscription status
# Paid users have no limits
# Free organizations use ORG_RATE_LIMIT_COUNT (100)
9.3 Over-Limit Handling#
- If over the limit, stop the workflow and return an upgrade prompt
- Return different prompt messages for different user types
- Paid users are never limited
9.4 Workflow State Management#
- Successful workflow completion status:
WORKFLOW_COMPLETED - Only successfully completed workflows are counted in usage statistics
- Failed or interrupted workflows are not billed
10. Complete Limiting Decision Tree and Business Logic#
10.1 User Types and Limit Matrix#
| User Type | Space Type | Limit Type | Limit Amount | Statistic Source |
|---|---|---|---|---|
| Anonymous User | Any | User-level | 10 times/month | agent_activity |
| Free Registered User | Public Space | User-level | 100 times/month | agent_activity |
| Free Registered User | Private Space | Organization-level | 100 times/month (shared by the entire organization) | billing_event |
| Pro/Enterprise User | Any | No Limit | ∞ | Subscription status check |
10.2 Summary of Key Business Rules#
-
Intelligent Hybrid Mode:
- Public spaces encourage individual participation (user-level limits)
- Private spaces support enterprise usage (organization-level limits)
-
Paid User Privileges:
- Unlimited usage in any space
- Includes free usage in open-source projects
-
Statistical Accuracy Assurance:
- Two independent statistical systems
- Monthly reset mechanism
- Only successful completions are billed
-
Smooth Upgrade Path:
- Seamless transition from user-level to organization-level
- Stripe’s dual-subscription model supports flexible billing