Solutions / B2B SaaS

Plans, seats and tenants are business rules too.

Entitlements live in billing, in feature flags and in your own tables. Downgrades, imports and admin tools are where a customer ends up with more than they pay for, or with someone else’s data. Invarly checks both, continuously.

Multi-tenant appsSeat-based plansFeature entitlementsAdmin tools

Why SaaS breaks quietly

A B2B product is a set of promises: this customer has these features, this many seats, and only sees their own data. Those promises are enforced in many places at once, from middleware to background jobs to the admin panel your support team uses.

When one path forgets a check, nothing breaks for the customer. They simply get more than they should, or see a row that isn’t theirs. Both are invisible until an audit, a renewal, or a screenshot on social media.

Five places it breaks

Cross-tenant writes

A bulk import or background job takes the organisation from the wrong place, and rows land in another tenant.

rulerow.org_id == parent.org_id

The billing webhook updates the plan, but cached entitlements or feature flags keep the old access.

ruleorg.features ⊆ org.plan.features

Seats past the plan

An SSO or SCIM sync adds users without checking the seat limit.

ruleorg.active_seats <= org.plan.seats

Billing and app disagree

A subscription is cancelled at the billing provider but still active in your database.

rulesubscription.status == billing.status

Tables without row-level security

A new table ships without the policy every other table has.

ruleevery table in schema has RLS enabled

A walkthrough

Composite example, combined from common multi-tenant bugs.

A bulk import tool is changed to reuse the organisation from the admin’s session to save a lookup. Support staff import orders on behalf of customers.

postgres.main · orderslive
orderorder.orgcustomer.org
88256org_203org_203✓
88258org_18org_18✓
88260org_412org_203≠
88263org_412org_77≠
88264org_203org_203✓
88265org_91org_91✓
88267org_412org_91≠
88270org_412org_77≠
88273org_412org_18≠
order-owner-matches-tenant≠ open
rule row.org_id == parent.org_id
rows in the wrong tenant
5
limit
≠ incident opened, 5 records
after deploy a1f9c2e “reuse org from session”
ai investigation
  • all 3 orders were created by bulk import from one support account
  • traced to a1f9c2e “reuse org from session” importer/bulk.go:88
  • blast radius 3 orders · 3 tenants

The rule catches the first misplaced row, not the hundredth, and the investigation shows exactly which code path wrote it.

The starter rule pack

The rules most teams in this space start with. Each one is a query against your own tables, adjusted to your schema during setup.

rule protects runs
row-org-matches-parent Tenant isolation 5 min
features-within-plan Revenue leakage 15 min
seats-within-plan Seat overages 1 hour
subscription-matches-billing Billing drift 1 hour
rls-on-every-table Data exposure on each deploy
admin-in-approved-list Privilege creep 1 hour

How it fits your stack

The agent connects to your application database with a read-only user. Rules about tenancy run on your own tables; rules about billing compare them with the billing data you already store.

Security teams get a continuous check on tenant isolation, not just a yearly pen test.

Common questions

Does it replace a pen test?

No. A pen test looks for ways in. Invarly checks that the data already in your system still respects the rules.

We use row-level security. Is that enough?

It covers reads through the database. Rules also catch writes from jobs and admin tools that bypass it, and tables that never got a policy.

Can it read our billing provider?

Through the data you already sync, for example subscription tables updated by webhooks.

Next industry
Usage-based billing
Read →

Check every tenant, every hour.

We’re onboarding a few SaaS teams first. Tell us how you keep tenants apart today.

Join waitlist