@rebasepro/server-core 0.1.0 → 0.2.1

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 (148) hide show
  1. package/LICENSE +22 -6
  2. package/dist/common/src/util/entities.d.ts +2 -2
  3. package/dist/common/src/util/relations.d.ts +1 -1
  4. package/dist/{index-DXVBFp5V.js → index-BZoAtuqi.js} +6 -2
  5. package/dist/index-BZoAtuqi.js.map +1 -0
  6. package/dist/index.es.js +15909 -16083
  7. package/dist/index.es.js.map +1 -1
  8. package/dist/index.umd.js +15847 -16017
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/server-core/src/auth/adapter-middleware.d.ts +33 -0
  11. package/dist/server-core/src/auth/admin-routes.d.ts +6 -0
  12. package/dist/server-core/src/auth/auth-overrides.d.ts +139 -0
  13. package/dist/server-core/src/auth/builtin-auth-adapter.d.ts +49 -0
  14. package/dist/server-core/src/auth/crypto-utils.d.ts +16 -0
  15. package/dist/server-core/src/auth/custom-auth-adapter.d.ts +39 -0
  16. package/dist/server-core/src/auth/index.d.ts +7 -0
  17. package/dist/server-core/src/auth/interfaces.d.ts +2 -0
  18. package/dist/server-core/src/auth/middleware.d.ts +18 -0
  19. package/dist/server-core/src/auth/rls-scope.d.ts +31 -0
  20. package/dist/server-core/src/auth/routes.d.ts +7 -1
  21. package/dist/server-core/src/env.d.ts +131 -0
  22. package/dist/server-core/src/index.d.ts +2 -0
  23. package/dist/server-core/src/init.d.ts +62 -3
  24. package/dist/types/src/controllers/auth.d.ts +9 -8
  25. package/dist/types/src/controllers/client.d.ts +3 -0
  26. package/dist/types/src/types/auth_adapter.d.ts +356 -0
  27. package/dist/types/src/types/collections.d.ts +67 -2
  28. package/dist/types/src/types/database_adapter.d.ts +94 -0
  29. package/dist/types/src/types/entity_actions.d.ts +7 -1
  30. package/dist/types/src/types/entity_callbacks.d.ts +1 -1
  31. package/dist/types/src/types/entity_views.d.ts +36 -1
  32. package/dist/types/src/types/index.d.ts +2 -0
  33. package/dist/types/src/types/plugins.d.ts +1 -1
  34. package/dist/types/src/types/properties.d.ts +24 -5
  35. package/dist/types/src/types/property_config.d.ts +6 -2
  36. package/dist/types/src/types/relations.d.ts +1 -1
  37. package/dist/types/src/types/translations.d.ts +8 -0
  38. package/dist/types/src/users/user.d.ts +5 -0
  39. package/package.json +26 -26
  40. package/src/api/errors.ts +1 -1
  41. package/src/api/graphql/graphql-schema-generator.ts +7 -0
  42. package/src/api/openapi-generator.ts +13 -1
  43. package/src/api/rest/api-generator-count.test.ts +14 -12
  44. package/src/api/rest/query-parser.ts +2 -20
  45. package/src/auth/adapter-middleware.ts +83 -0
  46. package/src/auth/admin-routes.ts +36 -43
  47. package/src/auth/auth-overrides.ts +172 -0
  48. package/src/auth/builtin-auth-adapter.ts +384 -0
  49. package/src/auth/crypto-utils.ts +31 -0
  50. package/src/auth/custom-auth-adapter.ts +85 -0
  51. package/src/auth/index.ts +10 -0
  52. package/src/auth/interfaces.ts +2 -0
  53. package/src/auth/jwt.ts +3 -1
  54. package/src/auth/middleware.ts +2 -46
  55. package/src/auth/rls-scope.ts +58 -0
  56. package/src/auth/routes.ts +74 -32
  57. package/src/cron/cron-scheduler.test.ts +9 -9
  58. package/src/cron/cron-scheduler.ts +1 -1
  59. package/src/env.ts +224 -0
  60. package/src/index.ts +4 -0
  61. package/src/init.ts +355 -135
  62. package/src/storage/routes.ts +1 -19
  63. package/src/utils/logging.ts +3 -3
  64. package/test/admin-routes.test.ts +10 -4
  65. package/test/auth-routes.test.ts +2 -2
  66. package/test/backend-hooks-admin.test.ts +32 -12
  67. package/test/custom-auth-adapter.test.ts +177 -0
  68. package/test/env.test.ts +138 -0
  69. package/test/query-parser.test.ts +0 -29
  70. package/tsconfig.json +3 -0
  71. package/app/frontend/node_modules/esbuild/LICENSE.md +0 -21
  72. package/app/frontend/node_modules/esbuild/README.md +0 -3
  73. package/app/frontend/node_modules/esbuild/bin/esbuild +0 -220
  74. package/app/frontend/node_modules/esbuild/install.js +0 -285
  75. package/app/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
  76. package/app/frontend/node_modules/esbuild/lib/main.js +0 -2239
  77. package/app/frontend/node_modules/esbuild/package.json +0 -46
  78. package/dist/index-DXVBFp5V.js.map +0 -1
  79. package/examples/firebase/node_modules/esbuild/LICENSE.md +0 -21
  80. package/examples/firebase/node_modules/esbuild/README.md +0 -3
  81. package/examples/firebase/node_modules/esbuild/bin/esbuild +0 -220
  82. package/examples/firebase/node_modules/esbuild/install.js +0 -285
  83. package/examples/firebase/node_modules/esbuild/lib/main.d.ts +0 -705
  84. package/examples/firebase/node_modules/esbuild/lib/main.js +0 -2239
  85. package/examples/firebase/node_modules/esbuild/package.json +0 -46
  86. package/examples/medmot-staging/frontend/node_modules/esbuild/LICENSE.md +0 -21
  87. package/examples/medmot-staging/frontend/node_modules/esbuild/README.md +0 -3
  88. package/examples/medmot-staging/frontend/node_modules/esbuild/bin/esbuild +0 -220
  89. package/examples/medmot-staging/frontend/node_modules/esbuild/install.js +0 -285
  90. package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.d.ts +0 -705
  91. package/examples/medmot-staging/frontend/node_modules/esbuild/lib/main.js +0 -2239
  92. package/examples/medmot-staging/frontend/node_modules/esbuild/package.json +0 -46
  93. package/examples/sdk-demo/node_modules/esbuild/LICENSE.md +0 -21
  94. package/examples/sdk-demo/node_modules/esbuild/README.md +0 -3
  95. package/examples/sdk-demo/node_modules/esbuild/bin/esbuild +0 -223
  96. package/examples/sdk-demo/node_modules/esbuild/install.js +0 -289
  97. package/examples/sdk-demo/node_modules/esbuild/lib/main.d.ts +0 -716
  98. package/examples/sdk-demo/node_modules/esbuild/lib/main.js +0 -2242
  99. package/examples/sdk-demo/node_modules/esbuild/package.json +0 -49
  100. package/packages/client/node_modules/esbuild/LICENSE.md +0 -21
  101. package/packages/client/node_modules/esbuild/README.md +0 -3
  102. package/packages/client/node_modules/esbuild/bin/esbuild +0 -220
  103. package/packages/client/node_modules/esbuild/install.js +0 -285
  104. package/packages/client/node_modules/esbuild/lib/main.d.ts +0 -705
  105. package/packages/client/node_modules/esbuild/lib/main.js +0 -2239
  106. package/packages/client/node_modules/esbuild/package.json +0 -46
  107. package/packages/client-postgresql/node_modules/esbuild/LICENSE.md +0 -21
  108. package/packages/client-postgresql/node_modules/esbuild/README.md +0 -3
  109. package/packages/client-postgresql/node_modules/esbuild/bin/esbuild +0 -220
  110. package/packages/client-postgresql/node_modules/esbuild/install.js +0 -285
  111. package/packages/client-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
  112. package/packages/client-postgresql/node_modules/esbuild/lib/main.js +0 -2239
  113. package/packages/client-postgresql/node_modules/esbuild/package.json +0 -46
  114. package/packages/common/node_modules/esbuild/LICENSE.md +0 -21
  115. package/packages/common/node_modules/esbuild/README.md +0 -3
  116. package/packages/common/node_modules/esbuild/bin/esbuild +0 -220
  117. package/packages/common/node_modules/esbuild/install.js +0 -285
  118. package/packages/common/node_modules/esbuild/lib/main.d.ts +0 -705
  119. package/packages/common/node_modules/esbuild/lib/main.js +0 -2239
  120. package/packages/common/node_modules/esbuild/package.json +0 -46
  121. package/packages/server-mongodb/node_modules/esbuild/LICENSE.md +0 -21
  122. package/packages/server-mongodb/node_modules/esbuild/README.md +0 -3
  123. package/packages/server-mongodb/node_modules/esbuild/bin/esbuild +0 -220
  124. package/packages/server-mongodb/node_modules/esbuild/install.js +0 -285
  125. package/packages/server-mongodb/node_modules/esbuild/lib/main.d.ts +0 -705
  126. package/packages/server-mongodb/node_modules/esbuild/lib/main.js +0 -2239
  127. package/packages/server-mongodb/node_modules/esbuild/package.json +0 -46
  128. package/packages/server-postgresql/node_modules/esbuild/LICENSE.md +0 -21
  129. package/packages/server-postgresql/node_modules/esbuild/README.md +0 -3
  130. package/packages/server-postgresql/node_modules/esbuild/bin/esbuild +0 -220
  131. package/packages/server-postgresql/node_modules/esbuild/install.js +0 -285
  132. package/packages/server-postgresql/node_modules/esbuild/lib/main.d.ts +0 -705
  133. package/packages/server-postgresql/node_modules/esbuild/lib/main.js +0 -2239
  134. package/packages/server-postgresql/node_modules/esbuild/package.json +0 -46
  135. package/packages/types/node_modules/esbuild/LICENSE.md +0 -21
  136. package/packages/types/node_modules/esbuild/README.md +0 -3
  137. package/packages/types/node_modules/esbuild/bin/esbuild +0 -220
  138. package/packages/types/node_modules/esbuild/install.js +0 -285
  139. package/packages/types/node_modules/esbuild/lib/main.d.ts +0 -705
  140. package/packages/types/node_modules/esbuild/lib/main.js +0 -2239
  141. package/packages/types/node_modules/esbuild/package.json +0 -46
  142. package/packages/utils/node_modules/esbuild/LICENSE.md +0 -21
  143. package/packages/utils/node_modules/esbuild/README.md +0 -3
  144. package/packages/utils/node_modules/esbuild/bin/esbuild +0 -220
  145. package/packages/utils/node_modules/esbuild/install.js +0 -285
  146. package/packages/utils/node_modules/esbuild/lib/main.d.ts +0 -705
  147. package/packages/utils/node_modules/esbuild/lib/main.js +0 -2239
  148. package/packages/utils/node_modules/esbuild/package.json +0 -46
