@kysera/rls 0.7.3 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @kysera/rls
2
2
 
3
- > **Row-Level Security Plugin for Kysera** - Declarative authorization policies with automatic query transformation and AsyncLocalStorage-based context management.
3
+ > **Row-Level Security Plugin for Kysera** - Declarative authorization policies with automatic query transformation through @kysera/executor's Unified Execution Layer and AsyncLocalStorage-based context management.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@kysera/rls.svg)](https://www.npmjs.com/package/@kysera/rls)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
@@ -19,13 +19,14 @@ RLS controls access to individual rows in database tables based on user context.
19
19
  **Key Features:**
20
20
 
21
21
  - **Declarative Policy DSL** - Define rules with `allow`, `deny`, `filter`, and `validate` builders
22
- - **Automatic Query Transformation** - SELECT queries are filtered automatically via `interceptQuery`
23
- - **Repository Extensions** - Wraps mutation methods via `extendRepository` for policy enforcement
24
- - **Type-Safe Context** - Full TypeScript inference for user context and policies
22
+ - **Automatic Query Transformation** - SELECT queries are filtered automatically via `interceptQuery` hook
23
+ - **Repository Extensions** - Wraps mutation methods via `extendRepository` hook for policy enforcement
24
+ - **Type-Safe Context** - Full TypeScript inference with reduced `any` usage through type utilities
25
25
  - **Multi-Tenant Isolation** - Built-in patterns for SaaS tenant separation
26
- - **Plugin Architecture** - Works with both Repository and DAL patterns via `@kysera/executor`
26
+ - **Plugin Architecture** - Works with both Repository and DAL patterns via @kysera/executor's Unified Execution Layer
27
27
  - **Zero Runtime Overhead** - Policies compiled at initialization
28
28
  - **AsyncLocalStorage Context** - Request-scoped context without prop drilling
29
+ - **Optional Dependency** - Listed in peerDependencies with `optional: true` for @kysera/repository
29
30
 
30
31
  ---
31
32
 
@@ -40,10 +41,13 @@ yarn add @kysera/rls kysely
40
41
  ```
41
42
 
42
43
  **Dependencies:**
44
+
43
45
  - `kysely` >= 0.28.8 (peer dependency)
44
- - `@kysera/core` - Core utilities (auto-installed)
45
- - `@kysera/executor` - Plugin execution layer (auto-installed)
46
- - `@kysera/repository` or `@kysera/dal` - For Repository or DAL patterns (install as needed)
46
+ - `@kysera/core` >= 0.7.0 - Core utilities (auto-installed)
47
+ - `@kysera/executor` >= 0.7.0 - Unified Execution Layer (auto-installed)
48
+ - `@kysera/repository` >= 0.7.0 or `@kysera/dal` >= 0.7.0 - For Repository or DAL patterns (install as needed)
49
+
50
+ **Note:** @kysera/rls is listed in peerDependencies with `optional: true` for @kysera/repository, allowing flexible installation based on your needs.
47
51
 
48
52
  ---
49
53
 
@@ -52,17 +56,17 @@ yarn add @kysera/rls kysely
52
56
  ### 1. Define RLS Schema
53
57
 
54
58
  ```typescript
55
- import { defineRLSSchema, filter, allow, validate } from '@kysera/rls';
59
+ import { defineRLSSchema, filter, allow, validate } from '@kysera/rls'
56
60
 
57
61
  interface Database {
58
62
  posts: {
59
- id: number;
60
- title: string;
61
- content: string;
62
- author_id: number;
63
- tenant_id: number;
64
- status: 'draft' | 'published';
65
- };
63
+ id: number
64
+ title: string
65
+ content: string
66
+ author_id: number
67
+ tenant_id: number
68
+ status: 'draft' | 'published'
69
+ }
66
70
  }
67
71
 
68
72
  const rlsSchema = defineRLSSchema<Database>({
@@ -72,44 +76,47 @@ const rlsSchema = defineRLSSchema<Database>({
72
76
  filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
73
77
 
74
78
  // Authors can edit their own posts
75
- allow(['update', 'delete'], ctx =>
76
- ctx.auth.userId === ctx.row.author_id
77
- ),
79
+ allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.author_id),
78
80
 
79
81
  // Validate new posts belong to user's tenant
80
- validate('create', ctx =>
81
- ctx.data.tenant_id === ctx.auth.tenantId
82
- ),
82
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
83
83
  ],
84
- defaultDeny: true, // Require explicit allow
85
- },
86
- });
84
+ defaultDeny: true // Require explicit allow
85
+ }
86
+ })
87
87
  ```
88
88
 
89
- ### 2. Create Plugin Container with RLS Plugin
89
+ ### 2. Register Plugin with Unified Execution Layer
90
90
 
91
91
  ```typescript
92
- import { createORM } from '@kysera/repository';
93
- import { rlsPlugin, rlsContext } from '@kysera/rls';
94
- import { Kysely, PostgresDialect } from 'kysely';
92
+ import { createExecutor } from '@kysera/executor'
93
+ import { createORM } from '@kysera/repository'
94
+ import { rlsPlugin, rlsContext } from '@kysera/rls'
95
+ import { Kysely, PostgresDialect } from 'kysely'
95
96
 
96
97
  const db = new Kysely<Database>({
97
- dialect: new PostgresDialect({ /* config */ }),
98
- });
98
+ dialect: new PostgresDialect({
99
+ /* config */
100
+ })
101
+ })
99
102
 
100
- const orm = await createORM(db, [
101
- rlsPlugin({ schema: rlsSchema }),
102
- ]);
103
+ // Step 1: Create executor with RLS plugin
104
+ const executor = await createExecutor(db, [
105
+ rlsPlugin({ schema: rlsSchema })
106
+ ])
107
+
108
+ // Step 2: Create ORM with plugin-enabled executor
109
+ const orm = await createORM(executor, [])
103
110
  ```
104
111
 
105
112
  ### 3. Execute Queries within RLS Context
106
113
 
107
114
  ```typescript
108
- import { rlsContext } from '@kysera/rls';
115
+ import { rlsContext } from '@kysera/rls'
109
116
 
110
117
  // In your request handler
