@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,1283 @@
1
+ ---
2
+ name: rebase-auth
3
+ description: Guide for setting up and using Rebase Authentication, roles, Row-Level Security (RLS) policies, MFA, API keys, OAuth providers, custom auth adapters, and lifecycle hooks. Use this skill when the user needs to add authentication, manage users and roles, secure data access, configure OAuth, set up MFA, create API keys, or customize the auth pipeline.
4
+ ---
5
+
6
+ # Rebase Authentication
7
+
8
+ Rebase ships a complete, built-in authentication system with JWT sessions, OAuth, MFA/TOTP, API keys, Row-Level Security, and lifecycle hooks — or you can plug in an external provider (Clerk, Auth0, Firebase Auth) via the `AuthAdapter` interface.
9
+
10
+ > **IMPORTANT FOR AGENTS:** Always read the `rebase-basics` skill first. The auth system is configured inside `initializeRebaseBackend()` which is covered there.
11
+
12
+ ## Table of Contents
13
+
14
+ - [Server-Side Configuration (RebaseAuthConfig)](#server-side-configuration)
15
+ - [OAuth Providers](#oauth-providers)
16
+ - [Auth Lifecycle Hooks](#auth-lifecycle-hooks)
17
+ - [MFA / TOTP](#mfa--totp)
18
+ - [API Keys](#api-keys)
19
+ - [REST Endpoints](#rest-endpoints)
20
+ - [Client SDK (auth module)](#client-sdk)
21
+ - [Row-Level Security (RLS)](#row-level-security)
22
+ - [Rate Limiting](#rate-limiting)
23
+ - [Custom Auth Adapters](#custom-auth-adapters)
24
+ - [Roles & Permissions](#roles--permissions)
25
+ - [Backend Hooks](#backend-hooks)
26
+ - [Email Configuration](#email-configuration)
27
+ - [Security Concepts](#security-concepts)
28
+ - [References](#references)
29
+
30
+ ---
31
+
32
+ ## Server-Side Configuration
33
+
34
+ Authentication is configured via the `auth` property of `initializeRebaseBackend()`. It accepts **either** a `RebaseAuthConfig` object (built-in auth) or an `AuthAdapter` (external auth).
35
+
36
+ > **Auth & multiple data sources.** The built-in auth system (users, sessions, API keys) is bootstrapped on the **default** data source — the auth collection must live there (the backend warns at boot otherwise). **RLS only protects Postgres**: server collections on engines without row-level security (e.g. MongoDB) still require authentication but enforce authorization at the app layer (the backend warns for these). **Direct data sources (e.g. Firestore) bypass Rebase auth entirely** — they're governed by the external backend's own rules/token; use an `AuthAdapter` (e.g. Firebase Auth) to unify identity. See the **rebase-collections** skill for the data-source model.
37
+
38
+ ### RebaseAuthConfig
39
+
40
+ | Property | Type | Default | Description |
41
+ |---|---|---|---|
42
+ | `collection` | `EntityCollection` | Built-in users collection | The collection used for auth user storage. Import `defaultUsersCollection` from `@rebasepro/common` or pass a custom collection with required auth fields. |
43
+ | `jwtSecret` | `string` | — | **Required.** Secret for signing JWT access tokens. |
44
+ | `accessExpiresIn` | `string` | `"1h"` | Access token lifetime (e.g. `"15m"`, `"2h"`). |
45
+ | `refreshExpiresIn` | `string` | `"30d"` | Refresh token lifetime. |
46
+ | `requireAuth` | `boolean` | `true` | When `true`, data routes return 401 for unauthenticated requests. Set to `false` to rely entirely on Postgres RLS. |
47
+ | `allowRegistration` | `boolean` | `false` | Enable self-service registration via `POST /auth/register`. |
48
+ | `serviceKey` | `string` | — | Static secret for server-to-server auth. Must be ≥ 32 characters. Requests with `Authorization: Bearer <serviceKey>` get admin access. |
49
+ | `defaultRole` | `string` | — | Role ID assigned to new users (except the first user, who always gets `"admin"`). **Must NOT be `"admin"`** — throws a security error at startup. |
50
+ | `providers` | `OAuthProvider<any>[]` | `[]` | Custom OAuth providers (use alongside the shorthand configs below). |
51
+ | `hooks` | `AuthHooks` | — | [Lifecycle hooks](#auth-lifecycle-hooks) to customize passwords, credentials, and auth events. |
52
+ | `email` | `EmailConfig` | — | [Email configuration](#email-configuration) for password resets, verification, and welcome emails. |
53
+ | `google` | `{ clientId, clientSecret? }` | — | Google OAuth shorthand. |
54
+ | `github` | `{ clientId, clientSecret }` | — | GitHub OAuth shorthand. |
55
+ | `microsoft` | `{ clientId, clientSecret, tenantId? }` | — | Microsoft/Entra ID shorthand. `tenantId` defaults to `"common"`. |
56
+ | `apple` | `{ clientId, teamId, keyId, privateKey }` | — | Apple Sign In shorthand. `privateKey` is the raw PEM (.p8) contents. |
57
+ | `facebook` | `{ clientId, clientSecret }` | — | Facebook/Meta OAuth. |
58
+ | `twitter` | `{ clientId, clientSecret }` | — | Twitter/X OAuth 2.0 with PKCE. |
59
+ | `discord` | `{ clientId, clientSecret }` | — | Discord OAuth. |
60
+ | `gitlab` | `{ clientId, clientSecret, baseUrl? }` | — | GitLab OAuth. `baseUrl` defaults to `"https://gitlab.com"` (supports self-hosted). |
61
+ | `linkedin` | `{ clientId, clientSecret }` | — | LinkedIn OAuth (OIDC). |
62
+ | `bitbucket` | `{ clientId, clientSecret }` | — | Bitbucket OAuth. |
63
+ | `slack` | `{ clientId, clientSecret }` | — | Slack OAuth (OIDC). |
64
+ | `spotify` | `{ clientId, clientSecret }` | — | Spotify OAuth. |
65
+
66
+ ### Minimal Example
67
+
68
+ ```typescript
69
+ import { initializeRebaseBackend } from "@rebasepro/server-core";
70
+ import { createPostgresAdapter } from "@rebasepro/server-postgresql";
71
+
72
+ await initializeRebaseBackend({
73
+ server,
74
+ app,
75
+ database: createPostgresAdapter({ connection: db, schema }),
76
+ auth: {
77
+ jwtSecret: process.env.JWT_SECRET!,
78
+ allowRegistration: true,
79
+ serviceKey: process.env.REBASE_SERVICE_KEY,
80
+ defaultRole: "member",
81
+ google: {
82
+ clientId: process.env.GOOGLE_CLIENT_ID!,
83
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
84
+ },
85
+ github: {
86
+ clientId: process.env.GITHUB_CLIENT_ID!,
87
+ clientSecret: process.env.GITHUB_CLIENT_SECRET!,
88
+ },
89
+ email: {
90
+ from: "noreply@myapp.com",
91
+ smtp: {
92
+ host: "smtp.resend.com",
93
+ port: 465,
94
+ secure: true,
95
+ auth: { user: "resend", pass: process.env.RESEND_API_KEY! },
96
+ },
97
+ appName: "MyApp",
98
+ resetPasswordUrl: "https://myapp.com",
99
+ verifyEmailUrl: "https://myapp.com",
100
+ },
101
+ },
102
+ });
103
+ ```
104
+
105
+ ### Collection-Level Auth Configuration
106
+
107
+ Instead of relying solely on the default database auth rules, you can mark any Postgres collection (such as `users.ts` or a custom `members.ts` collection) as the authentication collection. This is configured via the `auth` property on the collection itself:
108
+
109
+ ```typescript
110
+ import { PostgresCollection } from "@rebasepro/types";
111
+
112
+ const membersCollection: PostgresCollection = {
113
+ name: "Members",
114
+ slug: "members",
115
+ table: "members",
116
+ auth: {
117
+ enabled: true,
118
+
119
+ // Customize what happens when an admin creates a user via the REST API
120
+ onCreateUser: async (values, ctx) => {
121
+ const hash = await ctx.hashPassword("welcome123");
122
+ return {
123
+ values: { ...values, passwordHash: hash, emailVerified: true },
124
+ temporaryPassword: "welcome123"
125
+ };
126
+ },
127
+
128
+ // Customize what happens when an admin resets a user's password in the admin panel
129
+ onResetPassword: async (userId, ctx) => {
130
+ const tempPassword = "reset_" + Math.random().toString(36).substring(2, 8);
131
+ return {
132
+ temporaryPassword: tempPassword,
133
+ invitationSent: false
134
+ };
135
+ },
136
+
137
+ // Inject/override auth-specific actions (e.g. show/hide the reset password button)
138
+ actions: {
139
+ resetPassword: true // Or false to disable, or a custom EntityAction
140
+ }
141
+ },
142
+ properties: { ... }
143
+ };
144
+ ```
145
+
146
+ When custom hooks (`onCreateUser`, `onResetPassword`) are called, they receive an `AuthCollectionContext` facade containing:
147
+ - `hashPassword(password: string): Promise<string>` — Hash password using the configured hashing algorithm (e.g. scrypt).
148
+ - `sendEmail?: (options) => Promise<void>` — Send an email (only available when email service is configured).
149
+ - `emailConfigured: boolean` — Whether email service is configured.
150
+ - `appName: string` — The app name from email config.
151
+ - `resetPasswordUrl: string` — The password reset link base URL.
152
+
153
+ ### First-User Bootstrap
154
+
155
+ > **IMPORTANT FOR AGENTS:** The very first user registered (via `POST /auth/register` or OAuth) is automatically promoted to `"admin"`. This prevents the chicken-and-egg problem. All subsequent users receive the `defaultRole`.
156
+
157
+ ---
158
+
159
+ ## OAuth Providers
160
+
161
+ Rebase supports 12 built-in OAuth providers. Each provider is configured via a shorthand property on `RebaseAuthConfig` and automatically mounts a `POST /api/auth/{providerId}` endpoint.
162
+
163
+ ### Provider Reference
164
+
165
+ | Provider | ID | Config Properties | Client Payload |
166
+ |---|---|---|---|
167
+ | Google | `google` | `clientId`, `clientSecret?` | `{ idToken }` OR `{ accessToken }` OR `{ code, redirectUri }` |
168
+ | GitHub | `github` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
169
+ | Microsoft | `microsoft` | `clientId`, `clientSecret`, `tenantId?` | `{ code, redirectUri }` |
170
+ | Apple | `apple` | `clientId`, `teamId`, `keyId`, `privateKey` | `{ code, redirectUri, user? }` |
171
+ | Facebook | `facebook` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
172
+ | Twitter/X | `twitter` | `clientId`, `clientSecret` | `{ code, redirectUri, codeVerifier }` |
173
+ | Discord | `discord` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
174
+ | GitLab | `gitlab` | `clientId`, `clientSecret`, `baseUrl?` | `{ code, redirectUri }` |
175
+ | LinkedIn | `linkedin` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
176
+ | Bitbucket | `bitbucket` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
177
+ | Slack | `slack` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
178
+ | Spotify | `spotify` | `clientId`, `clientSecret` | `{ code, redirectUri }` |
179
+
180
+ ### Google Three-Path Support
181
+
182
+ Google is unique — it supports three verification paths:
183
+
184
+ 1. **ID Token** (One Tap / Sign In button) — `{ idToken }`. Cryptographic verification via Google's public keys. No `clientSecret` needed.
185
+ 2. **Access Token** (popup via `initTokenClient`) — `{ accessToken }`. Validated via Google's userinfo endpoint. No `clientSecret` needed.
186
+ 3. **Authorization Code** (most secure) — `{ code, redirectUri }`. Requires `clientSecret`. Tokens never touch the browser.
187
+
188
+ ### Apple Special Behavior
189
+
190
+ - Apple only sends the user's name on the **first** authorization. The frontend must capture and forward it: `{ code, redirectUri, user: { name: { firstName, lastName }, email } }`.
191
+ - Apple does not provide a profile photo (`photoUrl` is always `null`).
192
+ - The `privateKey` is the raw PEM contents of the `.p8` file downloaded from Apple Developer.
193
+
194
+ ### Twitter PKCE
195
+
196
+ Twitter uses OAuth 2.0 with PKCE. The client must send `codeVerifier` alongside `code` and `redirectUri`.
197
+
198
+ ### OAuth Account Linking
199
+
200
+ When an OAuth user signs in:
201
+ 1. If an identity record exists for `(providerId, provider)` → log in that user.
202
+ 2. If no identity exists but a user with the same email exists → **link** the provider to the existing account.
203
+ 3. If neither exists → create a new user, link the identity, assign `defaultRole`.
204
+
205
+ ### Custom OAuth Provider
206
+
207
+ You can register any OAuth provider by implementing the `OAuthProvider<T>` interface:
208
+
209
+ ```typescript
210
+ import { z } from "zod";
211
+ import type { OAuthProvider, OAuthProviderProfile } from "@rebasepro/server-core";
212
+
213
+ const myProvider: OAuthProvider<{ token: string }> = {
214
+ id: "my-provider",
215
+ schema: z.object({ token: z.string().min(1) }),
216
+ verify: async (payload): Promise<OAuthProviderProfile | null> => {
217
+ const userInfo = await verifyExternalToken(payload.token);
218
+ if (!userInfo) return null;
219
+ return {
220
+ providerId: userInfo.id,
221
+ email: userInfo.email,
222
+ displayName: userInfo.name || null,
223
+ photoUrl: userInfo.avatar || null,
224
+ };
225
+ },
226
+ };
227
+
228
+ // Use in config:
229
+ auth: {
230
+ jwtSecret: "...",
231
+ providers: [myProvider],
232
+ }
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Auth Lifecycle Hooks
238
+
239
+ The `AuthHooks` interface lets you customize specific behaviors of the built-in auth system. Every hook is optional — unset hooks fall through to built-in defaults.
240
+
241
+ ### Hook Reference
242
+
243
+ | Hook | Signature | Default | Behavior |
244
+ |---|---|---|---|
245
+ | `hashPassword` | `(password: string) => Promise<string>` | scrypt (Node crypto, 64-byte key, 32-byte salt) | Hash a cleartext password for storage. |
246
+ | `verifyPassword` | `(password: string, storedHash: string) => Promise<boolean>` | scrypt with timing-safe comparison | Verify cleartext password against stored hash. |
247
+ | `validatePasswordStrength` | `(password: string) => PasswordValidationResult` | Min 8 chars, 1 uppercase, 1 lowercase, 1 digit | Return `{ valid: boolean, errors: string[] }`. |
248
+ | `verifyCredentials` | `(email, password, repo: AuthRepository) => Promise<UserData \| null>` | `getUserByEmail` + `verifyPassword` | Override the entire login credential check. Return user or `null`. |
249
+ | `onAuthenticated` | `(user: UserData, method: AuthMethod) => Promise<void>` | — | Called after **any** successful auth event. Fire-and-forget. |
250
+ | `beforeUserCreate` | `(data: CreateUserData) => Promise<CreateUserData>` | Passthrough | Modify or reject user creation. Throw to abort. |
251
+ | `afterUserCreate` | `(user: UserData) => Promise<void>` | — | Called after user creation. Fire-and-forget. |
252
+ | `beforeLogin` | `(email: string, method: AuthMethod) => Promise<void>` | — | Pre-login validation. Throw to reject (e.g. account lockout). |
253
+ | `afterLogout` | `(userId: string) => Promise<void>` | — | Post-logout cleanup. Fire-and-forget. |
254
+ | `onMfaVerified` | `(userId: string, factorId: string) => Promise<void>` | — | Called after successful MFA verification. Fire-and-forget. |
255
+ | `customizeAccessToken` | `(claims: Record<string, unknown>, user: UserData) => Promise<Record<string, unknown>>` | — | Modify JWT access token claims before signing. |
256
+ | `transformAuthResponse` | `(response: AuthResponsePayload, context: TransformAuthResponseContext) => Promise<AuthResponsePayload>` | — | Transform the auth response before sending to client. Runs in-request (not fire-and-forget). Errors are caught and logged; untransformed response returned as fallback. |
257
+ | `onPasswordReset` | `(userId: string) => Promise<void>` | — | Called after successful password reset. Fire-and-forget. |
258
+ | `beforeUserDelete` | `(userId: string) => Promise<void>` | — | Throw to prevent deletion. |
259
+ | `afterUserDelete` | `(userId: string) => Promise<void>` | — | Post-deletion cleanup. Fire-and-forget. |
260
+
261
+ ### AuthMethod Values
262
+
263
+ `"login"` | `"register"` | `"oauth"` | `"refresh"` | `"password-reset"` | `"anonymous"` | `"magic-link"` | `"mfa"`
264
+
265
+ ### AuthResponsePayload
266
+
267
+ ```typescript
268
+ interface AuthResponsePayload {
269
+ user?: {
270
+ uid: string;
271
+ email: string;
272
+ displayName: string | null;
273
+ photoURL: string | null;
274
+ roles: string[];
275
+ metadata: Record<string, unknown>;
276
+ };
277
+ tokens: {
278
+ accessToken: string;
279
+ refreshToken: string;
280
+ accessTokenExpiresAt: number;
281
+ [key: string]: unknown;
282
+ };
283
+ }
284
+ ```
285
+
286
+ ### TransformAuthResponseContext
287
+
288
+ ```typescript
289
+ interface TransformAuthResponseContext {
290
+ userId: string;
291
+ method: "login" | "register" | "oauth" | "refresh" | "anonymous" | "magic-link" | "mfa";
292
+ request: Request;
293
+ }
294
+ ```
295
+
296
+ ### PasswordValidationResult
297
+
298
+ ```typescript
299
+ interface PasswordValidationResult {
300
+ valid: boolean;
301
+ errors: string[];
302
+ }
303
+ ```
304
+
305
+ ### Example: bcrypt Passwords
306
+
307
+ ```typescript
308
+ import bcrypt from "bcrypt";
309
+
310
+ auth: {
311
+ jwtSecret: "...",
312
+ hooks: {
313
+ hashPassword: (pw) => bcrypt.hash(pw, 12),
314
+ verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
315
+ validatePasswordStrength: (pw) => ({
316
+ valid: pw.length >= 6,
317
+ errors: pw.length < 6 ? ["Password must be at least 6 characters"] : [],
318
+ }),
319
+ },
320
+ }
321
+ ```
322
+
323
+ ### Example: Custom JWT Claims
324
+
325
+ ```typescript
326
+ hooks: {
327
+ customizeAccessToken: async (claims, user) => ({
328
+ ...claims,
329
+ org_id: user.metadata?.organizationId,
330
+ plan: user.metadata?.plan || "free",
331
+ }),
332
+ }
333
+ ```
334
+
335
+ ### Example: Audit Logging
336
+
337
+ ```typescript
338
+ hooks: {
339
+ onAuthenticated: async (user, method) => {
340
+ await auditLog.write({
341
+ event: "auth.success",
342
+ userId: user.id,
343
+ method,
344
+ timestamp: new Date(),
345
+ });
346
+ },
347
+ beforeLogin: async (email, method) => {
348
+ const isBlocked = await checkAccountLockout(email);
349
+ if (isBlocked) throw new Error("Account is locked");
350
+ },
351
+ }
352
+ ```
353
+
354
+ ### Example: Firebase Custom Token Bridge
355
+
356
+ ```typescript
357
+ import admin from "firebase-admin";
358
+
359
+ hooks: {
360
+ transformAuthResponse: async (response, context) => {
361
+ // Generate a Firebase Custom Token for the authenticated user
362
+ const firebaseToken = await admin.auth().createCustomToken(context.userId);
363
+ return {
364
+ ...response,
365
+ tokens: {
366
+ ...response.tokens,
367
+ firebaseToken,
368
+ },
369
+ };
370
+ },
371
+ }
372
+ ```
373
+
374
+ The frontend can then call `signInWithCustomToken(firebaseToken)` immediately after login.
375
+
376
+ ---
377
+
378
+ ## MFA / TOTP
379
+
380
+ Rebase supports Multi-Factor Authentication via TOTP (Time-based One-Time Password). The flow uses an enrollment → verify → challenge pattern with recovery codes.
381
+
382
+ ### MFA Flow
383
+
384
+ 1. **Enroll** — `POST /api/auth/mfa/enroll` returns a TOTP secret, URI (for QR), and 10 recovery codes.
385
+ 2. **Verify enrollment** — `POST /api/auth/mfa/verify` with a 6-digit TOTP code to confirm the factor.
386
+ 3. **Challenge on login** — After normal login (aal1), call `POST /api/auth/mfa/challenge` to create a challenge.
387
+ 4. **Complete challenge** — `POST /api/auth/mfa/challenge/verify` with TOTP or recovery code. Upgrades token from `aal1` → `aal2`.
388
+
389
+ ### MFA Endpoints
390
+
391
+ | Method | Endpoint | Auth | Description |
392
+ |---|---|---|---|
393
+ | `POST` | `/api/auth/mfa/enroll` | Required | Start enrollment. Returns `{ factor, totp: { secret, uri, qrUri }, recoveryCodes }`. |
394
+ | `POST` | `/api/auth/mfa/verify` | Required | Verify enrollment with `{ factorId, code }` (6-digit TOTP). |
395
+ | `POST` | `/api/auth/mfa/challenge` | Required | Create challenge with `{ factorId }`. Returns `{ challengeId, factorId, expiresAt }`. Challenge expires in 5 minutes. |
396
+ | `POST` | `/api/auth/mfa/challenge/verify` | Required | Complete challenge with `{ challengeId, code }`. Returns new tokens with `aal2`. Accepts TOTP (6 digits) or recovery code (>6 chars). |
397
+ | `GET` | `/api/auth/mfa/factors` | Required | List enrolled factors: `{ factors: [{ id, factorType, friendlyName, verified, createdAt }] }`. |
398
+ | `DELETE` | `/api/auth/mfa/unenroll` | Required | Remove factor with `{ factorId }` in body. Auto-cleans recovery codes when no verified factors remain. |
399
+
400
+ ### MFA Types
401
+
402
+ ```typescript
403
+ interface MfaFactor {
404
+ id: string;
405
+ userId: string;
406
+ factorType: "totp"; // Only TOTP is supported
407
+ friendlyName?: string;
408
+ verified: boolean;
409
+ createdAt: Date;
410
+ updatedAt: Date;
411
+ }
412
+
413
+ interface MfaChallengeInfo {
414
+ id: string;
415
+ factorId: string;
416
+ createdAt: Date;
417
+ verifiedAt?: Date;
418
+ ipAddress?: string;
419
+ }
420
+ ```
421
+
422
+ ### AAL (Authentication Assurance Levels)
423
+
424
+ | Level | Meaning |
425
+ |---|---|
426
+ | `aal1` | Standard authentication (email/password, OAuth). |
427
+ | `aal2` | Elevated after MFA challenge verification. |
428
+
429
+ ---
430
+
431
+ ## API Keys
432
+
433
+ API keys provide machine-to-machine authentication for agents, MCP servers, CI pipelines, cron jobs, and third-party integrations. They are scoped to specific collections and operations, and can optionally be granted full admin access.
434
+
435
+ ### Key Format
436
+
437
+ - Prefix: `rk_` (e.g. `rk_live_abc123...`)
438
+ - Storage: SHA-256 hash of the full key. The plaintext key is returned **exactly once** at creation.
439
+ - Display: Only the first 12 characters (`key_prefix`) are shown in subsequent API responses.
440
+
441
+ ### Admin Access for Agents / MCP
442
+
443
+ By default API keys get the `service` role (data access only). Set `"admin": true` to grant the key the `admin` role, which allows it to call **all admin routes** (`/api/admin/*`) — including schema management, user management, and API key management itself.
444
+
445
+ > **Use `admin: true` for agents, MCP servers, and CI pipelines that need full control over the Rebase instance.**
446
+
447
+ ```bash
448
+ # CLI — create an admin API key
449
+ rebase api-keys create --name "My Agent" --admin
450
+
451
+ # REST
452
+ curl -X POST http://localhost:3000/api/admin/api-keys \
453
+ -H "Authorization: Bearer <service-key>" \
454
+ -H "Content-Type: application/json" \
455
+ -d '{
456
+ "name": "My Agent",
457
+ "admin": true,
458
+ "permissions": [{ "collection": "*", "operations": ["read", "write", "delete"] }]
459
+ }'
460
+ ```
461
+
462
+ ### API Key Admin Endpoints
463
+
464
+ All endpoints are mounted under `/api/admin/api-keys` and require **admin** authentication (JWT with admin role or service key).
465
+
466
+ | Method | Endpoint | Description |
467
+ |---|---|---|
468
+ | `GET` | `/api/admin/api-keys` | List all API keys (masked — no hashes). |
469
+ | `POST` | `/api/admin/api-keys` | Create a new API key. Returns the full plaintext key once. |
470
+ | `GET` | `/api/admin/api-keys/:id` | Get single API key details (masked). |
471
+ | `PUT` | `/api/admin/api-keys/:id` | Update name, permissions, admin, rate_limit, or expires_at. |
472
+ | `DELETE` | `/api/admin/api-keys/:id` | Revoke (soft-delete) an API key. |
473
+
474
+ ### Create API Key Request
475
+
476
+ ```typescript
477
+ interface CreateApiKeyRequest {
478
+ name: string;
479
+ permissions: ApiKeyPermission[];
480
+ admin?: boolean; // true = grant admin role (access to all admin routes)
481
+ rate_limit?: number | null; // Requests per 15-min window. null = unlimited
482
+ expires_at?: string | null; // ISO-8601 timestamp. null = no expiration
483
+ }
484
+
485
+ interface ApiKeyPermission {
486
+ collection: string; // Collection slug, or "*" for all collections
487
+ operations: ("read" | "write" | "delete")[];
488
+ }
489
+ ```
490
+
491
+ ### Examples
492
+
493
+ **Scoped key (read-only on one collection):**
494
+
495
+ ```bash
496
+ curl -X POST http://localhost:3000/api/admin/api-keys \
497
+ -H "Authorization: Bearer <admin-token-or-service-key>" \
498
+ -H "Content-Type: application/json" \
499
+ -d '{
500
+ "name": "Analytics Pipeline",
501
+ "permissions": [
502
+ { "collection": "events", "operations": ["read", "write"] },
503
+ { "collection": "users", "operations": ["read"] }
504
+ ],
505
+ "rate_limit": 500,
506
+ "expires_at": "2025-12-31T23:59:59Z"
507
+ }'
508
+ ```
509
+
510
+ **Admin key (for agents / MCP / CI):**
511
+
512
+ ```bash
513
+ curl -X POST http://localhost:3000/api/admin/api-keys \
514
+ -H "Authorization: Bearer <admin-token-or-service-key>" \
515
+ -H "Content-Type: application/json" \
516
+ -d '{
517
+ "name": "CI Agent",
518
+ "admin": true,
519
+ "permissions": [{ "collection": "*", "operations": ["read", "write", "delete"] }]
520
+ }'
521
+ ```
522
+
523
+ ### Using an API Key
524
+
525
+ ```bash
526
+ curl http://localhost:3000/api/data/events \
527
+ -H "Authorization: Bearer rk_live_abc123..."
528
+ ```
529
+
530
+ ### API Key Middleware Behavior
531
+
532
+ When a request arrives with a `rk_` prefixed bearer token:
533
+ 1. The token is SHA-256 hashed and looked up in the `rebase.api_keys` table.
534
+ 2. Expiry and revocation status are checked.
535
+ 3. If `admin: true`, the key is assigned `roles: ["admin", "service"]` — granting access to admin routes. Otherwise `roles: ["service"]`.
536
+ 4. Permissions are validated against the requested collection and HTTP method (`GET` → `read`, `POST`/`PUT`/`PATCH` → `write`, `DELETE` → `delete`).
537
+ 5. The DataDriver is scoped with `withAuth()` using the key's service identity (bypasses RLS).
538
+ 6. Per-key rate limiting is enforced if `rate_limit` is set.
539
+
540
+ > **WARNING FOR AGENTS:** API keys bypass RLS. They are designed for trusted server-side use only. Never expose API keys to client-side code.
541
+
542
+ ### Role Summary
543
+
544
+ | Key type | `roles` assigned | Admin routes | Data routes |
545
+ |---|---|---|---|
546
+ | Default (no `admin`) | `["service"]` | ✗ | ✓ (scoped by `permissions`) |
547
+ | `admin: true` | `["admin", "service"]` | ✓ | ✓ |
548
+
549
+ ### API Key Response Types
550
+
551
+ ```typescript
552
+ // Returned once at creation (includes the full plaintext key)
553
+ interface ApiKeyWithSecret {
554
+ id: string;
555
+ name: string;
556
+ key_prefix: string; // First 12 chars, for display
557
+ key: string; // FULL plaintext key — save this immediately
558
+ permissions: ApiKeyPermission[];
559
+ admin: boolean;
560
+ rate_limit: number | null;
561
+ created_by: string;
562
+ created_at: string;
563
+ updated_at: string;
564
+ last_used_at: string | null;
565
+ expires_at: string | null;
566
+ revoked_at: string | null;
567
+ }
568
+
569
+ // All subsequent reads (no hash, no full key)
570
+ interface ApiKeyMasked {
571
+ id: string;
572
+ name: string;
573
+ key_prefix: string;
574
+ permissions: ApiKeyPermission[];
575
+ admin: boolean;
576
+ rate_limit: number | null;
577
+ created_by: string;
578
+ created_at: string;
579
+ updated_at: string;
580
+ last_used_at: string | null;
581
+ expires_at: string | null;
582
+ revoked_at: string | null;
583
+ }
584
+ ```
585
+
586
+ ### Also update `admin` on an existing key
587
+
588
+ ```bash
589
+ curl -X PUT http://localhost:3000/api/admin/api-keys/<id> \
590
+ -H "Authorization: Bearer <admin-token-or-service-key>" \
591
+ -H "Content-Type: application/json" \
592
+ -d '{ "admin": true }'
593
+ ```
594
+
595
+ ### CLI
596
+
597
+ ```bash
598
+ # List all keys
599
+ rebase api-keys list
600
+
601
+ # Create a scoped key
602
+ rebase api-keys create --name "Read Only" --permissions '[{"collection":"orders","operations":["read"]}]'
603
+
604
+ # Create an admin key (for agents / MCP / CI)
605
+ rebase api-keys create --name "My Agent" --admin
606
+
607
+ # Revoke a key
608
+ rebase api-keys revoke <key-id>
609
+ ```
610
+
611
+ ---
612
+
613
+ ## REST Endpoints
614
+
615
+ All auth endpoints are mounted under `/api/auth`. Admin endpoints are under `/api/admin`.
616
+
617
+ ### Public Auth Endpoints
618
+
619
+ | Method | Endpoint | Rate Limit | Auth | Description |
620
+ |---|---|---|---|---|
621
+ | `POST` | `/auth/register` | default (200/15min) | No | Create account. Body: `{ email, password, displayName? }`. |
622
+ | `POST` | `/auth/login` | default | No | Email/password login. Body: `{ email, password }`. |
623
+ | `POST` | `/auth/{providerId}` | default | No | OAuth sign-in. Body varies by provider. |
624
+ | `POST` | `/auth/refresh` | — | No | Refresh access token. Body: `{ refreshToken }`. Rotates refresh token. |
625
+ | `POST` | `/auth/logout` | — | No | Invalidate refresh token. Body: `{ refreshToken? }`. |
626
+ | `POST` | `/auth/anonymous` | strict (50/15min) | No | Create anonymous user with temp credentials. |
627
+ | `POST` | `/auth/forgot-password` | strict | No | Request password reset email. Body: `{ email }`. Always returns success (security). |
628
+ | `POST` | `/auth/reset-password` | strict | No | Reset password with token. Body: `{ token, password }`. Invalidates all sessions. |
629
+ | `GET` | `/auth/verify-email` | — | No | Verify email. Query: `?token=<token>`. |
630
+ | `GET` | `/auth/config` | default | No | Get auth capabilities for frontend: `{ needsSetup, registrationEnabled, emailServiceEnabled, enabledProviders }`. |
631
+
632
+ ### Authenticated Endpoints
633
+
634
+ | Method | Endpoint | Auth | Description |
635
+ |---|---|---|---|
636
+ | `GET` | `/auth/me` | Required | Get current user profile + roles. |
637
+ | `PATCH` | `/auth/me` | Required | Update profile. Body: `{ displayName?, photoURL? }`. |
638
+ | `POST` | `/auth/change-password` | Required | Change password. Body: `{ oldPassword, newPassword }`. Invalidates all sessions. |
639
+ | `POST` | `/auth/send-verification` | Required | Send email verification link. Requires email service. |
640
+ | `GET` | `/auth/sessions` | Required | List active sessions (refresh tokens). |
641
+ | `DELETE` | `/auth/sessions` | Required | Revoke all sessions (remote logout). |
642
+ | `DELETE` | `/auth/sessions/:id` | Required | Revoke a specific session. |
643
+ | `POST` | `/auth/anonymous/link` | Required | Upgrade anonymous → permanent. Body: `{ email, password }`. |
644
+
645
+ ### Auth Response Format
646
+
647
+ All login/register/OAuth endpoints return:
648
+
649
+ ```json
650
+ {
651
+ "user": {
652
+ "uid": "uuid",
653
+ "email": "user@example.com",
654
+ "displayName": "John",
655
+ "photoURL": null,
656
+ "roles": ["member"],
657
+ "metadata": {}
658
+ },
659
+ "tokens": {
660
+ "accessToken": "eyJ...",
661
+ "refreshToken": "hex-string",
662
+ "accessTokenExpiresAt": 1700000000000
663
+ }
664
+ }
665
+ ```
666
+
667
+ > **TIP:** Use the `transformAuthResponse` hook to inject additional tokens (e.g., Firebase Custom Tokens) or metadata into this response. See [Auth Lifecycle Hooks](#auth-lifecycle-hooks).
668
+
669
+ ### Error Response Format
670
+
671
+ ```json
672
+ {
673
+ "error": {
674
+ "message": "Invalid email or password",
675
+ "code": "INVALID_CREDENTIALS"
676
+ }
677
+ }
678
+ ```
679
+
680
+ ### Common Error Codes
681
+
682
+ | Code | HTTP | Description |
683
+ |---|---|---|
684
+ | `INVALID_CREDENTIALS` | 401 | Wrong email/password. |
685
+ | `INVALID_TOKEN` | 401 | Invalid or expired refresh/reset token. |
686
+ | `TOKEN_EXPIRED` | 401 | Refresh token has expired. |
687
+ | `REGISTRATION_DISABLED` | 403 | `allowRegistration` is `false`. |
688
+ | `EMAIL_EXISTS` | 409 | Email already registered. |
689
+ | `WEAK_PASSWORD` | 400 | Password fails strength validation. |
690
+ | `INVALID_INPUT` | 400 | Zod validation failure. |
691
+ | `EMAIL_NOT_CONFIGURED` | 503 | Email service not set up (password reset/verification unavailable). |
692
+ | `ALREADY_VERIFIED` | 400 | Email already verified. |
693
+ | `NOT_ANONYMOUS` | 400 | User is not anonymous (cannot link). |
694
+ | `RATE_LIMITED` | 429 | Too many requests. |
695
+
696
+ ---
697
+
698
+ ## Client SDK
699
+
700
+ The client SDK's `auth` module is created via `createAuth(transport, options?)`. It manages tokens, auto-refresh, session persistence, and state change listeners.
701
+
702
+ ### CreateAuthOptions
703
+
704
+ | Option | Type | Default | Description |
705
+ |---|---|---|---|
706
+ | `storage` | `AuthStorage` | `localStorage` (browser) or in-memory | Token persistence backend. |
707
+ | `authPath` | `string` | `"/auth"` | Base path for auth endpoints. |
708
+ | `autoRefresh` | `boolean` | `true` | Auto-refresh access tokens 2 minutes before expiry. |
709
+ | `persistSession` | `boolean` | `true` | Persist session to storage between page loads. |
710
+
711
+ ### Client SDK Methods
712
+
713
+ ```typescript
714
+ const { auth } = createRebaseClient({ baseUrl: "http://localhost:3000" });
715
+
716
+ // Email/password
717
+ await auth.signInWithEmail(email, password);
718
+ await auth.signUp(email, password, displayName?);
719
+
720
+ // OAuth (all return { user, accessToken, refreshToken })
721
+ await auth.signInWithGoogle({ idToken });
722
+ await auth.signInWithGoogle({ accessToken });
723
+ await auth.signInWithGoogle({ code, redirectUri });
724
+ await auth.signInWithGitHub(code, redirectUri);
725
+ await auth.signInWithMicrosoft(code, redirectUri);
726
+ await auth.signInWithApple(code, redirectUri, user?);
727
+ await auth.signInWithFacebook(code, redirectUri);
728
+ await auth.signInWithTwitter(code, redirectUri, codeVerifier);
729
+ await auth.signInWithDiscord(code, redirectUri);
730
+ await auth.signInWithGitLab(code, redirectUri);
731
+ await auth.signInWithLinkedin(code, redirectUri);
732
+ await auth.signInWithBitbucket(code, redirectUri);
733
+ await auth.signInWithSlack(code, redirectUri);
734
+ await auth.signInWithSpotify(code, redirectUri);
735
+ await auth.signInWithOAuth(providerId, payload); // Generic
736
+
737
+ // Session
738
+ await auth.signOut();
739
+ await auth.refreshSession();
740
+ auth.getSession(); // Returns RebaseSession | null (sync)
741
+
742
+ // Profile
743
+ await auth.getUser(); // GET /auth/me
744
+ await auth.updateUser({ displayName?, photoURL? });
745
+
746
+ // Password
747
+ await auth.resetPasswordForEmail(email);
748
+ await auth.resetPassword(token, newPassword);
749
+ await auth.changePassword(oldPassword, newPassword);
750
+
751
+ // Email verification
752
+ await auth.sendVerificationEmail();
753
+ await auth.verifyEmail(token);
754
+
755
+ // Sessions
756
+ await auth.getSessions(); // List active sessions
757
+ await auth.revokeSession(sessionId);
758
+ await auth.revokeAllSessions(); // Revokes all + signs out locally
759
+
760
+ // Config
761
+ await auth.getAuthConfig(); // GET /auth/config
762
+
763
+ // State listener
764
+ const unsubscribe = auth.onAuthStateChange((event, session) => {
765
+ // event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED"
766
+ console.log(event, session?.user);
767
+ });
768
+ ```
769
+
770
+ ### Client Types
771
+
772
+ ```typescript
773
+ interface RebaseUser {
774
+ uid: string;
775
+ email: string | null;
776
+ displayName: string | null;
777
+ photoURL: string | null;
778
+ emailVerified?: boolean;
779
+ roles?: string[];
780
+ providerId: string;
781
+ isAnonymous: boolean;
782
+ }
783
+
784
+ interface RebaseSession {
785
+ accessToken: string;
786
+ refreshToken: string;
787
+ expiresAt: number; // Timestamp (ms)
788
+ user: RebaseUser;
789
+ }
790
+
791
+ type AuthChangeEvent = "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | "USER_UPDATED";
792
+ ```
793
+
794
+ ### Custom Storage Backends
795
+
796
+ ```typescript
797
+ import { createMemoryStorage, createCookieStorage } from "@rebasepro/client";
798
+
799
+ // In-memory (Node.js / SSR)
800
+ const auth = createAuth(transport, {
801
+ storage: createMemoryStorage(),
802
+ });
803
+
804
+ // Cookie-based (SSR-friendly)
805
+ const auth = createAuth(transport, {
806
+ storage: createCookieStorage({
807
+ path: "/",
808
+ sameSite: "Lax",
809
+ secure: true,
810
+ domain: ".myapp.com",
811
+ maxAge: 365 * 24 * 60 * 60, // 1 year (default)
812
+ }),
813
+ });
814
+ ```
815
+
816
+ ### Session Restoration
817
+
818
+ On initialization (when `persistSession` is `true`):
819
+ 1. Load stored session from storage.
820
+ 2. If access token is still valid → restore session and schedule refresh.
821
+ 3. If access token is expired but refresh token exists → immediately attempt refresh.
822
+ 4. If refresh fails → clear session and emit `SIGNED_OUT`.
823
+
824
+ ---
825
+
826
+ ## Row-Level Security
827
+
828
+ Rebase implements RLS by scoping the DataDriver via `withAuth()` before each request. This injects the authenticated user's identity into the database context.
829
+
830
+ ### How RLS Scoping Works
831
+
832
+ 1. Auth middleware verifies the JWT (or API key / service key).
833
+ 2. The middleware calls `scopeDataDriver(driver, { uid, roles })`.
834
+ 3. If the driver supports `withAuth()` (e.g. Postgres), it returns a scoped clone with Postgres session variables set:
835
+ - `auth.uid()` — the user's ID
836
+ - `auth.jwt()` — the JWT claims
837
+ - `auth.roles()` — the user's role IDs
838
+ 4. All subsequent queries in that request use the scoped driver with RLS policies applied.
839
+
840
+ ### Fail-Closed Security
841
+
842
+ > **IMPORTANT FOR AGENTS:** If `withAuth()` throws an error, the request is **rejected** with 500. The system never falls back to unscoped access. This is by design (fail-closed).
843
+
844
+ ### Anonymous Users
845
+
846
+ When `requireAuth` is `false` and no token is provided, the driver is scoped with:
847
+ - `uid: "anon"`
848
+ - `roles: ["anon"]`
849
+
850
+ This allows Postgres RLS policies to handle public access explicitly.
851
+
852
+ ### Service Key Scoping
853
+
854
+ Requests with the `serviceKey` bypass RLS — they receive admin-level access with `uid: "service"` and `roles: ["admin"]`.
855
+
856
+ ### API Key Scoping
857
+
858
+ API keys use a service identity for RLS scoping: `uid: "api-key:{id}"`, `roles: ["service"]` (or `["admin", "service"]` when `admin: true`). They do not inherit the `created_by` user's identity.
859
+
860
+ ---
861
+
862
+ ## Rate Limiting
863
+
864
+ Rebase uses an in-memory sliding-window rate limiter with IP-based keying.
865
+
866
+ ### Pre-configured Limiters
867
+
868
+ | Limiter | Window | Limit | Applied To |
869
+ |---|---|---|---|
870
+ | `defaultAuthLimiter` | 15 minutes | 200 requests | `/auth/register`, `/auth/login`, `/auth/{provider}`, `/auth/config` |
871
+ | `strictAuthLimiter` | 15 minutes | 50 requests | `/auth/forgot-password`, `/auth/reset-password`, `/auth/anonymous` |
872
+
873
+ ### Rate Limit Response Headers
874
+
875
+ All rate-limited endpoints include:
876
+
877
+ | Header | Description |
878
+ |---|---|
879
+ | `X-RateLimit-Limit` | Maximum requests in the window. |
880
+ | `X-RateLimit-Remaining` | Remaining requests in current window. |
881
+ | `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets. |
882
+ | `Retry-After` | Seconds until the client can retry (only on 429). |
883
+
884
+ ### API Key Rate Limiting
885
+
886
+ API keys have their own per-key rate limiter. The `rate_limit` on each key specifies requests per 15-minute window. When `rate_limit` is `null`, a default of 1000 requests per 15 minutes is applied.
887
+
888
+ ### Rate Limit Error Response
889
+
890
+ ```json
891
+ {
892
+ "error": {
893
+ "message": "Too many requests, please try again later.",
894
+ "code": "RATE_LIMITED"
895
+ }
896
+ }
897
+ ```
898
+
899
+ ### Custom Rate Limiter
900
+
901
+ ```typescript
902
+ import { createRateLimiter } from "@rebasepro/server-core";
903
+
904
+ const myLimiter = createRateLimiter({
905
+ windowMs: 60 * 1000, // 1 minute
906
+ limit: 10, // 10 requests per minute
907
+ message: "Slow down!",
908
+ keyGenerator: (c) => {
909
+ // Custom keying (e.g., by user ID)
910
+ const user = c.get("user");
911
+ return user?.userId || c.req.header("x-forwarded-for") || "unknown";
912
+ },
913
+ });
914
+ ```
915
+
916
+ ---
917
+
918
+ ## Custom Auth Adapters
919
+
920
+ For external auth systems (Clerk, Auth0, Firebase Auth, or custom JWT), use the `AuthAdapter` interface or the `createCustomAuthAdapter()` helper.
921
+
922
+ ### AuthAdapter Interface
923
+
924
+ ```typescript
925
+ interface AuthAdapter {
926
+ readonly id: string;
927
+ verifyRequest(request: Request): Promise<AuthenticatedUser | null>;
928
+ verifyToken?(token: string): Promise<AuthenticatedUser | null>;
929
+ userManagement?: UserManagementAdapter;
930
+ createAuthRoutes?(): Hono<any> | undefined;
931
+ createAdminRoutes?(): Hono<any> | undefined;
932
+ getCapabilities(): AuthAdapterCapabilities | Promise<AuthAdapterCapabilities>;
933
+ initialize?(): Promise<void>;
934
+ destroy?(): Promise<void>;
935
+ serviceKey?: string;
936
+ transformAuthResponse?(response: AuthResponsePayload, context: TransformAuthResponseContext): Promise<AuthResponsePayload>;
937
+ }
938
+
939
+ interface AuthenticatedUser {
940
+ uid: string;
941
+ email: string;
942
+ displayName?: string | null;
943
+ photoUrl?: string | null;
944
+ roles: string[];
945
+ isAdmin: boolean;
946
+ rawToken?: string;
947
+ claims?: Record<string, unknown>;
948
+ }
949
+ ```
950
+
951
+ ### createCustomAuthAdapter
952
+
953
+ The simplest way to plug an existing auth system into Rebase. Only `verifyRequest` is required:
954
+
955
+ ```typescript
956
+ import { createCustomAuthAdapter } from "@rebasepro/server-core";
957
+ import jwt from "jsonwebtoken";
958
+
959
+ const auth = createCustomAuthAdapter({
960
+ verifyRequest: async (request) => {
961
+ const token = request.headers.get("Authorization")?.replace("Bearer ", "");
962
+ if (!token) return null;
963
+
964
+ try {
965
+ const decoded = jwt.verify(token, MY_SECRET) as any;
966
+ return {
967
+ uid: decoded.sub,
968
+ email: decoded.email,
969
+ displayName: decoded.name,
970
+ roles: decoded.roles || [],
971
+ isAdmin: decoded.roles?.includes("admin") ?? false,
972
+ };
973
+ } catch {
974
+ return null;
975
+ }
976
+ },
977
+
978
+ // Optional: separate token verification for WebSocket auth
979
+ verifyToken: async (token) => {
980
+ // Same as above but receives just the token string
981
+ // Default: synthesizes a Request and calls verifyRequest
982
+ },
983
+
984
+ // Optional: enable user management in admin panel
985
+ userManagement: { ... },
986
+
987
+ // Optional: static service key
988
+ serviceKey: process.env.REBASE_SERVICE_KEY,
989
+
990
+ // Optional: override default capabilities
991
+ capabilities: {
992
+ emailPasswordLogin: false,
993
+ registration: false,
994
+ enabledProviders: ["google"],
995
+ },
996
+
997
+ // Optional: enrich auth responses with external tokens
998
+ transformAuthResponse: async (response, context) => {
999
+ const externalToken = await generateExternalToken(context.userId);
1000
+ return {
1001
+ ...response,
1002
+ tokens: { ...response.tokens, externalToken },
1003
+ };
1004
+ },
1005
+ });
1006
+
1007
+ // Pass to initializeRebaseBackend:
1008
+ await initializeRebaseBackend({
1009
+ server, app,
1010
+ database: createPostgresAdapter({ connection: db, schema }),
1011
+ auth, // AuthAdapter directly
1012
+ });
1013
+ ```
1014
+
1015
+ ### AuthAdapterCapabilities
1016
+
1017
+ The frontend reads these from `GET /api/auth/config` to dynamically show/hide UI:
1018
+
1019
+ ```typescript
1020
+ interface AuthAdapterCapabilities {
1021
+ hasBuiltInAuthRoutes: boolean; // true for built-in, false for external
1022
+ emailPasswordLogin: boolean;
1023
+ registration: boolean;
1024
+ passwordReset: boolean;
1025
+ sessionManagement: boolean;
1026
+ profileUpdate: boolean;
1027
+ emailVerification: boolean;
1028
+ enabledProviders: string[]; // e.g. ["google", "github"]
1029
+ externalLoginUrl?: string; // Redirect URL for external auth
1030
+ needsSetup?: boolean; // true when no users exist
1031
+ registrationEnabled?: boolean;
1032
+ }
1033
+ ```
1034
+
1035
+ ### Default Capabilities for Custom Adapters
1036
+
1037
+ When using `createCustomAuthAdapter`, all capabilities default to `false`/`[]` unless overridden via `capabilities`.
1038
+
1039
+ ---
1040
+
1041
+ ## Roles & Permissions
1042
+
1043
+ ### Role Data Structure
1044
+
1045
+ ```typescript
1046
+ interface RoleData {
1047
+ id: string;
1048
+ name: string;
1049
+ isAdmin: boolean;
1050
+ defaultPermissions: {
1051
+ read?: boolean;
1052
+ create?: boolean;
1053
+ edit?: boolean;
1054
+ delete?: boolean;
1055
+ } | null;
1056
+ collectionPermissions: Record<string, {
1057
+ read?: boolean;
1058
+ create?: boolean;
1059
+ edit?: boolean;
1060
+ delete?: boolean;
1061
+ }> | null;
1062
+ }
1063
+ ```
1064
+
1065
+ ### Built-in Role Behavior
1066
+
1067
+ - The **first user** in the system is automatically assigned the `"admin"` role.
1068
+ - Subsequent users get the `defaultRole` (if configured).
1069
+ - Setting `defaultRole: "admin"` throws a startup error to prevent privilege escalation.
1070
+ - Admin status is determined by having a role with `id === "admin"` or `id === "schema-admin"`.
1071
+
1072
+ ### Admin Routes for User/Role Management
1073
+
1074
+ Admin user and role management is handled via dedicated admin routes (mounted under `/api/admin`) which require `requireAuth` + `requireAdmin` middleware.
1075
+
1076
+ ---
1077
+
1078
+ ## Backend Hooks
1079
+
1080
+ Backend hooks intercept data at the **API boundary** (after DB operations, before API responses). They are separate from auth hooks and collection-level `EntityCallbacks`.
1081
+
1082
+ ### BackendHooks Interface
1083
+
1084
+ ```typescript
1085
+ interface BackendHooks {
1086
+ users?: UserHooks;
1087
+ data?: DataHooks;
1088
+ }
1089
+ ```
1090
+
1091
+ ### UserHooks (Admin User Management)
1092
+
1093
+ | Hook | Signature | Description |
1094
+ |---|---|---|
1095
+ | `afterRead` | `(user, context) => AdminUser \| null` | Transform user after DB read. Return `null` to hide. |
1096
+ | `beforeSave` | `(data, context) => data` | Transform before write. Throw to abort. |
1097
+ | `afterSave` | `(user, context) => void` | After user create/update. Side effects. |
1098
+ | `beforeDelete` | `(userId, context) => void` | Throw to prevent deletion. |
1099
+ | `afterDelete` | `(userId, context) => void` | After user deleted. |
1100
+
1101
+ ### DataHooks (All Collection Entities)
1102
+
1103
+ | Hook | Signature | Description |
1104
+ |---|---|---|
1105
+ | `afterRead` | `(slug, entity, context) => entity \| null` | Transform entity after read. Return `null` to filter out. |
1106
+ | `beforeSave` | `(slug, values, entityId, context) => values` | Transform before write. Throw to abort. |
1107
+ | `afterSave` | `(slug, entity, context) => void` | After entity create/update. Side effects. |
1108
+ | `beforeDelete` | `(slug, entityId, context) => void` | Throw to prevent deletion. |
1109
+ | `afterDelete` | `(slug, entityId, context) => void` | After entity deleted. |
1110
+
1111
+ ### BackendHookContext
1112
+
1113
+ ```typescript
1114
+ interface BackendHookContext {
1115
+ requestUser?: { userId: string; roles: string[] };
1116
+ method: "GET" | "POST" | "PUT" | "DELETE";
1117
+ }
1118
+ ```
1119
+
1120
+ ### Example: PII Masking
1121
+
1122
+ ```typescript
1123
+ const hooks: BackendHooks = {
1124
+ data: {
1125
+ afterRead(slug, entity, ctx) {
1126
+ if (!ctx.requestUser?.roles.includes("admin") && entity.email) {
1127
+ return { ...entity, email: "***" };
1128
+ }
1129
+ return entity;
1130
+ },
1131
+ },
1132
+ users: {
1133
+ afterRead(user, ctx) {
1134
+ // Hide system users from admin panel
1135
+ if (user.email.endsWith("@system.internal")) return null;
1136
+ return user;
1137
+ },
1138
+ },
1139
+ };
1140
+
1141
+ await initializeRebaseBackend({
1142
+ // ...
1143
+ hooks,
1144
+ });
1145
+ ```
1146
+
1147
+ ---
1148
+
1149
+ ## Email Configuration
1150
+
1151
+ Email is required for password reset, email verification, and welcome emails. Configure via `auth.email`.
1152
+
1153
+ ### EmailConfig
1154
+
1155
+ | Property | Type | Required | Description |
1156
+ |---|---|---|---|
1157
+ | `from` | `string` | Yes | Sender address (e.g. `"MyApp <noreply@myapp.com>"`). |
1158
+ | `smtp` | `SMTPConfig` | One of `smtp` or `sendEmail` | SMTP server configuration. |
1159
+ | `sendEmail` | `(options) => Promise<void>` | One of `smtp` or `sendEmail` | Custom email sending function (e.g. AWS SES, Resend SDK). |
1160
+ | `resetPasswordUrl` | `string` | No | Base URL for reset links: `{url}/reset-password?token=xxx`. |
1161
+ | `verifyEmailUrl` | `string` | No | Base URL for verification links: `{url}/verify-email?token=xxx`. |
1162
+ | `appName` | `string` | No | App name in email templates. Defaults to `"Rebase"`. |
1163
+ | `templates` | Object | No | Custom template functions (see below). |
1164
+
1165
+ ### SMTPConfig
1166
+
1167
+ ```typescript
1168
+ interface SMTPConfig {
1169
+ host: string;
1170
+ port: number;
1171
+ secure?: boolean;
1172
+ auth?: { user: string; pass: string };
1173
+ name?: string;
1174
+ }
1175
+ ```
1176
+
1177
+ ### Custom Email Templates
1178
+
1179
+ ```typescript
1180
+ email: {
1181
+ from: "noreply@myapp.com",
1182
+ smtp: { host: "smtp.example.com", port: 587 },
1183
+ templates: {
1184
+ passwordReset: (resetUrl, user) => ({
1185
+ subject: "Reset your password",
1186
+ html: `<p>Hi ${user.displayName || user.email},</p><p><a href="${resetUrl}">Reset</a></p>`,
1187
+ text: `Reset your password: ${resetUrl}`,
1188
+ }),
1189
+ emailVerification: (verifyUrl, user) => ({
1190
+ subject: "Verify your email",
1191
+ html: `<a href="${verifyUrl}">Verify</a>`,
1192
+ }),
1193
+ welcomeEmail: (user, appName) => ({
1194
+ subject: `Welcome to ${appName}!`,
1195
+ html: `<p>Welcome, ${user.displayName || user.email}!</p>`,
1196
+ }),
1197
+ userInvitation: (setPasswordUrl, user) => ({
1198
+ subject: "You've been invited",
1199
+ html: `<p>Set your password: <a href="${setPasswordUrl}">here</a></p>`,
1200
+ }),
1201
+ },
1202
+ }
1203
+ ```
1204
+
1205
+ ### Custom Email Provider (Non-SMTP)
1206
+
1207
+ ```typescript
1208
+ import { Resend } from "resend";
1209
+ const resend = new Resend(process.env.RESEND_API_KEY);
1210
+
1211
+ email: {
1212
+ from: "noreply@myapp.com",
1213
+ sendEmail: async (options) => {
1214
+ await resend.emails.send({
1215
+ from: options.from || "noreply@myapp.com",
1216
+ to: options.to,
1217
+ subject: options.subject,
1218
+ html: options.html,
1219
+ text: options.text,
1220
+ });
1221
+ },
1222
+ appName: "MyApp",
1223
+ resetPasswordUrl: "https://myapp.com",
1224
+ }
1225
+ ```
1226
+
1227
+ ---
1228
+
1229
+ ## Security Concepts
1230
+
1231
+ ### Service Key
1232
+
1233
+ A static secret for server-to-server authentication. Generate with:
1234
+ ```bash
1235
+ node -e "console.log(require('crypto').randomBytes(48).toString('base64'))"
1236
+ ```
1237
+
1238
+ When a request includes `Authorization: Bearer <serviceKey>`:
1239
+ - It bypasses JWT verification.
1240
+ - It receives admin-level access (`uid: "service"`, `roles: ["admin"]`).
1241
+ - Comparison is done with constant-time comparison to prevent timing attacks.
1242
+ - Must be ≥ 32 characters (validated at startup).
1243
+
1244
+ ### Token Rotation
1245
+
1246
+ Refresh tokens are rotated on every use:
1247
+ 1. Client sends refresh token to `POST /auth/refresh`.
1248
+ 2. Server deletes the old refresh token and creates a new one.
1249
+ 3. New access + refresh tokens are returned.
1250
+
1251
+ ### Password Reset Security
1252
+
1253
+ - `POST /auth/forgot-password` always returns success (doesn't reveal whether email exists).
1254
+ - Reset tokens are stored as SHA-256 hashes.
1255
+ - Tokens expire in 1 hour.
1256
+ - After password reset, **all sessions are invalidated** (all refresh tokens deleted).
1257
+
1258
+ ### Zod Input Validation
1259
+
1260
+ All auth endpoints validate input with Zod schemas:
1261
+
1262
+ | Field | Validation |
1263
+ |---|---|
1264
+ | `email` | Valid email, max 255 chars |
1265
+ | `password` | Min 1 char, max 128 chars |
1266
+ | `displayName` | Max 255 chars |
1267
+ | `photoURL` | Valid URL, max 2048 chars |
1268
+ | `refreshToken` | Min 1 char |
1269
+
1270
+ ---
1271
+
1272
+ ## References
1273
+
1274
+ - Source: `packages/server-core/src/auth/` — All auth implementation
1275
+ - Source: `packages/server-core/src/auth/routes.ts` — REST auth endpoints
1276
+ - Source: `packages/server-core/src/auth/auth-hooks.ts` — Lifecycle hooks
1277
+ - Source: `packages/server-core/src/auth/api-keys/` — API key system
1278
+ - Source: `packages/server-core/src/auth/rate-limiter.ts` — Rate limiting
1279
+ - Source: `packages/server-core/src/init.ts` — `RebaseAuthConfig` and backend init
1280
+ - Source: `packages/client/src/auth.ts` — Client SDK auth module
1281
+ - Source: `packages/types/src/types/auth_adapter.ts` — `AuthAdapter` interface
1282
+ - Source: `packages/server-core/src/auth/rls-scope.ts` — RLS scoping
1283
+ - Source: `packages/server-core/src/email/types.ts` — Email configuration