package/src/init.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin, BackendHooks } from "@rebasepro/types";
1
+ import { DataDriver, EntityCollection, BackendBootstrapper, BootstrappedAuth, RealtimeProvider, HealthCheckResult, InitializedDriver, isSQLAdmin, BackendHooks, AuthAdapter, DatabaseAdapter } from "@rebasepro/types";
2
2
  import { BackendCollectionRegistry } from "./collections/BackendCollectionRegistry";
3
3
  import { loadCollectionsFromDirectory } from "./collections/loader";
4
4
  import { DriverRegistry, DEFAULT_DRIVER_ID, DefaultDriverRegistry } from "./services/driver-registry";
@@ -6,6 +6,8 @@ import { Server } from "http";
6
6
 
7
7
  import { RestApiGenerator } from "./api/rest/api-generator";
8
8
  import { createAuthMiddleware } from "./auth/middleware";
9
+ import { createAdapterAuthMiddleware } from "./auth/adapter-middleware";
10
+ import { createBuiltinAuthAdapter } from "./auth/builtin-auth-adapter";
9
11
  import { errorHandler } from "./api/errors";
10
12
  import { Hono } from "hono";
11
13
  import { bodyLimit } from "hono/body-limit";
@@ -21,6 +23,7 @@ import { createHistoryRoutes } from "./history";
21
23
  import { EmailConfig, createEmailService } from "./email";
