<?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: Yash Pouranik</title>
    <description>The latest articles on DEV Community by Yash Pouranik (@yashpouranik).</description>
    <link>https://dev.to/yashpouranik</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%2F3724261%2Fe9a9161f-4667-4e0c-b618-d946044923ce.jpeg</url>
      <title>DEV Community: Yash Pouranik</title>
      <link>https://dev.to/yashpouranik</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yashpouranik"/>
    <language>en</language>
    <item>
      <title>I Had Zero Idea What RLS Was. So I Built It for MongoDB.</title>
      <dc:creator>Yash Pouranik</dc:creator>
      <pubDate>Wed, 24 Jun 2026 19:37:39 +0000</pubDate>
      <link>https://dev.to/yashpouranik/i-had-zero-idea-what-rls-was-so-i-built-it-for-mongodb-3en3</link>
      <guid>https://dev.to/yashpouranik/i-had-zero-idea-what-rls-was-so-i-built-it-for-mongodb-3en3</guid>
      <description>&lt;p&gt;A MERN dev’s journey from “never seen it” to shipping application-level Row Level Security.&lt;/p&gt;

&lt;p&gt;I’ve been building urBackend since November 2025 — an open-source, MongoDB-native Backend-as-a-Service. The core bet: vendor lock-in is a solved problem, and we can solve it better than Firebase or Supabase. Connect your own MongoDB instance, get production-ready auth, storage, mails, webhooks, and database access. Your data never touches our servers. The source is fully open — unlike Supabase, whose SaaS layer is closed.&lt;/p&gt;

&lt;p&gt;After shipping v0.3.0 in March, we hit a wall. A real one.&lt;/p&gt;

&lt;p&gt;We needed RLS.&lt;/p&gt;

&lt;p&gt;The Honest Starting Point I had heard the term Row Level Security exactly twice before this. Never used it. Never seen it implemented. Didn’t know what it actually did at a systems level.&lt;/p&gt;

&lt;p&gt;So I did what any builder does: I read the docs. Supabase’s RLS docs. Postgres’s RLS docs. And honestly — it clicked. Postgres’s implementation is elegant. You define policies, the database engine enforces them at the executor level before returning rows. The “row” part is literal: the database physically filters out rows the user isn’t allowed to see, and the application never even knows they existed.&lt;/p&gt;

&lt;p&gt;Then I hit the wall.&lt;/p&gt;

&lt;p&gt;MongoDB doesn’t have this.&lt;/p&gt;

&lt;p&gt;RLS in Postgres is a database-level feature. MongoDB has no equivalent. And I’m building a BaaS where the whole premise is MongoDB-native, MongoDB-first, MongoDB-everything.&lt;/p&gt;

&lt;p&gt;So we had two options: skip RLS, or build it ourselves.&lt;/p&gt;

&lt;p&gt;We built it.&lt;/p&gt;

&lt;p&gt;Rethinking the Layer The key insight: if the database can’t enforce row-level access, push the constraint up one layer.&lt;/p&gt;

&lt;p&gt;In Postgres, enforcement happens at query execution inside the engine. We’d do the same thing — but at the application layer, sitting between the client’s request and the DB driver. The principle doesn’t change. The layer does.&lt;/p&gt;

&lt;p&gt;This is what we called application-level RLS over MongoDB — a middleware-driven query modification pattern.&lt;/p&gt;

&lt;p&gt;The mental model is simple:&lt;/p&gt;

&lt;p&gt;Client Request -&amp;gt; [RLS Middleware] -&amp;gt; Modified Query -&amp;gt; MongoDB -&amp;gt; Response The middleware is not optional routing logic. It is the security boundary.&lt;/p&gt;

&lt;p&gt;Implementation: What Actually Runs Before RLS could work, we needed a concept of “who is making this request.” In v0.3.0, we had already shipped Publishable Keys (PK) — keys meant for frontend use with limited scope. The scoping was always planned; RLS was the reason.&lt;/p&gt;

&lt;p&gt;The enforcement works through two middleware functions:&lt;/p&gt;

&lt;p&gt;authorizeReadOps — protects GET requests For collections with RLS enabled, every read request gets a filter injected into the query before it touches MongoDB:&lt;/p&gt;

