{
  "id": 4723796,
  "title": "How I tested Row Level Security before shipping a SaaS starter kit (so one user can't see another's data)",
  "url": "https://urgent.news/2026/08/31/how-i-tested-row-level-security-before-shipping-a-saas-starter-kit-so",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-08-31T21:31:55.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/flocirica/how-i-tested-row-level-security-before-shipping-a-saas-starter-kit-so-one-user-cant-see-anothers-3c6o"
  },
  "original_language": "en",
  "account": "When constructing a multi-tenant application, one of the most critical bugs to avoid is a user inadvertently viewing another user's data. This privacy failure far surpasses any other issue, such as a crash or broken button. To ensure the security of the database layer, I recently created a next.js, supabase, and stripe starter kit. Before deeming the database layer complete, I needed to confirm that the security measures were effectively in place, rather than merely assuming that everything was correct due to the code appearing to be right.\n\nSupabase's row level security (RLS) allows for the direct writing of policies in Postgres that filter rows according to the identity of the user making the request. The database becomes the security boundary, rather than relying solely on application code. While powerful, this approach means that a single incorrect policy (or the absence of a policy) could inadvertently expose all data. In order to verify the effectiveness of the policies, I employed a specific policy pattern for an owner-scoped table:\n\ncreate policy \"projects: owner reads\" on public.projects for select to authenticated using ((select auth.uid()) = owner_id);\n\nWhile the policy may seem straightforward, it is essential to verify it rather than merely trusting it. The actual test involved running an impersonation test within the SQL editor. By briefly pretending to be a specific user (inside a transaction that does not affect any data), I could check what that user would actually see. If the test was performed using the real owner's ID, the expected result was their own rows. However, if a different, fake, or nonexistent user's ID was used, the result should have been zero rows returned, not an error. This positive outcome, rather than an error, indicated that the Row Level Security (RLS) policy was functioning correctly and effectively filtering out unauthorized data.\n\nThe same check was conducted for write operations, such as attempting to delete another user's row from a different account's session. This ensured that the deletion operation also respected the RLS policy and returned 0 rows affected instead of either successfully deleting the row or throwing an error. This meticulous testing process took only a couple of minutes but could catch potential failures that might only be discovered in a production environment, involving real user data.\n\nThis approach is particularly crucial for anyone building a similar application. Here are a few key points to keep in mind:\n\n- Test with at least two different accounts to ensure that the policy works as intended for legitimate owners and blocks access for other users.\n- Perform separate tests for both read and write operations. A table can have a working select policy but may have a broken or missing delete policy.\n- The success condition for a negative test often involves silence, such as returning 0 rows, rather than encountering an error. Understanding what success looks like is paramount before executing the test.\n\nI compiled this testing pattern, along with tested Stripe billing and authentication, into a small starter kit available at https://liviug.gumroad.com/l/saas-starter. The RLS testing pattern, as discussed, is beneficial even beyond the context of this specific starter kit.",
  "summary": "When you're building a multi-tenant app, there's one bug category that's worse than any other: a user seeing someone else's data. Not a crash, not a broken button — a genuine privacy failure. I recently built a Next.js + Supabase + Stripe starter kit, and before I'd call the database layer \"done,\" I wanted to actually prove the security held, not just assume it did because the code looked right.…",
  "key_points": [
    "Tested Row Level Security with Supabase to prevent users from viewing other users' data.",
    "Created a policy pattern for owner-scoped tables to filter rows based on authenticated user's ID.",
    "Conducted impersonation tests to verify RLS policy effectiveness in both read and write operations."
  ],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}