<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Cadensa</title>
    <description>The latest articles on DEV Community by Cadensa (@cadensa).</description>
    <link>https://dev.to/cadensa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4003346%2F1452be8b-46b3-4d72-991b-8399a3c256d5.png</url>
      <title>DEV Community: Cadensa</title>
      <link>https://dev.to/cadensa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/cadensa"/>
    <language>en</language>
    <item>
      <title>How we handle multi-tenant billing for EU agencies</title>
      <dc:creator>Cadensa</dc:creator>
      <pubDate>Sun, 05 Jul 2026 19:46:42 +0000</pubDate>
      <link>https://dev.to/cadensa/how-we-handle-multi-tenant-billing-for-eu-agencies-cpd</link>
      <guid>https://dev.to/cadensa/how-we-handle-multi-tenant-billing-for-eu-agencies-cpd</guid>
      <description>&lt;h2&gt;
  
  
  The constraint: one product, multiple billing realities
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we built: per-workspace rate cards
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What was hard: webhooks and tenant resolution
&lt;/h2&gt;

&lt;p&gt;The hardest part wasn't the rate card model. It was figuring out which tenant a Mollie webhook belonged to.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we'd do differently
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Building Cadensa — EU-native time tracking for agencies. &lt;a href="https://cadensa.io" rel="noopener noreferrer"&gt;cadensa.io&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>mongodb</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Why we built EU-native time tracking (and what Schrems II has to do with it)</title>
      <dc:creator>Cadensa</dc:creator>
      <pubDate>Fri, 26 Jun 2026 06:12:23 +0000</pubDate>
      <link>https://dev.to/cadensa/why-we-built-eu-native-time-tracking-and-what-schrems-ii-has-to-do-with-it-2e7o</link>
      <guid>https://dev.to/cadensa/why-we-built-eu-native-time-tracking-and-what-schrems-ii-has-to-do-with-it-2e7o</guid>
      <description>&lt;p&gt;In 2020, the Court of Justice of the European Union invalidated the EU–US Privacy Shield. The ruling — known as Schrems II — declared that US surveillance law makes it impossible for US companies to guarantee the privacy of EU citizens' data, even if that data is stored "in Europe."&lt;/p&gt;

&lt;p&gt;Most SaaS founders in the US probably shrugged. Most EU agency owners probably didn't notice at all. But the implications were significant: transferring personal data to a US-based processor became legally risky, even if those processors had EU data centers.&lt;/p&gt;

&lt;p&gt;I built Cadensa because I kept running into this exact problem while advising small EU agencies on their software stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem hiding in plain sight
&lt;/h2&gt;

&lt;p&gt;Let's say you run a 12-person digital agency in Amsterdam. You use a popular US time tracking tool — one that has a "Frankfurt data center" checkbox. You tick the box, feel good about it, and move on.&lt;/p&gt;

&lt;p&gt;Here's what you might not know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The vendor is a US company subject to FISA Section 702 and CLOUD Act requests&lt;/li&gt;
&lt;li&gt;Even if your data sits on EU servers, US authorities can compel access&lt;/li&gt;
&lt;li&gt;Your DPA is using standard contractual clauses that post-Schrems II case law increasingly views as insufficient&lt;/li&gt;
&lt;li&gt;Your enterprise clients — especially in finance, healthcare, or public sector — are starting to ask for a data processing agreement that names the actual sub-processors&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is FUD. It's just the current legal reality.&lt;/p&gt;

&lt;p&gt;And the truly ironic part: &lt;strong&gt;time tracking data is surprisingly sensitive&lt;/strong&gt;. It contains project names, client names, employee working hours, task descriptions, and billing rates. It's exactly the kind of data that triggers GDPR Article 5(1)(f) — integrity and confidentiality.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we built instead
&lt;/h2&gt;

&lt;p&gt;Cadensa runs entirely on &lt;strong&gt;Hetzner Cloud, Frankfurt&lt;/strong&gt;. Not "Frankfurt as a region on AWS." Hetzner is a German company, incorporated in Germany, subject to German law and the GDPR — not the CLOUD Act.&lt;/p&gt;

&lt;p&gt;GDPR compliance isn't a toggle you flip on the Enterprise plan. It's on every plan, including Free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Data stored in the EU (Germany)&lt;/li&gt;
&lt;li&gt;Right to erasure: full account deletion with 7-day reversible grace period&lt;/li&gt;
&lt;li&gt;Right to portability: JSON export of all your data, on demand&lt;/li&gt;
&lt;li&gt;Data processing agreement available at signup, not behind a sales call&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Multi-tenancy is database-level, not row-level. Each organizational unit gets its own isolated MongoDB database. A breach in one tenant can't cascade to another. This is more expensive to operate, but the security and compliance story is clean.&lt;/p&gt;




&lt;h2&gt;
  
  
  The technical part (for the devs reading this)
&lt;/h2&gt;

&lt;p&gt;The stack: &lt;strong&gt;Express.js + TypeScript&lt;/strong&gt; on the backend, &lt;strong&gt;React 18 + Vite&lt;/strong&gt; on the frontend, &lt;strong&gt;MongoDB&lt;/strong&gt; (multi-database architecture), &lt;strong&gt;Socket.io&lt;/strong&gt; for real-time presence and live updates.&lt;/p&gt;

&lt;p&gt;The thing that surprised me most architecturally: implementing GDPR rights as first-class API citizens is genuinely hard. "Delete my account" sounds simple. In practice, it means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Soft-delete with a 7-day grace period (user can reverse)&lt;/li&gt;
&lt;li&gt;Hard-delete: cascade across multiple databases (global user record + all tenant databases)&lt;/li&gt;
&lt;li&gt;Anonymize data that legally must be retained (financial records, audit logs) rather than delete it&lt;/li&gt;
&lt;li&gt;Export: traverse the entire data graph and serialize it to a portable format&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We have a &lt;code&gt;gdprRights.service.ts&lt;/code&gt; that handles this. It's one of the more complex parts of the codebase, and it's not optional — it's a legal requirement.&lt;/p&gt;

&lt;p&gt;Email hashing was another non-obvious decision: user emails in the global database are stored as SHA-256 hashes. We never store plaintext emails globally. This limits some email-based lookups, but it means a global DB dump leaks no PII.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd tell other EU founders
&lt;/h2&gt;

&lt;p&gt;If you're building a SaaS for European SMBs or agencies, &lt;strong&gt;EU data residency is becoming a selling point, not just a checkbox&lt;/strong&gt;. We've had prospects come to us specifically because their enterprise clients required it.&lt;/p&gt;

&lt;p&gt;You don't need to be perfect on day one. But you do need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A clear answer to "where is my data?"&lt;/li&gt;
&lt;li&gt;A DPA you can actually show to a client's legal team&lt;/li&gt;
&lt;li&gt;No hand-waving about "we're GDPR compliant" without specifics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The 12-person agency in Amsterdam doesn't have a dedicated compliance officer. They need a tool that makes the right answer easy to give.&lt;/p&gt;

&lt;p&gt;That's what we're building with Cadensa.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Cadensa&lt;/strong&gt; is EU-native time tracking for agencies — free plan, no credit card required. If you're building something similar or have thoughts on GDPR + SaaS, I'd love to hear from you in the comments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cadensa.io" rel="noopener noreferrer"&gt;cadensa.io&lt;/a&gt; — building in public, one sprint at a time.&lt;/p&gt;

</description>
      <category>sass</category>
      <category>webdev</category>
      <category>buildinpublic</category>
      <category>gdpr</category>
    </item>
  </channel>
</rss>