22
24
  import type { EmailService } from "./email";
23
25
  import type { OAuthProvider } from "./auth/interfaces";
26
+ import type { AuthOverrides } from "./auth/auth-overrides";
24
27
  import { _initRebase } from "./singleton";
25
28
 
26
29
  export interface RebaseAuthConfig {
@@ -56,7 +59,26 @@ export interface RebaseAuthConfig {
56
59
  slack?: { clientId: string; clientSecret: string };
57
60
  spotify?: { clientId: string; clientSecret: string };
58
61
  defaultRole?: string;
62
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
59
63
  providers?: OAuthProvider<any>[];
64
+ /**
65
+ * Override specific parts of the built-in auth implementation.
66
+ *
67
+ * Each override replaces one piece of the default behavior while
68
+ * keeping everything else intact. Unset overrides fall through
69
+ * to the built-in defaults (scrypt passwords, standard validation, etc.).
70
+ *
71
+ * @example bcrypt passwords with a custom hash
72
+ * ```ts
73
+ * import bcrypt from "bcrypt";
74
+ *
75
+ * overrides: {
76
+ * hashPassword: (pw) => bcrypt.hash(pw, 12),
77
+ * verifyPassword: (pw, hash) => bcrypt.compare(pw, hash),
78
+ * }
79
+ * ```
80
+ */
81
+ overrides?: AuthOverrides;
60
82
  [key: string]: unknown;
61
83
  }
62
84
 
@@ -66,11 +88,40 @@ export interface RebaseBackendConfig {
66
88
  server: Server;
67
89
  app: Hono<HonoEnv>;
68
90
  basePath?: string;
69
- bootstrappers: BackendBootstrapper[];
91
+
92
+ /**
93
+ * Database bootstrappers.
94
+ */
95
+ bootstrappers?: BackendBootstrapper[];
96
+ /**
97
+ * Database adapter.
98
+ *
99
+ * When set, this takes precedence over `bootstrappers`.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * import { createPostgresAdapter } from "@rebasepro/server-postgresql";
104
+ * database: createPostgresAdapter({ connection: db, schema }),
105
+ * ```
106
+ */
107
+ database?: DatabaseAdapter;
108
+
70
109
  logging?: {
71
110
  level?: "error" | "warn" | "info" | "debug";
72
111
  };
73
- auth?: RebaseAuthConfig;
112
+
113
+ /**
114
+ * Authentication configuration.
115
+ *
116
+ * Accepts **either**:
117
+ * - `RebaseAuthConfig` — built-in configuration
118
+ * - `AuthAdapter` — pluggable adapter for external auth (Clerk, Auth0, etc.)
119
+ *
120
+ * When a plain config object is provided, the built-in adapter is created
121
+ * automatically from the bootstrapper's `initializeAuth()` result.
122
+ */
123
+ auth?: RebaseAuthConfig | AuthAdapter;
124
+
74
125
  /**
75
126
  * Storage configuration. Accepts:
76
127
  *
@@ -83,6 +134,12 @@ export interface RebaseBackendConfig {
83
134
  enableSwagger?: boolean;
84
135
  functionsDir?: string;
85
136
  cronsDir?: string;
137
+ /**
138
+ * Enable/disable database persistence for cron job execution logs.
139
+ * When set to false, cron jobs will run but logs will not be persisted to the database.
140
+ * Default: true.
141
+ */
142
+ cronPersistence?: boolean;
86
143
  /**
87
144
  * Maximum request body size in bytes for API routes (default: 10MB).
88
145
  * Set to 0 to disable the global limit entirely.
@@ -118,6 +175,34 @@ export interface RebaseBackendConfig {
118
175
  hooks?: BackendHooks;
119
176
  }
120
177
 
178
+ /**
179
+ * Type guard to detect whether the `auth` config is an `AuthAdapter`
180
+ * (has a `verifyRequest` method) vs a plain `RebaseAuthConfig` (plain object).
181
+ */
182
+ export function isAuthAdapter(auth: RebaseAuthConfig | AuthAdapter): auth is AuthAdapter {
183
+ return typeof auth === "object" && auth !== null && "verifyRequest" in auth && typeof (auth as AuthAdapter).verifyRequest === "function";
184
+ }
185
+
186
+ /**
187
+ * Type guard to detect whether `database` is a `DatabaseAdapter`.
188
+ */
189
+ export function isDatabaseAdapter(db: unknown): db is DatabaseAdapter {
190
+ return typeof db === "object" && db !== null && "initializeDriver" in db && "type" in db && !("initializeAuth" in db);
191
+ }
192
+
193
+ /**
194
+ * Resolve the `requireAuth` flag from either a `RebaseAuthConfig` or an `AuthAdapter`.
195
+ *
196
+ * - `RebaseAuthConfig` has an explicit `requireAuth` boolean
197
+ * - `AuthAdapter` always implies auth is required (secure by default)
198
+ * - If no auth config is provided at all, default to `true`
199
+ */
200
+ function resolveRequireAuth(auth?: RebaseAuthConfig | AuthAdapter): boolean {
201
+ if (!auth) return true;
202
+ if (isAuthAdapter(auth)) return true; // AuthAdapters are always secure-by-default
203
+ return (auth as RebaseAuthConfig).requireAuth !== false;
204
+ }
205
+
121
206
  export interface RebaseBackendInstance {
122
207
  driverRegistry: DriverRegistry;
123
208
  driver: DataDriver;
@@ -159,7 +244,7 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
159
244
  configureLogLevel();
160
245
  }
161
246
 
162
- logger.info("Initializing Rebase Backend (Bootstrapper Protocol V2)");
247
+ logger.info("Initializing Rebase Backend");
163
248
 
164
249
  const basePath = config.basePath || "/api";
165
250
  const isProduction = process.env.NODE_ENV === "production";
@@ -205,10 +290,31 @@ dir: config.collectionsDir });
205
290
 