111
118
  app.use(async (req, res, next) => {
112
- const user = await authenticate(req);
119
+ const user = await authenticate(req)
113
120
 
114
121
  await rlsContext.runAsync(
115
122
  {
@@ -117,51 +124,63 @@ app.use(async (req, res, next) => {
117
124
  userId: user.id,
118
125
  tenantId: user.tenantId,
119
126
  roles: user.roles,
120
- isSystem: false,
127
+ isSystem: false
121
128
  },
122
- timestamp: new Date(),
129
+ timestamp: new Date()
123
130
  },
124
131
  async () => {
125
132
  // All queries automatically filtered by policies
126
- const posts = await orm.posts.findAll();
127
- res.json(posts);
133
+ const posts = await orm.posts.findAll()
134
+ res.json(posts)
128
135
  }
129
- );
130
- });
136
+ )
137
+ })
131
138
  ```
132
139
 
133
140
  ---
134
141
 
135
142
  ## Plugin Architecture
136
143
 
137
- ### Integration with @kysera/executor
144
+ ### Integration with @kysera/executor's Unified Execution Layer
138
145
 
139
- The RLS plugin is built on `@kysera/executor`, which provides a unified plugin system that works with both Repository and DAL patterns.
146
+ The RLS plugin is built on @kysera/executor's Unified Execution Layer, which provides seamless plugin support that works with both Repository and DAL patterns.
140
147
 
141
148
  **Plugin Metadata:**
142
149
 
143
150
  ```typescript
144
151
  {
145
152
  name: '@kysera/rls',
146
- version: '0.7.0',
153
+ version: '0.8.0',
147
154
  priority: 50, // Runs after soft-delete (0), before audit (100)
148
155
  dependencies: [],
149
156
  }
150
157
  ```
151
158
 
159
+ **Type Utilities:**
160
+
161
+ The plugin uses type utilities to reduce `any` usage and improve type safety:
162
+ - Conditional types for precise type inference
163
+ - Generic constraints for database schemas
164
+ - Utility types for context and policy definitions
165
+
152
166
  ### How It Works
153
167
 
154
- The RLS plugin implements two key hooks from the `@kysera/executor` plugin system:
168
+ The RLS plugin implements two key hooks from @kysera/executor's plugin system:
155
169
 
156
170
  #### 1. `interceptQuery` - Query Filtering (SELECT)
157
171
 
158
- The `interceptQuery` hook intercepts all query builder operations to apply RLS filtering:
172
+ Registered via `createExecutor()`, the `interceptQuery` hook intercepts all query builder operations to apply RLS filtering:
159
173
 
160
174
  ```typescript
161
- // When you execute a SELECT query:
162
- const posts = await orm.posts.findAll();
175
+ // Step 1: Register plugin with Unified Execution Layer
176
+ const executor = await createExecutor(db, [
177
+ rlsPlugin({ schema: rlsSchema })
178
+ ])
179
+
180
+ // Step 2: Execute a SELECT query
181
+ const posts = await orm.posts.findAll()
163
182
 
164
- // The plugin interceptQuery hook:
183
+ // Step 3: The plugin interceptQuery hook:
165
184
  // 1. Checks for RLS context (rlsContext.getContextOrNull())
166
185
  // 2. Checks if system user (ctx.auth.isSystem) or bypass role
167
186
  // 3. Applies filter policies as WHERE conditions via SelectTransformer
@@ -170,19 +189,23 @@ const posts = await orm.posts.findAll();
170
189
  ```
171
190
 
172
191
  **Key behavior:**
192
+
173
193
  - SELECT operations: Policies are applied immediately as WHERE clauses
174
194
  - INSERT/UPDATE/DELETE: Marked for validation (actual enforcement in `extendRepository`)
175
- - Skip conditions: `skipTables`, `metadata['skipRLS']`, `requireContext`, system user, bypass roles
195
+ - Skip conditions: `excludeTables`, `metadata['skipRLS']`, `requireContext`, system user, bypass roles
176
196
 
177
197
  #### 2. `extendRepository` - Mutation Enforcement (CREATE/UPDATE/DELETE)
178
198
 
179
- The `extendRepository` hook wraps repository mutation methods to enforce RLS policies:
199
+ Registered via `createExecutor()`, the `extendRepository` hook wraps repository mutation methods to enforce RLS policies:
180
200
 
181
201
  ```typescript
182
- // When you call a mutation:
183
- await repo.update(postId, { title: 'New Title' });
202
+ // Step 1: Plugin registered with Unified Execution Layer
203
+ const executor = await createExecutor(db, [rlsPlugin({ schema: rlsSchema })])
184
204
 
185
- // The plugin extendRepository hook:
205
+ // Step 2: Call a mutation
206
+ await repo.update(postId, { title: 'New Title' })
207
+
208
+ // Step 3: The plugin extendRepository hook:
186
209
  // 1. Wraps create/update/delete methods
187
210
  // 2. Fetches existing row using getRawDb() (bypasses RLS filtering)
188
211
  // 3. Evaluates allow/deny policies via MutationGuard
@@ -191,7 +214,7 @@ await repo.update(postId, { title: 'New Title' });
191
214
  // 6. Adds withoutRLS() and canAccess() utility methods
192
215
  ```
193
216
 
194
- **Why use `getRawDb()`?** To prevent infinite recursion - we need to fetch the existing row without triggering RLS filtering. The `getRawDb()` function from `@kysera/executor` returns the original Kysely instance that bypasses all plugin hooks.
217
+ **Why use `getRawDb()`?** To prevent infinite recursion - we need to fetch the existing row without triggering RLS filtering. The `getRawDb()` function from @kysera/executor returns the original Kysely instance that bypasses all plugin hooks.
195
218
 
196
219
  ---
197
220
 
@@ -247,7 +270,7 @@ filter('read', ctx =>
247
270
  // Multiple conditions
248
271
  filter('read', ctx => ({
249
272
  organization_id: ctx.auth.organizationIds?.[0],
250
- deleted_at: null,
273
+ deleted_at: null
251
274
  }))
252
275
  ```
253
276
 
@@ -257,31 +280,28 @@ Validates data during CREATE/UPDATE operations.
257
280
 
258
281
  ```typescript
259
282
  // Validate tenant ownership
260
- validate('create', ctx =>
261
- ctx.data.tenant_id === ctx.auth.tenantId
262
- )
283
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
263
284
 
264
285
  // Validate status transitions
265
286
  validate('update', ctx => {
266
287
  const validTransitions = {
267
288
  draft: ['published', 'archived'],
268
289
  published: ['archived'],
269
- archived: [],
270
- };
271
- return !ctx.data.status ||
272
- validTransitions[ctx.row.status]?.includes(ctx.data.status);
290
+ archived: []
291
+ }
292
+ return !ctx.data.status || validTransitions[ctx.row.status]?.includes(ctx.data.status)
273
293
  })
274
294
  ```
275
295
 
276
296
  ### Operations
277
297
 
