<?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: wendel andrady</title>
    <description>The latest articles on DEV Community by wendel andrady (@wendel_andrady).</description>
    <link>https://dev.to/wendel_andrady</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%2F4112475%2F24dcd45e-07f9-4aa7-a8d0-2698df11e4fa.png</url>
      <title>DEV Community: wendel andrady</title>
      <link>https://dev.to/wendel_andrady</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wendel_andrady"/>
    <language>en</language>
    <item>
      <title>Your B2B SaaS Isn't Production-Ready Until You Test These 10 Failure Cases</title>
      <dc:creator>wendel andrady</dc:creator>
      <pubDate>Wed, 09 Sep 2026 21:55:52 +0000</pubDate>
      <link>https://dev.to/wendel_andrady/your-b2b-saas-isnt-production-ready-until-you-test-these-10-failure-cases-2282</link>
      <guid>https://dev.to/wendel_andrady/your-b2b-saas-isnt-production-ready-until-you-test-these-10-failure-cases-2282</guid>
      <description>&lt;p&gt;Most B2B SaaS security testing focuses on the happy path.&lt;/p&gt;

&lt;p&gt;A user signs in.&lt;/p&gt;

&lt;p&gt;They can access their organization's data.&lt;/p&gt;

&lt;p&gt;The right role can perform the right action.&lt;/p&gt;

&lt;p&gt;Stripe sends a webhook and the subscription updates.&lt;/p&gt;

&lt;p&gt;Everything looks correct.&lt;/p&gt;

&lt;p&gt;But production failures usually happen somewhere else:&lt;/p&gt;

&lt;p&gt;When someone does something they're not supposed to be able to do.&lt;/p&gt;

&lt;p&gt;Here are 10 failure cases I think every multi-tenant B2B SaaS should test before production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User A can access User B's data&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Create two organizations:&lt;/p&gt;

&lt;p&gt;Organization A&lt;br&gt;
└── User A&lt;br&gt;
    └── Project A&lt;/p&gt;

&lt;p&gt;Organization B&lt;br&gt;
└── User B&lt;br&gt;
    └── Project B&lt;/p&gt;

&lt;p&gt;Then try to access Project B while authenticated as User A.&lt;/p&gt;

&lt;p&gt;Don't only test through the UI.&lt;/p&gt;

&lt;p&gt;Try:&lt;/p&gt;

&lt;p&gt;Direct API requests&lt;br&gt;
Known resource IDs&lt;br&gt;
Modified URLs&lt;br&gt;
Nested resources&lt;br&gt;
Search endpoints&lt;/p&gt;

&lt;p&gt;The expected result should always be denial.&lt;/p&gt;

&lt;p&gt;A tenant_id column by itself doesn't provide tenant isolation.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Changing a resource ID bypasses authorization&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Suppose your endpoint is:&lt;/p&gt;

&lt;p&gt;GET /api/projects/123&lt;/p&gt;

&lt;p&gt;What happens if User A changes 123 to 456, where 456 belongs to another organization?&lt;/p&gt;

&lt;p&gt;This is a classic place for authorization bugs.&lt;/p&gt;

&lt;p&gt;The server needs to verify both:&lt;/p&gt;

&lt;p&gt;The resource exists.&lt;br&gt;
The current user is allowed to access it.&lt;/p&gt;

&lt;p&gt;Never treat possession of a resource ID as authorization.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;RBAC works in the UI but not the API&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Hiding an admin button from a member isn't security.&lt;/p&gt;

&lt;p&gt;Try calling the underlying endpoint directly as a member.&lt;/p&gt;

&lt;p&gt;For every sensitive action, test:&lt;/p&gt;

&lt;p&gt;Owner&lt;br&gt;
Admin&lt;br&gt;
Member&lt;br&gt;
Unauthenticated user&lt;/p&gt;

&lt;p&gt;Authorization needs to exist at the server boundary, not just in the frontend.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A revoked invitation can still be used&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Invitations have more states than:&lt;/p&gt;

&lt;p&gt;invited → accepted&lt;/p&gt;

&lt;p&gt;You should test:&lt;/p&gt;

