Document that RLS denies a read silently but throws on writes - #290
Open
daniellekorn wants to merge 7 commits into
Open
daniellekorn wants to merge 7 commits into
daniellekorn wants to merge 7 commits into
Conversation
…n writes list() and filter() return an empty result on a denied read, the same as a query that matched nothing. create(), update(), and delete() throw with an HTTP 403 instead. Matches the same fact already documented for the REST entities API and the mintlify-docs security page.
🚀 Package Preview Available!Install this PR's preview build with npm: npm i @base44-preview/sdk@0.8.50-pr.290.cf1f6a8Prefer not to change any import paths? Install using npm alias so your code still imports npm i "@base44/sdk@npm:@base44-preview/sdk@0.8.50-pr.290.cf1f6a8"Or add it to your {
"dependencies": {
"@base44/sdk": "npm:@base44-preview/sdk@0.8.50-pr.290.cf1f6a8"
}
}
Preview published to npm registry — try new features instantly! |
…d get() Per review feedback: the prose repeated itself, and describing get() as throwing 'the same as if the record didn't exist' was circular. The real point is that get() throws not-found whether or not the record actually exists. Also drops delete() from the write/403 bucket, its RLS check runs as part of the lookup query itself (the same mechanism as reads), so a denied delete usually comes back as not-found too, not 403. That's a separate finding, not documented here yet.
Accidentally committed by an earlier 'git add -A': .gitignore's node_modules/ pattern only matches a real directory, and this worktree links node_modules as a symlink for local docs regen, so the pattern never matched it.
…TP codes Per review feedback: two sentences up front on the reasoning (reads stay silent about existence, writes always signal a blocked change), and explicit status codes on the table rows. delete() stays out of the 403 row, its RLS check runs inside the lookup query like a read, so a denied delete usually surfaces as not-found instead.
wixysam
reviewed
Sep 24, 2026
| * - **Anonymous or User authentication** (`base44.entities`): Access is scoped to the current user's permissions. Anonymous users can only access public entities, while authenticated users can access entities they have permission to view or modify. | ||
| * - **Service role authentication** (`base44.asServiceRole.entities`): Operations bypass entity access rules and field-level security entirely. Can read and write any record in any entity. | ||
| * | ||
| * ## How denied reads and writes differ |
Collaborator
There was a problem hiding this comment.
This section should probably be after the Built-in User one.
| * - **Anonymous or User authentication** (`base44.entities`): Access is scoped to the current user's permissions. Anonymous users can only access public entities, while authenticated users can access entities they have permission to view or modify. | ||
| * - **Service role authentication** (`base44.asServiceRole.entities`): Operations bypass entity access rules and field-level security entirely. Can read and write any record in any entity. | ||
| * | ||
| * ## How denied reads and writes differ |
Collaborator
There was a problem hiding this comment.
Suggested change
| * ## How denied reads and writes differ | |
| * ## Denied reads and writes |
| * ## How denied reads and writes differ | ||
| * | ||
| * Denied reads behave as if the data doesn't exist, so they don't reveal | ||
| * what's there. Denied writes throw, so a blocked change never fails silently. |
Collaborator
There was a problem hiding this comment.
Suggested change
| * what's there. Denied writes throw, so a blocked change never fails silently. | |
| * what's there. Denied writes throw an error, so a blocked change never fails silently. |
| * - **Anonymous or User authentication** (`base44.entities`): Access is scoped to the current user's permissions. Anonymous users can only access public entities, while authenticated users can access entities they have permission to view or modify. | ||
| * - **Service role authentication** (`base44.asServiceRole.entities`): Operations bypass entity access rules and field-level security entirely. Can read and write any record in any entity. | ||
| * | ||
| * ## How denied reads and writes differ |
Collaborator
There was a problem hiding this comment.
Actually, I wonder if this should be up here or we should just add the relevant information to the various methods listed in the table below. I think that might be a better option. WDYT?
…en heading Per Sam's review: the section reads better after the concepts it refers to (handlers, the methods they expose) are already introduced, and the heading matches the noun-phrase style of its siblings.
…l section Per Sam's review question about discoverability: keeps the table as the one source of truth, but adds a one-line pointer to each affected method's own doc, since that's what an IDE hover tooltip shows and the module-level section isn't visible there.
A builder-reported observation (survey respondent 35: a denied delete returned 404 instead of the 403 other writes get, with the row still present) independently confirms what the code traces to: delete() applies its row-level security rule as part of the lookup query, the same mechanism get() uses, so a denied delete usually surfaces as not-found rather than 403.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Added a note to the
EntitiesModuleJSDoc: a row-level security rule that denies a read doesn't error,list()andfilter()return an empty result indistinguishable from a genuine no-match. A deniedcreate(),update(), ordelete()throws instead, with an HTTP 403.Why
This is the same fact already documented on the mintlify-docs security page (base44-dev/mintlify-docs#2168) and the Apps REST API's count endpoint page, but it was missing from the one place a developer actually calling
list()/filter()would look, the SDK reference itself.How we know
BLOCK_ACCESS_QUERYis ANDed into the query rather than raising an error. https://github.com/base44-dev/apper/blob/1ab0caaf5e52b86435aadd33995aa184c20b10a6/backend/app/user_apps/entities/mongo_rls.py#L61create/update/deletecall axios directly with no try/catch, so a 403 response rejects the returned promise: src/modules/entities.ts, thecreate/update/deletemethods.Docs regeneration
This needs a paired mintlify-docs PR (base44-dev/mintlify-docs#2173) to publish the change. I did NOT run a plain
npm run create-docs-localand commit its full output, that would have pulled in a large amount of unrelated accumulated drift already sitting in this module (cursor pagination, newcount()/aggregate()/upsert()methods, formatting changes) into an unrelated PR. Instead I hand-applied just this one section to the affected page and its locale mirrors. The regen drift is real but belongs in its own PR; flagging it separately rather than bundling it here.