&lt;p&gt;// What the client sends: GET /api/data/notes // What actually hits MongoDB: db.notes.find({ ...originalFilter, ownerId: req.user.userId }) The client never has to think about this. The middleware intercepts, reads the JWT from the Publishable Key auth, extracts the userId, and injects it as a mandatory filter condition. If the token is invalid — request dies. If the token is missing — request dies.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async (req, res, next) =&amp;gt; { try { if (req.keyRole === 'secret') { req.rlsFilter = {}; return next(); }
    const { collectionName } = req.params;
    const project = req.project;
    const collectionConfig = project.collections.find(c =&amp;gt; c.name === collectionName);

    if (!collectionConfig) {
        return next(new AppError(404, 'Collection not found'));
    }

    const rls = collectionConfig.rls || {};
    if (!rls.enabled) {
        req.rlsFilter = {};
        return next();
    }

    const modeRaw = rls.mode || 'public-read';
    const mode = modeRaw === 'owner-write-only' ? 'public-read' : modeRaw;

    if (mode === 'private') {
        if (!req.authUser?.userId) {
            return next(new AppError(401, 'Provide a valid user Bearer token for private reads.', 'Authentication required'));
        }

        const ownerField = rls.ownerField || 'userId';
        req.rlsFilter = { [ownerField]: req.authUser.userId };
        return next();
    }

    if (mode === 'public-read') {
        req.rlsFilter = {};
        return next();
    }

    return next(new AppError(403, 'Unsupported RLS mode'));
} catch (err) {
    console.error('[authorizeReadOperation] Unexpected error:', err);
    return next(new AppError(500, 'Internal Server Error'));
}};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;authorizeWriteOps — protects POST, PUT, PATCH, DELETE This one handles three cases:&lt;/p&gt;

&lt;p&gt;Become a Medium member&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;POST without an owner field The middleware auto-injects the userId as the owner. The document gets created with ownership baked in.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;POST with an owner field that doesn’t match the token Rejected. You can’t create a document owned by someone else.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;PUT/PATCH that tries to modify the owner field Rejected. Ownership transfer is blocked at the middleware layer entirely.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both middlewares check the key type first — PK triggers RLS, Secret Key bypasses it for admin access. This is the admin escape hatch that every BaaS needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async (req, res, next) =&amp;gt; { try { if (req.keyRole === 'secret') { return next(); }

    const { collectionName, id } = req.params;
    const project = req.project;
    const collectionConfig = project.collections.find(c =&amp;gt; c.name === collectionName);

    if (!collectionConfig) {
        return next(new AppError(404, 'The requested collection does not exist.', 'Collection not found'));
    }

    const rls = collectionConfig.rls || {};
    if (!rls.enabled) {
        return next(new AppError(403, 'Enable RLS for this collection to allow publishable-key writes.', 'Write blocked for publishable key'));
    }

    if (rls.requireAuthForWrite &amp;amp;&amp;amp; !req.authUser?.userId) {
        return next(new AppError(401, 'Provide a valid user Bearer token for write operations.', 'Authentication required'));
    }

    const modeRaw = rls.mode || 'public-read';
    const allowedModes = new Set(['public-read', 'private', 'owner-write-only']);
    if (!allowedModes.has(modeRaw)) {
        return next(new AppError(403, 'The collection RLS mode is invalid.', 'Unsupported RLS mode'));
    }

    const ownerField = rls.ownerField || 'userId';

    if (!req.authUser?.userId) {
        return next(new AppError(401, 'Provide a valid user Bearer token for write operations.', 'Authentication required'));
    }

    const authUserId = String(req.authUser.userId);
    const method = String(req.method || '').toUpperCase();

    if (method === 'POST') {
        if (ownerField === '_id') {
            return next(new AppError(403, "RLS ownerField '_id' is not valid for insert ownership checks.", 'Insert denied'));
        }

        const bodyItems = Array.isArray(req.body) ? req.body : [req.body];

      if (bodyItems.length === 0) {
return next(new AppError(400, 'Request body cannot be an empty array.', 'Invalid request body'));

}

for (let i = 0; i &amp;lt; bodyItems.length; i++) { const item = bodyItems\[i\];

if (!item || typeof item !== 'object' || Array.isArray(item)) {
    return next(new AppError(400, `Item at index ${i} must be a valid object`, 'Invalid request body'));
}

const incomingOwner = item?.[ownerField];

if (incomingOwner === undefined || incomingOwner === null || incomingOwner === '') {
    item[ownerField] = authUserId;
    continue;
}

if (String(incomingOwner) !== authUserId) {
    return next(new AppError(403, `Item at index ${i} must have ${ownerField} equal to your user id`, 'RLS owner mismatch'));
}

}

        return next();
    }

    if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
        if (!id || !mongoose.Types.ObjectId.isValid(id)) {
            return next(new AppError(400, 'The provided document ID is not valid.', 'Invalid ID format'));
        }

        req.rlsFilter = { [ownerField]: authUserId };

        if (method === 'PUT' || method === 'PATCH') {
            if (
                req.body &amp;amp;&amp;amp;
                Object.prototype.hasOwnProperty.call(req.body, ownerField) &amp;amp;&amp;amp;
                String(req.body[ownerField]) !== authUserId
            ) {
                return next(new AppError(403, `${ownerField} cannot be changed under RLS.`, 'Owner field immutable'));
            }
        }

        return next();
    }

    return next();
} catch (err) {
    console.error('[authorizeWriteOperation] Unexpected error:', err);
    return next(new AppError(500, 'Internal Server Error'));
}};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Two Modes&lt;/strong&gt;, Because One Isn’t Enough After shipping the initial version in v0.4.0, we realized RLS isn’t one-size-fits-all. Different products have different access patterns:&lt;/p&gt;

