Skip to content

Conversation

@psincraian
Copy link
Collaborator

@psincraian psincraian commented Nov 5, 2025

@pieterbeulque
Copy link
Contributor

Great write-up, thank you! The problem statement & tenets are a good definition of what we're trying to fix.

I have been thinking about this myself and I believe I figured out a more backwards-compatible option. The impact throughout the app will be most limited if we can consider ACME & Lolo the customers in our current data model. Billing, tracking usage, … will then all be grouped to the business entity without requiring any changes. From that angle, it's worthwhile exploring if we can keep that assumption as a ground truth and design seat-based billing around that.

Up until we introduced seat-based billing, Polar was designed with a implicit 1:1 relationship between the customer (i.e. the entity who pays for a transaction) and the benefit recipient (i.e. the entity who gets granted the benefits from said transaction). With seat-based billing, this relationship is no longer 1:1 but 1:many, causing the issues you aptly pointed out.

I'm wondering whether we can flip the script on option #6. With that, I mean instead of adding the business umbrella layer "above" multiple customers, keep the customers as-is and add a member layer "below" a single customer.

This way, we can design this customer:beneficiaries relationship, keeping it implicit for backwards compatibility and requiring it only for seats. In a way, through the customer_seats table, we're already halfway there. It's just that upon assigning a customer seat to a member of your organization, this would create a Beneficiary (not a good name) instance instead of a Customer.

This way, everything is still tracked under the Customer umbrella, but we can opt-in to more granular behavior where we want to without introducing breaking changes. For example, for event ingestion, we could expand with a beneficiary_id or member_id if you want to track usage to the account but also to the specific member of that team.

If a product/subscription is not seat-based, the Customer itself is the Beneficiary, or we could run a one-time migration that creates a CustomerMember for each Customer to make it explicit instead of synthetic (implementation detail).

We could then either make this a one-to-one relationship (a member can belong to only one customer), or we immediately go all the way and introduce a many-to-many table that could also hold metadata (like authorization / role / …) on the relationship between the member and the customer.

Copy link
Member

@frankie567 frankie567 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice write-up!

If I were to choose an option, I would go for Option 1, even if it has the lowest score. It feels like the most natural one while allowing for maximum flexibility. I'm not a big fan of Option 6 with synthetic business, since it'll add lot of burden for a vast majority of users who don't care about those features...

...which brings me my main point 😄 Do we really want to go that far now? Yes, it was asked by one big potential customer. But as I mention in one of my comment, I'm not sure multi-tenants with single authentication layer will be super common. Who does that in the industry today, apart behemoth like Slack?

My point is that in most SaaS businesses, you have one organization with its set of customers. If you as an individual are part of several organizations, you'll likely have several accounts with different emails and logout/login.

All in all, I'm not sure it's worth the hassle.

customer_id: UUID

# NEW - for business-owned subscriptions
business_id: UUID | None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if they omit the business_id? Should we reject the event or invoice it "best effort" like we do today?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the customer has a seat based product with usage-based billing, I would reject it.


#### Tenets
1. ✅ Billing accuracy: events are attributed to a single customer
2. ❌ Backward compatibility: customers that enabled seat-based billing will receive null customerIds in the API responses for some entities.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we detail how seat-based billing will look like in that context? In particular CustomerSeat?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomerSeats will work as it is now. BusinessCustomer it's only used to keep track of billing managers and other roles that we will have. Maybe BusinessManager is a better name.

#### Tenets
1. ✅ Billing accuracy: events are attributed to a single customer
2. ❌ Backward compatibility: customers that enabled seat-based billing will receive null customerIds in the API responses for some entities.
3. ✅ Customer experience: individual customers and business customers can have a tailored experience based on their needs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have an issue to tackle then with the customer portal. Since a customer with an email can belong to different teams (businesses) with different subscriptions, we'll need to know which team they want to log into. Meaning:

  • From the API: require the merchant to pass business_id to generate the portal link
  • From a customer-flow: have a team selector after successful email authentication

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we can display all data across all businesses, similar to when a user has multiple subscriptions.

