@rebasepro/agent-skills 0.7.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.
Files changed (38) hide show
  1. package/README.md +96 -0
  2. package/package.json +15 -0
  3. package/skills/rebase-admin/SKILL.md +816 -0
  4. package/skills/rebase-api/SKILL.md +673 -0
  5. package/skills/rebase-api/references/.gitkeep +3 -0
  6. package/skills/rebase-auth/SKILL.md +1283 -0
  7. package/skills/rebase-auth/references/.gitkeep +3 -0
  8. package/skills/rebase-backend-postgres/SKILL.md +633 -0
  9. package/skills/rebase-backend-postgres/references/.gitkeep +3 -0
  10. package/skills/rebase-basics/SKILL.md +750 -0
  11. package/skills/rebase-basics/references/.gitkeep +3 -0
  12. package/skills/rebase-collections/SKILL.md +1383 -0
  13. package/skills/rebase-collections/references/.gitkeep +3 -0
  14. package/skills/rebase-cron-jobs/SKILL.md +699 -0
  15. package/skills/rebase-cron-jobs/references/.gitkeep +1 -0
  16. package/skills/rebase-custom-functions/SKILL.md +233 -0
  17. package/skills/rebase-deployment/SKILL.md +885 -0
  18. package/skills/rebase-deployment/references/.gitkeep +3 -0
  19. package/skills/rebase-design-language/SKILL.md +692 -0
  20. package/skills/rebase-email/SKILL.md +701 -0
  21. package/skills/rebase-email/references/.gitkeep +1 -0
  22. package/skills/rebase-entity-history/SKILL.md +485 -0
  23. package/skills/rebase-entity-history/references/.gitkeep +1 -0
  24. package/skills/rebase-local-env-setup/SKILL.md +189 -0
  25. package/skills/rebase-local-env-setup/references/.gitkeep +3 -0
  26. package/skills/rebase-realtime/SKILL.md +759 -0
  27. package/skills/rebase-realtime/references/.gitkeep +3 -0
  28. package/skills/rebase-sdk/SKILL.md +594 -0
  29. package/skills/rebase-sdk/references/.gitkeep +0 -0
  30. package/skills/rebase-security/SKILL.md +606 -0
  31. package/skills/rebase-storage/SKILL.md +765 -0
  32. package/skills/rebase-storage/references/.gitkeep +3 -0
  33. package/skills/rebase-studio/SKILL.md +772 -0
  34. package/skills/rebase-studio/references/.gitkeep +3 -0
  35. package/skills/rebase-ui-components/SKILL.md +1488 -0
  36. package/skills/rebase-ui-components/references/.gitkeep +3 -0
  37. package/skills/rebase-webhooks/SKILL.md +623 -0
  38. package/skills/rebase-webhooks/references/.gitkeep +1 -0