&lt;p&gt;Expired invitation&lt;br&gt;
Revoked invitation&lt;br&gt;
Already-used invitation&lt;br&gt;
Invitation belonging to another organization&lt;/p&gt;

&lt;p&gt;An invitation should not become a permanent access token.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A revoked API key still works&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;API keys are another authentication path.&lt;/p&gt;

&lt;p&gt;Test:&lt;/p&gt;

&lt;p&gt;Valid key       → allowed&lt;br&gt;
Invalid key     → denied&lt;br&gt;
Revoked key     → denied&lt;br&gt;
Expired key     → denied&lt;br&gt;
Wrong tenant    → denied&lt;/p&gt;

&lt;p&gt;Also verify that API-key permissions are enforced just like browser-based authentication.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Usage limits can be bypassed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your SaaS has usage limits, don't only test:&lt;/p&gt;

&lt;p&gt;User reaches limit → request fails.&lt;/p&gt;

&lt;p&gt;Try concurrent requests.&lt;/p&gt;

&lt;p&gt;For example, if the limit is 100 operations, what happens when 20 requests arrive simultaneously while the tenant has 95 operations remaining?&lt;/p&gt;

&lt;p&gt;If the application checks and increments usage incorrectly, several requests may pass before the limit is updated.&lt;/p&gt;

&lt;p&gt;Usage enforcement needs to happen server-side and should account for concurrency where relevant.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The same Stripe webhook produces the effect twice&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Webhook providers retry events.&lt;/p&gt;

&lt;p&gt;Your application should assume that the same event can arrive more than once.&lt;/p&gt;

&lt;p&gt;Test:&lt;/p&gt;

&lt;p&gt;Webhook received&lt;br&gt;
Webhook received again&lt;/p&gt;

&lt;p&gt;The second delivery should not:&lt;/p&gt;

&lt;p&gt;Create a duplicate subscription&lt;br&gt;
Apply a credit twice&lt;br&gt;
Process an upgrade twice&lt;br&gt;
Trigger duplicate provisioning&lt;/p&gt;

&lt;p&gt;Webhook processing should be idempotent.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A background job loses tenant context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Your HTTP request may correctly know:&lt;/p&gt;

&lt;p&gt;organization_id = A&lt;/p&gt;

&lt;p&gt;But what happens when that work moves into a background job?&lt;/p&gt;

&lt;p&gt;The job needs enough trusted context to know which tenant it belongs to.&lt;/p&gt;

&lt;p&gt;Test jobs that:&lt;/p&gt;

&lt;p&gt;Read tenant data&lt;br&gt;
Modify tenant data&lt;br&gt;
Delete tenant data&lt;br&gt;
Process billing events&lt;br&gt;
Process usage&lt;/p&gt;

&lt;p&gt;Background jobs are still part of your application's security boundary.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A new table doesn't have tenant isolation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is one of the easiest problems to introduce as a SaaS grows.&lt;/p&gt;

&lt;p&gt;You add:&lt;/p&gt;

&lt;p&gt;reports&lt;/p&gt;

&lt;p&gt;The application works.&lt;/p&gt;

&lt;p&gt;Existing tests pass.&lt;/p&gt;

&lt;p&gt;But the new table doesn't have the same isolation protections as your other tenant-owned resources.&lt;/p&gt;

&lt;p&gt;For every tenant-owned table, verify:&lt;/p&gt;

&lt;p&gt;[ ] Ownership relationship exists&lt;br&gt;
[ ] Database policy exists&lt;br&gt;
[ ] Server authorization exists&lt;br&gt;
[ ] Cross-tenant read tested&lt;br&gt;
[ ] Cross-tenant write tested&lt;br&gt;
[ ] Cross-tenant delete tested&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Related resources create an indirect access path&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Imagine:&lt;/p&gt;

&lt;p&gt;Organization&lt;br&gt;
└── Project&lt;br&gt;
    └── Document&lt;/p&gt;

&lt;p&gt;You may correctly protect Projects but forget that someone can access a Document directly.&lt;/p&gt;

&lt;p&gt;Test the entire chain.&lt;/p&gt;