278
- | Operation | SQL | Description |
279
- |-----------|-----|-------------|
280
- | `read` | SELECT | Control what users can view |
281
- | `create` | INSERT | Control what users can create |
282
- | `update` | UPDATE | Control what users can modify |
283
- | `delete` | DELETE | Control what users can remove |
284
- | `all` | All | Apply to all operations |
298
+ | Operation | SQL | Description |
299
+ | --------- | ------ | ----------------------------- |
300
+ | `read` | SELECT | Control what users can view |
301
+ | `create` | INSERT | Control what users can create |
302
+ | `update` | UPDATE | Control what users can modify |
303
+ | `delete` | DELETE | Control what users can remove |
304
+ | `all` | All | Apply to all operations |
285
305
 
286
306
  ```typescript
287
307
  // Single operation
@@ -316,6 +336,7 @@ deny('all', ctx => ctx.auth.suspended)
316
336
  ```
317
337
 
318
338
  **Priority System:**
339
+
319
340
  - Higher priority = evaluated first
320
341
  - Deny policies default to priority `100`
321
342
  - Allow/filter/validate default to priority `0`
@@ -334,10 +355,10 @@ defineRLSSchema<Database>({
334
355
  allow('read', ctx => ctx.auth.premium, { priority: 50 }),
335
356
 
336
357
  // Default priority
337
- allow('read', ctx => ctx.row.public),
338
- ],
339
- },
340
- });
358
+ allow('read', ctx => ctx.row.public)
359
+ ]
360
+ }
361
+ })
341
362
  ```
342
363
 
343
364
  ---
@@ -365,8 +386,8 @@ allow('read', ctx => ctx.auth.verified, {
365
386
 
366
387
  // Async condition
367
388
  allow('update', async ctx => {
368
- const hasPermission = await checkPermission(ctx.auth.userId, 'posts:edit');
369
- return hasPermission;
389
+ const hasPermission = await checkPermission(ctx.auth.userId, 'posts:edit')
390
+ return hasPermission
370
391
  })
371
392
  ```
372
393
 
@@ -405,9 +426,9 @@ filter('read', ctx => ({
405
426
  // Dynamic filter
406
427
  filter('read', ctx => {
407
428
  if (ctx.auth.roles.includes('admin')) {
408
- return {}; // No filtering
429
+ return {} // No filtering
409
430
  }
410
- return { status: 'published', public: true };
431
+ return { status: 'published', public: true }
411
432
  })
412
433
 
413
434
  // With hints
@@ -426,8 +447,8 @@ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
426
447
 
427
448
  // Validate update
428
449
  validate('update', ctx => {
429
- const allowedFields = ['title', 'content', 'tags'];
430
- return Object.keys(ctx.data).every(key => allowedFields.includes(key));
450
+ const allowedFields = ['title', 'content', 'tags']
451
+ return Object.keys(ctx.data).every(key => allowedFields.includes(key))
431
452
  })
432
453
 
433
454
  // Both create and update
@@ -439,18 +460,18 @@ validate('all', ctx => !ctx.data.price || ctx.data.price >= 0)
439
460
  ```typescript
440
461
  interface PolicyOptions {
441
462
  /** Policy name for debugging */
442
- name?: string;
463
+ name?: string
443
464
 
444
465
  /** Priority (higher runs first) */
445
- priority?: number;
466
+ priority?: number
446
467
 
447
468
  /** Performance hints */
448
469
  hints?: {
449
- indexColumns?: string[];
450
- selectivity?: 'high' | 'medium' | 'low';
451
- leakproof?: boolean;
452
- stable?: boolean;
453
- };
470
+ indexColumns?: string[]
471
+ selectivity?: 'high' | 'medium' | 'low'
472
+ leakproof?: boolean
473
+ stable?: boolean
474
+ }
454
475
  }
455
476
  ```
456
477
 
@@ -465,28 +486,29 @@ The RLS context is stored and managed using AsyncLocalStorage, providing automat
465
486
  ```typescript
466
487
  interface RLSContext<TUser = unknown, TMeta = unknown> {
467
488
  auth: {
468
- userId: string | number; // Required
469
- roles: string[]; // Required
470
- tenantId?: string | number; // Optional
471
- organizationIds?: (string | number)[];
472
- permissions?: string[];
473
- attributes?: Record<string, unknown>;
474
- user?: TUser;
475
- isSystem?: boolean; // Default: false
476
- };
489
+ userId: string | number // Required
490
+ roles: string[] // Required
491
+ tenantId?: string | number // Optional
492
+ organizationIds?: (string | number)[]
493
+ permissions?: string[]
494
+ attributes?: Record<string, unknown>
495
+ user?: TUser
496
+ isSystem?: boolean // Default: false
497
+ }
477
498
  request?: {
478
- requestId?: string;
479
- ipAddress?: string;
480
- userAgent?: string;
481
- timestamp: Date;
482
- headers?: Record<string, string>;
483
- };
484
- meta?: TMeta;
485
- timestamp: Date;
499
+ requestId?: string
500
+ ipAddress?: string
501
+ userAgent?: string
502
+ timestamp: Date
503
+ headers?: Record<string, string>
504
+ }
505
+ meta?: TMeta
506
+ timestamp: Date
486
507
  }
487
508
  ```
488
509
 
489
510
  **Context Storage:** The plugin uses `AsyncLocalStorage` internally to store the RLS context, which:
511
+
490
512
  - Automatically propagates through async/await chains
491
513
  - Is isolated per request (no cross-contamination)
492
514
  - Requires no manual passing of context objects
@@ -507,18 +529,18 @@ await rlsContext.runAsync(
507
529
  userId: 123,
508
530
  roles: ['user'],
509
531
  tenantId: 'acme-corp',
510
- isSystem: false,
532
+ isSystem: false
511
533
  },
512
- timestamp: new Date(),
534
+ timestamp: new Date()
513
535
  },
514
536
  async () => {
515
537
  // All queries within this block use this context
516
- const posts = await orm.posts.findAll();
538
+ const posts = await orm.posts.findAll()
517
539
 
518
540
  // Context propagates through async operations
519
- await orm.posts.create({ title: 'New Post' });
541
+ await orm.posts.create({ title: 'New Post' })
520
542
  }
521
- );
543
+ )
522
544
  ```
523
545
 
524
546
  #### `rlsContext.run(context, fn)`
@@ -528,8 +550,8 @@ Run synchronous function within RLS context:
528
550
  ```typescript
529
551
  const result = rlsContext.run(context, () => {
530
552
  // Synchronous operations
531
- return someValue;
532
- });
553
+ return someValue
554
+ })
533
555
  ```
534
556
 
535
557
  #### `createRLSContext(options)`
@@ -537,30 +559,30 @@ const result = rlsContext.run(context, () => {
537
559
  Create and validate RLS context with proper defaults:
538
560
 
539
561
  ```typescript