@@ -0,0 +1,606 @@
1
+ ---
2
+ name: rebase-security
3
+ description: Comprehensive guide to the Rebase backend security architecture. Use this skill when the user asks about securing their application, backend-level access control, request interception, DataHooks for security, fail-closed design, or how security works without database-level RLS. Also use when the user needs to implement PII masking, tenant isolation, role-based access control at the API layer, or cross-cutting security concerns.
4
+ ---
5
+
6
+ # Rebase Security Architecture
7
+
8
+ Rebase implements a **multi-layered, defense-in-depth** security architecture. Security is enforced at the **application level** — not just at the database level. This means your data is protected regardless of whether the underlying database supports native Row-Level Security (RLS) or not.
9
+
10
+ > **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` and `rebase-auth` skills for auth configuration details. This skill focuses on the **security architecture** and **backend-level enforcement mechanisms**.
11
+
12
+ ## Table of Contents
13
+
14
+ - [Security Architecture Overview](#security-architecture-overview)
15
+ - [Request Pipeline](#request-pipeline)
16
+ - [Layer 1: Auth Middleware](#layer-1-auth-middleware)
17
+ - [Layer 2: API Key Permission Guard](#layer-2-api-key-permission-guard)
18
+ - [Layer 3: DataHooks (API Boundary)](#layer-3-datahooks-api-boundary)
19
+ - [Layer 4: Scoped DataDriver](#layer-4-scoped-datadriver)
20
+ - [Layer 5: Entity Callbacks](#layer-5-entity-callbacks)
21
+ - [Fail-Closed Design](#fail-closed-design)
22
+ - [Securing Without Database RLS](#securing-without-database-rls)
23
+ - [Common Security Patterns](#common-security-patterns)
24
+ - [Security Checklist](#security-checklist)
25
+ - [References](#references)
26
+
27
+ ---
28
+
29
+ ## Security Architecture Overview
30
+
31
+ Every request — REST, GraphQL, and WebSocket — passes through **5 security layers** before data is returned to the client. These layers are enforced at the **application level** and work independently of any database-native security mechanism.
32
+
33
+ ```
34
+ ┌──────────────────────────────────────────────────────┐
35
+ │ CLIENT REQUEST │
36
+ │ (REST / GraphQL / WebSocket) │
37
+ └──────────────┬───────────────────────────────────────┘
38
+
39
+ ┌──────────────▼───────────────────────────────────────┐
40
+ │ Layer 1: Auth Middleware │
41
+ │ JWT / Service Key / API Key / Custom AuthAdapter │
42
+ │ → Identifies user, scopes the DataDriver │
43
+ └──────────────┬───────────────────────────────────────┘
44
+
45
+ ┌──────────────▼───────────────────────────────────────┐
46
+ │ Layer 2: API Key Permission Guard │
47
+ │ Per-collection, per-operation permission check │
48
+ │ (only for API key requests) │
49
+ └──────────────┬───────────────────────────────────────┘
50
+
51
+ ┌──────────────▼───────────────────────────────────────┐
52
+ │ Layer 3: DataHooks (API Boundary) │
53
+ │ Cross-cutting interception for ALL collections │
54
+ │ afterRead / beforeSave / beforeDelete │
55
+ └──────────────┬───────────────────────────────────────┘
56
+
57
+ ┌──────────────▼───────────────────────────────────────┐
58
+ │ Layer 4: Scoped DataDriver │
59
+ │ driver.withAuth(user) — applies RLS policies │
60
+ │ (PostgreSQL: SET LOCAL session vars + native RLS) │
61
+ └──────────────┬───────────────────────────────────────┘
62
+
63
+ ┌──────────────▼───────────────────────────────────────┐
64
+ │ Layer 5: Entity Callbacks (Per-Collection) │
65
+ │ beforeSave / afterRead / beforeDelete │
66
+ │ Per-collection hooks inside the DataDriver │
67
+ └──────────────────────────────────────────────────────┘
68
+ ```
69
+
70
+ > **KEY INSIGHT:** Layers 1–3 and Layer 5 are **application-level** — they don't depend on the database. Even if you cannot configure database-level RLS policies, these layers provide full security coverage.
71
+
72
+ ---
73
+
74
+ ## Request Pipeline
75
+
76
+ All three protocols share the same security middleware stack:
77
+
78
+ ### REST
79
+ ```
80
+ HTTP Request → Auth Middleware → API Key Guard → DataHooks → Scoped Driver → Entity Callbacks → Response
81
+ ```
82
+
83
+ ### GraphQL
84
+ ```
85
+ GraphQL Request → Auth Middleware → API Key Guard → Scoped Driver → Entity Callbacks → Response
86
+ ```
87
+ GraphQL shares the same Hono middleware chain as REST. Resolvers extract the scoped driver from context and throw if unavailable.
88
+
89
+ ### WebSocket
90
+ ```
91
+ WS Connect → AUTHENTICATE message → Token Verification → Per-Operation Scoped Driver → Response
92
+ ```
93
+ WebSocket auth follows a message-based flow:
94
+ 1. Client sends an `AUTHENTICATE` message with a JWT token.
95
+ 2. Token is verified via `extractUserFromToken(token)`.
96
+ 3. Session is marked as authenticated.
97
+ 4. Each subsequent data operation calls `getScopedDelegate()` to create a user-scoped driver.
98
+ 5. Admin-only operations (e.g., `EXECUTE_SQL`) require `isAdminSession()`.
99
+ 6. Rate limiting: 2000 messages per 60 seconds.
100
+
101
+ ---
102
+
103
+ ## Layer 1: Auth Middleware
104
+
105
+ The auth middleware is the **first line of defense**. It runs on every request and does two things:
106
+
107
+ 1. **Identifies the user** — Extracts credentials from the `Authorization` header (JWT, service key, API key, or custom adapter).
108
+ 2. **Scopes the DataDriver** — Calls `scopeDataDriver(driver, user)` which invokes `driver.withAuth(user)` to return a security-scoped clone.
109
+
110
+ The scoped driver is placed into `c.set("driver", scopedDriver)`. The raw, unscoped driver is **never** placed in the request context.
111
+
112
+ ### How Scoping Works
113
+
114
+ ```typescript
115
+ // From packages/server-core/src/auth/rls-scope.ts
116
+ export async function scopeDataDriver(
117
+ driver: DataDriver,
118
+ user: { uid: string; roles?: string[] }
119
+ ): Promise<DataDriver> {
120
+ if (isRLSScopedDriver(driver)) {
121
+ // Fail closed — if withAuth() throws, the request is DENIED
122
+ return await driver.withAuth(user);
123
+ }
124
+ return driver;
125
+ }
126
+ ```
127
+
128
+ ### Identity Types
129
+
130
+ | Auth Method | `uid` | `roles` | RLS Behavior |
131
+ |---|---|---|---|
132
+ | JWT (authenticated user) | User's ID | User's app roles | Full RLS enforcement |
133
+ | Service Key | `"service"` | `["admin"]` | Bypasses RLS (admin access) |
134
+ | API Key (default) | `"api-key:{id}"` | `["service"]` | Bypasses RLS, scoped by permissions |
135
+ | API Key (admin) | `"api-key:{id}"` | `["admin", "service"]` | Bypasses RLS, full admin access |
136
+ | Anonymous (`requireAuth: false`) | `"anon"` | `["anon"]` | RLS with anonymous identity |
137
+ | No token + `requireAuth: true` | — | — | **Rejected (401)** |
138
+
139
+ ---
140
+
141
+ ## Layer 2: API Key Permission Guard
142
+
143
+ When a request is authenticated via an API key (prefixed `rk_`), the permission guard enforces **per-collection, per-operation** access control:
144
+
145
+ ```typescript
146
+ interface ApiKeyPermission {
147
+ collection: string; // Collection slug, or "*" for all
148
+ operations: ("read" | "write" | "delete")[];
149
+ }
150
+ ```
151
+
152
+ - `GET` → requires `"read"` permission
153
+ - `POST` / `PUT` / `PATCH` → requires `"write"` permission
154
+ - `DELETE` → requires `"delete"` permission
155
+
156
+ This layer runs in both REST and GraphQL. If the API key lacks the required permission, the request is rejected with **403 Forbidden**.
157
+
158
+ ---
159
+
160
+ ## Layer 3: DataHooks (API Boundary)
161
+
162
+ DataHooks are the **primary mechanism for backend-level security** when you cannot or do not want to use database-level RLS. They intercept **all** collection operations at the REST API boundary — a single cross-cutting point for every collection.
163
+
164
+ ### Configuration
165
+
166
+ DataHooks are configured via the `hooks` property of `initializeRebaseBackend()`:
167
+
168
+ ```typescript
169
+ import { initializeRebaseBackend } from "@rebasepro/server-core";
170
+ import type { BackendHooks, BackendHookContext } from "@rebasepro/types";
171
+
172
+ const hooks: BackendHooks = {
173
+ data: {
174
+ // Intercept ALL reads across ALL collections
175
+ afterRead(slug, entity, ctx) {
176
+ // Return entity to allow, return null to filter out
177
+ },
178
+
179
+ // Intercept ALL writes across ALL collections
180
+ beforeSave(slug, values, entityId, ctx) {
181
+ // Return values to allow, throw to reject
182
+ },
183
+
184
+ // Intercept ALL deletes across ALL collections
185
+ beforeDelete(slug, entityId, ctx) {
186
+ // Return void to allow, throw to reject
187
+ },
188
+
189
+ // Post-write side effects
190
+ afterSave(slug, entity, ctx) { /* fire-and-forget */ },
191
+ afterDelete(slug, entityId, ctx) { /* fire-and-forget */ },
192
+ },
193
+ };
194
+
195
+ await initializeRebaseBackend({
196
+ server,
197
+ app,
198
+ database: createPostgresAdapter({ connection: db, schema }),
199
+ auth: { jwtSecret: "...", /* ... */ },
200
+ hooks, // ← Backend-level security hooks
201
+ });
202
+ ```
203
+
204
+ ### DataHooks Interface
205
+
206
+ ```typescript
207
+ interface DataHooks {
208
+ afterRead?(slug: string, entity: Record<string, unknown>, context: BackendHookContext):
209
+ Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
210
+
211
+ beforeSave?(slug: string, values: Record<string, unknown>, entityId: string | undefined, context: BackendHookContext):
212
+ Record<string, unknown> | Promise<Record<string, unknown>>;
213
+
214
+ afterSave?(slug: string, entity: Record<string, unknown>, context: BackendHookContext):
215
+ void | Promise<void>;
216
+
217
+ beforeDelete?(slug: string, entityId: string, context: BackendHookContext):
218
+ void | Promise<void>;
219
+
220
+ afterDelete?(slug: string, entityId: string, context: BackendHookContext):
221
+ void | Promise<void>;
222
+ }
223
+
224
+ interface BackendHookContext {
225
+ requestUser?: { userId: string; roles: string[] };
226
+ method: "GET" | "POST" | "PUT" | "DELETE";
227
+ }
228
+ ```
229
+
230
+ ### Blocking vs Fire-and-Forget
231
+
232
+ | Hook | Can Block? | How to Block |
233
+ |---|---|---|
234
+ | `afterRead` | Yes | Return `null` to filter out the entity |
235
+ | `beforeSave` | Yes | Throw an error to abort the save |
236
+ | `beforeDelete` | Yes | Throw an error to prevent deletion |
237
+ | `afterSave` | No | Fire-and-forget (errors are caught and logged) |
238
+ | `afterDelete` | No | Fire-and-forget |
239
+
240
+ ### Execution Order
241
+
242
+ DataHooks run **after** per-collection `EntityCallbacks` (which execute inside the DataDriver, closer to the database) and **before** the API response is sent to the client. This gives you two opportunities to enforce security:
243
+
244
+ 1. **EntityCallbacks** — per-collection, inside the driver
245
+ 2. **DataHooks** — cross-cutting, at the API boundary
246
+
247
+ ---
248
+
249
+ ## Layer 4: Scoped DataDriver
250
+
251
+ The scoped DataDriver is the layer where database-level RLS is enforced. For PostgreSQL, `withAuth()` wraps every operation in a transaction with session variables:
252
+
253
+ ```sql
254
+ SELECT
255
+ set_config('app.user_id', :userId, true),
256
+ set_config('app.user_roles', :rolesString, true),
257
+ set_config('app.jwt', :jwtClaims, true)
258
+ ```
259
+
260
+ PostgreSQL RLS policies use `auth.uid()`, `auth.roles()`, and `auth.jwt()` to read these session variables and enforce row-level access control.
261
+
262
+ > **IMPORTANT:** This layer is database-specific. If your project does not use PostgreSQL RLS, security is still enforced by Layers 1–3 and Layer 5. See [Securing Without Database RLS](#securing-without-database-rls).
263
+
264
+ ---
265
+
266
+ ## Layer 5: Entity Callbacks
267
+
268
+ Entity callbacks are per-collection lifecycle hooks that run **inside** the DataDriver, close to the database. They provide collection-specific security enforcement:
269
+
270
+ ```typescript
271
+ const ordersCollection: PostgresCollection = {
272
+ name: "Orders",
273
+ slug: "orders",
274
+ table: "orders",
275
+ callbacks: {
276
+ beforeSave: async ({ values, context }) => {
277
+ // Enforce business rule: only admins can set high-value orders
278
+ const user = context.user;
279
+ if (values.total > 10000 && !user?.roles?.includes("admin")) {
280
+ throw new Error("High-value orders require admin approval");
281
+ }
282
+ return values;
283
+ },
284
+ beforeDelete: async ({ entity, context }) => {
285
+ // Prevent deletion of fulfilled orders
286
+ if (entity.values.status === "fulfilled") {
287
+ throw new Error("Cannot delete fulfilled orders");
288
+ }
289
+ },
290
+ },
291
+ properties: { /* ... */ }
292
+ };
293
+ ```
294
+
295
+ For full documentation on entity callbacks, see the `rebase-collections` skill.
296
+
297
+ ---
298
+
299
+ ## Fail-Closed Design
300
+
301
+ Rebase follows a **fail-closed** security model throughout the stack:
302
+
303
+ 1. **Scoped driver or nothing** — The REST API's `getScopedDriver()` throws if no scoped driver is available. It **never** falls back to the unscoped driver:
304
+ ```typescript
305
+ private getScopedDriver(c): DataDriver {
306
+ const driver = c.get("driver") as DataDriver | undefined;
307
+ if (!driver) throw ApiError.internal("Scoped driver not available");
308
+ return driver;
309
+ }
310
+ ```
311
+
312
+ 2. **RLS scoping failures are fatal** — If `driver.withAuth()` throws, the error propagates and the request is rejected with 500. The system does not silently skip RLS.
313
+
314
+ 3. **Unauthenticated requests are rejected** — When `requireAuth: true` (the default), requests without a valid token receive 401. The unscoped driver never reaches the handler.
315
+
316
+ 4. **API keys with missing permissions are rejected** — If an API key lacks the required permission for a collection/operation, the request is rejected with 403.
317
+
318
+ ---
319
+
320
+ ## Securing Without Database RLS
321
+
322
+ If you cannot modify database-level RLS policies — or your database doesn't support them — use **DataHooks** and **Entity Callbacks** to enforce security entirely at the application level.
323
+
324
+ ### Strategy: DataHooks as Your Security Layer
325
+
326
+ ```typescript
327
+ import { ApiError } from "@rebasepro/server-core";
328
+ import type { BackendHooks, BackendHookContext } from "@rebasepro/types";
329
+
330
+ const hooks: BackendHooks = {
331
+ data: {
332
+ // ── READ SECURITY ──
333
+ // Filter entities based on user role and ownership
334
+ afterRead(slug, entity, ctx) {
335
+ const user = ctx.requestUser;
336
+
337
+ // Admins see everything
338
+ if (user?.roles.includes("admin")) return entity;
339
+
340
+ // For the "orders" collection, users only see their own
341
+ if (slug === "orders") {
342
+ if (entity.user_id !== user?.userId) return null;
343
+ }
344
+
345
+ // For "internal_notes", non-admins never see them
346
+ if (slug === "internal_notes") return null;
347
+
348
+ return entity;
349
+ },
350
+
351
+ // ── WRITE SECURITY ──
352
+ // Validate and enforce ownership on creates/updates
353
+ beforeSave(slug, values, entityId, ctx) {
354
+ const user = ctx.requestUser;
355
+ if (!user) throw ApiError.unauthorized("Authentication required");
356
+
357
+ // Enforce ownership: stamp the user_id on creation
358
+ if (!entityId) {
359
+ values.user_id = user.userId;
360
+ }
361
+
362
+ // Prevent role escalation: non-admins can't set role fields
363
+ if (!user.roles.includes("admin")) {
364
+ delete values.role;
365
+ delete values.is_admin;
366
+ }
367
+
368
+ return values;
369
+ },
370
+
371
+ // ── DELETE SECURITY ──
372
+ // Only admins can delete, or owners of their own records
373
+ beforeDelete(slug, entityId, ctx) {
374
+ const user = ctx.requestUser;
375
+ if (!user) throw ApiError.unauthorized("Authentication required");
376
+
377
+ if (!user.roles.includes("admin")) {
378
+ // For non-admins, you may need to fetch the entity first
379
+ // to verify ownership. Use Entity Callbacks for this pattern
380
+ // since they receive the full entity.
381
+ throw ApiError.forbidden("Only admins can delete records");
382
+ }
383
+ },
384
+ },
385
+ };
386
+ ```
387
+
388
+ ### Strategy: Entity Callbacks for Ownership Checks
389
+
390
+ Entity Callbacks receive the full entity data, making them ideal for ownership verification on deletes and updates:
391
+
392
+ ```typescript
393
+ const ordersCollection: PostgresCollection = {
394
+ name: "Orders",
395
+ slug: "orders",
396
+ table: "orders",
397
+ callbacks: {
398
+ beforeSave: async ({ values, entityId, context, previousValues }) => {
399
+ const user = context.user;
400
+ if (!user) throw new Error("Unauthorized");
401
+
402
+ // On update, verify the current user owns this order
403
+ if (entityId && previousValues) {
404
+ if (previousValues.user_id !== user.uid && !user.roles?.includes("admin")) {
405
+ throw new Error("You can only edit your own orders");
406
+ }
407
+ }
408
+
409
+ // On create, stamp ownership
410
+ if (!entityId) {
411
+ values.user_id = user.uid;
412
+ }
413
+
414
+ return values;
415
+ },
416
+
417
+ beforeDelete: async ({ entity, context }) => {
418
+ const user = context.user;
419
+ if (!user) throw new Error("Unauthorized");
420
+
421
+ if (entity.values.user_id !== user.uid && !user.roles?.includes("admin")) {
422
+ throw new Error("You can only delete your own orders");
423
+ }
424
+ },
425
+ },
426
+ properties: { /* ... */ }
427
+ };
428
+ ```
429
+
430
+ ### Combining Both Layers
431
+
432
+ For maximum security, use both DataHooks and Entity Callbacks together:
433
+
434
+ | Concern | Best Layer | Why |
435
+ |---|---|---|
436
+ | Cross-cutting read filtering | DataHooks `afterRead` | Applies to ALL collections in one place |
437
+ | Cross-cutting write validation | DataHooks `beforeSave` | Single enforcement point for all writes |
438
+ | PII masking / field redaction | DataHooks `afterRead` | Cross-cutting, role-based |
439
+ | Ownership checks on writes/deletes | Entity Callbacks | Has access to the full entity for comparison |
440
+ | Business rule validation | Entity Callbacks | Collection-specific, typed values |
441
+ | Audit logging | DataHooks `afterSave` / `afterDelete` | Cross-cutting, fire-and-forget |
442
+
443
+ ---
444
+
445
+ ## Common Security Patterns
446
+
447
+ ### PII Masking
448
+
449
+ ```typescript
450
+ const hooks: BackendHooks = {
451
+ data: {
452
+ afterRead(slug, entity, ctx) {
453
+ if (ctx.requestUser?.roles.includes("admin")) return entity;
454
+
455
+ // Mask sensitive fields across all collections
456
+ if (entity.email) entity.email = "***@***.***";
457
+ if (entity.phone) entity.phone = "***-***-****";
458
+ if (entity.ssn) entity.ssn = "***-**-****";
459
+
460
+ return entity;
461
+ },
462
+ },
463
+ };
464
+ ```
465
+
466
+ ### Tenant Isolation (Multi-Tenancy)
467
+
468
+ ```typescript
469
+ const hooks: BackendHooks = {
470
+ data: {
471
+ afterRead(slug, entity, ctx) {
472
+ const userTenantId = ctx.requestUser?.roles
473
+ .find(r => r.startsWith("tenant:"))
474
+ ?.replace("tenant:", "");
475
+
476
+ if (!userTenantId) return null;
477
+ if (entity.tenant_id !== userTenantId) return null;
478
+
479
+ return entity;
480
+ },
481
+
482
+ beforeSave(slug, values, entityId, ctx) {
483
+ const userTenantId = ctx.requestUser?.roles
484
+ .find(r => r.startsWith("tenant:"))
485
+ ?.replace("tenant:", "");
486
+
487
+ if (!userTenantId) throw ApiError.forbidden("No tenant assigned");
488
+
489
+ // Stamp tenant on creation, prevent cross-tenant writes
490
+ if (!entityId) {
491
+ values.tenant_id = userTenantId;
492
+ }
493
+
494
+ return values;
495
+ },
496
+
497
+ beforeDelete(slug, entityId, ctx) {
498
+ // Tenant isolation on deletes is best handled in Entity Callbacks
499
+ // where you have access to the entity's tenant_id
500
+ },
501
+ },
502
+ };
503
+ ```
504
+
505
+ ### Role-Based Collection Access
506
+
507
+ ```typescript
508
+ const hooks: BackendHooks = {
509
+ data: {
510
+ // Define which roles can access which collections
511
+ beforeSave(slug, values, entityId, ctx) {
512
+ const roleAccess: Record<string, string[]> = {
513
+ "financial_reports": ["admin", "finance"],
514
+ "hr_records": ["admin", "hr"],
515
+ "system_config": ["admin"],
516
+ };
517
+
518
+ const allowedRoles = roleAccess[slug];
519
+ if (allowedRoles) {
520
+ const userRoles = ctx.requestUser?.roles || [];
521
+ const hasAccess = allowedRoles.some(r => userRoles.includes(r));
522
+ if (!hasAccess) {
523
+ throw ApiError.forbidden(`Insufficient permissions for ${slug}`);
524
+ }
525
+ }
526
+
527
+ return values;
528
+ },
529
+
530
+ afterRead(slug, entity, ctx) {
531
+ const roleAccess: Record<string, string[]> = {
532
+ "financial_reports": ["admin", "finance"],
533
+ "hr_records": ["admin", "hr"],
534
+ "system_config": ["admin"],
535
+ };
536
+
537
+ const allowedRoles = roleAccess[slug];
538
+ if (allowedRoles) {
539
+ const userRoles = ctx.requestUser?.roles || [];
540
+ if (!allowedRoles.some(r => userRoles.includes(r))) return null;
541
+ }
542
+
543
+ return entity;
544
+ },
545
+ },
546
+ };
547
+ ```
548
+
549
+ ### Immutable Records (Soft Delete Only)
550
+
551
+ ```typescript
552
+ const hooks: BackendHooks = {
553
+ data: {
554
+ beforeDelete(slug, entityId, ctx) {
555
+ const immutableCollections = ["audit_logs", "transactions", "invoices"];
556
+ if (immutableCollections.includes(slug)) {
557
+ throw ApiError.forbidden(
558
+ `Records in "${slug}" cannot be deleted. Use soft-delete instead.`
559
+ );
560
+ }
561
+ },
562
+
563
+ beforeSave(slug, values, entityId, ctx) {
564
+ const appendOnlyCollections = ["audit_logs"];
565
+ if (appendOnlyCollections.includes(slug) && entityId) {
566
+ throw ApiError.forbidden(
567
+ `Records in "${slug}" are append-only and cannot be updated.`
568
+ );
569
+ }
570
+ return values;
571
+ },
572
+ },
573
+ };
574
+ ```
575
+
576
+ ---
577
+
578
+ ## Security Checklist
579
+
580
+ Use this checklist when setting up security for a Rebase project:
581
+
582
+ - [ ] **Auth is configured** — `auth.jwtSecret` is set with a strong secret (≥ 32 chars)
583
+ - [ ] **`requireAuth` is `true`** — The default. Only set to `false` if you explicitly need unauthenticated access
584
+ - [ ] **Service key is set** — `auth.serviceKey` with ≥ 32 chars for server-to-server auth
585
+ - [ ] **Default role is NOT admin** — `auth.defaultRole` must never be `"admin"` (startup error)
586
+ - [ ] **DataHooks enforce access control** — If not using database RLS, `hooks.data` enforces read/write/delete permissions
587
+ - [ ] **Sensitive fields are masked** — `afterRead` masks PII for non-admin users
588
+ - [ ] **Ownership is enforced** — `beforeSave` stamps `user_id` on creation; Entity Callbacks verify ownership on update/delete
589
+ - [ ] **API keys are scoped** — API keys have minimal permissions (specific collections + operations)
590
+ - [ ] **API keys are never client-side** — API keys bypass RLS; only use server-side
591
+ - [ ] **CORS is configured** — Restrict origins in production
592
+ - [ ] **Rate limiting is in place** — Default limiters apply to auth endpoints; add custom limiters for sensitive operations
593
+
594
+ ---
595
+
596
+ ## References
597
+
598
+ - **RLS Scope**: `packages/server-core/src/auth/rls-scope.ts` — `scopeDataDriver()` implementation
599
+ - **Auth Middleware**: `packages/server-core/src/auth/middleware.ts` — JWT/service key/API key middleware
600
+ - **Adapter Middleware**: `packages/server-core/src/auth/adapter-middleware.ts` — Custom auth adapter middleware
601
+ - **API Key Guard**: `packages/server-core/src/auth/api-keys/api-key-permission-guard.ts`
602
+ - **REST API Generator**: `packages/server-core/src/api/rest/api-generator.ts` — DataHooks integration
603
+ - **Backend Hooks Types**: `packages/types/src/types/backend_hooks.ts` — `DataHooks`, `BackendHooks` interfaces
604
+ - **Backend Init**: `packages/server-core/src/init.ts` — `hooks` config property
605
+ - **Entity Callbacks**: See `rebase-collections` skill → Entity Callbacks section
606
+ - **Auth Configuration**: See `rebase-auth` skill → Server-Side Configuration section