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