540
- import { createRLSContext } from '@kysera/rls';
562
+ import { createRLSContext } from '@kysera/rls'
541
563
 
542
564
  const ctx = createRLSContext({
543
565
  auth: {
544
566
  userId: 123,
545
567
  roles: ['user', 'editor'],
546
568
  tenantId: 'acme-corp',
547
- permissions: ['posts:read', 'posts:write'],
569
+ permissions: ['posts:read', 'posts:write']
548
570
  },
549
571
  // Optional request context
550
572
  request: {
551
573
  requestId: 'req-abc123',
552
574
  ipAddress: '192.168.1.1',
553
- timestamp: new Date(),
575
+ timestamp: new Date()
554
576
  },
555
577
  // Optional metadata
556
578
  meta: {
557
- featureFlags: ['beta_access'],
558
- },
559
- });
579
+ featureFlags: ['beta_access']
580
+ }
581
+ })
560
582
 
561
583
  await rlsContext.runAsync(ctx, async () => {
562
584
  // ...
563
- });
585
+ })
564
586
  ```
565
587
 
566
588
  #### Context Helper Methods
@@ -569,10 +591,10 @@ The `rlsContext` singleton provides helper methods for accessing context:
569
591
 
570
592
  ```typescript
571
593
  // Get current context (throws RLSContextError if not set)
572
- const ctx = rlsContext.getContext();
594
+ const ctx = rlsContext.getContext()
573
595
 
574
596
  // Get context or null (safe, no throw)
575
- const ctx = rlsContext.getContextOrNull();
597
+ const ctx = rlsContext.getContextOrNull()
576
598
 
577
599
  // Check if running within context
578
600
  if (rlsContext.hasContext()) {
@@ -580,13 +602,13 @@ if (rlsContext.hasContext()) {
580
602
  }
581
603
 
582
604
  // Get auth context (throws if no context)
583
- const auth = rlsContext.getAuth();
605
+ const auth = rlsContext.getAuth()
584
606
 
585
607
  // Get user ID (throws if no context)
586
- const userId = rlsContext.getUserId();
608
+ const userId = rlsContext.getUserId()
587
609
 
588
610
  // Get tenant ID (throws if no context)
589
- const tenantId = rlsContext.getTenantId();
611
+ const tenantId = rlsContext.getTenantId()
590
612
 
591
613
  // Check if user has role
592
614
  if (rlsContext.hasRole('admin')) {
@@ -606,13 +628,13 @@ if (rlsContext.isSystem()) {
606
628
  // Run as system user (bypass RLS)
607
629
  await rlsContext.asSystemAsync(async () => {
608
630
  // All operations bypass RLS policies
609
- const allPosts = await orm.posts.findAll();
610
- });
631
+ const allPosts = await orm.posts.findAll()
632
+ })
611
633
 
612
634
  // Synchronous system context
613
635
  const result = rlsContext.asSystem(() => {
614
- return someOperation();
615
- });
636
+ return someOperation()
637
+ })
616
638
  ```
617
639
 
618
640
  **Important:** The context helpers (`getContext`, `getAuth`, etc.) throw `RLSContextError` if called outside of a context. Always use `getContextOrNull()` or `hasContext()` if you need to check conditionally.
@@ -630,19 +652,19 @@ Bypass RLS policies for specific operations by running them in a system context:
630
652
  ```typescript
631
653
  // Fetch all posts including other tenants (bypasses RLS)
632
654
  const allPosts = await repo.withoutRLS(async () => {
633
- return repo.findAll();
634
- });
655
+ return repo.findAll()
656
+ })
635
657
 
636
658
  // Compare filtered vs unfiltered results
637
659
  await rlsContext.runAsync(userContext, async () => {
638
- const userPosts = await repo.findAll(); // Filtered by RLS policies
660
+ const userPosts = await repo.findAll() // Filtered by RLS policies
639
661
 
640
662
  const allPosts = await repo.withoutRLS(async () => {
641
- return repo.findAll(); // Bypasses RLS, returns all records
642
- });
663
+ return repo.findAll() // Bypasses RLS, returns all records
664
+ })
643
665
 
644
- console.log(`User can see ${userPosts.length} of ${allPosts.length} total posts`);
645
- });
666
+ console.log(`User can see ${userPosts.length} of ${allPosts.length} total posts`)
667
+ })
646
668
  ```
647
669
 
648
670
  **Implementation:** `withoutRLS` internally calls `rlsContext.asSystemAsync(fn)`, which sets `auth.isSystem = true` for the duration of the callback.
@@ -652,35 +674,36 @@ await rlsContext.runAsync(userContext, async () => {
652
674
  Check if the current user can perform an operation on a specific row:
653
675
 
654
676
  ```typescript
655
- const post = await repo.findById(postId);
677
+ const post = await repo.findById(postId)
656
678
 
657
679
  // Check read access
658
- const canRead = await repo.canAccess('read', post);
680
+ const canRead = await repo.canAccess('read', post)
659
681
 
660
682
  // Check update access before showing edit UI
661
- const canUpdate = await repo.canAccess('update', post);
683
+ const canUpdate = await repo.canAccess('update', post)
662
684
  if (canUpdate) {
663
685
  // Show edit button in UI
664
686
  }
665
687
 
666
688
  // Pre-flight check to avoid policy violations
667
689
  if (await repo.canAccess('delete', post)) {
668
- await repo.delete(post.id);
690
+ await repo.delete(post.id)
669
691
  } else {
670
- console.log('User cannot delete this post');
692
+ console.log('User cannot delete this post')
671
693
  }
672
694
 
673
695
  // Check multiple operations
674
- const operations = ['read', 'update', 'delete'] as const;
696
+ const operations = ['read', 'update', 'delete'] as const
675
697
  for (const op of operations) {
676
- const allowed = await repo.canAccess(op, post);
677
- console.log(`${op}: ${allowed}`);
698
+ const allowed = await repo.canAccess(op, post)
699
+ console.log(`${op}: ${allowed}`)
678
700
  }
679
701
  ```
680
702
 
681
703
  **Implementation:** `canAccess` evaluates the RLS policies against the provided row using the `MutationGuard`, returning `true` if allowed and `false` if denied or no context exists.
682
704
 
683
705
  **Supported Operations:**
706
+
684
707
  - `'read'` - Check if user can view the row
685
708
  - `'create'` - Check if user can create with this data
686
709
  - `'update'` - Check if user can update the row
@@ -690,51 +713,45 @@ for (const op of operations) {
690
713
 
691
714
  ## DAL Pattern Support
692
715
 
693
- RLS works seamlessly with the DAL pattern:
716
+ RLS works seamlessly with the DAL pattern through @kysera/executor's Unified Execution Layer:
694
717
 
695
718
  ```typescript
