From 072e2c08c1930e6d279cc6435034aec069aa1ccd Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 12:44:09 +0300 Subject: [PATCH 1/7] docs(entities): document that RLS denies a read silently but throws on 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. --- node_modules | 1 + src/modules/entities.types.ts | 4 ++++ 2 files changed, 5 insertions(+) create mode 120000 node_modules diff --git a/node_modules b/node_modules new file mode 120000 index 00000000..2f9a8ded --- /dev/null +++ b/node_modules @@ -0,0 +1 @@ +/Users/danielleko/Desktop/work/base/javascript-sdk/node_modules \ No newline at end of file diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index e96265a7..0b55a51b 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -1030,6 +1030,10 @@ type DynamicEntitiesModule = { * - **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. * + * ## Read and write denial + * + * A row-level security rule that denies a read doesn't raise an error. `list()` and `filter()` return an empty result, indistinguishable from a query that matched nothing. A denied `create()`, `update()`, or `delete()` throws instead, with an HTTP 403 status. + * * ## Entity Handlers * * An entity handler is the object you get when you access an entity through `base44.entities.EntityName`. Every entity in your app automatically gets a handler with CRUD methods for managing records. From 75dfa13042f7600b4ab5dd23fdf91b7dfb61c26c Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:04:19 +0300 Subject: [PATCH 2/7] docs(entities): restructure the read/write denial note as a table, add 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. --- src/modules/entities.types.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 0b55a51b..da53c271 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -1030,9 +1030,15 @@ type DynamicEntitiesModule = { * - **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. * - * ## Read and write denial + * ## How denied reads and writes differ * - * A row-level security rule that denies a read doesn't raise an error. `list()` and `filter()` return an empty result, indistinguishable from a query that matched nothing. A denied `create()`, `update()`, or `delete()` throws instead, with an HTTP 403 status. + * Row-level security handles reads and writes differently. + * + * | Operation | When a rule denies it | + * | --- | --- | + * | `list()`, `filter()` | Returns an empty result, the same as if nothing matched. No error is thrown. | + * | `get()` | Throws a not-found error, whether or not the record exists. | + * | `create()`, `update()` | Throws a permission error with HTTP 403. | * * ## Entity Handlers * From 4f2b8851e905a93c8ee47a9bb95ebcd277f11723 Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:08:26 +0300 Subject: [PATCH 3/7] chore: stop tracking the node_modules symlink 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. --- node_modules | 1 - 1 file changed, 1 deletion(-) delete mode 120000 node_modules diff --git a/node_modules b/node_modules deleted file mode 120000 index 2f9a8ded..00000000 --- a/node_modules +++ /dev/null @@ -1 +0,0 @@ -/Users/danielleko/Desktop/work/base/javascript-sdk/node_modules \ No newline at end of file From f591a726334310a266658f7441c355f43884e965 Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:13:24 +0300 Subject: [PATCH 4/7] docs(entities): explain why reads and writes fail differently, add HTTP 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. --- src/modules/entities.types.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index da53c271..e0048619 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -1032,13 +1032,14 @@ type DynamicEntitiesModule = { * * ## How denied reads and writes differ * - * Row-level security handles reads and writes differently. + * 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. * * | Operation | When a rule denies it | * | --- | --- | * | `list()`, `filter()` | Returns an empty result, the same as if nothing matched. No error is thrown. | - * | `get()` | Throws a not-found error, whether or not the record exists. | - * | `create()`, `update()` | Throws a permission error with HTTP 403. | + * | `get()` | Throws a not-found error (HTTP 404), whether or not the record exists. | + * | `create()`, `update()` | Throws a permission error (HTTP 403). | * * ## Entity Handlers * From 943b8c4f501eef57dbde481ecd80c3583e163bba Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:38:39 +0300 Subject: [PATCH 5/7] docs(entities): move denial section after Built-in User Entity, shorten 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. --- src/modules/entities.types.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index e0048619..0eaf7018 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -1030,17 +1030,6 @@ type DynamicEntitiesModule = { * - **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 - * - * 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. - * - * | Operation | When a rule denies it | - * | --- | --- | - * | `list()`, `filter()` | Returns an empty result, the same as if nothing matched. No error is thrown. | - * | `get()` | Throws a not-found error (HTTP 404), whether or not the record exists. | - * | `create()`, `update()` | Throws a permission error (HTTP 403). | - * * ## Entity Handlers * * An entity handler is the object you get when you access an entity through `base44.entities.EntityName`. Every entity in your app automatically gets a handler with CRUD methods for managing records. @@ -1055,6 +1044,17 @@ type DynamicEntitiesModule = { * * Regular users can only read and update their own user record. With service role authentication, you can read, update, and delete any user. You can't create users using the entities module. Instead, use the functions of the {@link AuthModule | auth module} to invite or register new users. * + * ## Denied reads and writes + * + * 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. + * + * | Operation | When a rule denies it | + * | --- | --- | + * | `list()`, `filter()` | Returns an empty result, the same as if nothing matched. No error is thrown. | + * | `get()` | Throws a not-found error (HTTP 404), whether or not the record exists. | + * | `create()`, `update()` | Throws a permission error (HTTP 403). | + * * ## Generated Types * * If you're working in a TypeScript project, you can generate types from your entity schemas to get autocomplete and type checking on all entity methods. See the [Dynamic Types](/developers/references/sdk/getting-started/dynamic-types) guide to get started. From ff8fb90e3ffe644152710c5379d2f4634a35bedc Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:42:34 +0300 Subject: [PATCH 6/7] docs(entities): cross-link list/filter/get/create/update to the denial 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. --- src/modules/entities.types.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index 0eaf7018..b4ebe3da 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -379,6 +379,9 @@ export interface EntityHandler { * and records deleted between pages never shift the boundary. `skip` is kept * for existing code and is deprecated for loops. * + * **Note:** Returns an empty result if a row-level security rule denies + * this call. See {@link EntitiesModule | Denied reads and writes}. + * * @typeParam K - The fields to include in the response. Defaults to all fields. * @param sort - Sort parameter, such as `'-created_date'` for descending. Defaults to `'-created_date'`. * @param limit - Maximum number of results to return. Defaults to `5000`. @@ -454,6 +457,9 @@ export interface EntityHandler { * and records deleted between pages never shift the boundary. `skip` is kept * for existing code and is deprecated for loops. * + * **Note:** Returns an empty result if a row-level security rule denies + * this call. See {@link EntitiesModule | Denied reads and writes}. + * * @typeParam K - The fields to include in the response. Defaults to all fields. * @param query - Query object with field-value pairs. Each key should be a field name * from your entity schema, and each value is the criteria to match. Records matching all @@ -587,6 +593,10 @@ export interface EntityHandler { * * Retrieves a specific record using its unique identifier. * + * **Note:** Throws a not-found error, whether or not the record exists, + * if a row-level security rule denies this call. See + * {@link EntitiesModule | Denied reads and writes}. + * * @param id - The unique identifier of the record. * @returns Promise resolving to the record. * @@ -604,6 +614,9 @@ export interface EntityHandler { * * Creates a new record with the provided data. * + * **Note:** Throws a permission error if a row-level security rule + * denies this call. See {@link EntitiesModule | Denied reads and writes}. + * * @param data - Object containing the record data. * @returns Promise resolving to the created record. * @@ -631,6 +644,9 @@ export interface EntityHandler { * To update multiple specific records with different data each, use * {@linkcode bulkUpdate | bulkUpdate()}. * + * **Note:** Throws a permission error if a row-level security rule + * denies this call. See {@link EntitiesModule | Denied reads and writes}. + * * @param id - The unique identifier of the record to update. * @param data - Object containing the fields to update. * @returns Promise resolving to the updated record. From c20028a59510450b13b78d292242f94cb580c90b Mon Sep 17 00:00:00 2001 From: danielle korn Date: Thu, 24 Sep 2026 13:51:52 +0300 Subject: [PATCH 7/7] docs(entities): add delete() to the denial table 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. --- src/modules/entities.types.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/modules/entities.types.ts b/src/modules/entities.types.ts index b4ebe3da..58d6b478 100644 --- a/src/modules/entities.types.ts +++ b/src/modules/entities.types.ts @@ -676,6 +676,10 @@ export interface EntityHandler { * * Permanently removes a record from the database. * + * **Note:** Usually throws a not-found error if a row-level security rule + * denies this call, some rules can instead throw a permission error. See + * {@link EntitiesModule | Denied reads and writes}. + * * @param id - The unique identifier of the record to delete. * @returns Promise resolving to the deletion result. * @@ -1070,6 +1074,7 @@ type DynamicEntitiesModule = { * | `list()`, `filter()` | Returns an empty result, the same as if nothing matched. No error is thrown. | * | `get()` | Throws a not-found error (HTTP 404), whether or not the record exists. | * | `create()`, `update()` | Throws a permission error (HTTP 403). | + * | `delete()` | Usually throws a not-found error like {@linkcode EntityHandler.get | get()}. Some rules can instead throw a permission error (HTTP 403). | * * ## Generated Types *