206
291
  const realtimeServices: Record<string, RealtimeProvider> = {};
207
292
  const delegates: Record<string, DataDriver> = {};
208
- const bootstrappers = config.bootstrappers || [];
293
+
294
+ // ─── Resolve bootstrappers ───────────────────────────────────────────
295
+ let bootstrappers: BackendBootstrapper[] = config.bootstrappers || [];
296
+ if (config.database) {
297
+ const dbAdapter = config.database;
298
+ logger.info("Using DatabaseAdapter", { type: dbAdapter.type });
299
+ const wrappedBootstrapper: BackendBootstrapper = {
300
+ type: dbAdapter.type,
301
+ initializeDriver: (initConfig: unknown) =>
302
+ dbAdapter.initializeDriver(initConfig as import("@rebasepro/types").DatabaseAdapterInitConfig),
303
+ initializeRealtime: dbAdapter.initializeRealtime
304
+ ? (_config: unknown, driverResult: InitializedDriver) =>
305
+ dbAdapter.initializeRealtime!(driverResult)
306
+ : undefined,
307
+ initializeAuth: dbAdapter.initializeAuth,
308
+ initializeHistory: dbAdapter.initializeHistory,
309
+ initializeWebsockets: dbAdapter.initializeWebsockets,
310
+ getAdmin: dbAdapter.getAdmin,
311
+ mountRoutes: dbAdapter.mountRoutes,
312
+ };
313
+ bootstrappers = [wrappedBootstrapper];
314
+ }
209
315
 
210
316
  if (bootstrappers.length === 0) {
211
- throw new Error("No bootstrappers provided. Cannot initialize database drivers.");
317
+ throw new Error("No bootstrappers or database adapter provided. Cannot initialize database drivers.");
212
318
  }
213
319
 
214
320
  let defaultDriverId = DEFAULT_DRIVER_ID;
@@ -250,36 +356,75 @@ collectionRegistry });
250
356
  // 2. Initialize Auth & History via the default driver's bootstrapper
251
357
  let authConfigResult: BootstrappedAuth | undefined = undefined;
252
358
  let serviceKey: string | undefined;
359
+ let authAdapter: AuthAdapter | undefined;
253
360
 