696
- import { createExecutor } from '@kysera/executor';
697
- import { createContext, createQuery, withTransaction } from '@kysera/dal';
698
- import { rlsPlugin, defineRLSSchema, filter, rlsContext } from '@kysera/rls';
719
+ import { createExecutor } from '@kysera/executor'
720
+ import { createContext, createQuery, withTransaction } from '@kysera/dal'
721
+ import { rlsPlugin, defineRLSSchema, filter, rlsContext } from '@kysera/rls'
699
722
 
700
723
  // Define schema
701
724
  const rlsSchema = defineRLSSchema<Database>({
702
725
  posts: {
703
- policies: [
704
- filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
705
- ],
706
- },
707
- });
726
+ policies: [filter('read', ctx => ({ tenant_id: ctx.auth.tenantId }))]
727
+ }
728
+ })
708
729
 
709
- // Create executor with RLS
710
- const executor = await createExecutor(db, [
711
- rlsPlugin({ schema: rlsSchema }),
712
- ]);
730
+ // Step 1: Register RLS plugin with Unified Execution Layer
731
+ const executor = await createExecutor(db, [rlsPlugin({ schema: rlsSchema })])
713
732
 
714
- // Create DAL context
715
- const dalCtx = createContext(executor);
733
+ // Step 2: Create DAL context - plugins automatically apply
734
+ const dalCtx = createContext(executor)
716
735
 
717
- // Define queries - RLS applied automatically
718
- const getPosts = createQuery((ctx) =>
719
- ctx.db.selectFrom('posts').selectAll().execute()
720
- );
736
+ // Step 3: Define queries - RLS applied automatically
737
+ const getPosts = createQuery(ctx => ctx.db.selectFrom('posts').selectAll().execute())
721
738
 
722
739
  // Execute within RLS context
723
740
  await rlsContext.runAsync(
724
741
  {
725
742
  auth: { userId: 1, tenantId: 'acme', roles: ['user'], isSystem: false },
726
- timestamp: new Date(),
743
+ timestamp: new Date()
727
744
  },
728
745
  async () => {
729
746
  // Automatically filtered by tenant
730
- const posts = await getPosts(dalCtx);
747
+ const posts = await getPosts(dalCtx)
731
748
 
732
749
  // Transactions propagate RLS context
733
- await withTransaction(dalCtx, async (txCtx) => {
734
- const txPosts = await getPosts(txCtx);
735
- });
750
+ await withTransaction(dalCtx, async txCtx => {
751
+ const txPosts = await getPosts(txCtx)
752
+ })
736
753
  }
737
- );
754
+ )
738
755
  ```
739
756
 
740
757
  ---
@@ -746,54 +763,140 @@ await rlsContext.runAsync(
746
763
  ```typescript
747
764
  interface RLSPluginOptions<DB = unknown> {
748
765
  /** RLS policy schema (required) */
749
- schema: RLSSchema<DB>;
766
+ schema: RLSSchema<DB>
750
767
 
751
- /** Tables to skip RLS (always bypass) */
752
- skipTables?: string[];
768
+ /**
769
+ * Tables to exclude from RLS (always bypass)
770
+ */
771
+ excludeTables?: string[]
753
772
 
754
773
  /** Roles that bypass RLS entirely */
755
- bypassRoles?: string[];
774
+ bypassRoles?: string[]
756
775
 
757
776
  /** Logger for RLS operations */
758
- logger?: KyseraLogger;
759
-
760
- /** Require RLS context (throws if missing) */
761
- requireContext?: boolean;
777
+ logger?: KyseraLogger
778
+
779
+ /**
780
+ * Require RLS context (throws if missing)
781
+ * @default true - SECURE BY DEFAULT
782
+ *
783
+ * SECURITY: Changed to true in v0.7.3+ for secure-by-default.
784
+ * When true, missing context throws RLSContextError.
785
+ * Only set to false if you have other security controls.
786
+ */
787
+ requireContext?: boolean
788
+
789
+ /**
790
+ * Allow unfiltered queries when context is missing
791
+ * @default false - SECURE BY DEFAULT
792
+ *
793
+ * WARNING: Setting true allows queries without RLS filtering
794
+ * when context is missing. Only enable if you understand the
795
+ * security implications (e.g., background jobs, system ops).
796
+ *
797
+ * When requireContext=false and allowUnfilteredQueries=false:
798
+ * - Missing context returns empty results with warnings
799
+ */
800
+ allowUnfilteredQueries?: boolean
762
801
 
763
802
  /** Enable audit logging of decisions */
764
- auditDecisions?: boolean;
803
+ auditDecisions?: boolean
765
804
 
766
805
  /** Custom violation handler */
767
- onViolation?: (violation: RLSPolicyViolation) => void;
806
+ onViolation?: (violation: RLSPolicyViolation) => void
807
+
808
+ /** Primary key column name (default: 'id') */
809
+ primaryKeyColumn?: string
768
810
  }
769
811
  ```
770
812
 
771
813
  **Example:**
772
814
 
773
815
  ```typescript
774
- import { rlsPlugin } from '@kysera/rls';
775
- import { createLogger } from '@kysera/core';
816
+ import { rlsPlugin } from '@kysera/rls'
817
+ import { createLogger } from '@kysera/core'
776
818
 