&lt;p&gt;Can User A access a Document belonging to Organization B by knowing:&lt;/p&gt;

&lt;p&gt;The document ID?&lt;br&gt;
The project ID?&lt;br&gt;
A nested endpoint?&lt;br&gt;
A search query?&lt;br&gt;
A download URL?&lt;/p&gt;

&lt;p&gt;Tenant isolation needs to survive indirect access paths too.&lt;/p&gt;

&lt;p&gt;The test that matters most&lt;/p&gt;

&lt;p&gt;Don't only ask:&lt;/p&gt;

&lt;p&gt;"Does my SaaS work?"&lt;/p&gt;

&lt;p&gt;Ask:&lt;/p&gt;

&lt;p&gt;"Can I make my SaaS do something I'm not authorized to do?"&lt;/p&gt;

&lt;p&gt;That's the test that exposes the gaps.&lt;/p&gt;

&lt;p&gt;Free B2B SaaS production checklist&lt;/p&gt;

&lt;p&gt;I put the complete production-readiness checklist and a deeper tenant-isolation guide into a free GitHub repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/wendelandrady/production-saas-readiness" rel="noopener noreferrer"&gt;production-saas-readiness&lt;/a&gt;&lt;br&gt;
It covers authentication, multi-tenancy, RLS, RBAC, invitations, API keys, usage limits, billing, webhooks, background jobs, and production verification.&lt;/p&gt;

&lt;p&gt;If you'd rather start with this infrastructure already implemented instead of building it yourself, I've also built a B2B SaaS OS around these components.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://andrady.co" rel="noopener noreferrer"&gt;andrady.co&lt;/a&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>How I approached multi-tenancy in Next.js with PostgreSQL RLS</title>
      <dc:creator>wendel andrady</dc:creator>
      <pubDate>Sun, 06 Sep 2026 15:19:35 +0000</pubDate>
      <link>https://dev.to/wendel_andrady/how-i-approached-multi-tenancy-in-nextjs-with-postgresql-rls-1d8e</link>
      <guid>https://dev.to/wendel_andrady/how-i-approached-multi-tenancy-in-nextjs-with-postgresql-rls-1d8e</guid>
      <description>&lt;p&gt;Multi-tenancy looks simple at first.&lt;/p&gt;

&lt;p&gt;You add an &lt;code&gt;organization_id&lt;/code&gt; to your tables, check it in your queries, and move on.&lt;/p&gt;

&lt;p&gt;The problem is that this puts a lot of responsibility on application code.&lt;/p&gt;

&lt;p&gt;I wanted tenant isolation to be enforced at the database level too, so I used PostgreSQL Row Level Security (RLS).&lt;/p&gt;

&lt;p&gt;The basic architecture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users belong to organizations&lt;/li&gt;
&lt;li&gt;Organization membership determines access&lt;/li&gt;
&lt;li&gt;Server-side authorization handles application permissions&lt;/li&gt;
&lt;li&gt;PostgreSQL RLS provides another layer of tenant isolation&lt;/li&gt;
&lt;li&gt;Roles such as Owner, Admin, and Member control what users can do&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This also makes other parts of the application more interesting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Team invitations&lt;/li&gt;
&lt;li&gt;Organization switching&lt;/li&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;Usage limits&lt;/li&gt;
&lt;li&gt;Billing tied to organizations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The biggest lesson for me was that multi-tenancy isn't really a single feature.&lt;/p&gt;

&lt;p&gt;It's an authorization model that needs to stay consistent across the application and database.&lt;/p&gt;

&lt;p&gt;I built these patterns into a reusable B2B SaaS foundation because I got tired of rebuilding them for every project.&lt;/p&gt;

&lt;p&gt;If you're building a multi-tenant SaaS with Next.js and Supabase, I've put the full foundation here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://andrady.co" rel="noopener noreferrer"&gt;https://andrady.co&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'd be interested to hear how other Next.js developers approach tenant isolation. Do you rely mainly on application-level checks, RLS, or both?&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>database</category>
      <category>nextjs</category>
      <category>security</category>
    </item>
  </channel>
</rss>