254
361
  if (config.auth) {
255
- // Secure JWT setup proactively within core package memory to eliminate dual-package hazards
256
- const safeAuthConfig = config.auth;
257
- if (safeAuthConfig.jwtSecret) {
258
- configureJwt({
259
- secret: safeAuthConfig.jwtSecret,
260
- accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
261
- refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
262
- });
263
- }
362
+ if (isAuthAdapter(config.auth)) {
363
+ // ── New path: User provided an AuthAdapter directly ──────────
364
+ authAdapter = config.auth;
365
+ serviceKey = authAdapter.serviceKey;
264
366
 
265
- // ── Service Key Validation ───────────────────────────────────────
266
- if (safeAuthConfig.serviceKey) {
267
- if (safeAuthConfig.serviceKey.length < 32) {
268
- throw new Error(
269
- "REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
270
- "Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
271
- );
367
+ if (authAdapter.initialize) {
368
+ await authAdapter.initialize();
272
369
  }
273
- serviceKey = safeAuthConfig.serviceKey;
274
- logger.info("Service key configured for script/server-to-server authentication");
275
- }
276
370
 
277
- if (defaultBootstrapper.initializeAuth) {
278
- logger.info("Bootstrapping authentication via driver protocol");
279
- authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
280
- logger.info("Authentication initialized");
371
+ logger.info("Using AuthAdapter", { id: authAdapter.id });
372
+
373
+ // Populate authConfigResult for backward compatibility
374
+ // (the return type still exposes `auth?: BootstrappedAuth`)
375
+ authConfigResult = {
376
+ userService: authAdapter.userManagement ?? {},
377
+ roleService: authAdapter.roleManagement ?? {},
378
+ };
281
379
  } else {
282
- logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
380
+ // ── RebaseAuthConfig wrap in built-in adapter ──
381
+ const safeAuthConfig = config.auth as RebaseAuthConfig;
382
+ if (safeAuthConfig.jwtSecret) {
383
+ configureJwt({
384
+ secret: safeAuthConfig.jwtSecret,
385
+ accessExpiresIn: safeAuthConfig.accessExpiresIn || "1h",
386
+ refreshExpiresIn: safeAuthConfig.refreshExpiresIn || "30d"
387
+ });
388
+ }
389
+
390
+ // ── Service Key Validation ───────────────────────────────────
391
+ if (safeAuthConfig.serviceKey) {
392
+ if (safeAuthConfig.serviceKey.length < 32) {
393
+ throw new Error(
394
+ "REBASE_SERVICE_KEY is too short. Must be at least 32 characters. " +
395
+ "Generate one with: node -e \"console.log(require('crypto').randomBytes(48).toString('base64'))\""
396
+ );
397
+ }
398
+ serviceKey = safeAuthConfig.serviceKey;
399
+ logger.info("Service key configured for script/server-to-server authentication");
400
+ }
401
+
402
+ if (defaultBootstrapper.initializeAuth) {
403
+ logger.info("Bootstrapping authentication via driver protocol");
404
+ authConfigResult = await defaultBootstrapper.initializeAuth(config.auth, defaultDriverResult);
405
+
406
+ // Build the built-in auth adapter from bootstrapper results
407
+ if (authConfigResult) {
408
+ const oauthProviders: OAuthProvider<unknown>[] = [...(safeAuthConfig.providers || [])];
409
+ // OAuth providers are resolved later in route mounting,
410
+ // but we need them here for the adapter
411
+ authAdapter = createBuiltinAuthAdapter({
412
+ authRepository: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
413
+ emailService: authConfigResult.emailService as import("./email").EmailService,
414
+ emailConfig: safeAuthConfig.email,
415
+ allowRegistration: safeAuthConfig.allowRegistration ?? false,
416
+ defaultRole: safeAuthConfig.defaultRole,
417
+ oauthProviders,
418
+ serviceKey,
419
+ hooks: config.hooks,
420
+ overrides: safeAuthConfig.overrides,
421
+ });
422
+ }
423
+
424
+ logger.info("Authentication initialized");
425
+ } else {
426
+ logger.warn("Auth requested but default bootstrapper does not support initializeAuth");
427
+ }
283
428
  }
284
429
  }
285
430
 
@@ -350,87 +495,110 @@ collectionRegistry });
350
495
  // basePath already resolved above
351
496
 
352
497
  // 4. Mount API Routes