777
819
  const plugin = rlsPlugin({
778
820
  schema: rlsSchema,
779
- skipTables: ['audit_logs', 'migrations'],
821
+ excludeTables: ['audit_logs', 'migrations'],
780
822
  bypassRoles: ['admin', 'system'],
781
823
  logger: createLogger({ level: 'info' }),
782
824
  requireContext: true,
783
825
  auditDecisions: true,
784
- onViolation: (violation) => {
826
+ onViolation: violation => {
785
827
  auditLog.record({
786
828
  type: 'rls_violation',
787
829
  operation: violation.operation,
788
830
  table: violation.table,
789
- timestamp: new Date(),
790
- });
791
- },
792
- });
831
+ timestamp: new Date()
832
+ })
833
+ }
834
+ })
835
+
836
+ const orm = await createORM(db, [plugin])
837
+ ```
838
+
839
+ ### Security Configuration (v0.7.3+)
840
+
841
+ **BREAKING CHANGE**: Starting in v0.7.3, `requireContext` defaults to `true` for secure-by-default behavior.
842
+
843
+ #### Secure Defaults (Recommended)
844
+
845
+ ```typescript
846
+ // Default behavior - secure by default
847
+ const plugin = rlsPlugin({
848
+ schema: rlsSchema
849
+ // requireContext: true (implicit)
850
+ // allowUnfilteredQueries: false (implicit)
851
+ })
852
+
853
+ // Missing context throws RLSContextError
854
+ await orm.posts.findAll() // ❌ Throws: RLS context required
855
+ ```
856
+
857
+ #### Background Jobs / System Operations
858
+
859
+ For operations that legitimately run without user context (e.g., cron jobs, system maintenance):
860
+
861
+ ```typescript
862
+ const plugin = rlsPlugin({
863
+ schema: rlsSchema,
864
+ requireContext: false, // Don't throw on missing context
865
+ allowUnfilteredQueries: true // Allow queries without filtering
866
+ })
867
+
868
+ // OR use system context for privileged operations:
869
+ await rlsContext.asSystemAsync(async () => {
870
+ await orm.posts.findAll() // ✅ Runs as system user
871
+ })
872
+ ```
873
+
874
+ #### Defensive Mode (No throws, but safe)
793
875
 
794
- const orm = await createORM(db, [plugin]);
876
+ For applications transitioning to RLS or with mixed code paths:
877
+
878
+ ```typescript
879
+ const plugin = rlsPlugin({
880
+ schema: rlsSchema,
881
+ requireContext: false, // Don't throw
882
+ allowUnfilteredQueries: false // Return empty results
883
+ // Missing context logs warning and returns no rows
884
+ })
795
885
  ```
796
886
 
887
+ **Security Matrix:**
888
+
889
+ | requireContext | allowUnfilteredQueries | Missing Context Behavior |
890
+ | -------------- | ---------------------- | --------------------------------------- |
891
+ | `true` (default) | N/A | **Throws RLSContextError** (secure) |
892
+ | `false` | `false` (default) | **Returns empty results** (safe) |
893
+ | `false` | `true` | **Allows unfiltered access** (unsafe) |
894
+
895
+ ⚠️ **Security Warning**: Only use `allowUnfilteredQueries: true` if you:
896
+ 1. Understand the security implications
897
+ 2. Have other security controls in place
898
+ 3. Are running background jobs or system operations without user context
899
+
797
900
  ---
798
901
 
799
902
  ## Error Handling
@@ -804,13 +907,13 @@ The RLS plugin provides specialized error classes for different failure scenario
804
907
 
805
908
  ```typescript
806
909
  import {
807
- RLSError, // Base error class
808
- RLSContextError, // Missing context
809
- RLSPolicyViolation, // Access denied (expected)
810
- RLSPolicyEvaluationError, // Bug in policy code (unexpected)
811
- RLSSchemaError, // Invalid schema
812
- RLSContextValidationError, // Invalid context
813
- } from '@kysera/rls';
910
+ RLSError, // Base error class
911
+ RLSContextError, // Missing context
912
+ RLSPolicyViolation, // Access denied (expected)
913
+ RLSPolicyEvaluationError, // Bug in policy code (unexpected)
914
+ RLSSchemaError, // Invalid schema
915
+ RLSContextValidationError // Invalid context
916
+ } from '@kysera/rls'
814
917
  ```
815
918
 
816
919
  ### Error Scenarios
@@ -822,16 +925,17 @@ Thrown when RLS context is missing but required:
822
925
  ```typescript
823
926
  try {
824
927
  // No context set, but requireContext: true
825
- await orm.posts.findAll();
928
+ await orm.posts.findAll()
826
929
  } catch (error) {
827
930
  if (error instanceof RLSContextError) {
828
931
  // error.code === 'RLS_CONTEXT_MISSING'
829
- console.error('No RLS context found. Ensure code runs within rlsContext.runAsync()');
932
+ console.error('No RLS context found. Ensure code runs within rlsContext.runAsync()')
830
933
  }
831
934
  }
832
935
  ```
833
936
 
834
937
  **When thrown:**
938
+
835
939
  - Operations executed outside `rlsContext.runAsync()` when `requireContext: true`
836
940
  - Calling `rlsContext.getContext()` without active context
837
941
  - Attempting `asSystem()` without existing context
@@ -843,27 +947,28 @@ Thrown when operation is denied by policies (this is expected, not a bug):
843
947
  ```typescript
844
948
  try {
845
949
  // User tries to update a post they don't own
846
- await orm.posts.update(1, { title: 'New Title' });
950
+ await orm.posts.update(1, { title: 'New Title' })
847
951
  } catch (error) {
848
952
  if (error instanceof RLSPolicyViolation) {
849
953
  // error.code === 'RLS_POLICY_VIOLATION'
850
954
  console.error({
851
- operation: error.operation, // 'update'
852
- table: error.table, // 'posts'
853
- reason: error.reason, // 'User does not own this post'
854
- policyName: error.policyName, // 'ownership_policy' (if named)
855
- });
955
+ operation: error.operation, // 'update'
956
+ table: error.table, // 'posts'
957
+ reason: error.reason, // 'User does not own this post'
958
+ policyName: error.policyName // 'ownership_policy' (if named)
959
+ })
856
960
 
857
961
  // Return 403 Forbidden to client
858
962
  res.status(403).json({
859
963
  error: 'Access denied',
860
- message: error.reason,
861
- });
964
+ message: error.reason
965
+ })
862
966
  }
863
967
  }
864
968
  ```
865
969
 
866
970
  **When thrown:**
971
+
867
972
  - `deny` policy condition evaluates to `true`
868
973
  - No `allow` policy matches and `defaultDeny: true`
869
974
  - `validate` policy fails during CREATE/UPDATE
@@ -874,16 +979,16 @@ Thrown when policy condition throws an error (this is a bug in your policy code)
874
979
 
875
980
  ```typescript
876
981
  try {
877
- await orm.posts.findAll();
982
+ await orm.posts.findAll()
878
983
  } catch (error) {
879
984
  if (error instanceof RLSPolicyEvaluationError) {
880
985
  // error.code === 'RLS_POLICY_EVALUATION_ERROR'
881
986
  console.error({
882
- operation: error.operation, // 'read'
883
- table: error.table, // 'posts'
884
- policyName: error.policyName, // 'tenant_filter'
885
- originalError: error.originalError, // TypeError: Cannot read property 'tenantId' of undefined
886
- });
987
+ operation: error.operation, // 'read'
988
+ table: error.table, // 'posts'
989
+ policyName: error.policyName, // 'tenant_filter'
990
+ originalError: error.originalError // TypeError: Cannot read property 'tenantId' of undefined
991
+ })
887
992
 