&lt;p&gt;private-read-private-write Only the owner can read or write their own documents. Personal data, user settings, private notes. Classic security model.&lt;/p&gt;

&lt;p&gt;public-read-private-write Anyone can read. Only the owner can write. Social apps, public posts with protected editing. This one's subtle — reads don't require auth, but writes are still fully enforced.&lt;/p&gt;

&lt;p&gt;Both modes shipped in v0.7.0.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbu3nvyn962zhirmamo1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhbu3nvyn962zhirmamo1.png" alt=" " width="607" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What the Config Looks Like RLS is configured at the project-collection level. Each collection stores an RLS config object in the DB:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{ "collectionName": 
"notes", 
"rls": { "enabled": true, 
"mode": "private-read-private-write", 
"ownerField": "userId" } 
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This also means toggles are dynamic — you can enable/disable RLS on a collection without redeploying anything. The middleware reads the config at request time.&lt;/p&gt;

&lt;p&gt;What I’d Tell Myself Before Starting The hard part wasn’t the code. The middleware injection took maybe two days. The hard part was the design — understanding what RLS is actually doing at a semantic level, then figuring out how to replicate those semantics one layer up.&lt;/p&gt;

&lt;p&gt;The thing Postgres RLS gives you for free: the enforcement is invisible to the application. You can’t forget to add a WHERE clause — the engine adds it. Replicating that guarantee at the application layer means your middleware has to be mandatory, not optional. There’s no “sometimes we check RLS” — every request touching a protected collection goes through those two functions, no exceptions.&lt;/p&gt;

&lt;p&gt;Second thing: ownerField design matters more than the enforcement logic. We chose userId as the default, but making it configurable opened up use cases we didn't anticipate — team-owned resources, org-level access, etc. Build for the field to be flexible from day one.&lt;/p&gt;

&lt;p&gt;Third: test with a bad actor, not a good user. The test case that matters isn’t “does Alice see her own notes.” It’s “can Alice craft a request to see Bob’s notes.” Write those tests explicitly.&lt;/p&gt;

&lt;p&gt;Why This Gap Exists (and Why It Matters) MongoDB’s document model and its lack of native RLS creates a real gap for teams building multi-tenant applications. Most solutions either:&lt;/p&gt;

&lt;p&gt;Implement access control entirely in application code (fragile, easy to miss) Use a separate auth layer that doesn’t know about data ownership Switch to Postgres just for RLS urBackend’s approach: make application-level RLS a first-class, infrastructure-level feature. You configure it once, and the BaaS layer enforces it everywhere the SDK touches. Even if your application code has a bug, the middleware holds.&lt;/p&gt;

&lt;p&gt;The whole thing — from “I’ve heard the term RLS” to a shipped, two-mode, configurable implementation — took about three weeks. MongoDB might not support it at the DB level. But the layer above it can.&lt;/p&gt;

&lt;p&gt;urBackend is open source under AGPL-3.0 license. If you’re building on MongoDB and dealing with multi-tenant access control, take a look: github.com/geturbackend&lt;/p&gt;

</description>
      <category>mongodb</category>
      <category>opensource</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>[Boost]</title>
      <dc:creator>Yash Pouranik</dc:creator>
      <pubDate>Sat, 28 Mar 2026 07:51:37 +0000</pubDate>
      <link>https://dev.to/yashpouranik/-14a6</link>
      <guid>https://dev.to/yashpouranik/-14a6</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9" class="crayons-story__hidden-navigation-link"&gt;BugVaulty — Auto-Track Every Error to Notion with AI Solutions 🐛&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
      &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9" class="crayons-article__context-note crayons-article__context-note__feed"&gt;&lt;p&gt;Notion MCP Challenge Submission 🧠&lt;/p&gt;

&lt;/a&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/nitinkumaryadav1307" class="crayons-avatar  crayons-avatar--l  "&gt;
            &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3728348%2Fc4d0b99f-ce87-4b39-88ec-7254012820f5.jpeg" alt="nitinkumaryadav1307 profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/nitinkumaryadav1307" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Nitin Kumar Yadav
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Nitin Kumar Yadav
                
              
              &lt;div id="story-author-preview-content-3419101" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/nitinkumaryadav1307" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&gt;
                        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3728348%2Fc4d0b99f-ce87-4b39-88ec-7254012820f5.jpeg" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Nitin Kumar Yadav&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Mar 28&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9" id="article-link-3419101"&gt;
          BugVaulty — Auto-Track Every Error to Notion with AI Solutions 🐛
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/notionchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;notionchallenge&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mcp"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mcp&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/raised-hands-74b2099fd66a39f2d7eed9305ee0f4553df0eb7b4f11b01b6b1b499973048fe5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;7&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/nitinkumaryadav1307/bugvaulty-auto-track-every-error-to-notion-with-ai-solutions-am9#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            2 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;


</description>
      <category>devchallenge</category>
      <category>notionchallenge</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