#### Tenets
Same as option 1, but with:
1. ✅ Billing accuracy: events are attributed to a single customer
2. **🟡 Backward compatibility**: we will always return the customerId in the response, but now it can be a businessId or a customer itself. But business ids will not be available under `/v1/customers/{customerId}`. This will affect only orgs that enabled seat-based pricing.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another problem too: existing customer ID will change (because they'll now be BillingCustomer ID); unless we do trickery during migration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of doing trickery with migrations and having id == customerId 🙈

4. 🟡 Merchant Developer experience: merchant will treat all the customers the same way. But a first migration is needed.
5. 🟡 Operational Flexibility: this maps to WorkOS and allows all features requested by our customers.
6. ❌ Performance: one extra joined load on every customer query
7. **✅ Polar developer experience:** we need to be aware of the branching and boundaries of what is a Customer is less defined.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a 🟡, if we want to be consistent

@psincraian
Copy link
Collaborator Author

Hey @pieterbeulque,

Thanks for the new suggestion. Before adding it, I have a couple of doubts.

  1. Customers can have a polymorphic value, Customers or Business, right? This adds confusion, as in Option 2. Maybe I should add a "Semantic Clarity" tenet 🤔
  2. Is beneficiaries a subclass of customer? If not, we will have the same problem as in Option 2 where /v1/customers/ doesn't return all customers.
  3. If we have beneficiaries as a replacement of customer seats we still have a problem with "billing managers". I don't see how we can have a role of admins or billing managers without occupiying a seat.
  4. Sometimes benefits are granted to beneficiaries, therefore we still have breaking changes on the /v1/benefits. Similar to option 1.

I think the semantics looks like this:

Approach "Customer" means "Person who uses product" "Entity who pays" Clear?
Current Person Customer Customer ✅ Yes
Option 1 Person Customer Customer OR Business ✅ Yes
Option 5 Person or Business (discriminator) Customer Customer ❌ No
Option 6 Person Customer Business 🟡 Overhead
Beneficiary Person or Business (implicit) Customer OR Beneficiary Customer ❌ No

And here is the table with tenets.

Option Weight Option 1: Business + BusinessCustomer Option 2: Single Table Inheritance Option 6: Synthetic Business Option 7: Beneficiaries
Billing Accuracy 7
Backward compatibility 6 🟡 🟡 🟡
Customer experience 5
Merchant dev experience 4 🟡
Operational flexibility 3 🟡
Performance 2 🟡
Polar dev experience 1 🟡 🟡 🟡
  1. ✅ Billing accuracy: events are attributed to a single customer
  2. ❌ Backward compatibility: some endpoints will not work as before, like /v1/benefits/grants or /v1/customers/{id}/state as we are using beneficiary instead of customerId. This will affect only orgs that enabled seat-based pricing.
  3. ✅ Customer experience: individual customers and business customers can have a tailored experience based on their needs.
  4. ❌ Merchant Developer experience: the merchant will need to do branching depending if the customer is a business or an individual customer.
  5. ✅ Operational Flexibility: no option of having multiple managers like in problem 4.
  6. ❌ Performance: one extra joinload on all queries that affect customers or businesses.
  7. 🟡 Polar developer experience: we need to be aware of the branching.

Let me know if I misunderstood something.

@pieterbeulque
Copy link
Contributor

Thanks Petru! I realize my initial explanation wasn't clear enough - the terminology is definitely confusing. Let me try again with clearer definitions.

I'm proposing to keep the current Customer model as the billing entity (what you call Business in most options). I believe you're interpreting the customer to be the end user (what I'm calling Beneficiary or Member in my proposal). I'm saying Customer (= entity that pays = ACME/Lolo/an individual/…) + Beneficiary (= entity that uses Slack = ACME employee/an individual/…). In your option 1 or 6 you name those respectively Business (paying entity) + Customer (using entity) which may introduce some confusion.

So, not:

Business (ACME) ← New
  └── Customer (Alice) ← Current Polar Customer model
  └── Customer (Bob) ← Current Polar Customer model

but:

Customer (ACME)  ← Already our billing entity
  └── Member (Alice)   ← New
  └── Member (Bob)  ← New

Basically, the core of my proposal is to consider ACME/Lolo as first-class customers in our system so that we can preserve our entire billing infrastructure and while solving seat-based as an isolated feature, instead of having this change become a major refactor. My proposal simply formalizes that ACME is the customer, not a new entity type above customers. This is both semantically accurate (ACME is literally our customer) and architecturally simpler.

Just making sure that we're entirely clear on that distinction.

To address your other questions:

  • No polymorphism. Customers remain exactly what they are today - the billing entity. Some customers are businesses (ACME), some are individuals (solo users). This doesn't require us to change anything about our current Customer model
  • Beneficiaries are not a subclass of customers, they're completely separate. This is a new entity that represents product users. For backward compatibility, when there's no seat-based billing, the Customer implicitly is also the sole beneficiary.
  • It's a good point that implementing billing manager without occupying a seat is not straight-forward in this design, but it's definitely solvable (e.g. have an billable flag on the customer-beneficiary relationship, or don't count the role: billing_manager towards the seat usage, …).
  • On backwards compatibility: I think it's a very backwards compatible option. It reduces the surface area of changes introduced to the people that opt in to seat-based billing. Consider:
    • Non-seat-based subscriptions: Zero changes. Customer pays, Customer benefits. Member/Beneficiary layer doesn't exist or matter.
    • Seat-based subscriptions: Customer (ACME) pays, Members benefit. All existing endpoints continue working:
      • /v1/customers returns ACME (correct - they're the customer)
      • /v1/benefits/grants?customer_id=acme returns all ACME's grants (correct)
      • NEW: /v1/benefits/grants?beneficiary_id=alice for granular queries
    • Migration: Existing customers need no changes. Only seat-based products use the Beneficiary/Member model.
  • In a similar vein, there will indeed be opt-in changes to the CRUD on /v1/benefits, that's true. But again, this approach introduces changes only for seat-based subscriptions. We will have to introduce some changes somewhere to support seat-based, regardless of which architecture we choose.

Why I think this approach deserves consideration: the billing entity (Customer) remains unchanged, so all payment, invoice, subscription logic stays intact, making seat-based an opt-in feature layer, not a fundamental restructure. No need to retrofit all existing customers into a Business model, and to me it's semantically clear: ACME is our customer. Their employees are members/users of ACME's subscription, not direct customers of Slack.

@Yopi
Copy link

Yopi commented Nov 6, 2025

This is really well formulated and thought out Petru!

I am a bit inclined to either Option 1 or what Pieter is suggesting.

In an ideal world this change becomes negligible or a no-op for customers who are B2C today, and don't really care about seats or selling to businesses. Whereas for the B2B customers it doesn't add too much complexity. Both of these might fall under the "Customer experience"-tenet.

This is what I like about option 1, in that it's only for the business-type customers (events) that you need to specify it, and all else just continues to work as it is.

For your suggestion Pieter are you thinking the same as in Option 1 when it comes to specifying the individual who is tracked by an event (or a meter if we would want to have per customer+member based limits in the future)?

Sidenote:
When thinking about this on my own I also had a thought if we were to add a concept above Organizations similarly to Pieters line of thinking about adding a concept to the leaf of the existing compacts, to have as low impact as possible to the existing workings of things. I don't think it solves all of the issues without adding a ton of complexity for us internally, but a user can already today jump between organizations so it wouldn't necessarily have to be too bloody.

Merchant (What an organization is today) 
  └  Organization (ACME)   
       └─ Customer (Alice)   
       └─ Customer (Bob)  

@pieterbeulque
Copy link
Contributor

@Yopi — Yes, regarding event tracking, in my proposal it would work out of the box to ingest events for customers (i.e. the ACME) with the current customer_id, and we can introduce a new optional property member_id that could be used to assign that event to both ACME and Alice. Does that answer your question?

(Not to go into the details that that should then probably be external_member_id to align with our current external_customer_id event attribution flow, but that'd take us too far)


#### Tenets
1. ✅ Billing accuracy: events are attributed to a single customer
2. 🟡 Backward compatibility: customers that enabled seat-based billing will receive null customerIds in the API responses for some entities.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that? Wouldn't a business always at least have one customer tied to it (the one who set up the subscription/order)?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this particular proposal not anymore. Here all billing entities, Order, Subscription, PaymentMehtod, Refund, etc. will have a customerId OR businessId set.

This is because we want billing entities attached to business, so when a member changes we don't need to do any transfer of data.

1. ✅ Billing accuracy: events are attributed to a single customer
2. 🟡 Backward compatibility: customers that enabled seat-based billing will receive null customerIds in the API responses for some entities.
3. ✅ Customer experience: individual customers and business customers can have a tailored experience based on their needs.
4. ❌ Merchant Developer experience: the merchant will need to do branching depending if the entities and customer is a business or an individual customer.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For organizations with this model, I think the branching is actually helpful. E.g Better Auth, WorkOS and others have the business / customer model (although different names) and carries such IDs in their sessions. So passing them forward 1:1 for our named properties in the API feels easier vs. other potential solutions.

I worry more around the notion of moving orders and subscriptions to businesses. Since that would be a breaking change, e.g someone who starts to sell a seat-based product from previously only having standard SaaS tiers could now receive webhooks for orders/subscriptions with a potential null for customer_id?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, if merchant enable seat based billing they will receive null on customer_id and a value on business_id.

There is proposal 6 where this doesn't happen

@birkjernstrom
Copy link
Member

Fantastic write-up Petru!

At first, I was in favour of Option 1 – provided timeline/scope would not be too brutal (see prioritization below). Since it maps 1:1 to how such models are reflected elsewhere, e.g Better Auth or WorkOS (different terms) etc. Combined with optionally & incrementally introducing complexity as needed, i.e business_id is only used in case needed.

However, I'm against that it branches core resources like orders and subscriptions with business_id and/or customer_id and subsequently introduces breaking changes to those resources. That can cause a lot of pain:

  • Existing integrations expect customer_id on all such resources and suddenly new ones could come in with business_id-only - causing an exception/breakage
  • You have to check both and reason about the structure to determine a type

Overall, I started second guessing having multiple explicit entities on our end. For Auth providers or multi-tenant applications, it makes total sense given more distinct requirements for expanded data models surrounding them and ancillary tables with relationships to them each respectively. However, for us, they're both quite slim and arguably desirably so for a SaaS integration to simply map internal IDs to billing resources on our end.

Additionally, it adds unnecessary depth of filtration or ingestion. Since an organization can have multiple subscriptions we would have three parameters to consider in an ingestion:

  1. customer_id
  2. business_id
  3. subscription_id - solving for multi-subscription/customer organizations

Combined with terminology and settings on our end, e.g "Allow multiple subscriptions per customer". Could easily become a setting both for "business" and "customers". In short: By introducing 2 entities, we risk multiplying everything over time.

So I started seeing more value in an approach like Option 5 focused on retaining one entity. But I have an alternative proposal for it and inspired by Pieter's proposal although it's flattened.

Nested Customers

Inspired by events and our introduction of parent_id. However, only allowing 1 level of depth.

class Member(RecordModel):
    customer_id: UUID
    role: MemberRole

class Customer(RecordModel):
   # existing attributes

   members: list[CustomerMember]
   type: 'individual' | 'business'  # potentially not needed

Pros:

  • Maps 1:1 to preferred customer view/design: Seeing all root customers (B2C or B2B), but B2B customers having their business name and a > indication showing X members and treated as customers
  • No breaking change to our data models or resources (API/Webhooks)
  • Maps to how other billing solutions work, e.g Stripe, Metronome, Paddle and others with only customers, but with added flexibility given members. Interestingly, Orb has a similar concept.
  • Solves that the root customer is the "billed business" with members that could access billing for the parent
  • Easier integration: Only customer_id and subscription_id in event ingestion worst-case. Progressive complexity as one opts in for this, and it's easier to manage vs. different entities on orders/subscriptions/events imo.

Cons:

  • Customers can be of different "types", but still the same shape. Ultimately, however, I think this has the least amount of complexity for an integration and us though.

Re: Prioritization
Before build, let's 1) assess timeline & 2) share with customer for feedback. I spoke with them and they already have one-to-many relationships for one of their customers, but have themselves not implemented this natively, i.e TBD on urgency in the short-term. Nonetheless, it's an important abstraction to get right for the future and align on. So great to finalize so we're ready.

3.**Customer experience**: Clear separation - Customer = payer, Member = user
4. 🟡 **Merchant Developer experience**:
- ✅ Non-seat-based: No changes
- ❌ Seat-based: Need to track customer_id as team or individuals
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me rephrase this, on the Customers api the merchant should be aware if the customer is a "business" or an individual member


**Key Points**:
1. **CustomerSeat is NOT replaced** - Member is an additive layer that extends CustomerSeat with role management
2. **Every Member must link to individual Customer** - Required for authentication and benefits
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I 100% understand where you're coming from on this thanks to your previous comments, but I still have to warm up to the idea of circling back the members into the customers table.

I've been doing a lot of metrics & grouping and segmenting things per customer lately for cost analytics, so I may be over-indexing on the importance of the split between paying vs product-using entities.

It'd be great for cost analytics knowing that if you query by customer_id it's always ACME & Lolo without having to filter out the Alice's for each query. Especially since you don't know on the customer itself if it's a paying entity or a member entity, you'd have to do a double join to be able to filter these out?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if that's the problem, I think we can add a "type" that is either: individual, business, or member.

We may still have problems with individuals that are members also 🤔

Something like this should be exposed in the API so merchants can decide it on the fly, but at the beggining I was thinking of computing it but maybe it's worth having it stored in the DB.

customer_id: UUID # Who pays (unchanged)

# NEW - explicit user attribution for seat-based scenarios
member_id: UUID | None # Who used (optional)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the main thing I don't understand with this approach. Currently, we push our users to use external_customer_id, which will likely be the customer who triggers the event, not the one who is paying (they don't care about that).

So it looks to me we're flipping the script a bit + require them to track an opaque Polar-generated ID: member_id, which is not ideal in terms of DX.

Could we consider something like this instead:

(external_)customer_id: UUID # Who used 

# NEW - explicit billing attribution for seat-based scenarios
(external_)billing_customer_id: UUID | None # Who pays

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.

Design Document: Add business concept into polar

6 participants