888
993
  // This is a bug - fix your policy code!
889
994
  // Example: Policy tried to access ctx.auth.tenantId but it was undefined
@@ -892,6 +997,7 @@ try {
892
997
  ```
893
998
 
894
999
  **When thrown:**
1000
+
895
1001
  - Policy condition function throws an error
896
1002
  - Policy tries to access undefined properties
897
1003
  - Async policy rejects with an error
@@ -907,16 +1013,16 @@ try {
907
1013
  const ctx = createRLSContext({
908
1014
  auth: {
909
1015
  // Missing userId!
910
- roles: ['user'],
911
- },
912
- });
1016
+ roles: ['user']
1017
+ }
1018
+ })
913
1019
  } catch (error) {
914
1020
  if (error instanceof RLSContextValidationError) {
915
1021
  // error.code === 'RLS_CONTEXT_INVALID'
916
1022
  console.error({
917
- message: error.message, // 'userId is required in auth context'
918
- field: error.field, // 'userId'
919
- });
1023
+ message: error.message, // 'userId is required in auth context'
1024
+ field: error.field // 'userId'
1025
+ })
920
1026
  }
921
1027
  }
922
1028
  ```
@@ -934,31 +1040,31 @@ try {
934
1040
  { type: 'invalid-type', operation: 'read', condition: () => true }
935
1041
  ]
936
1042
  }
937
- });
1043
+ })
938
1044
  } catch (error) {
939
1045
  if (error instanceof RLSSchemaError) {
940
1046
  // error.code === 'RLS_SCHEMA_INVALID'
941
- console.error(error.details);
1047
+ console.error(error.details)
942
1048
  }
943
1049
  }
944
1050
  ```
945
1051
 
946
1052
  ### Error Comparison Table
947
1053
 
948
- | Error | Meaning | Severity | Action |
949
- |-------|---------|----------|--------|
950
- | `RLSContextError` | Missing context | Error | Ensure code runs in `rlsContext.runAsync()` |
951
- | `RLSPolicyViolation` | Access denied | Expected | Return 403 to client, normal behavior |
952
- | `RLSPolicyEvaluationError` | Policy bug | Critical | Fix the policy code immediately |
953
- | `RLSContextValidationError` | Invalid context | Error | Fix context creation |
954
- | `RLSSchemaError` | Invalid schema | Error | Fix schema definition |
1054
+ | Error | Meaning | Severity | Action |
1055
+ | --------------------------- | --------------- | -------- | ------------------------------------------- |
1056
+ | `RLSContextError` | Missing context | Error | Ensure code runs in `rlsContext.runAsync()` |
1057
+ | `RLSPolicyViolation` | Access denied | Expected | Return 403 to client, normal behavior |
1058
+ | `RLSPolicyEvaluationError` | Policy bug | Critical | Fix the policy code immediately |
1059
+ | `RLSContextValidationError` | Invalid context | Error | Fix context creation |
1060
+ | `RLSSchemaError` | Invalid schema | Error | Fix schema definition |
955
1061
 
956
1062
  ### Error Codes
957
1063
 
958
1064
  All RLS errors include a `code` property for programmatic handling:
959
1065
 
960
1066
  ```typescript
961
- import { RLSErrorCodes } from '@kysera/rls';
1067
+ import { RLSErrorCodes } from '@kysera/rls'
962
1068
 
963
1069
  // RLSErrorCodes.RLS_CONTEXT_MISSING
964
1070
  // RLSErrorCodes.RLS_POLICY_VIOLATION
@@ -997,21 +1103,25 @@ The RLS plugin follows the standard `@kysera/executor` plugin lifecycle:
997
1103
  ### Key Components
998
1104
 
999
1105
  **PolicyRegistry:**
1106
+
1000
1107
  - Stores and indexes compiled policies by table and operation
1001
1108
  - Validates schema structure
1002
1109
  - Provides fast policy lookup
1003
1110
 
1004
1111
  **SelectTransformer:**
1112
+
1005
1113
  - Transforms SELECT queries by adding WHERE conditions
1006
1114
  - Combines multiple filter policies with AND logic
1007
1115
  - Evaluates filter conditions in context
1008
1116
 
1009
1117
  **MutationGuard:**
1118
+
1010
1119
  - Evaluates allow/deny policies for mutations
1011
1120
  - Enforces policy evaluation order (deny → allow → validate)
1012
1121
  - Throws `RLSPolicyViolation` or `RLSPolicyEvaluationError`
1013
1122
 
1014
1123
  **AsyncLocalStorage:**
1124
+
1015
1125
  - Provides context isolation per request
1016
1126
  - Automatic propagation through async/await chains
1017
1127
  - No manual context passing required
@@ -1019,21 +1129,25 @@ The RLS plugin follows the standard `@kysera/executor` plugin lifecycle:
1019
1129
  ### Performance Considerations
1020
1130
 
1021
1131
  **Compiled Policies:**
1132
+
1022
1133
  - Policies are compiled once at initialization
1023
1134
  - No runtime parsing or compilation overhead
1024
1135
 
1025
1136
  **Filter Application:**
1137
+
1026
1138
  - Filters applied as SQL WHERE clauses
1027
1139
  - Database handles filtering efficiently
1028
1140
  - Index hints available via `PolicyOptions.hints`
1029
1141
 
1030
1142
  **Context Access:**
1143
+
1031
1144
  - AsyncLocalStorage is very fast (V8-optimized)
1032
1145
  - Context lookup has negligible overhead
1033
1146
 
1034
1147
  **Bypass Mechanisms:**
1148
+
1035
1149
  - System context bypass is immediate (no policy evaluation)
1036
- - `skipTables` bypass is immediate (no policy evaluation)
1150
+ - `excludeTables` bypass is immediate (no policy evaluation)
1037
1151
  - Bypass roles checked before policy evaluation
1038
1152
 
1039
1153
  ### Transaction Support
@@ -1043,21 +1157,21 @@ RLS context automatically propagates through transactions:
1043
1157
  ```typescript
1044
1158
  await rlsContext.runAsync(userContext, async () => {
1045
1159
  // Context available in transaction
1046
- await orm.transaction(async (trx) => {
1160
+ await orm.transaction(async trx => {
1047
1161
  // All queries use the same RLS context
1048
- const user = await trx.users.findById(userId);
1049
- await trx.posts.create({ title: 'Post', authorId: userId });
1050
- });
1051
- });
1162
+ const user = await trx.users.findById(userId)
1163
+ await trx.posts.create({ title: 'Post', authorId: userId })
1164
+ })
1165
+ })
1052
1166
  ```
