<?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: Latch Vector</title>
    <description>The latest articles on DEV Community by Latch Vector (@latchvector).</description>
    <link>https://dev.to/latchvector</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%2F4075749%2Feb449a07-75ee-4aac-905e-6b1dc87806ac.png</url>
      <title>DEV Community: Latch Vector</title>
      <link>https://dev.to/latchvector</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/latchvector"/>
    <language>en</language>
    <item>
      <title>Why enforcing tenant boundaries in application code is a ticking time bomb</title>
      <dc:creator>Latch Vector</dc:creator>
      <pubDate>Fri, 21 Aug 2026 08:35:03 +0000</pubDate>
      <link>https://dev.to/latchvector/why-enforcing-tenant-boundaries-in-application-code-is-a-ticking-time-bomb-117</link>
      <guid>https://dev.to/latchvector/why-enforcing-tenant-boundaries-in-application-code-is-a-ticking-time-bomb-117</guid>
      <description>&lt;p&gt;Here is how most multi-tenant SaaS applications isolate customer data when they start out:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Looks safe, until someone forgets it in a new endpoint&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;invoices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SELECT * FROM invoices WHERE company_id = ? AND id = ?&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;companyId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It works fine for the first year. Then the team grows, junior engineers join, someone builds an internal bulk export feature, or a developer writes a quick direct SQL query during a midnight hotfix.&lt;/p&gt;

&lt;p&gt;Eventually, a route ships where someone forgets WHERE company_id = ?. A customer sees another customer’s invoices, and you spend the next 72 hours writing an incident report to your legal team.&lt;/p&gt;

&lt;p&gt;Relying exclusively on application code for multi-tenant isolation means your security boundary depends on 100% human diligence across 100% of your codebase, forever.&lt;/p&gt;

&lt;p&gt;That is a mathematically guaranteed failure mode over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Defence in Depth" Tenant Model&lt;/strong&gt;&lt;br&gt;
When we built Latch Vector, we approached tenant isolation with a simple rule: The innermost security layer must not care whether the application developer remembered the check.&lt;/p&gt;

