The constraint: one product, multiple billing realities
Cadensa is a time tracking tool for EU agencies. Each agency is an organizational unit in the system — isolated MongoDB database, isolated workspace hierarchy, isolated team permissions. The multi-tenant architecture was straightforward to design because the isolation requirements were clear.
Billing isolation is less obvious. At the surface, it looks like a subscription management problem. Under that, it's a per-tenant rate card problem, a currency problem, a VAT calculation problem, and a payment method availability problem. Stripe's global defaults work fine if your customers are mostly US SaaS buyers. For EU agencies paying by SEPA direct debit or iDEAL, Mollie is the right tool. For the actual billing logic — rate cards per workspace, billable hour calculations per project, invoice generation per billing cycle — Mollie handles payment collection, not the data model.
What we built: per-workspace rate cards
The billing model works at two levels. At the unit level, a subscription defines the plan tier (Free / Pro / Enterprise) and the billing cycle. At the workspace level, rate cards define hourly rates per team member, per project type, or flat fee per engagement. These are what agencies actually use to calculate client invoices.
The rate card data lives in the unit's own MongoDB database, not in a shared billing service. That decision was deliberate. Under GDPR, the right to erasure applies to all data the controller holds. If rate card history lived in a shared billing database, deleting a unit would require coordinating across two systems. With the data in the unit's isolated database, deletion is a single operation: drop the database.
The Mollie integration handles the subscription layer only. Payment intents, webhooks, plan upgrades, subscription lifecycle — all of that goes through Mollie. The rate cards, billable hours, and generated invoice data stay inside the tenant.
What was hard: webhooks and tenant resolution
The hardest part wasn't the rate card model. It was figuring out which tenant a Mollie webhook belonged to.
Mollie sends webhook events to a single endpoint. The event contains a payment or subscription ID, not a tenant identifier. On the first attempt, we looked up the tenant by iterating over active subscriptions. That works at low scale. It doesn't work when there are enough tenants that the lookup becomes an O(n) database scan on every payment event.
The fix was non-obvious: when creating a subscription in Mollie, we pass the unit's bundle ID as metadata. The webhook handler reads that metadata first, resolves the correct tenant database, and processes the event inside that tenant's context. One lookup instead of a scan.
What we'd do differently
Build the tenant resolver before the billing integration, not after. The shape of the multi-tenant routing logic determines how every external service call gets structured. Getting that wrong early means refactoring webhook handlers, subscription creation flows, and invoice generation simultaneously — which is exactly what we did.
EU billing also requires accepting that there is no universal solution. Payment method availability, VAT rules, invoice formatting requirements, and currency expectations vary by member state in ways that no single billing library handles cleanly. The right model is a thin, tenant-aware billing service that delegates payment collection to a provider built for European markets, and keeps everything else in the tenant's own data layer.
Whether that's the right tradeoff for every multi-tenant SaaS is a separate question. For a GDPR-first product, it's the only tradeoff that actually works.
Building Cadensa — EU-native time tracking for agencies. cadensa.io
Top comments (0)