1053
1167
 
1054
1168
  **Note:** DAL transactions with executor preserve RLS context:
1055
1169
 
1056
1170
  ```typescript
1057
- await withTransaction(executor, async (txCtx) => {
1171
+ await withTransaction(executor, async txCtx => {
1058
1172
  // RLS context preserved in transaction
1059
- const posts = await getPosts(txCtx);
1060
- });
1173
+ const posts = await getPosts(txCtx)
1174
+ })
1061
1175
  ```
1062
1176
 
1063
1177
  ---
@@ -1071,23 +1185,23 @@ const schema = defineRLSSchema<Database>({
1071
1185
  posts: {
1072
1186
  policies: [
1073
1187
  filter('read', ctx => ({ tenant_id: ctx.auth.tenantId })),
1074
- validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId),
1188
+ validate('create', ctx => ctx.data.tenant_id === ctx.auth.tenantId)
1075
1189
  ],
1076
- defaultDeny: true,
1077
- },
1078
- });
1190
+ defaultDeny: true
1191
+ }
1192
+ })
1079
1193
 
1080
1194
  app.use(async (req, res, next) => {
1081
- const user = await authenticate(req);
1195
+ const user = await authenticate(req)
1082
1196
 
1083
1197
  await rlsContext.runAsync(
1084
1198
  { auth: { userId: user.id, tenantId: user.tenant_id, roles: user.roles } },
1085
1199
  async () => {
1086
- const posts = await orm.posts.findAll();
1087
- res.json(posts);
1200
+ const posts = await orm.posts.findAll()
1201
+ res.json(posts)
1088
1202
  }
1089
- );
1090
- });
1203
+ )
1204
+ })
1091
1205
  ```
1092
1206
 
1093
1207
  ### Owner-Based Access
@@ -1103,12 +1217,10 @@ const schema = defineRLSSchema<Database>({
1103
1217
  allow('read', ctx => ctx.auth.userId === ctx.row.author_id),
1104
1218
 
1105
1219
  // Only owner can update/delete
1106
- allow(['update', 'delete'], ctx =>
1107
- ctx.auth.userId === ctx.row.author_id
1108
- ),
1109
- ],
1110
- },
1111
- });
1220
+ allow(['update', 'delete'], ctx => ctx.auth.userId === ctx.row.author_id)
1221
+ ]
1222
+ }
1223
+ })
1112
1224
  ```
1113
1225
 
1114
1226
  ### Role-Based Access Control
@@ -1121,15 +1233,13 @@ const schema = defineRLSSchema<Database>({
1121
1233
  allow('all', ctx => ctx.auth.roles.includes('admin')),
1122
1234
 
1123
1235
  // Editors can read and update
1124
- allow(['read', 'update'], ctx =>
1125
- ctx.auth.roles.includes('editor')
1126
- ),
1236
+ allow(['read', 'update'], ctx => ctx.auth.roles.includes('editor')),
1127
1237
 
1128
1238
  // Regular users read only
1129
- allow('read', ctx => ctx.auth.roles.includes('user')),
1130
- ],
1131
- },
1132
- });
1239
+ allow('read', ctx => ctx.auth.roles.includes('user'))
1240
+ ]
1241
+ }
1242
+ })
1133
1243
  ```
1134
1244
 
1135
1245
  ---
@@ -1141,30 +1251,30 @@ Full type inference for policies:
1141
1251
  ```typescript
1142
1252
  interface Database {
1143
1253
  posts: {
1144
- id: number;
1145
- title: string;
1146
- author_id: number;
1147
- tenant_id: string;
1148
- };
1254
+ id: number
1255
+ title: string
1256
+ author_id: number
1257
+ tenant_id: string
1258
+ }
1149
1259
  }
1150
1260
 
1151
1261
  const schema = defineRLSSchema<Database>({
1152
1262
  posts: {
1153
1263
  policies: [
1154
1264
  allow('read', ctx => {
1155
- const post = ctx.row; // Type: Database['posts']
1156
- const userId = ctx.auth.userId; // Type: string | number
1157
- return post.author_id === userId;
1265
+ const post = ctx.row // Type: Database['posts']
1266
+ const userId = ctx.auth.userId // Type: string | number
1267
+ return post.author_id === userId
1158
1268
  }),
1159
1269
 
1160
1270
  validate('update', ctx => {
1161
- const data = ctx.data; // Type: Partial<Database['posts']>
1162
- const title = data.title; // Type: string | undefined
1163
- return !title || title.length > 0;
1164
- }),
1165
- ],
1166
- },
1167
- });
1271
+ const data = ctx.data // Type: Partial<Database['posts']>
1272
+ const title = data.title // Type: string | undefined
1273
+ return !title || title.length > 0
1274
+ })
1275
+ ]
1276
+ }
1277
+ })
1168
1278
  ```
1169
1279
 
1170
1280
  ---
@@ -1175,13 +1285,13 @@ const schema = defineRLSSchema<Database>({
1175
1285
 
1176
1286
  ```typescript
1177
1287
  // Schema definition
1178
- export { defineRLSSchema, mergeRLSSchemas } from '@kysera/rls';
1288
+ export { defineRLSSchema, mergeRLSSchemas } from '@kysera/rls'
1179
1289
 
1180
1290
  // Policy builders
1181
- export { allow, deny, filter, validate, type PolicyOptions } from '@kysera/rls';
1291
+ export { allow, deny, filter, validate, type PolicyOptions } from '@kysera/rls'
1182
1292
 
1183
1293
  // Plugin
1184
- export { rlsPlugin, type RLSPluginOptions } from '@kysera/rls';
1294
+ export { rlsPlugin, type RLSPluginOptions } from '@kysera/rls'
1185
1295
 
1186
1296
  // Context management
1187
1297
  export {
@@ -1189,8 +1299,8 @@ export {
1189
1299
  createRLSContext,
1190
1300
  withRLSContext,
1191
1301
  withRLSContextAsync,
1192
- type RLSContext,
1193
- } from '@kysera/rls';
1302
+ type RLSContext
1303
+ } from '@kysera/rls'
1194
1304
 
1195
1305
  // Errors
1196
1306
  export {
@@ -1200,8 +1310,8 @@ export {
1200
1310
  RLSPolicyEvaluationError,
1201
1311
  RLSSchemaError,
1202
1312
  RLSContextValidationError,
1203
- RLSErrorCodes,
1204
- } from '@kysera/rls';
1313
+ RLSErrorCodes
1314
+ } from '@kysera/rls'
1205
1315
  ```
1206
1316
 
1207
1317
  ---