&lt;p&gt;We implemented a 3-layer boundary where every layer operates independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request ---&amp;gt; [ Layer 1: Application Guard ]
↓ (validated)
[ Layer 2: Postgres Row-Level Security (RLS) ]
↓ (enforced)
[ Layer 3: Database Schema &amp;amp; Triggers ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is how those layers actually work behind the scenes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: Fail-Closed Service Guards&lt;/strong&gt;&lt;br&gt;
Every request carrying an organization ID is checked against the caller's granted scope (SELF or SUBTREE).&lt;/p&gt;

&lt;p&gt;If a token carries no scope, it is refused outright rather than trusted. If an endpoint requires an organization context and none is provided, it fails closed immediately before hitting the storage layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: Postgres Row-Level Security (RLS)&lt;/strong&gt;&lt;br&gt;
Layer 1 protects the paths that remember to call it. But what about a new repository method added next month? Or an ORM query built dynamically?&lt;/p&gt;

&lt;p&gt;This is where the database enforces its own boundary. We run Postgres with Row-Level Security (RLS) on a dedicated, non-superuser application role:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Postgres RLS Policy Example&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;POLICY&lt;/span&gt; &lt;span class="n"&gt;tenant_isolation_policy&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;invoices&lt;/span&gt;
&lt;span class="k"&gt;FOR&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
&lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tenant_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'app.current_tenant_id'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a developer writes SELECT * FROM invoices without a WHERE clause, the database itself filters the rows based on the session's tenant context. An un-scoped query returns zero rows, not all rows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Schema Constraints &amp;amp; State Immutability&lt;/strong&gt;&lt;br&gt;
The deepest layer prevents illegal states from ever being written:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composite Foreign Keys&lt;/strong&gt;: Prevent a child record's tenant_id from disagreeing with its parent entity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Materialized Path Triggers&lt;/strong&gt;: Automatically derive an organization's position in the customer hierarchy, like Health System to Hospital to Department, so an org tree node cannot be corrupted by an invalid update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Immutable Audit Trail&lt;/strong&gt;: The append-only audit log table strictly rejects UPDATE and DELETE operations at the database driver and schema level.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Measuring the Boundary (Not Asserting It)&lt;/strong&gt;&lt;br&gt;
We do not just assert that tenant boundaries hold on a marketing datasheet.&lt;/p&gt;

&lt;p&gt;Our test suite includes an automated 165-check cross-tenant leak matrix. It actively attempts cross-tenant reads, sibling branch traversal, and privilege escalation attacks against real Postgres databases (not emulated mocks) before any build is tagged for release.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you enforce tenancy?&lt;/strong&gt;&lt;br&gt;
If you are building multi-tenant software, where does your security boundary live today? Is it purely in your ORM or application middleware, or are you enforcing it down at the database layer?&lt;/p&gt;

&lt;p&gt;We packaged this 3-layer isolation pattern, along with multi-tenant SSO, audit logging, and official SDKs (Node, Python, PHP, Java) into Latch Vector.&lt;/p&gt;

&lt;p&gt;You can read the full architecture details in our public docs: &lt;a href="https://docs.latchvector.com" rel="noopener noreferrer"&gt;Documentation&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Curious to hear how other backend engineering teams handle tenant isolation in their stacks!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>architecture</category>
      <category>security</category>
    </item>
    <item>
      <title>The most common SSO breach is a config mistake. So we removed the config.</title>
      <dc:creator>Latch Vector</dc:creator>
      <pubDate>Thu, 13 Aug 2026 08:01:42 +0000</pubDate>
      <link>https://dev.to/latchvector/the-most-common-sso-breach-is-a-config-mistake-so-we-removed-the-config-4b9k</link>
      <guid>https://dev.to/latchvector/the-most-common-sso-breach-is-a-config-mistake-so-we-removed-the-config-4b9k</guid>
      <description>&lt;p&gt;Here's a bug that has shipped to production in more companies than anyone wants to admit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// looks fine. it isn't.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid signature. Right issuer. Not expired. And minted for a &lt;strong&gt;different application&lt;/strong&gt;. One your user also has access to. Maybe one that isn't even yours. Nothing in that line checks who the token was &lt;em&gt;for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;That's the &lt;code&gt;aud&lt;/code&gt; claim. It's optional in most libraries, off by default in a lot of setups, and the single most common way an SSO integration turns into a lateral-movement path.&lt;/p&gt;

&lt;p&gt;We got tired of writing that check in code review comments, so we built an identity platform where it isn't a check you can forget.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Latch Vector is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Multi-tenant SSO and identity for regulated software.&lt;/strong&gt; Tokens, organizations and sub-organizations, fine-grained permissions, and immediate notification when access changes, with tenant isolation, encryption at rest and a real audit trail built in rather than assembled by you.&lt;/p&gt;

&lt;p&gt;It's built for teams who would have to explain a cross-tenant leak to a regulator.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four things worth your attention
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The audience check has no off switch.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyAuthorizationHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.approve&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No variant of that call skips the audience. A token minted for another application is rejected, always. A machine token is refused where a user token is expected, and the reverse. The pattern is &lt;em&gt;fail-closed&lt;/em&gt;: social login with no configured client id gets refused rather than trusted, and missing configuration raises an error instead of quietly allowing something.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Tenant isolation at two independent layers.&lt;/strong&gt; Postgres row-level security through a dedicated non-superuser role, &lt;strong&gt;plus&lt;/strong&gt; an application-layer tenant guard on every org-scoped endpoint. Neither one alone is the security boundary, so a bug in one doesn't become a breach.&lt;/p&gt;

&lt;p&gt;Organizations are a materialized-path tree. You can model a health system → hospitals → departments and delegate administration down it with SELF and SUBTREE scopes. We verify the wall with an automated 165-check cross-org leak matrix instead of asserting it in a datasheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Your app finds out the moment access changes.&lt;/strong&gt; Role assigned or revoked, permissions edited, account disabled or erased: HMAC-SHA256 signed, replay-protected, delivered at-least-once through a transactional outbox, so the event and the business change commit together or not at all. Editing a role also invalidates the current tokens of everyone holding it, so revoked access doesn't linger until expiry.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;verifyWebhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawBody&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;4. Migration that doesn't cost your users their passwords.&lt;/strong&gt; Describe your whole estate in one payload that references records by &lt;em&gt;your own ids&lt;/em&gt;. A dry-run reports every bad email, duplicate and broken link before a single row is written, then commit provisions it in one transaction. bcrypt hashes carry over verbatim, so those users keep the password they already have. The import is idempotent, so retries are safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Also in the box
&lt;/h2&gt;

&lt;p&gt;TOTP MFA. Refresh-token rotation with reuse detection, where a replayed token gets treated as a compromise signal rather than an ordinary 401. AES-GCM encryption at rest with rotatable keys and a blind index so encrypted emails stay searchable. GDPR erasure that scrubs personal data while keeping internal ids stable. SDKs for &lt;strong&gt;Node, Python, PHP and Java/Spring&lt;/strong&gt;, all enforcing the same defaults.&lt;/p&gt;




&lt;p&gt;Live today. Public docs, published SDKs, and a sandbox tenant you can spin up without talking to anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.latchvector.com" rel="noopener noreferrer"&gt;docs.latchvector.com&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://latchvector.com/services" rel="noopener noreferrer"&gt;latchvector.com/services&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Hit any of these problems yourself? I'd like to hear how you solved it. Responses are open.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Identity for software that gets audited.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
      <category>saas</category>
    </item>
  </channel>
</rss>
