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.


#### 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.

@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)

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.


**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.

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.

Totally aligned with the proposed approach and implementation.

Strong opinion however on the way we should solve the business vs individual customer in the code using polymorphic entities. I suggest you take a look :)

**Feature 2**: Update logic to handle both individual and business customers.
2. **Update Event Model**. We should validate that the event model has a business_customer_id if the customer is on multiple businesses.
3. **Update billing entities logic**: we should make sure that the person who is updating the subscription/order/etc. has permissions to do it.
4. **Checkout Flow Updates**: if we are purchasing for a business, we should create 2 customers, one for the business, and another one for the 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.

Could we skip the individual customer creation and only create it if/when they claim their seat?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The problem happens with "Workflow A: that is someone selling themes". In this case, they would only have a checkout link with seat based billing enabled.

In that case, we need to create 2 customers:

  1. Business customer: that holds everything
  2. At least a member (therefore a customer): that can manage the business cusotmer

customer_id: UUID # Usage actor (always individual)
billing_customer_id: UUID # Billing payer (individual OR business)

# Relationships with explicit naming
Copy link
Member

Choose a reason for hiding this comment

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

Excellent callout!

Copy link

@Yopi Yopi left a comment

Choose a reason for hiding this comment

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

Nice update Petru!

I think this makes sense.

Just to clarify, I think this is the case but I was confused by this earlier as well. We will now get an try per business and per user in the customers table. So we will end up with e.g.

- ACME (type business)
- ACME employee Alice (type individual)
- ACME employee Bob (type individual)

2. **Billing Transfer**: Allow transfer of subscriptions to another billing manager
3. **Clear Attribution**: Easily know who is the billing customer of an event

### Ideal Workflows
Copy link

Choose a reason for hiding this comment

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

Very clear, good idea to write this down!

Comment on lines +452 to +453
customer_id: UUID # Usage actor (always individual)
billing_customer_id: UUID # Billing payer (individual OR business)
Copy link

Choose a reason for hiding this comment

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

How will this work on the ingestion API? Will you need to specify both?

Copy link
Member

Choose a reason for hiding this comment

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

I'm not in love with this naming. I'm also not sure if it solves the problem of proper attribution? E.g scenario:

  • Customer A is a member of Business A with Subscription 1
  • Customer A is also a member of Business B with Subscription 2
  • Customer A is also a member and user within Business B for Subscription 3 (same product as Subscription 2)

We want to know which subscription to incur the usage charges for, right? By only specifying Customer A and Business B, we still don't know whether to attribute it to Subscription 2 or 3.

Therefore, I believe it's inevitable to require subscription_id and as a consequence whether that's sufficient here, e.g customer_id (required) and subscription_id from which we can also infer and even automatically decorate the business. Just to keep the number of IDs and reference passed to a minimum for improved DX combined with avoiding the mental challenge of distinguishing customer_id vs. billing_customer_id

Otherwise, I think calling it business_customer_id might be better. Keeping the naming and customer.type the same?

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 still have this dilemma, but I would say it's a minor thing. We have three options:

Option 1: Require only if the organization has seat-based billing enabled
This way we will require it for ALL event ingestions as customerId can be both an individual or business

  • + Merchants who switch will get a warning that they need to modify the integration to send billing_customer_id
  • ~ The API will have the same behaviour of failing for all customers if not specified
  • - On B2C only customers will always be the same.

Option 2: require it only if customer is a member of an business

  • + For B2C customers will work out of the box
  • ~ Merchant needs to know if the customer is B2B and then send the business_id

Option 3: Polar tries to assing the correct billing_customer_id

  • ~ On all B2C only customers this will work, on B2B should also work as customer are usually on on a single business. This will only fail in 2 cases:
    • If the customer is a B2C and B2B
    • If the customer is 2 B2B business.

Both cases should be rare. The problem is that merchant will get 4XX on those requests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@birkjernstrom I'm in favor of the subscription id, there is only 1 problem. What happens if the customer doesn't have any subscription?

Nowadays we can ingest events without any products attached to the customer.

- **Simpler mental model**: Everything is a customer - individuals and businesses
- **Easier migration**: Just add `type` field and Member table, no entity splitting required

**Key Decision Rationale**: Option 2 scores lower (62/81 vs 66/81), we're prioritizing architectural simplicity over developer experience. Having a single Customer entity is conceptually simpler and avoids the complexity of splitting billing from usage across two entities. The developer experience trade-offs (type filtering) are manageable with proper tooling. We can migrate to BillingAccount if this became a need.
Copy link
Member

Choose a reason for hiding this comment

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

When we say developer experience here we're only referring to our own internally, I believe? Think that's important to align on and clarify.

Because the DX for our merchants should be 💯

@birkjernstrom
Copy link
Member

Nice update Petru! I think we're closing in. I have three concerns remaining:

  1. Naming, documentation and clarity: I like the approach of nested customers to offer flexibility and more advanced models while still retaining backward compatibility and avoiding too many resources/attributes to keep track of as part of integrations. But I worry that billing_customer_id and customer_id at first glance will seem confusing and always make developers have to think hard on which one to use for what. I believe subscription_id might be a requirement for the problem we want to solve and potentially avoids the need for billing_customer_id entirely. Or we should potentially rename it and keep it consistent with customers.type, e.g business_customer_id. Additionally, I think we need to write an example on how this can be used that we can share with a few people for quick sanity check. Such a core architectural change so important one to get right and make sure it's clear to external people.

  2. I do think we need an additional flag/attribute on the customers, e.g billing: boolean (naming TBD). What I don't like about our model which seats and this feature amplifies is the following: We'll have a lot of "customers" that are not customers, i.e paying for products themselves directly. Exposing them all in our dashboard under the "Customer"-view will create confusion and frustration amongst our merchants. That list should only reflect customers that have paid or setup billing on their own, e.g businesses or individuals buying. Customers that are members should only be shown as nested members within their top-level business entity AND standalone in case they are also individual customers themselves.

  3. We state that we create 2 customers in case of a business checkout. But how will that look like for the person who integrates? Will they get the customer_id of the user we create for the business? But I assume the order and subscription will be tied to the business_customer_id? So using their customer_id for searching orders etc will not work?

The latter point worries me the most. I think it would be helpful to see this fully from an integration perspective in terms of a documentation example, i.e what calls they would make, expected responses, what they would store and use in future calls.

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