353
- if (config.auth && authConfigResult) {
354
- const oauthProviders: OAuthProvider<any>[] = [...(config.auth.providers || [])];
498
+ if (config.auth && authAdapter) {
499
+ // ── Auth Capabilities Endpoint ───────────────────────────────────
500
+ // Exposes adapter capabilities so the frontend knows what's available
501
+ // (login form vs external redirect, OAuth providers, etc.)
502
+ config.app.get(`${basePath}/auth/config`, async (c) => {
503
+ const capabilities = await authAdapter!.getCapabilities();
504
+ return c.json(capabilities);
505
+ });
355
506
 
356
- if (config.auth.google?.clientId) {
357
- const { createGoogleProvider } = await import("./auth");
358
- oauthProviders.push(createGoogleProvider(config.auth.google));
359
- }
507
+ if (!isAuthAdapter(config.auth)) {
508
+ const safeAuthConfig = config.auth as RebaseAuthConfig;
509
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
510
+ const oauthProviders: OAuthProvider<any>[] = [...(safeAuthConfig.providers || [])];
360
511
 
361
- if (config.auth.linkedin?.clientId && config.auth.linkedin?.clientSecret) {
362
- const { createLinkedinProvider } = await import("./auth");
363
- oauthProviders.push(createLinkedinProvider(config.auth.linkedin as { clientId: string; clientSecret: string }));
364
- }
512
+ if (safeAuthConfig.google?.clientId) {
513
+ const { createGoogleProvider } = await import("./auth");
514
+ oauthProviders.push(createGoogleProvider(safeAuthConfig.google));
515
+ }
365
516
 
366
- if (config.auth.github?.clientId && config.auth.github?.clientSecret) {
367
- const { createGitHubProvider } = await import("./auth");
368
- oauthProviders.push(createGitHubProvider(config.auth.github));
369
- }
517
+ if (safeAuthConfig.linkedin?.clientId && safeAuthConfig.linkedin?.clientSecret) {
518
+ const { createLinkedinProvider } = await import("./auth");
519
+ oauthProviders.push(createLinkedinProvider(safeAuthConfig.linkedin as { clientId: string; clientSecret: string }));
520
+ }
370
521
 
371
- if (config.auth.microsoft?.clientId && config.auth.microsoft?.clientSecret) {
372
- const { createMicrosoftProvider } = await import("./auth");
373
- oauthProviders.push(createMicrosoftProvider(config.auth.microsoft));
374
- }
522
+ if (safeAuthConfig.github?.clientId && safeAuthConfig.github?.clientSecret) {
523
+ const { createGitHubProvider } = await import("./auth");
524
+ oauthProviders.push(createGitHubProvider(safeAuthConfig.github));
525
+ }
375
526
 
376
- if (config.auth.apple?.clientId && config.auth.apple?.teamId && config.auth.apple?.keyId && config.auth.apple?.privateKey) {
377
- const { createAppleProvider } = await import("./auth");
378
- oauthProviders.push(createAppleProvider(config.auth.apple));
379
- }
527
+ if (safeAuthConfig.microsoft?.clientId && safeAuthConfig.microsoft?.clientSecret) {
528
+ const { createMicrosoftProvider } = await import("./auth");
529
+ oauthProviders.push(createMicrosoftProvider(safeAuthConfig.microsoft));
530
+ }
380
531
 
381
- if (config.auth.facebook?.clientId && config.auth.facebook?.clientSecret) {
382
- const { createFacebookProvider } = await import("./auth");
383
- oauthProviders.push(createFacebookProvider(config.auth.facebook));
384
- }
532
+ if (safeAuthConfig.apple?.clientId && safeAuthConfig.apple?.teamId && safeAuthConfig.apple?.keyId && safeAuthConfig.apple?.privateKey) {
533
+ const { createAppleProvider } = await import("./auth");
534
+ oauthProviders.push(createAppleProvider(safeAuthConfig.apple));
535
+ }
385
536
 
386
- if (config.auth.twitter?.clientId && config.auth.twitter?.clientSecret) {
387
- const { createTwitterProvider } = await import("./auth");
388
- oauthProviders.push(createTwitterProvider(config.auth.twitter));
389
- }
537
+ if (safeAuthConfig.facebook?.clientId && safeAuthConfig.facebook?.clientSecret) {
538
+ const { createFacebookProvider } = await import("./auth");
539
+ oauthProviders.push(createFacebookProvider(safeAuthConfig.facebook));
540
+ }
390
541
 
391
- if (config.auth.discord?.clientId && config.auth.discord?.clientSecret) {
392
- const { createDiscordProvider } = await import("./auth");
393
- oauthProviders.push(createDiscordProvider(config.auth.discord));
394
- }
542
+ if (safeAuthConfig.twitter?.clientId && safeAuthConfig.twitter?.clientSecret) {
543
+ const { createTwitterProvider } = await import("./auth");
544
+ oauthProviders.push(createTwitterProvider(safeAuthConfig.twitter));
545
+ }
395
546
 
396
- if (config.auth.gitlab?.clientId && config.auth.gitlab?.clientSecret) {
397
- const { createGitLabProvider } = await import("./auth");
398
- oauthProviders.push(createGitLabProvider(config.auth.gitlab));
399
- }
547
+ if (safeAuthConfig.discord?.clientId && safeAuthConfig.discord?.clientSecret) {
548
+ const { createDiscordProvider } = await import("./auth");
549
+ oauthProviders.push(createDiscordProvider(safeAuthConfig.discord));
550
+ }
400
551
 
401
- if (config.auth.bitbucket?.clientId && config.auth.bitbucket?.clientSecret) {
402
- const { createBitbucketProvider } = await import("./auth");
403
- oauthProviders.push(createBitbucketProvider(config.auth.bitbucket));
404
- }
552
+ if (safeAuthConfig.gitlab?.clientId && safeAuthConfig.gitlab?.clientSecret) {
553
+ const { createGitLabProvider } = await import("./auth");
554
+ oauthProviders.push(createGitLabProvider(safeAuthConfig.gitlab));
555
+ }
405
556
 
406
- if (config.auth.slack?.clientId && config.auth.slack?.clientSecret) {
407
- const { createSlackProvider } = await import("./auth");
408
- oauthProviders.push(createSlackProvider(config.auth.slack));
557
+ if (safeAuthConfig.bitbucket?.clientId && safeAuthConfig.bitbucket?.clientSecret) {
558
+ const { createBitbucketProvider } = await import("./auth");
559
+ oauthProviders.push(createBitbucketProvider(safeAuthConfig.bitbucket));
560
+ }
561
+
562
+ if (safeAuthConfig.slack?.clientId && safeAuthConfig.slack?.clientSecret) {
563
+ const { createSlackProvider } = await import("./auth");
564
+ oauthProviders.push(createSlackProvider(safeAuthConfig.slack));
565
+ }
566
+
567
+ if (safeAuthConfig.spotify?.clientId && safeAuthConfig.spotify?.clientSecret) {
568
+ const { createSpotifyProvider } = await import("./auth");
569
+ oauthProviders.push(createSpotifyProvider(safeAuthConfig.spotify));
570
+ }
571
+
572
+ // Re-create the built-in adapter with all resolved OAuth providers
573
+ authAdapter = createBuiltinAuthAdapter({
574
+ authRepository: authConfigResult!.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult!.userService as import("./auth/interfaces").AuthRepository,
575
+ emailService: authConfigResult!.emailService as import("./email").EmailService,
576
+ emailConfig: safeAuthConfig.email,
577
+ allowRegistration: safeAuthConfig.allowRegistration ?? false,
578
+ defaultRole: safeAuthConfig.defaultRole,
579
+ oauthProviders,
580
+ serviceKey,
581
+ hooks: config.hooks,
582
+ overrides: safeAuthConfig.overrides,
583
+ });
409
584
  }
410
585
 
411
- if (config.auth.spotify?.clientId && config.auth.spotify?.clientSecret) {
412
- const { createSpotifyProvider } = await import("./auth");
413
- oauthProviders.push(createSpotifyProvider(config.auth.spotify));
586
+ // ── Mount auth & admin routes via the adapter ────────────────────
587
+ if (authAdapter.createAuthRoutes) {
588
+ const authRoutes = authAdapter.createAuthRoutes();
589
+ if (authRoutes) {
590
+ config.app.route(`${basePath}/auth`, authRoutes);
591
+ logger.info("Auth routes mounted via adapter", { adapter: authAdapter.id });
592
+ }
414
593
  }
415
594
 
416
- const authRoutes = createAuthRoutes({
417
- authRepo: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
418
- emailService: authConfigResult.emailService as import("./email").EmailService,
419
- emailConfig: config.auth.email,
420
- allowRegistration: config.auth.allowRegistration ?? false,
421
- defaultRole: config.auth.defaultRole,
422
- oauthProviders
423
- });
424
- config.app.route(`${basePath}/auth`, authRoutes);
425
-
426
- const adminRoutes = createAdminRoutes({
427
- authRepo: authConfigResult.authRepository as import("./auth/interfaces").AuthRepository ?? authConfigResult.userService as import("./auth/interfaces").AuthRepository,
428
- emailService: authConfigResult.emailService as import("./email").EmailService,
429
- emailConfig: config.auth.email,
430
- serviceKey,
431
- hooks: config.hooks
432
- });
433
- config.app.route(`${basePath}/admin`, adminRoutes);
595
+ if (authAdapter.createAdminRoutes) {
596
+ const adminRoutes = authAdapter.createAdminRoutes();
597
+ if (adminRoutes) {
598
+ config.app.route(`${basePath}/admin`, adminRoutes);
599
+ logger.info("Admin routes mounted via adapter", { adapter: authAdapter.id });
600
+ }
601
+ }
434
602
  }
435
603
 
436
604
  if (config.collectionsDir) {
@@ -438,7 +606,13 @@ collectionRegistry });
438
606
  const { createSchemaEditorRoutes } = await import("./api/schema-editor-routes");
439
607
  const schemaEditorRoutes = createSchemaEditorRoutes(config.collectionsDir);
440
608
 
441
- if (config.auth?.requireAuth !== false && !!config.auth?.jwtSecret) {
609
+ if (authAdapter && !isAuthAdapter(config.auth!)) {
610
+ const safeAuth = config.auth as RebaseAuthConfig;
611
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) {
612
+ schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
613
+ }
614
+ } else if (authAdapter) {
615
+ // External auth adapter — still protect schema editor
442
616
  schemaEditorRoutes.use("/*", requireAuth, requireAdmin);
443
617
  }
444
618
 
@@ -458,7 +632,7 @@ collectionRegistry });
458
632
 
