@rebasepro/agent-skills 0.9.1-canary.fd3754b → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/agent-skills",
3
- "version": "0.9.1-canary.fd3754b",
3
+ "version": "0.10.0",
4
4
  "private": false,
5
5
  "description": "Bundled AI agent skills for Rebase",
6
6
  "type": "module",
@@ -745,6 +745,36 @@ Views participate in the same navigation group system as collections. Use the `g
745
745
 
746
746
  ---
747
747
 
748
+ ## 11. Where the Admin Lives — Mounting Under a URL Prefix
749
+
750
+ In the default scaffold the frontend **is** the admin panel: `App.tsx` renders `<RebaseAuth>` + `<RebaseAdmin>` + `<RebaseShell>` at the root, so the deployed URL shows the admin directly. There is no separate admin URL — in production one server serves both the API (`/api/*`) and this SPA.
751
+
752
+ When a project ships its **own product app** as the frontend, the admin is mounted under a prefix (commonly `/admin`) instead. Split the Vite entry by URL so each app is lazy-loaded and product visitors never download the admin bundle:
753
+
754
+ ```tsx
755
+ // frontend/src/main.tsx
756
+ const isAdmin = window.location.pathname.startsWith("/admin");
757
+ const ProductApp = lazy(() => import("./App"));
758
+ const AdminApp = lazy(() => import("./AdminApp"));
759
+
760
+ if (isAdmin) {
761
+ // The admin uses useBlocker → needs a DATA router
762
+ const router = createBrowserRouter([{ path: "/admin/*", element: <AdminApp /> }]);
763
+ root.render(<RouterProvider router={router} />);
764
+ } else {
765
+ root.render(<BrowserRouter><ProductApp /></BrowserRouter>);
766
+ }
767
+ ```
768
+
769
+ ```tsx
770
+ // frontend/src/AdminApp.tsx — tell the CMS about the prefix
771
+ <RebaseAdmin collections={collections} basePath="/admin" />
772
+ ```
773
+
774
+ > **IMPORTANT FOR AGENTS:** Choose **either** a router `basename="/admin"` **or** `<RebaseAdmin basePath="/admin">` — never both, or the prefix is applied twice. Without either, collection views hang on a spinner because URL⇄collection resolution never matches. `<RebaseAdmin>` requires a **data router** (`createBrowserRouter`) because it uses `useBlocker`.
775
+
776
+ ---
777
+
748
778
  ## Exported Components
749
779
 
750
780
  The admin package exports the following components (from `@rebasepro/admin`):
@@ -337,11 +337,17 @@ Authorization: Bearer rk_live_abc123...
337
337
 
338
338
  If an API key lacks the required permission for an operation, a `403 API_KEY_FORBIDDEN` error is returned.
339
339
 
340
- **Admin API keys** — set `"admin": true` when creating a key to grant it the `admin` role. This gives access to all `/api/admin/*` routes (schema, users, other API keys, etc.). Use this for agents, MCP servers, and CI pipelines:
340
+ Beyond collections, the permission list also covers custom functions
341
+ (`"functions"` for all, `"functions/<name>"` for one) and file storage
342
+ (`"storage"`); the global `"*"` wildcard grants all three. API keys do NOT
343
+ bypass RLS: admin keys pass via the built-in admin policies, while non-admin
344
+ keys only see rows a security rule grants to the `service` role or the public.
345
+
346
+ **Admin API keys** — set `"admin": true` when creating a key to grant it the `admin` role. This gives access to all `/api/admin/*` routes (schema, users, other API keys, etc.) plus cron, backups, and logs. Use this for agents, MCP servers, and CI pipelines:
341
347
 
342
348
  ```bash
343
- # CLI
344
- rebase api-keys create --name "My Agent" --admin
349
+ # CLI (an explicit scope is required: --permissions '<json>' or --full-access)
350
+ rebase api-keys create --name "My Agent" --admin --full-access
345
351
 
346
352
  # REST
347
353
  POST /api/admin/api-keys
@@ -219,11 +219,48 @@ Twitter uses OAuth 2.0 with PKCE. The client must send `codeVerifier` alongside
219
219
 
220
220
  ### OAuth Account Linking
221
221
 
222
- When an OAuth user signs in:
223
- 1. If an identity record exists for `(providerId, provider)` → log in that user.
224
- 2. If no identity exists but a user with the same email exists **link** the provider to the existing account.
222
+ When an OAuth user signs in via `POST /api/auth/{provider}`:
223
+
224
+ 1. If an identity record exists for `(provider, providerId)` log in that user. The email is not consulted.
225
+ 2. If no identity exists but a user with the same email exists:
226
+ - **The provider asserted `emailVerified: true`** → **link** the provider to the existing account and log in as that user. One account, two sign-in methods.
227
+ - **The provider did NOT verify the email** → reject with `403 EMAIL_NOT_VERIFIED`. Nothing is created or modified.
225
228
  3. If neither exists → create a new user, link the identity, assign `defaultRole`.
226
229
 
230
+ > **IMPORTANT FOR AGENTS:** A second account is **never** silently created for
231
+ > an email that already exists. If asked "does signing in with Google create a
232
+ > duplicate user?", the answer is no — it either links (verified) or errors
233
+ > (unverified). This is **not configurable**; there is deliberately no option to
234
+ > auto-link on unverified emails, because that would let anyone who can make a
235
+ > provider emit an address they don't own take over the matching account.
236
+ > Google always asserts `email_verified` for real Google accounts, so linking
237
+ > is the normal path for Google sign-in.
238
+
239
+ ### Linking a Provider to a Signed-In Account
240
+
241
+ `POST /api/auth/link/{provider}` attaches a provider identity to the **already
242
+ authenticated** account (requires `Authorization: Bearer <token>`). The body is
243
+ the same payload the provider's sign-in route takes, e.g. `{ idToken }`.
244
+
245
+ This is the escape hatch from an `EMAIL_NOT_VERIFIED` rejection, and the way to
246
+ attach a provider whose email differs from the account's.
247
+
248
+ Unlike sign-in, linking here does **not** require a verified email and does not
249
+ require the emails to match — on sign-in the provider's email is the only
250
+ evidence tying the identity to an account, whereas here the caller has already
251
+ proven ownership by holding a valid session.
252
+
253
+ - `409 IDENTITY_ALREADY_LINKED` if that provider identity belongs to another user.
254
+ - Idempotent (`alreadyLinked: true`) if already linked to the caller.
255
+
256
+ ### Adding a Password to a Provider-Only Account
257
+
258
+ A user who signed up via OAuth has no `passwordHash`:
259
+
260
+ - `POST /auth/register` with the same email → `409 EMAIL_EXISTS`.
261
+ - `POST /auth/change-password` → `400 INVALID_ACCOUNT` (no existing password to verify).
262
+ - **`forgot-password` → `reset-password` is the supported path.** It re-proves ownership of the address by email, after which the account has both sign-in methods.
263
+
227
264
  ### Custom OAuth Provider
228
265
 
229
266
  You can register any OAuth provider by implementing the `OAuthProvider<T>` interface:
@@ -468,7 +505,7 @@ By default API keys get the `service` role (data access only). Set `"admin": tru
468
505
 
469
506
  ```bash
470
507
  # CLI — create an admin API key
471
- rebase api-keys create --name "My Agent" --admin
508
+ rebase api-keys create --name "My Agent" --admin --full-access
472
509
 
473
510
  # REST
474
511
  curl -X POST http://localhost:3000/api/admin/api-keys \
@@ -624,7 +661,7 @@ rebase api-keys list
624
661
  rebase api-keys create --name "Read Only" --permissions '[{"collection":"orders","operations":["read"]}]'
625
662
 
626
663
  # Create an admin key (for agents / MCP / CI)
627
- rebase api-keys create --name "My Agent" --admin
664
+ rebase api-keys create --name "My Agent" --admin --full-access
628
665
 
629
666
  # Revoke a key
630
667
  rebase api-keys revoke <key-id>
@@ -659,6 +696,7 @@ All auth endpoints are mounted under `/api/auth`. Admin endpoints are under `/ap
659
696
  | `PATCH` | `/auth/me` | Required | Update profile. Body: `{ displayName?, photoURL? }`. |
660
697
  | `POST` | `/auth/change-password` | Required | Change password. Body: `{ oldPassword, newPassword }`. Invalidates all sessions. |
661
698
  | `POST` | `/auth/send-verification` | Required | Send email verification link. Requires email service. |
699
+ | `POST` | `/auth/link/{provider}` | Required | Link an OAuth provider to the current account. Body: the provider's sign-in payload (e.g. `{ idToken }`). `409 IDENTITY_ALREADY_LINKED` if it belongs to another user. |
662
700
  | `GET` | `/auth/sessions` | Required | List active sessions (refresh tokens). |
663
701
  | `DELETE` | `/auth/sessions` | Required | Revoke all sessions (remote logout). |
664
702
  | `DELETE` | `/auth/sessions/:id` | Required | Revoke a specific session. |
@@ -95,7 +95,7 @@ const { db, pool, connectionString } = createPostgresDatabaseConnection(
95
95
 
96
96
  **Signature:**
97
97
 
98
- ```typescript
98
+ ```typescript no-verify
99
99
  function createPostgresDatabaseConnection(
100
100
  connectionString: string,
101
101
  schema?: Record<string, unknown>,
@@ -120,7 +120,7 @@ const readResources = createReadReplicaConnection(
120
120
 
121
121
  **Signature:**
122
122
 
123
- ```typescript
123
+ ```typescript no-verify
124
124
  function createReadReplicaConnection(
125
125
  connectionString: string,
126
126
  schema?: Record<string, unknown>,
@@ -145,7 +145,7 @@ const directResources = createDirectDatabaseConnection(
145
145
 
146
146
  **Signature:**
147
147
 
148
- ```typescript
148
+ ```typescript no-verify
149
149
  function createDirectDatabaseConnection(
150
150
  connectionString: string,
151
151
  schema?: Record<string, unknown>,
@@ -327,12 +327,13 @@ The `initializeRebaseBackend()` coordinator calls bootstrapper methods in this o
327
327
  | `auth` | `RebaseAuthConfig \| AuthAdapter` | — | Authentication configuration or external adapter |
328
328
  | `storage` | `BackendStorageConfig \| StorageController \| Record<string, ...>` | — | File storage configuration |
329
329
  | `history` | `true \| { retention?: number }` | — | Enable entity audit trail. Pass `true` or `{ retention: 90 }` for TTL in days |
330
- | `defaultSecurityRules` | `SecurityRule[]` | — | Default RLS rules for collections without explicit rules |
331
330
  | `enableSwagger` | `boolean` | `true` | Enable OpenAPI/Swagger documentation |
332
331
  | `cronPersistence` | `boolean` | `true` | Persist cron execution logs to database |
333
332
  | `maxBodySize` | `number` | `10485760` (10MB) | Max request body size in bytes. `0` to disable |
334
333
  | `csrf` | `{ origin: string \| string[] \| (origin: string) => boolean }` | — | CSRF protection (opt-in, disabled by default) |
335
- | `hooks` | `BackendHooks` | — | Backend-level hooks for intercepting admin data |
334
+ | `callbacks` | `CollectionCallbacks` | — | Global lifecycle callbacks applied to every collection, on every data path (REST, realtime, server-side `rebase.data`) |
335
+ | `mode` | `"cms" \| "baas"` | `"cms"` | `baas` derives collections from the live database at boot instead of config files, and turns the schema editor off |
336
+ | `baas` | `BaasOptions` | — | `baas` mode only: `{ unprotectedTables?: "exclude" \| "serve" }`. Default `"exclude"` — a table without RLS has no authorization model, so serving it would hand every row to every logged-in user. `"serve"` overrides, for trusted callers only |
336
337
  | `logging` | `{ level?: "error" \| "warn" \| "info" \| "debug" }` | — | Log level configuration |
337
338
 
338
339
  ### Auth Configuration (RebaseAuthConfig)
@@ -386,7 +387,8 @@ export const env = loadEnv();
386
387
 
387
388
  ```typescript
388
389
  import dotenv from "dotenv";
389
- import { loadEnv, z } from "@rebasepro/server";
390
+ import { loadEnv } from "@rebasepro/server";
391
+ import { z } from "zod";
390
392
 
391
393
  dotenv.config({ path: "../../.env" });
392
394
 
@@ -436,9 +438,9 @@ In non-production mode, `loadEnv()` **auto-generates** ephemeral `JWT_SECRET` an
436
438
  | `ALLOW_LOCALHOST_IN_PRODUCTION` | No | `false` | Allow localhost URLs in production env vars |
437
439
  | `CORS_ORIGINS` | No (⚠ required in prod) | — | Comma-separated allowed origins |
438
440
  | `FRONTEND_URL` | No | — | Frontend URL (used for CORS) |
439
- | `STORAGE_TYPE` | No | `local` | `local` or `s3` |
441
+ | `STORAGE_TYPE` | No | `local` | `local`, `s3`, or `gcs` |
440
442
  | `STORAGE_PATH` | No | — | Path for local file storage |
441
- | `FORCE_LOCAL_STORAGE` | No | `false` | Suppress local storage warning in production |
443
+ | `FORCE_LOCAL_STORAGE` | No | `false` | Allow local storage in production — **the backend refuses to boot without it** |
442
444
  | `S3_BUCKET` | No (if s3) | — | S3 bucket name |
443
445
  | `S3_REGION` | No | — | S3 region |
444
446
  | `S3_ACCESS_KEY_ID` | No (if s3) | — | S3 access key |
@@ -168,12 +168,28 @@ A BaaS install is `server` + a driver + `client`, with no React in the tree.
168
168
 
169
169
  | Option | Alias | Description |
170
170
  |--------|-------|-------------|
171
+ | `--flavor` | `-f` | `baas` or `cms`. Picks what you get — see below. Prompts when omitted |
172
+ | `--template` | `-t` | Starter template to scaffold from |
171
173
  | `--git` | `-g` | Initialize a git repository |
172
174
  | `--install` | `-i` | Install dependencies with the detected PM |
173
175
  | `--database-url` | — | PostgreSQL connection string (skip prompt) |
174
176
  | `--introspect` | — | Auto-introspect the database after init |
175
177
  | `--yes` | `-y` | Non-interactive mode (use all defaults) |
176
178
 
179
+ #### Flavors
180
+
181
+ ```bash
182
+ rebase init my-api --flavor baas # backend/ alone — REST, auth, storage, realtime, backups. No config files, no UI, no React
183
+ rebase init my-app --flavor cms # config/ + backend/ + frontend/ — BaaS plus the admin panel (default)
184
+ ```
185
+
186
+ `baas` introspects the collections from the database at boot rather than reading
187
+ config files, so there is nothing to declare and nothing to keep in sync. It
188
+ serves only tables with `ENABLE ROW LEVEL SECURITY` — a table without RLS has no
189
+ authorization model, so serving it would hand every row to every logged-in user.
190
+
191
+ `dev`, `build` and `start` detect a missing `frontend/` and run backend-only.
192
+
177
193
  ```bash
178
194
  # Interactive mode
179
195
  rebase init my-app
@@ -392,7 +408,7 @@ When `extend` is provided, the base `rebaseEnvSchema` is merged (`.merge()`) wit
392
408
  | `DB_POOL_CONNECT_TIMEOUT` | `string` → `number` | `"10000"` | No | Pool connect timeout (ms) |
393
409
  | `STORAGE_TYPE` | `"local" \| "s3" \| "gcs"` | `"local"` | No | File storage backend type |
394
410
  | `STORAGE_PATH` | `string` | — | No | Local storage directory path |
395
- | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | — | No | Force local storage even in production |
411
+ | `FORCE_LOCAL_STORAGE` | `"true" \| "false"` | — | No | Allow local storage in production — **the backend refuses to boot without it**, since the container filesystem loses uploads on restart |
396
412
  | `S3_BUCKET` | `string` | — | When S3 | S3 bucket name |
397
413
  | `S3_REGION` | `string` | — | When S3 | S3 region |
398
414
  | `S3_ACCESS_KEY_ID` | `string` | — | When S3 | S3 access key |
@@ -436,6 +452,7 @@ import { initializeRebaseBackend, RebaseBackendConfig } from "@rebasepro/server"
436
452
  |----------|------|---------|-------------|
437
453
  | `server` | `Server` (Node `http.Server`) | — | **Required.** The HTTP server instance |
438
454
  | `app` | `Hono<HonoEnv>` | — | **Required.** The Hono application instance |
455
+ | `mode` | `"cms" \| "baas"` | `"cms"` | How much of Rebase to run. `cms` takes collections from `collections`/`collectionsDir`; `baas` takes no collection config at all and derives them from the live database at boot, so every RLS-protected table is served with nothing to define. The schema editor is off in `baas` — it exists to write collection files back to disk. Both serve the same control plane (auth, storage, realtime, backups, cron, functions, OpenAPI); neither serves the admin SPA, which is the application's call via `serveSPA` |
439
456
  | `collections` | `CollectionConfig[]` | `[]` | Inline collection definitions |
440
457
  | `collectionsDir` | `string` | — | Directory to auto-discover collection files (used if `collections` is empty) |
441
458
  | `basePath` | `string` | `"/api"` | Base path for all API routes |
@@ -445,14 +462,16 @@ import { initializeRebaseBackend, RebaseBackendConfig } from "@rebasepro/server"
445
462
  | `auth` | `RebaseAuthConfig \| AuthAdapter` | — | Authentication config or pluggable adapter |
446
463
  | `storage` | `BackendStorageConfig \| StorageController \| Record<string, ...>` | — | File storage configuration. Supports `"local"`, `"s3"`, and `"gcs"` (GCS/Firebase Storage) backends. Use `Record<string, StorageController>` for multi-backend setups with named sources |
447
464
  | `history` | `unknown` | — | Entity history/audit-log configuration |
448
- | `defaultSecurityRules` | `SecurityRule[]` | — | Default RLS rules for collections without their own |
449
465
  | `enableSwagger` | `boolean` | `true` | Enable OpenAPI spec at `/api/docs` and Swagger UI at `/api/swagger` (dev only) |
450
466
  | `functionsDir` | `string` | — | Directory for auto-discovered custom function handlers |
451
467
  | `cronsDir` | `string` | — | Directory for auto-discovered cron job handlers |
452
468
  | `cronPersistence` | `boolean` | `true` | Persist cron job execution logs to the database |
453
469
  | `maxBodySize` | `number` | `10485760` (10 MB) | Max request body size in bytes. Set `0` to disable |
454
470
  | `csrf` | `{ origin: string \| string[] \| ((origin: string) => boolean) }` | — | CSRF protection (opt-in, disabled by default) |
455
- | `hooks` | `BackendHooks` | — | Backend-level hooks for intercepting admin data at the API boundary |
471
+ | `callbacks` | `CollectionCallbacks` | — | Global lifecycle callbacks applied to every collection. Same type as per-collection `callbacks`, and fires on **every** data path (REST, realtime/WebSocket, server-side `rebase.data`). Order: global → collection → property |
472
+ | `baas` | `BaasOptions` | — | `baas` mode only: `{ unprotectedTables?: "exclude" \| "serve" }`. Default `"exclude"` — a table with RLS disabled carries no authorization model, and every authenticated request runs as `rebase_user`, so serving one hands every row to every logged-in user. Excluded tables are logged with the SQL to protect them. `"serve"` serves them anyway; only sensible when every caller is already trusted |
473
+ | `schemaEditor` | `boolean` | — | Force the schema-editor routes on or off. Defaults to enabled when `collectionsDir` is set, outside production, in `cms` mode |
474
+ | `storageSources` | `StorageSourceDefinition[]` | — | Named storage backends for multi-source setups. Each has a `key` that collection properties point at via `StorageConfig.storageSource` |
456
475
  | `logging` | `{ level?: "error" \| "warn" \| "info" \| "debug" }` | `"info"` | Log level configuration |
457
476
 
458
477
  > **IMPORTANT FOR AGENTS:** `maxBodySize` applies to all API routes under `basePath`. Storage upload routes have their **own** limit derived from the storage config's `maxFileSize` property (default: 50 MB), which overrides the global limit.
@@ -554,15 +573,37 @@ await initializeRebaseBackend({
554
573
  // Single backend: { type: "local" | "s3" | "gcs" }
555
574
  // Multi-backend: Record<string, StorageController> with named sources
556
575
  storage: { type: env.STORAGE_TYPE },
557
- defaultSecurityRules: [
558
- { operation: "select", access: "public" },
559
- { operations: ["insert", "update", "delete"], roles: ["admin"] },
560
- ],
561
576
  });
562
577
 
563
578
  console.log(`Server running at http://localhost:${env.PORT}`);
564
579
  ```
565
580
 
581
+ ## Default security rules live with the collections, not here
582
+
583
+ `defaultSecurityRules` is **not** a `RebaseBackendConfig` option. Declare it in
584
+ `config/collections/index.ts`, beside the collections it applies to:
585
+
586
+ ```typescript
587
+ // config/collections/index.ts
588
+ import type { SecurityRule } from "@rebasepro/types";
589
+
590
+ export const defaultSecurityRules: SecurityRule[] = [
591
+ { operation: "select", access: "public" },
592
+ { operations: ["insert", "update", "delete"], roles: ["admin"] }
593
+ ];
594
+ ```
595
+
596
+ Any collection in that directory that declares no `securityRules` inherits these;
597
+ one that declares its own keeps them; one with neither is **locked by default**
598
+ (admin-only).
599
+
600
+ It belongs there because `db push` generates the Postgres policies — the only
601
+ thing that actually enforces access — from the collection *files*, and never sees
602
+ the running server. A default on the backend config could never reach the
603
+ database while reading exactly like an authorization setting. In `baas` mode there
604
+ are no collection files and no `db push`, so the database's own RLS is the whole
605
+ model and there is nothing to default.
606
+
566
607
  # The `rebase` Singleton
567
608
 
568
609
  After `initializeRebaseBackend()` completes, a server-side singleton is available:
@@ -495,7 +495,7 @@ validation: {
495
495
 
496
496
  Maps store nested objects as `JSONB` in PostgreSQL. They can define their own inner properties schema.
497
497
 
498
- ```typescript
498
+ ```typescript no-verify
499
499
  address: {
500
500
  name: "Address",
501
501
  type: "map",
@@ -663,7 +663,7 @@ Every property supports a `validation` object with these common options:
663
663
 
664
664
  Use the `enum` property on `string` or `number` types to define picklist options:
665
665
 
666
- ```typescript
666
+ ```typescript no-verify
667
667
  status: {
668
668
  name: "Status",
669
669
  type: "string",
@@ -687,7 +687,7 @@ Each `EnumValueConfig` entry supports:
687
687
 
688
688
  `EnumValues` can also be a `Record<string, string | EnumValueConfig>` for simpler definitions:
689
689
 
690
- ```typescript
690
+ ```typescript no-verify
691
691
  enum: {
692
692
  draft: "Draft",
693
693
  published: { label: "Published", color: "green" }
@@ -38,6 +38,36 @@ rebase deploy
38
38
  rebase deploy --env dev
39
39
  ```
40
40
 
41
+ ### What a Cloud Deployment Serves
42
+
43
+ `rebase deploy` ships **one container** per project, served at `https://<project>.apps.rebase.pro`. That container runs your backend, which handles:
44
+
45
+ - **`/api/*`** — the data API, auth, realtime, storage
46
+ - **everything else** — your built `frontend/` as a static SPA (via `serveSPA()`, see below)
47
+
48
+ There is **no separate admin URL** — the admin panel is part of your frontend, so where it appears depends on what your frontend is:
49
+
50
+ | Project type | What the root URL shows | Where the admin is |
51
+ |--------------|------------------------|--------------------|
52
+ | Default scaffold (`rebase init`, cms flavor) | The admin panel itself (login / bootstrap) | `/` — the frontend **is** the admin |
53
+ | Custom product frontend | Your product app | Wherever you mount it — commonly `/admin` (see below) |
54
+ | Backend-only (`--flavor backend`) | Nothing (API only) | Not deployed |
55
+
56
+ > **IMPORTANT FOR AGENTS:** On the **first visit** to a freshly deployed project's admin, Rebase shows the bootstrap screen ("Create your admin account"). The earliest-registered account receives admin privileges — the project owner should claim it immediately after deploying. Never fill this form on the user's behalf.
57
+
58
+ **Mounting the admin at `/admin` alongside a product app** — a single Vite entry can serve both, split by URL, so visitors never download the admin bundle:
59
+
60
+ ```tsx
61
+ // frontend/src/main.tsx
62
+ const isAdmin = window.location.pathname.startsWith("/admin");
63
+ const ProductApp = lazy(() => import("./App"));
64
+ const AdminApp = lazy(() => import("./AdminApp")); // renders <RebaseAdmin basePath="/admin" .../>
65
+
66
+ // route "/admin/*" → AdminApp, everything else → ProductApp
67
+ ```
68
+
69
+ Set **either** a router `basename="/admin"` **or** `<RebaseAdmin basePath="/admin">` — not both, or the prefix is applied twice.
70
+
41
71
  ---
42
72
 
43
73
  ## Docker (Self-Hosted)
@@ -318,7 +348,8 @@ Use the `extend` option to add your own typed env variables on top of the base R
318
348
 
319
349
  ```typescript
320
350
  import dotenv from "dotenv";
321
- import { loadEnv, z } from "@rebasepro/server";
351
+ import { loadEnv } from "@rebasepro/server";
352
+ import { z } from "zod";
322
353
 
323
354
  dotenv.config({ path: "../../.env" });
324
355
 
@@ -369,11 +400,16 @@ All variables recognized by the base `rebaseEnvSchema`:
369
400
  | `DATABASE_READ_URL` | `string` (URL) | No | — | Read replica database URL |
370
401
  | `STORAGE_TYPE` | `enum` | No | `"local"` | `"local"`, `"s3"`, or `"gcs"` |
371
402
  | `STORAGE_PATH` | `string` | No | — | Local file storage directory |
372
- | `FORCE_LOCAL_STORAGE` | `optionalBoolString` | No | `undefined` → `false` | Suppress local-storage-in-production warning |
403
+ | `FORCE_LOCAL_STORAGE` | `optionalBoolString` | No | `undefined` → `false` | Allow `STORAGE_TYPE=local` in production **the backend refuses to boot without it** |
373
404
  | `S3_BUCKET` | `string` | If S3 | — | S3 bucket name |
374
405
  | `S3_REGION` | `string` | No | — | S3 region |
375
406
  | `S3_ACCESS_KEY_ID` | `string` | If S3 | — | S3 access key |
376
407
  | `S3_SECRET_ACCESS_KEY` | `string` | If S3 | — | S3 secret key |
408
+ | `GCS_BUCKET` | `string` | If GCS | — | GCS bucket name |
409
+ | `GCS_PROJECT_ID` | `string` | No | — | GCP project (auto-detected from credentials) |
410
+ | `GCS_KEY_FILENAME` | `string` | No | — | Service-account key file; omit on GKE (Workload Identity/ADC) |
411
+
412
+ > **Deploying with `STORAGE_TYPE=local` fails to boot in production.** Local storage is the container filesystem, so uploads are destroyed on the next restart — silently, with no error at write or read time. Set `STORAGE_TYPE=s3`/`gcs`, or `FORCE_LOCAL_STORAGE=true` if a durable volume really is mounted at `STORAGE_PATH`. A crashed rollout is recoverable; lost user files are not.
377
413
  | `S3_ENDPOINT` | `string` (URL) | No | — | Custom S3 endpoint (MinIO, R2) |
378
414
  | `S3_FORCE_PATH_STYLE` | `optionalBoolString` | No | `undefined` → `false` | Use path-style S3 URLs (required for MinIO) |
379
415
  | `GCS_BUCKET` | `string` | If GCS | — | Google Cloud Storage bucket name |
@@ -50,8 +50,8 @@ import {
50
50
  Avatar, SearchBar, ErrorBoundary,
51
51
  // Utilities
52
52
  cls, defaultBorderMixin, cardMixin, cardClickableMixin, paperMixin,
53
- // Icons (all from @rebasepro/ui)
54
- AddIcon, DeleteIcon
53
+ // Icons (all from @rebasepro/ui, under their lucide names)
54
+ PlusIcon, Trash2Icon
55
55
  } from "@rebasepro/ui";
56
56
  ```
57
57
 
@@ -202,7 +202,7 @@ The typography scale is compact and uses semibold weights for headings — desig
202
202
 
203
203
  ### Typography Anti-Patterns
204
204
 
205
- ```tsx
205
+ ```tsx no-verify
206
206
  // ❌ NEVER: Gradient text on headings
207
207
  <Typography className="bg-gradient-to-r from-violet-600 to-cyan-500 bg-clip-text text-transparent">
208
208
 
@@ -270,7 +270,7 @@ The `Card` component from `@rebasepro/ui` automatically applies `cardMixin` and,
270
270
  // cardClickableMixin = "hover:bg-surface-50 dark:hover:bg-surface-800 cursor-pointer transition-colors duration-150"
271
271
  ```
272
272
 
273
- ```tsx
273
+ ```tsx no-verify
274
274
  // ✅ CORRECT: Using Card component
275
275
  <Card className="p-4">
276
276
  <Typography variant="subtitle1">Title</Typography>
@@ -407,7 +407,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
407
407
 
408
408
  ### Button Anti-Patterns
409
409
 
410
- ```tsx
410
+ ```tsx no-verify
411
411
  // ❌ NEVER: Casting variant to bypass types
412
412
  <Button variant={"standard" as any}>
413
413
 
@@ -432,7 +432,7 @@ The `Button` component accepts these variants and colors. Buttons use `rounded-l
432
432
 
433
433
  ### Use the `Alert` Component
434
434
 
435
- ```tsx
435
+ ```tsx no-verify
436
436
  // ✅ CORRECT
437
437
  <Alert color="success" size="small">Lead captured successfully!</Alert>
438
438
  <Alert color="error">Could not load dashboard statistics.</Alert>
@@ -508,7 +508,7 @@ When displaying numeric KPIs, use plain muted icons (matching NavigationCard)
508
508
 
509
509
  ### Metric Card Anti-Patterns
510
510
 
511
- ```tsx
511
+ ```tsx no-verify
512
512
  // ❌ NEVER: Gradient icon backgrounds
513
513
  <div className="p-4 bg-gradient-to-br from-blue-500 to-indigo-600 text-white rounded-2xl shadow-lg shadow-blue-500/20">
514
514
 
@@ -555,7 +555,7 @@ For larger content sections (like a table, form, or embedded panel), use `Paper`
555
555
 
556
556
  ### Panel Anti-Patterns
557
557
 
558
- ```tsx
558
+ ```tsx no-verify
559
559
  // ❌ WRONG: Custom panel styling
560
560
  <div className="p-6 rounded-2xl border bg-white dark:bg-surface-900/50 backdrop-blur-md shadow-sm">
561
561
 
@@ -217,7 +217,7 @@ import {
217
217
 
218
218
  #### getPasswordResetTemplate
219
219
 
220
- ```typescript
220
+ ```typescript no-verify
221
221
  function getPasswordResetTemplate(
222
222
  resetUrl: string,
223
223
  user: { email: string; displayName?: string | null },
@@ -229,7 +229,7 @@ The template includes a "Reset Password" button linking to `resetUrl`, a plain-t
229
229
 
230
230
  #### getEmailVerificationTemplate
231
231
 
232
- ```typescript
232
+ ```typescript no-verify
233
233
  function getEmailVerificationTemplate(
234
234
  verifyUrl: string,
235
235
  user: { email: string; displayName?: string | null },
@@ -241,7 +241,7 @@ The template includes a "Verify Email Address" button linking to `verifyUrl`.
241
241
 
242
242
  #### getUserInvitationTemplate
243
243
 
244
- ```typescript
244
+ ```typescript no-verify
245
245
  function getUserInvitationTemplate(
246
246
  setPasswordUrl: string,
247
247
  user: { email: string; displayName?: string | null },
@@ -253,7 +253,7 @@ Sent when an admin creates a user via `POST /api/admin/users` without specifying
253
253
 
254
254
  #### getWelcomeEmailTemplate
255
255
 
256
- ```typescript
256
+ ```typescript no-verify
257
257
  function getWelcomeEmailTemplate(
258
258
  user: { email: string; displayName?: string | null },
259
259
  appName?: string, // default: "Rebase"