Status: Draft
Created: November 5, 2025
Last Updated: November 14, 2025
Problem Statement
Current Limitations
- Ambiguous Event Attribution: When a customer belongs to multiple seat-based subscriptions from different organizations (e.g., Alice works for both Acme and Slack), events lack business context to determine which customer should be billed.
- Inflexible Billing Management: Changing the billing manager becomes tricky because billing information (payment methods, orders) is tied to a single Customer entity.
- No Business-Level Aggregation: When a business has multiple subscriptions from the same merchant, it’s difficult to aggregate usage or provide consolidated billing.
Requirements
- Subscription Visibility: Know what subscriptions a business customer has
- Billing Transfer: Allow transfer of subscriptions to another billing manager
- Clear Attribution: Easily know who is the billing customer of an event
- Backward Compatibility: Existing B2C integrations must continue working without changes
Current Merchant Workflows (Must Preserve)
Workflow A: Public Checkout (B2C - Current)
- Customer visits merchant site, clicks checkout link
- Polar Checkout: Buyer enters email + card
- Polar auto-creates individual customer
- Post-purchase: Customer manages subscription via Customer Portal
Workflow B: Public Checkout (B2B - Current seats flow)
- Customer visits merchant site, clicks checkout link
- Polar Checkout: Buyer enters email + card
- Polar auto-creates billing manager
- Post-purchase: Buyer invites team members, manages via Customer Portal
Workflow C: Checkout Sessions (API-driven)
- Merchant app creates customer and business in their system
- Merchant creates Polar customer + business inside Polar via API
- Billing manager clicks checkout in merchant app
- Polar Checkout: Pre-filled information, enters card
- Returns to merchant app (optional: Customer Portal access)
Solution
Decision: Option 1 (Member Model) is the preferred solution. After evaluating both approaches using our weighted decision matrix, Option 1 scores 69 vs Option 2’s 62. The Member Model provides superior billing accuracy, cleaner architecture, and better long-term maintainability. While it introduces breaking changes for existing B2B/seat-based customers, these are limited. The clear separation of concerns (Customer = billing, Member = usage) eliminates ambiguity and aligns perfectly with our Auth providers integration goals. We evaluated two architectural approaches. Both solve the core problem but differ in complexity, migration risk, and developer experience.Tenets
These tenets guide our decision-making, weighted by importance:- Billing Accuracy (weight: 7) - Charge the right customer, events hit correct paying customer
- Backward Compatibility (weight: 6) - Existing B2C and B2B customers continue working unchanged.
- Simplicity (weight: 5) - Seamless checkout, straightforward queries, simple event ingestion
- Minimal Changes for B2B (weight: 4) - Merchants make minimal changes when adding B2B
- Polar Developer Experience (weight: 3) - Avoid excessive filtering or complex queries
- WorkOS/Auth0 Integration (weight: 2) - Support enterprise identity providers
(Preferred) Option 1: Member Model
Concept: Introduce aMember entity that represents “who uses the product”. Customer becomes purely a billing entity. Every Customer has one or more Members - even individual customers have a single member (themselves). This creates uniform architecture with no special cases.
High-Level Architecture:
| Tenet | Score | Rationale |
|---|---|---|
| Billing Accuracy | 🟢 3 | Perfect separation - Customer = billing, Member = usage |
| Backward Compatibility | 🔴 1 | Current integration with B2B requires migration |
| Simplicity | 🟢 3 | Uniform 1:N model, no type discrimination, no special cases |
| Minimal B2B Changes | 🟢 3 | Just add members, existing customer_id queries work |
| Polar Dev Experience | 🟢 3 | Clean queries, no type filtering, clear separation |
| WorkOS Integration | 🟢 3 | Members map directly to WorkOS users/members |
| Weighted Total | 69 |
- No polymorphism: Customer is always billing entity, no
typefield needed - Uniform API: All authentication goes through Member, single code path
- Easy multi-membership: Alice has multiple member records (personal, ACME, Lolo). Each one has a different ID.
- Clean separation: Customer = billing, Member = usage/access
- Perfect WorkOS fit: Members map 1:1 to WorkOS/BetterAuth members
- Unique per customer: Email and external_id are unique per customer, allowing same values across different customers.
- Extra entity for every user.
- More joins for some queries (member -> customer)
- Conceptual shift: “authenticate as member, not customer”
Option 2: Customer Type + Member
Concept: Extend existingCustomer entity with a type discriminator field (individual | business). Business customers have Members (a separate junction table) linking to individual customers. Single Customer entity for both billing and usage.
High-Level Architecture:
- ZERO BREAKING CHANGES: All existing API calls work identically
typedefaults to"individual"if not specified- Existing customers auto-migrated to
type="individual" - B2B features are opt-in via
type="business"parameter
| Tenet | Score | Rationale |
|---|---|---|
| Billing Accuracy | 🟡 2 | Customer serves dual purpose, but naming conventions mitigate |
| Backward Compatibility | 🟢 3 | Zero breaking changes, additive-only schema |
| Simplicity | 🟡 2 | Single entity concept, but polymorphism adds query complexity |
| Minimal B2B Changes | 🟡 2 | Just add type parameter, similar code patterns |
| Polar Dev Experience | 🟡 2 | Requires type filtering, mitigated by SQLAlchemy polymorphism |
| WorkOS Integration | 🟢 3 | Individual customers map cleanly to WorkOS users |
| Weighted Total | 62 |
- Minimal schema changes (add one field, one table)
- Zero breaking changes for existing merchants
- Lower migration risk (additive-only)
- “A customer is a customer” - simpler mental model
- Type discrimination in queries (mitigated by SQLAlchemy polymorphism)
- Customer entity serves dual purpose (usage + billing)
- Need clear naming conventions to avoid confusion
Decision Matrix
Scale: 🔴 1 (poor) | 🟡 2 (acceptable) | 🟢 3 (excellent)| Tenet | Weight | Option 1: Member Model | Option 2: Customer Type + Member |
|---|---|---|---|
| Billing Accuracy | 7 | 🟢 3 (21) | 🟡 2 (14) |
| Backward compatibility | 6 | 🔴 3 (6) | 🟢 3 (18) |
| Simplicity | 5 | 🟢 3 (15) | 🟡 2 (10) |
| Minimal changes for B2B | 4 | 🟢 3 (12) | 🟡 2 (8) |
| Polar dev experience | 3 | 🟢 3 (9) | 🟡 2 (6) |
| WorkOS Integration | 2 | 🟢 3 (6) | 🟢 3 (6) |
| Total Score | - | 69 | 62 |
Migration Plan
Overview
The migration introduces theMember model while maintaining backward compatibility for existing B2C customers. B2B/seat-based customers will experience breaking changes and require coordinated migration. The rollout uses feature flags to minimize risk and allow gradual adoption.
Timeline: ~1 month for a single SDE for implementation + 2-4 weeks for coordinated merchant migration
Phase 1: B2C Customer Migration (Automatic, Zero-Downtime)
All existing individual customers receive automatic migration with full backward compatibility: What happens:- Schema changes deployed: Add
memberstable with foreign key tocustomers- UNIQUE constraint:
(customer_id, email)- ensures email uniqueness per customer - UNIQUE constraint:
(customer_id, external_id)- ensures external_id uniqueness per customer
- UNIQUE constraint:
- Auto-create default members: Migration script creates one member per customer
member.customer_id→ existing customer IDmember.email→ customer emailmember.name→ customer namemember.external_id→ customer external_id (if exists)member.role→"owner"(default)
- Service layer dual support: Endpoints accept both
customer_id(legacy) andmember_id(new)- When
customer_idprovided → resolve to default member automatically - When
member_idprovided → use directly
- When
- Migrate usage data:
benefit_grants.member_id→ default member ID (keepcustomer_idfor backward compat queries)customer_sessions.member_id→ default member IDevents.member_id→ default member ID (keepcustomer_iddenormalized)customer_seats.member_id→ NULL initially (migrated in Phase 2)
Phase 2: B2B/Seat-Based Customer Migration (Coordinated)
Existing seat-based subscriptions require structural changes and merchant coordination: Current state:- Billing manager = Customer with payment method
- Seat holders = Separate Customer entities linked via
customer_seatstable - Usage events reference individual customer IDs
- Billing manager = Customer (unchanged) with admin Member
- Seat holders = Members of the billing customer
- Usage events reference member IDs
-
Identify seat-based customers: Query subscriptions/orders with
seats > 0 -
Transform billing managers:
-
Transform seat holders:
-
Migrate usage data:
- Benefit grants: Reassign from seat holder customer → member
- Events: Reassign from seat holder customer → member (update both
member_idand keepcustomer_idpointing to billing customer) - Customer sessions: Migrate to member sessions
-
Update merchant integrations (⚠️ Breaking changes):
📖 See Appendix A: Merchant Migration Guide for detailed instructions, code examples, and FAQ.
Flow Current (uses customer_id) New (uses member_id) Event ingestion POST /events { customer_id: "seat_holder_cust_123" }POST /events { customer_id: "billing_cust_456", member_id: "mem_123" }Customer portal sessions POST /customer-sessions { customer_id: "seat_holder_cust_123" }POST /customer-sessions { member_id: "mem_123" }Benefit downloads (B2B) GET /benefit-grants?customer_id=seat_holder_cust_123GET /benefit-grants?member_id=mem_123
- 2 weeks before migration: Email merchants with seat-based products
- Explain breaking changes
- Provide Appendix A: Merchant Migration Guide with code examples
- Offer dedicated support channel
- 1 week before migration: Second reminder with migration deadline
- Migration day: Enable feature flag, monitor metrics
- Post-migration: Support merchants during transition period
Rollback Plan
Feature flag:member_model_enabled (organization-level or global toggle)
During the migration period, if a bug is found or a merchant complaint, disable the feature flag to revert to the previous functionality. All code should work with feature flag enabled or disabled.
Implementation Plan
Phase 1: Schema
Goal: Database ready, feature flag in place, no production impact yet-
Create member table with indexes
- Add table:
members(id, customer_id FK, email, name, external_id, role, created_at, updated_at) - Indexes:
customer_id,(customer_id, email)UNIQUE,(customer_id, external_id)UNIQUE
- Add table:
-
Add feature flag infrastructure
- Organization-level feature flag:
member_model_enabled(default: false)
- Organization-level feature flag:
Phase 1: Customer
Goal: Customer onboarding should create members. Expose members to customers API.Phase 2: Benefits
Goal: benefits should be granted to members.Phase 3: Member Management
Goal: Member management should support CRUD operations.Phase 4: Customer Portal
Goal: Member should be able to authenticate with customer portal and see their benefits. Billing managers and owners should be able to manage members.Phase 5: Dashboard
Goal: The dashboard should display member-related information. Benefits should point to members. Members should be shown for B2B customers only.Phase 6: Event ingestion
Goal: Event ingestion should support members. CustomerMeter should be the aggregation of all member events and a new MemberEventMeter should be created.Phase 7: Seats
Goal: Seats should point to members.Phase 8: Webhooks
Goal: Webhooks should include member information. Create webhooks for member events.Phase 9: Adapters
Goal: Adapters should work out of the box with the member model.Phase 10: Mobile app
Goal: Mobile app should work out of the box with the member model.Phase 11: Documentation
Goal: Document member model and migration process.Phase 12: Rollout
Goal: Rollout member model to production for B2C and B2B customers. Migrate existing B2B customers to member model.Appendices
Appendix A: Merchant Migration Guide (B2B/Seat-Based Products)
Audience: Merchants using seat-based pricing or B2B subscriptions TL;DR: If you use seat-based pricing, you’ll need to update your integration to usemember_id instead of customer_id for B2B customers. B2C customers are unaffected.
What is Changing?
We’re introducing a new Member entity to improve how Polar handles team subscriptions and usage-based billing. This change provides:- Better billing accuracy: Clear separation between who pays (Customer) and who uses the product (Member)
- Multi-company support: Users can be members of multiple companies without confusion
- Clearer usage attribution: Events and benefits are always tied to the specific person who used them
Why This Change?
Problem we’re solving: When a user (e.g., Alice) works for multiple companies (Acme Corp and Slack Inc), and both companies purchase your product, Polar doesn’t know which company to bill when Alice generates usage events. This causes billing ambiguity. This will allow other futures in the comming weeks, like better analytics and insights. Solution: The Member model ensures every action is attributed to a specific member of a specific customer, eliminating ambiguity.Breaking Changes (B2B/Seat-Based Only)
Imagine that before we had a seat based subscription with:- Alice being the billing manager and a seat holder
- Ben being a seat holder
1. Customer API + Member API
Given the example above, we will have the following schema on our database:- Customer Alice:
- Member Alice (owner)
- Member Bob (member)
- Customer Bob (new customer with no subscriptions and nothing attached to him):
- Member Bob (owner)
externalCustomerId pointed to Ben, a seat holder (a separate customer). Now you should use the members to get the data of Ben.
1. Event Ingestion API
Before (current):externalCustomerId pointed to the seat holder (a separate customer). Now it must point to the billing customer, with member_id identifying the specific team member.
2. Customer Portal Sessions
Before (current):3. Benefit grants
Before (current):3. Other endpoints
All the other endpoints where a customerId/externalCustomerId are you passing you will need to:- Make sure that the customerId or externalCustomerId points to the customerId/externalCustomerId of the billing manager
- Pass the
externalMemberIdalongsideexternalCustomerId
- GET:
/v1/customers. - GET:
/v1/customers/{customerId}. - PATCH:
/v1/customers/{customerId}. - DELETE:
/v1/customers/{customerId} - GET:
/v1/customers/external/{externalId}. - PATCH:
/v1/customers/external/{externalId}. - DELETE:
/v1/customers/external/{externalId}
member entity. customer_id will be removed.
- POST:
/v1/seats: we will need to pass themail,externalMemberId,memberId, along withsubscriptionId - GET:
/v1/seats/{seatId} - PATCH:
/v1/seats/{seatId}. - DELETE:
/v1/seats/{seatId}
Migration Checklist
Use this checklist to ensure your integration is updated:-
Update SDK to latest version
- TypeScript:
npm install @polar-sh/sdk@latest - Python:
pip install --upgrade polar-sdk
- TypeScript:
-
Update code
- Change
customer_idfrom seat holder customer to billing customer - Add
member_idparameter for all B2B events
- Change
-
Monitor after migration
- Test that you can log inside customer portal
- Check error rates in your application logs
- Confirm billing accuracy for B2B customers
How to Identify If You’re Affected
You ARE affected if:- ✅ You sell products with seat-based pricing
- ✅ You use the Customer Seats API (
/v1/customer-seats) - ✅ You have subscriptions or orders with
seats > 0 - ✅ You track usage events for team members
- ❌ You only sell B2C products (individual subscriptions)
- ❌ You don’t use seat-based pricing
- ❌ You don’t use our API directly.
Migration Timeline
| Phase | What Happens | Action Required |
|---|---|---|
| Week -2 | Migration announcement email sent | Review this guide, plan updates |
| Week -1 | Staging environment updated | Test your integration in staging |
| Week 0 | Production migration begins | Deploy updated code, monitor |
| Week 1-2 | One-on-one migration support | Work with Polar team if issues arise |
Getting Help
Before migration:- Review the updated API documentation (link to come)
- Test in staging environment (feature flag enabled for your org)
- Email support@polar.sh with questions
- Schedule 1-on-1 call if needed.
- Report issues immediately: support@polar.sh
- Check status page: status.polar.sh
Webhook Changes
The following webhook payloads will include newmembers array and member_id fields:
subscription.created / subscription.updated:
benefit_grant.created:
customer_seat.claimed:
- Extract and store
member_idfrom webhooks - Use
member_idfor subsequent API calls - Handle the new
membersarray in customer objects
FAQ
Q: Will my existing customers break? A: No. B2C customers continue working with zero changes. B2B customers will need your updated integration deployed before we migrate their data. Q: What happens if I don’t migrate? A: Your B2B customers will experience errors when trying to access the portal, generate events, or claim benefits. B2C customers are unaffected. Q: Can I test before production migration? A: Yes! We’ll enable the feature flag in staging 2 weeks before production. Test thoroughly with seat-based subscriptions. Q: How long do I have to migrate? A: We’ll coordinate with you to schedule a migration window. Most merchants complete the update in 1-2 days. We provide 2 weeks notice minimum. Q: Will historical data change? A: No. Past events, benefit grants, and subscriptions remain unchanged. Only new data uses the member model. Q: What if Alice works for multiple companies? A: Perfect! That’s why we built this. Alice will have separatemember_id values for each company (e.g., mem_alice_at_acme_123 and mem_alice_at_slack_456).
Q: Do I need to migrate B2C customers?
A: No. B2C customers are automatically migrated with full backward compatibility. Your existing code works unchanged.
Appendix E: ER Diagram (Option 1: Member Model)
Key Points:- Separation of Concerns: Customer = billing entity, Member = usage/access entity
- No Polymorphism: Customer table has no
typediscriminator, always represents billing - 1:1 for B2C: Individual customers have 1 default member
- 1:N for B2B: Business customers have N members (team members)
- Unique Constraints:
(customer_id, email)- Same email allowed across different customers, unique per customer(customer_id, external_id)- Same external_id allowed across different customers, unique per customer
- Event Attribution: Events always reference
member_id(never ambiguous across customers. Can be ambiguous across same customer with multiple subscriptions) - Denormalization:
customer_idkept in usage tables for query performance (optional) - Authentication: Customer portal sessions authenticate as Member, not Customer
- Backward Compatibility: Service layer accepts both
customer_id(resolves to default member) andmember_id
Appendix F: Event Attribution Strategy
This appendix details how we handle event attribution in B2B scenarios. Problem: When an individual customer is a member of multiple businesses, which subscription should be billed for their usage events? Solution: Usesubscription_id (optional) with automatic inference.
Inference Logic
Edge Cases
Case 1: Alice has her own B2C subscription AND is a member of Acme Corp- Inference: Fails (2 possible subscriptions)
- Merchant action: Must specify
subscription_id
- Inference: Fails (2 business subscriptions)
- Merchant action: Must specify
subscription_id
- Inference: Success (only 1 subscription)
- Merchant action: No action needed, auto-inferred
API Error Response
Appendix H: Complete Integration Flows (Side-by-Side Comparison)
This appendix shows how merchants integrate with Polar for common workflows, comparing Option 1 (Member Model) vs Option 2 (Customer Type + Member) side-by-side.Flow 1: Create B2C Customer
Option 1: Member Model
- Creates 1 Customer (billing entity)
- Auto-creates 1 Member (usage entity) with same email
- Member linked to customer
Option 2: Customer Type + Member
- Creates 1 Customer with
type="individual"(default) - No separate Member entity needed for individuals
Key Difference:
- Option 1: Always creates Customer + Member (uniform 1:N model)
- Option 2: Creates only Customer, type discriminates individual vs business
Flow 2: Create B2B Customer with Billing Manager
Option 1: Member Model
Option 2: Customer Type + Member
Key Difference:
- Option 1: Beneficiaries don’t exist outside the Customer.
- Option 2: Individual customers exist independently.
Flow 3: One-Time Product Purchase (B2C) with Checkout Sessions
Scenario: Alice purchases a $49 theme (one-time payment) using checkout sessions.Option 1: Member Model
Option 2: Customer Type + Member
Key Difference:
- Option 1: Benefit grant links to
member_id - Option 2: Benefit grant links to
customer_id(individual customer)
Flow 4: Seat-Based Product Purchase (B2B, 4 Seats) with Checkout Sessions
Scenario: Acme Corp purchases a 4-seat subscription ($199/month) for their team.Option 1: Member Model
{ "id": "cust_acme_123", ... }
Option 2: Customer Type + Member
{ "id": "cust_acme_123", "type": "business", ... }
Key Difference:
- Option 1: Seats link to
member_id - Option 2: Seats link to
customer_id(individual customer)
Flow 5: Seat-Based Product with Checkout Links (Public)
Scenario: Public checkout link for team subscription - auto-creates business on checkout.Option 1: Member Model
Option 2: Customer Type + Member
Key Difference:
- Option 1: Single customer created
- Option 2: Two customers created, one business and one individual
Flow 6: Download Benefits
Flow 6a: B2C Customer Download Benefit
Scenario: Bob (individual customer) access to a downloadable benefit.Option 1: Member Model
- Customer session endpoints accepts both
customer_id(legacy) andmember_id(new) - B2C customers have auto-created “default” member (1:1 mapping)
customer_id=cust_bob_123internally resolves tomember_id=ben_bob_default_456
Flow 6b: B2B Team Member Claims Benefit
⚠️ This contains a breaking change with seats functionalityScenario: Alice (team member at ACME) claims access to a downloadable benefit.
Option 1: Member Model
- ❌ Cannot use
customer_idquery param for B2B (ambiguous - which member?) - ✅ Must use
member_idto identify which team member - ✅ Authentication token contains member context
Option 2: Customer Type + Member
Key Differences:
- Option 1 (B2C):
customer_idbackward compatible, internally maps to default member - Option 1 (B2B): ⚠️ Uses
member_idfor explicit team member identification - Option 2: Always uses
customer_id(individual customer for both B2C and B2B members)
Flow 7: Customer Portal Link Generation
Flow 7a: B2C Customer Portal
Scenario: Bob (individual customer) needs to access Customer Portal to manage his subscription.Option 1: Member Model
- ✅ Endpoint accepts both
customer_id(legacy) andmember_id(new) - ✅ If
customer_idprovided → resolve to default member → create session - ✅ If
member_idprovided → create session directly
- Bob’s subscriptions (from customer_id: cust_bob_123)
- Bob’s benefit grants (from member_id: ben_bob_default_456)
- Bob’s orders and invoices
Option 2: Customer Type + Member
- Bob’s subscriptions
- Bob’s benefit grants
- Bob’s orders and invoices
Flow 7b: B2B Customer Portal
Scenario: Jane (billing manager at Acme) needs to see company billing info.Option 1: Member Model
⚠️ This contains a breaking change with seats functionalityWe can only use the member id. The customerId has multiple benefficieres and we don’t know which one to pick.
- Acme’s subscriptions (from customer_id)
- All members (list all team members)
- Company orders and invoices
- Aggregated usage across all members
Option 2: Customer Type + Member
- Check if cust_jane_456 is a member of any business customers
- Find member record: business_customer_id = cust_acme_123, role = “billing_manager”
- Show billing manager view
- Acme’s subscriptions (from business customer)
- All members (query members of cust_acme_123)
- Company orders and invoices
- Aggregated usage across all members
Key Difference:
- Option 1: ⚠️ Single
member_idgives access to both personal and business context. Not compatible with B2B. - Option 2:
customer_idrequires lookup via Member table to find business context.
Flow 9: Event Ingestion
Flow 9a: B2C Event Ingestion
Scenario: Bob (individual customer) makes an API call - track usage.Option 1: Member Model
- Endpoint accepts both
customer_id(legacy) andmember_id(new) - If
customer_idprovided → resolve to default member → record event - If
member_idprovided → record event directly
Option 2: Customer Type + Member
Flow 9b: B2B Event Ingestion
⚠️ This contains a breaking change with seats functionalityScenario: Alice (team member at Acme) makes an API call - bill Acme’s subscription.
Option 1: Member Model
- Bills Acme’s subscription
Option 2: Customer Type + Member
- Polar checks if cust_alice_123 is a member of any businesses
- Finds member record: business_customer_id = cust_acme_456
- If Alice is member of ONLY ONE business and doesn’t have a subscription, we will bill that business
Flow 10: Customer Portal Login
The user access: https://polar.sh/petru-test/portal/requestFlow 10.a: B2C login
In both cases work as expected.Flow 10.b: B2B login
Option 1: Member Model
Member emails are not unique across per organization, they are unique per Customer. This means that a single email address can be associated with multiple members within the same organization. These are different entities that can be associated with the same email address.Option 2: Customer Type + Member
Every customer has a unique email address. This means that a single email address, but they can have subscriptions attached to them and being a member of multiple business customers.Summary: API Call Comparison
| Flow | Option 1: Member | Option 2: Customer Type + Member |
|---|---|---|
| 1. Create B2C | Easy | Easy |
| 2. Create B2B + Manager | Easy | Easy |
| 3. Checkout Session (B2C) | Easy | Easy |
| 4. Checkout Sessions (B2B) | Easy | Easy |
| 5. Checkout Links (B2B) | Easy | Easy |
| 6a. Download Files B2C | Easy | Easy |
| 6b. Download Files B2B | ⚠️ Breaking change | Easy |
| 7a. B2C Customer Portal Session | Easy | Easy |
| 7b. B2B Customer Portal Session | ⚠️ Breaking change | Easy |
| 9a. B2C Event Ingestion | Easy | Easy |
| 9b. B2B Event Ingestion | ⚠️ Breaking change | 🟡 Medium |
| 10a. Customer Portal Login (B2C) | Easy | Easy |
| 10b. Customer Portal Login (B2B) | 🟡 Medium | 🟡 Medium |