459
633
  const storageRoutes = createStorageRoutes({
460
634
  controller: storageController,
461
- requireAuth: config.auth?.requireAuth ?? true
635
+ requireAuth: resolveRequireAuth(config.auth)
462
636
  });
463
637
 
464
638
  // Apply a permissive body limit specifically for the upload endpoint
@@ -484,7 +658,7 @@ collectionRegistry });
484
658
  // Secure by default: require auth when auth is configured.
485
659
  // Developers who intentionally want public data access (relying
486
660
  // entirely on Postgres RLS) must explicitly set `auth.requireAuth: false`.
487
- const dataRequireAuth = config.auth?.requireAuth !== false;
661
+ const dataRequireAuth = resolveRequireAuth(config.auth);
488
662
 
489
663
  if (!dataRequireAuth) {
490
664
  logger.warn(
@@ -495,11 +669,21 @@ collectionRegistry });
495
669
  );
496
670
  }
497
671
 
498
- dataRouter.use("/*", createAuthMiddleware({
499
- driver: defaultDriver,
500
- requireAuth: dataRequireAuth,
501
- serviceKey
502
- }));
672
+ // Use adapter middleware when an AuthAdapter is available,
673
+ // falling back to the built-in JWT middleware otherwise.
674
+ if (authAdapter) {
675
+ dataRouter.use("/*", createAdapterAuthMiddleware({
676
+ adapter: authAdapter,
677
+ driver: defaultDriver,
678
+ requireAuth: dataRequireAuth,
679
+ }));
680
+ } else {
681
+ dataRouter.use("/*", createAuthMiddleware({
682
+ driver: defaultDriver,
683
+ requireAuth: dataRequireAuth,
684
+ serviceKey
685
+ }));
686
+ }
503
687
 
504
688
  // Mount history routes BEFORE the REST API subcollection catch-all so
505
689
  // that /:slug/:entityId/history is matched by the dedicated handler first.
@@ -525,7 +709,7 @@ collectionRegistry });
525
709
  config.app.get(`${basePath}/docs`, (c) => {
526
710
  const spec = generateOpenApiSpec(activeCollections, {
527
711
  basePath,
528
- requireAuth: config.auth?.requireAuth !== false
712
+ requireAuth: resolveRequireAuth(config.auth)
529
713
  });
530
714
  return c.json(spec);
531
715
  });
@@ -560,6 +744,7 @@ collectionRegistry });
560
744
  baseUrl: "http://localhost",
561
745
  apiPath: basePath,
562
746
  websocketUrl: "",
747
+ token: serviceKey,
563
748
  fetch: async (input: RequestInfo | URL, init?: RequestInit) => {
564
749
  return await config.app.request(input as string | Request | URL, init);
565
750
  }
@@ -570,8 +755,8 @@ collectionRegistry });
570
755
  let emailService: EmailService | undefined;
571
756
  if (authConfigResult?.emailService) {
572
757
  emailService = authConfigResult.emailService as EmailService;
573
- } else if (config.auth?.email) {
574
- emailService = createEmailService(config.auth.email);
758
+ } else if (config.auth && !isAuthAdapter(config.auth) && (config.auth as RebaseAuthConfig).email) {
759
+ emailService = createEmailService((config.auth as RebaseAuthConfig).email!);
575
760
  }
576
761
 
577
762
  if (emailService) {
@@ -607,13 +792,22 @@ collectionRegistry });
607
792
 
608
793
  // Custom functions follow the same auth policy as data routes.
609
794
  // Per-route auth can be further refined inside individual functions.
610
- const functionsRequireAuth = config.auth?.requireAuth !== false;
611
-
612
- functionsRouter.use("/*", createAuthMiddleware({
613
- driver: defaultDriver,
614
- requireAuth: functionsRequireAuth,
615
- serviceKey
616
- }));
795
+ const functionsRequireAuth = resolveRequireAuth(config.auth);
796
+
797
+ // Use adapter middleware when available, fallback to built-in
798
+ if (authAdapter) {
799
+ functionsRouter.use("/*", createAdapterAuthMiddleware({
800
+ adapter: authAdapter,
801
+ driver: defaultDriver,
802
+ requireAuth: functionsRequireAuth,
803
+ }));
804
+ } else {
805
+ functionsRouter.use("/*", createAuthMiddleware({
806
+ driver: defaultDriver,
807
+ requireAuth: functionsRequireAuth,
808
+ serviceKey
809
+ }));
810
+ }
617
811
 
618
812
  const fnRoutes = createFunctionRoutes(loadedFunctions);
619
813
  functionsRouter.route("/", fnRoutes);
@@ -642,8 +836,9 @@ path: `${basePath}/functions` });
642
836
 
643
837
  cronScheduler.registerJobs(loadedCronJobs);
644
838
 
645
- // Attach database persistence if the driver supports SQL
646
- const store = createCronStore(defaultDriver);
839
+ // Attach database persistence if the driver supports SQL and persistence is enabled
840
+ const admin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
841
+ const store = (admin && config.cronPersistence !== false) ? createCronStore(defaultDriver) : undefined;
647
842
  if (store) {
648
843
  await store.ensureTable();
649
844
  cronScheduler.setStore(store);
@@ -652,7 +847,12 @@ path: `${basePath}/functions` });
652
847
  const cronRouter = new Hono<HonoEnv>();
653
848
 
654
849
  // Cron admin routes require authentication + admin role
655
- if (config.auth?.requireAuth !== false && !!config.auth?.jwtSecret) {
850
+ if (authAdapter && !isAuthAdapter(config.auth!)) {
851
+ const safeAuth = config.auth as RebaseAuthConfig;
852
+ if (safeAuth.requireAuth !== false && !!safeAuth.jwtSecret) {
853
+ cronRouter.use("/*", requireAuth, requireAdmin);
854
+ }
855
+ } else if (authAdapter) {
656
856
  cronRouter.use("/*", requireAuth, requireAdmin);
657
857
  }
658
858
 
@@ -668,7 +868,7 @@ path: `${basePath}/cron` });
668
868
  }
669
869
 
670
870
  if ((defaultBootstrapper as BackendBootstrapper & { initializeWebsockets?: (...args: unknown[]) => unknown }).initializeWebsockets) {
671
- await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth);
871
+ await (defaultBootstrapper as BackendBootstrapper & { initializeWebsockets: (...args: unknown[]) => unknown }).initializeWebsockets(config.server, defaultRealtimeService, defaultDriver, config.auth, authAdapter);
672
872
  }
673
873
 
674
874
  logger.info("Rebase Backend Initialized");
@@ -708,27 +908,47 @@ latencyMs };
708
908
  // ── Graceful Shutdown ─────────────────────────────────────────────────
709
909
  const shutdown = (timeoutMs = 15_000): Promise<void> => {
710
910
  return new Promise<void>((resolve) => {
711
- logger.info("Shutting down Rebase Backend...");
911
+ (async () => {
912
+ logger.info("Shutting down Rebase Backend...");
712
913
 
713
- // 1. Stop cron scheduler
714
- if (cronScheduler) {
715
- cronScheduler.stop();
716
- logger.info("Cron scheduler stopped");
717
- }
914
+ // 1. Stop cron scheduler
915
+ if (cronScheduler) {
916
+ cronScheduler.stop();
917
+ logger.info("Cron scheduler stopped");
918
+ }
718
919
 
719
- // 2. Close the HTTP server (stop accepting, drain in-flight)
720
- config.server.close(() => {
721
- logger.info("HTTP server closed");
722
- resolve();
723
- });
920
+ // 2. Tear down realtime services (LISTEN clients, debounce timers,
921
+ // subscriptions). Must happen BEFORE pool.end() so that pending
922
+ // timer callbacks don't fire against a closed pool.
923
+ for (const [key, rt] of Object.entries(realtimeServices)) {
924
+ try {
925
+ const rtWithLifecycle = rt as RealtimeProvider & { destroy?: () => Promise<void>; stopListening?: () => Promise<void> };
926
+ if (typeof rtWithLifecycle.destroy === "function") {
927
+ await rtWithLifecycle.destroy();
928
+ logger.info(`Realtime service "${key}" destroyed`);
929
+ } else if (typeof rtWithLifecycle.stopListening === "function") {
930
+ await rtWithLifecycle.stopListening();
931
+ logger.info(`Realtime service "${key}" LISTEN client stopped`);
932
+ }
933
+ } catch (err) {
934
+ logger.warn(`Error destroying realtime service "${key}":`, { error: err });
935
+ }
936
+ }
724
937
 
725
- // 3. Force-resolve after timeout (unless disabled with 0)
726
- if (timeoutMs > 0) {
727
- setTimeout(() => {
728
- logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
938
+ // 3. Close the HTTP server (stop accepting, drain in-flight)
939
+ config.server.close(() => {
940
+ logger.info("HTTP server closed");
729
941
  resolve();
730
- }, timeoutMs).unref();
731
- }
942
+ });
943
+
944
+ // 4. Force-resolve after timeout (unless disabled with 0)
945
+ if (timeoutMs > 0) {
946
+ setTimeout(() => {
947
+ logger.warn(`Forced shutdown after ${timeoutMs / 1000}s timeout`);
948
+ resolve();
949
+ }, timeoutMs).unref();
950
+ }
951
+ })();
732
952
  });
733
953
  };
734
954