@mantajs/core 0.2.0-beta.1 → 0.2.0-beta.10

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 (48) hide show
  1. package/dist/adapters/file-memory.d.ts +2 -5
  2. package/dist/adapters/file-memory.d.ts.map +1 -1
  3. package/dist/adapters/file-memory.js +1 -1
  4. package/dist/adapters/file-memory.js.map +1 -1
  5. package/dist/config/types.d.ts +36 -16
  6. package/dist/config/types.d.ts.map +1 -1
  7. package/dist/config/types.js +4 -2
  8. package/dist/config/types.js.map +1 -1
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/ports/file.d.ts +9 -5
  13. package/dist/ports/file.d.ts.map +1 -1
  14. package/dist/ports/index.d.ts +1 -1
  15. package/dist/ports/index.d.ts.map +1 -1
  16. package/dist/query/define-query.d.ts +17 -1
  17. package/dist/query/define-query.d.ts.map +1 -1
  18. package/dist/query/define-query.js.map +1 -1
  19. package/dist/query/index.d.ts.map +1 -1
  20. package/dist/query/index.js +7 -1
  21. package/dist/query/index.js.map +1 -1
  22. package/dist/service/define.d.ts +9 -1
  23. package/dist/service/define.d.ts.map +1 -1
  24. package/dist/service/define.js.map +1 -1
  25. package/dist/service/instantiate.d.ts +7 -1
  26. package/dist/service/instantiate.d.ts.map +1 -1
  27. package/dist/service/instantiate.js +9 -2
  28. package/dist/service/instantiate.js.map +1 -1
  29. package/dist/user/auto-routes.d.ts +6 -0
  30. package/dist/user/auto-routes.d.ts.map +1 -1
  31. package/dist/user/auto-routes.js +70 -8
  32. package/dist/user/auto-routes.js.map +1 -1
  33. package/docs/00-overview.md +9 -10
  34. package/docs/01-getting-started.md +26 -29
  35. package/docs/03-services.md +23 -5
  36. package/docs/04-users.md +47 -3
  37. package/docs/05-commands.md +1 -1
  38. package/docs/06-queries.md +54 -23
  39. package/docs/10-spa.md +63 -1
  40. package/docs/11-config.md +158 -39
  41. package/docs/14-adapters.md +52 -2
  42. package/docs/15-hosts.md +17 -2
  43. package/docs/16-reference.md +32 -3
  44. package/docs/17-dashboard.md +4 -2
  45. package/docs/AGENT.md +51 -4
  46. package/package.json +5 -3
  47. package/skills/mantajs/SKILL.md +67 -0
  48. package/skills/mantajs/agents/openai.yaml +3 -0
@@ -1,14 +1,17 @@
1
- # Queries — defineQuery() & defineQueryGraph()
1
+ # Queries — Drizzle-first defineQuery()
2
2
 
3
- Queries are the CQRS read side. They expose data as GET endpoints. Two primitives:
3
+ Queries are the CQRS read side. They expose data as GET endpoints.
4
4
 
5
- - `defineQuery()` named query with custom handler (specific reads)
6
- - `defineQueryGraph()` expose the query graph to frontend (flexible reads)
5
+ - `defineQuery()` is the primary application API. New code should query with the schema-aware Drizzle client.
6
+ - `defineQueryGraph()` is reserved for generated/admin/dynamic surfaces that need entity-level browsing.
7
+
8
+ Manta does not try to replace SQL. `defineModel()` generates the Drizzle schema and migrations; `defineQuery()` gives handlers the Drizzle client, generated schema, tagged SQL helper, auth context, logging, and request headers.
7
9
 
8
10
  ## defineQuery()
9
11
 
10
12
  ```typescript
11
13
  // src/queries/admin/list-products.ts
14
+ import { eq } from 'drizzle-orm'
12
15
  import { z } from 'zod'
13
16
 
14
17
  export default defineQuery({
@@ -18,12 +21,19 @@ export default defineQuery({
18
21
  status: z.string().optional(),
19
22
  ...listParams(),
20
23
  }),
21
- handler: async (input, { query, log, auth, headers }) => {
22
- return query.graph({
23
- entity: 'product',
24
- filters: input.status ? { status: input.status } : undefined,
25
- pagination: { take: input.limit, skip: input.offset },
26
- })
24
+ handler: async (input, { drizzle, schema, sql }) => {
25
+ const products = schema.products
26
+ return drizzle
27
+ .select({
28
+ id: products.id,
29
+ title: products.title,
30
+ status: products.status,
31
+ totalVariants: sql<number>`count(*) over()::int`,
32
+ })
33
+ .from(products)
34
+ .where(input.status ? eq(products.status, input.status) : undefined)
35
+ .limit(input.limit)
36
+ .offset(input.offset)
27
37
  },
28
38
  })
29
39
  ```
@@ -43,14 +53,21 @@ defineQuery({
43
53
 
44
54
  ```typescript
45
55
  {
46
- query: QueryService, // query.graph() for cross-module joins
56
+ drizzle: unknown, // schema-aware Drizzle client
57
+ db: unknown, // alias for drizzle
58
+ schema: Record<string, unknown>, // generated Drizzle tables
59
+ sql: typeof import('drizzle-orm').sql, // tagged SQL helper
60
+ database: IDatabasePort, // raw SQL, pool, transactions
61
+ query: QueryService, // generated/admin query graph
47
62
  log: ILoggerPort, // Structured logging
48
63
  auth: AuthContext | null, // Authenticated user
49
64
  headers: Record<string, string | undefined>, // Raw request headers
50
65
  }
51
66
  ```
52
67
 
53
- **No `app` access** queries are forced to use `query.graph()` for reads. This ensures consistent access control.
68
+ Prefer Drizzle for all application reads, including simple lists, dashboards, analytics, counts, search, and joins. Use `sql` fragments when a PostgreSQL feature is clearer than query-builder syntax.
69
+
70
+ `query.graph()` remains available for generated admin screens and dynamic entity browsing. It must not be used to hide analytical work in JavaScript. If a query needs `COUNT`, `SUM`, `GROUP BY`, `FILTER`, CTEs, window functions, full-text search, or dashboard KPIs, write it as Drizzle/SQL in `defineQuery()`.
54
71
 
55
72
  ### Routing
56
73
 
@@ -65,6 +82,18 @@ Query parameters are passed as URL query string:
65
82
  GET /api/admin/list-products?status=active&limit=10&offset=0
66
83
  ```
67
84
 
85
+ ### Serverless fast routes
86
+
87
+ For serverless and worker builds, Manta generates route-level functions for compatible `defineQuery()` files. These fast routes do not import `bootstrapApp()`, jobs, workflows, AI, command registries, or the catch-all Manta adapter.
88
+
89
+ Fast routes are generated when:
90
+
91
+ - the project uses filesystem V2 contexts (`src/queries/{context}`), not legacy `src/contexts/*.ts`
92
+ - the file exports `defineQuery()`, not `defineQueryGraph()`
93
+ - the query does not call `query.graph()`, `query.graphAndCount()`, `query.index()`, or `query.gql()`
94
+
95
+ Pure synthetic queries do not initialize the database or generated schema. Queries that reference `drizzle`, `db`, `schema`, or `database` initialize only the DB adapter and generated Drizzle schema needed by the handler.
96
+
68
97
  ### Input helpers
69
98
 
70
99
  ```typescript
@@ -85,22 +114,24 @@ z.object({
85
114
  ### Auth in queries
86
115
 
87
116
  ```typescript
88
- handler: async (input, { query, auth }) => {
117
+ handler: async (input, { drizzle, schema, auth }) => {
89
118
  // auth.id — user ID
90
119
  // auth.type — context ('admin', 'customer')
91
120
  // auth.email — user email
92
121
 
93
122
  // Example: only return MY orders
94
- return query.graph({
95
- entity: 'order',
96
- filters: { customer_id: auth!.id },
97
- })
123
+ return drizzle
124
+ .select()
125
+ .from(schema.orders)
126
+ .where(eq(schema.orders.customer_id, auth!.id))
98
127
  }
99
128
  ```
100
129
 
101
130
  ## defineQueryGraph()
102
131
 
103
- Exposes the query graph to the frontend. The frontend can compose arbitrary queries.
132
+ Exposes the query graph to the frontend. The frontend can compose entity browsing queries for generated/admin screens.
133
+
134
+ Do not use the graph as the main application query language. It is intentionally less expressive than SQL. Complex reads belong in `defineQuery()` with Drizzle.
104
135
 
105
136
  ### Wildcard — full access (admin/AI)
106
137
 
@@ -163,11 +194,11 @@ POST /api/admin/graph
163
194
 
164
195
  | Use case | Primitive | Why |
165
196
  |----------|-----------|-----|
166
- | Admin dashboard | `defineQueryGraph('*')` | Admin sees everything, AI needs full access |
167
- | Storefront public catalog | `defineQueryGraph({ product: true })` | Frontend composes queries freely, scoped |
168
- | Storefront orders | `defineQuery` or scoped graph | Need row-level filtering by customer |
169
- | Complex aggregation | `defineQuery` | Custom handler with business logic |
170
- | API for third parties | `defineQuery` | Fixed contract, no graph exposure |
197
+ | Generated admin entity table | `defineQueryGraph('*')` | Dynamic browsing over known entities |
198
+ | Storefront catalog | `defineQuery` | Stable contract, optimized Drizzle query |
199
+ | Customer orders | `defineQuery` | Auth-scoped SQL with explicit filters |
200
+ | Dashboard / analytics | `defineQuery` | Aggregations run in Postgres |
201
+ | Third-party API | `defineQuery` | Fixed contract, no graph exposure |
171
202
 
172
203
  ## extendQueryGraph() — external entity resolvers
173
204
 
package/docs/10-spa.md CHANGED
@@ -34,7 +34,13 @@ src/spa/admin/
34
34
  - **`.ts`** — exports `definePage()` or `defineForm()` spec (declarative, no React)
35
35
  - **`.tsx`** — exports a React component (full control, for complex cases)
36
36
 
37
- **Defaults**: `@mantajs/dashboard` shell + `@mantajs/ui` preset. Override in config if needed:
37
+ **Defaults**: `@mantajs/dashboard` shell + `@mantajs/ui` preset. The generated entry imports the public stylesheet export:
38
+
39
+ ```typescript
40
+ import '@mantajs/dashboard/index.css'
41
+ ```
42
+
43
+ Override in config if needed:
38
44
 
39
45
  ```typescript
40
46
  // manta.config.ts — optional, only for overrides
@@ -46,6 +52,62 @@ export default defineConfig({
46
52
  })
47
53
  ```
48
54
 
55
+ ### Mount path
56
+
57
+ By default, a SPA named `admin` is mounted at `/admin`:
58
+
59
+ | Generated setting | Default for `src/spa/admin` |
60
+ | --- | --- |
61
+ | React Router basename | `/admin` |
62
+ | Vite `base` | `/admin/` |
63
+ | Static output | `public/admin` |
64
+ | Vercel SPA rewrite | `/admin/:path*` → `/admin/index.html` |
65
+
66
+ Use `mountPath: '/'` for a dashboard on a dedicated subdomain, such as `admin.fancypalas.com/paniers`:
67
+
68
+ ```typescript
69
+ // manta.config.ts
70
+ export default defineConfig({
71
+ spa: {
72
+ admin: { mountPath: '/' },
73
+ },
74
+ })
75
+ ```
76
+
77
+ With `mountPath: '/'`, React Router uses `basename="/"`, Vite uses `base: '/'`, the SPA builds into `public/`, and generated Vercel rewrites exclude `/api/*`.
78
+
79
+ ### Dev Vite port
80
+
81
+ `manta dev` starts one Vite server per detected SPA. Ports default to `5200`, then increment for additional SPAs.
82
+
83
+ Configure the port in `manta.config.ts` when another project already uses the default:
84
+
85
+ ```typescript
86
+ export default defineConfig({
87
+ spa: {
88
+ admin: { vitePort: 5310 },
89
+ },
90
+ })
91
+ ```
92
+
93
+ Environment overrides are also supported:
94
+
95
+ ```bash
96
+ MANTA_VITE_PORT=5310 manta dev
97
+ MANTA_VITE_PORT_ADMIN=5311 manta dev
98
+ ```
99
+
100
+ The SPA-specific variable wins over the global one. The suffix uses the SPA name uppercased with non-alphanumeric characters replaced by `_`.
101
+
102
+ ### Local production fallback
103
+
104
+ `manta build` writes a SPA fallback manifest into `.manta/server/spa-manifest.ts`. For the Node preset, `manta start` uses it to mirror the Vercel SPA behavior locally:
105
+
106
+ - `/` redirects to the single SPA mount path when the SPA is not mounted at `/`
107
+ - `/login`, `/reset-password`, and `/accept-invite` redirect under that mount path
108
+ - `/admin/*` and other configured SPA mount paths serve the built `index.html`
109
+ - `/api/*` and asset URLs remain server-side/static and are not swallowed by the SPA fallback
110
+
49
111
  ---
50
112
 
51
113
  ## SPA Configuration — defineSpa()
package/docs/11-config.md CHANGED
@@ -17,12 +17,15 @@ Creates a complete, functional Manta project. After `pnpm install`, the project
17
17
 
18
18
  ```
19
19
  my-app/
20
- ├── manta.config.ts # defineConfig() — database, http, admin: true
20
+ ├── manta.config.ts # defineConfig() — database, http, spa, presets
21
+ ├── nitro.config.ts # Nitro host config for standalone builds
21
22
  ├── package.json # @mantajs/core, @mantajs/cli, @mantajs/host-nitro, @mantajs/dashboard
22
23
  ├── tsconfig.json # ES2022, ESNext, JSX support, strict
23
24
  ├── .env / .env.example # DATABASE_URL, PORT, ANTHROPIC_API_KEY
24
- ├── .gitignore # .manta/, .manta/types/, node_modules/, .env
25
+ ├── .gitignore # .manta/, node_modules/, .env
25
26
  ├── AGENT.md # AI instructions (copied from @mantajs/core/docs/)
27
+ ├── .codex/skills/mantajs/ # Codex skill pointing to the shipped Manta docs
28
+ ├── .claude/skills/mantajs/ # Claude skill pointing to the same docs
26
29
  └── src/
27
30
  ├── modules/ # Your business modules (entities + services)
28
31
  ├── commands/ # Application commands (cross-module workflows)
@@ -31,48 +34,44 @@ my-app/
31
34
  ├── links/ # Cross-module relations
32
35
  ├── queries/ # CQRS read endpoints (defineQuery, defineQueryGraph)
33
36
  ├── agents/ # AI steps
34
- └── admin/ # Dashboard (index.html + main.tsx)
35
- ├── index.html
36
- └── main.tsx
37
+ └── spa/admin/ # Dashboard SPA pages, config, blocks
38
+ ├── config.ts
39
+ ├── pages/
40
+ └── blocks/
37
41
  ```
38
42
 
39
43
  **What it does NOT generate:**
40
- - No `nitro.config.ts` — the host adapter handles this internally
41
44
  - No `drizzle.config.ts` — the CLI handles this internally
42
45
  - No `src/api/` — routes are auto-generated from commands + queries
43
46
 
44
- The only config file the developer maintains is `manta.config.ts`.
47
+ The developer normally maintains `manta.config.ts`, `nitro.config.ts` only when deployment/static asset behavior needs customization, and the generated `AGENT.md` when project-specific conventions need to be added.
45
48
 
46
- ### `manta setup` — Add Manta to existing project
49
+ ### Existing projects
50
+
51
+ The current CLI does not expose a separate `manta setup` command. Existing projects should install or update the Manta packages, keep a `manta.config.ts` at the project root, then run:
47
52
 
48
53
  ```bash
49
- cd my-nextjs-app
50
- npx manta setup
54
+ manta dev
51
55
  ```
52
56
 
53
- Detects the existing framework and adapts:
54
-
55
- | Detected | Context | What it generates |
56
- |----------|---------|------------------|
57
- | `next.config.*` | Next.js | `AGENT.md` with Next.js + Manta instructions, `src/manta/` subdirectory |
58
- | `nuxt.config.*` | Nuxt | `AGENT.md` with Nuxt + Manta instructions, `server/manta/` subdirectory |
59
- | `workspaces` in package.json | Monorepo | `AGENT.md` at root, `packages/backend/` with Manta structure |
60
- | Nothing detected | Standalone | Full Manta project structure in current directory |
57
+ `manta dev` refreshes **`.codex/skills/mantajs/SKILL.md`** and **`.claude/skills/mantajs/SKILL.md`** from the installed `@mantajs/core` package before boot validation. This keeps Codex and Claude pointed at the docs shipped in `node_modules/@mantajs/core/docs/` even when a project already existed before the current framework version.
61
58
 
62
- In all cases, `manta setup`:
63
- 1. Creates `manta.config.ts`
64
- 2. Creates module/command/subscriber directories
65
- 3. Generates **`AGENT.md` at the project root** — the first file an AI reads
66
- 4. The `AGENT.md` is context-aware (mentions the detected framework)
67
- 5. `AGENT.md` is committed to git (not gitignored) — every clone has it
68
-
69
- ### The AGENT.md
59
+ ### Agent docs and Codex skill
70
60
 
71
61
  The `AGENT.md` lives at the root of every Manta project. It's the first file an AI reads.
72
62
 
73
63
  **Canonical location:** `@mantajs/core/docs/AGENT.md` — alongside all other framework documentation. The CLI copies it to the project root during `manta init`.
74
64
 
75
- For `manta setup` (existing projects), context-specific templates exist in the CLI:
65
+ The Codex skill is intentionally separate and smaller:
66
+
67
+ ```
68
+ .codex/skills/mantajs/SKILL.md
69
+ .claude/skills/mantajs/SKILL.md
70
+ ```
71
+
72
+ It is copied from `@mantajs/core/skills/mantajs/SKILL.md` and tells Codex/Claude to load the relevant file from `node_modules/@mantajs/core/docs/` before changing Manta code. It is not installed with an npm `postinstall` hook; `manta init` creates it and `manta dev` refreshes it so existing projects stay aligned with the installed framework docs.
73
+
74
+ For future setup flows and framework-specific projects, context-specific templates exist in the CLI:
76
75
 
77
76
  ```
78
77
  packages/cli/src/templates/agent/
@@ -80,7 +79,7 @@ packages/cli/src/templates/agent/
80
79
  └── nuxt.md # Nuxt + Manta
81
80
  ```
82
81
 
83
- `manta init` copies the canonical AGENT.md from `@mantajs/core/docs/`. `manta setup` detects the context and uses the appropriate template. The developer can then edit it to add project-specific context (business domain, team conventions, etc.).
82
+ `manta init` copies the canonical AGENT.md from `@mantajs/core/docs/`. Framework-specific templates can be used by future setup flows or custom scaffolding. The developer can then edit the project AGENT.md to add project-specific context (business domain, team conventions, etc.).
84
83
 
85
84
  Each AGENT.md contains:
86
85
  - Stack description (what framework + Manta)
@@ -100,7 +99,9 @@ export default {
100
99
  pool: { min: 2, max: 10 },
101
100
  },
102
101
  http: { port: 3000 },
103
- admin: { enabled: true },
102
+ spa: {
103
+ admin: {}, // mounted at /admin by default
104
+ },
104
105
  }
105
106
  ```
106
107
 
@@ -113,7 +114,12 @@ export default defineConfig({
113
114
  auth: {
114
115
  jwtSecret: process.env.JWT_SECRET,
115
116
  },
116
- admin: { enabled: true },
117
+ spa: {
118
+ admin: {
119
+ // Default mount path is "/admin"; use "/" for admin.example.com routes.
120
+ mountPath: '/admin',
121
+ },
122
+ },
117
123
  strict: false,
118
124
  })
119
125
  ```
@@ -127,12 +133,61 @@ export default defineConfig({
127
133
  | `database` | `url`, `pool.min`, `pool.max` | Required |
128
134
  | `http` | `port` | `9000` |
129
135
  | `auth` | `jwtSecret`, `session.enabled`, `session.cookieName` | Dev: auto-generated secret |
130
- | `query` | `maxEntities`, `defaultLimit` | `10000`, `100` |
131
- | `admin` | `enabled` | `false` |
132
- | `preset` | `'dev'`, `'vercel'` | Auto-detected from `APP_ENV` |
136
+ | `query` | `maxTotalEntities` | `10000` |
137
+ | `admin` | Reserved legacy object | `{}` |
138
+ | `spa` | `{ [name]: { dashboard, preset, mountPath, vitePort } }` | Auto-detected from `src/spa/{name}` |
139
+ | `preset` | `'dev'`, `'vercel'`, custom preset object | Auto-detected from `APP_ENV` |
133
140
  | `strict` | `true`/`false` | `false` |
134
141
  | `plugins` | Array of plugin configs | `[]` |
135
142
 
143
+ ### SPA mount path
144
+
145
+ Standalone/Nitro SPAs are discovered from `src/spa/{name}/`. By default, the public mount path is `/{name}`:
146
+
147
+ ```typescript
148
+ export default defineConfig({
149
+ spa: {
150
+ admin: {}, // mounted at /admin
151
+ },
152
+ })
153
+ ```
154
+
155
+ For a dedicated admin subdomain such as `admin.fancypalas.com`, mount the SPA at the root:
156
+
157
+ ```typescript
158
+ export default defineConfig({
159
+ spa: {
160
+ admin: { mountPath: '/' },
161
+ },
162
+ })
163
+ ```
164
+
165
+ With `mountPath: '/'`, the Vite SPA uses `base: '/'`, builds into `public/`, and generated Vercel rewrites exclude `/api/*` so API routes remain server-side.
166
+
167
+ ### SPA dev port
168
+
169
+ `manta dev` starts SPA Vite servers at `5200`, then increments for additional SPAs. Use `vitePort` when a local project already owns that port:
170
+
171
+ ```typescript
172
+ export default defineConfig({
173
+ spa: {
174
+ admin: {
175
+ mountPath: '/admin',
176
+ vitePort: 5310,
177
+ },
178
+ },
179
+ })
180
+ ```
181
+
182
+ Environment overrides are supported for temporary local conflicts:
183
+
184
+ ```bash
185
+ MANTA_VITE_PORT=5310 manta dev
186
+ MANTA_VITE_PORT_ADMIN=5311 manta dev
187
+ ```
188
+
189
+ Precedence is `spa.{name}.vitePort`, then `MANTA_VITE_PORT_{NAME}`, then `MANTA_VITE_PORT`, then the default sequence.
190
+
136
191
  ## Presets (adapter bundles)
137
192
 
138
193
  The framework auto-detects the environment and loads appropriate adapters:
@@ -149,6 +204,64 @@ The framework auto-detects the environment and loads appropriate adapters:
149
204
 
150
205
  Detection: `APP_ENV` > `NODE_ENV` > default `'development'`.
151
206
 
207
+ ### Vercel Blob access
208
+
209
+ The Vercel preset wires `IFilePort` to `@mantajs/adapter-file-vercel-blob`. The adapter defaults to `access: 'public'` for compatibility with existing stores. For sensitive files, create a private Vercel Blob store and configure the adapter explicitly:
210
+
211
+ ```typescript
212
+ export default defineConfig({
213
+ preset: 'vercel',
214
+ adapters: {
215
+ IFilePort: {
216
+ adapter: '@mantajs/adapter-file-vercel-blob',
217
+ options: {
218
+ access: 'private',
219
+ signedUrlExpiresInSeconds: 300,
220
+ },
221
+ },
222
+ },
223
+ })
224
+ ```
225
+
226
+ Private file reads use Vercel Blob's server-side `get()` API via `getDownloadStream()`/`getAsBuffer()`. If the application needs a browser URL, `getPresignedDownloadUrl()` returns a short-lived signed URL instead of exposing the private Blob URL directly.
227
+
228
+ ### Durable cache for auth revocation
229
+
230
+ Production auth uses `ICachePort` for logout token revocation, refresh-token blacklist checks, reset flows, and session state. The Vercel and Cloudflare presets default to Upstash:
231
+
232
+ ```typescript
233
+ export default defineConfig({
234
+ preset: 'vercel',
235
+ adapters: {
236
+ ICachePort: {
237
+ adapter: '@mantajs/adapter-cache-upstash',
238
+ options: {
239
+ url: process.env.UPSTASH_REDIS_REST_URL,
240
+ token: process.env.UPSTASH_REDIS_REST_TOKEN,
241
+ },
242
+ },
243
+ },
244
+ })
245
+ ```
246
+
247
+ If Upstash REST credentials are not available or not reliable in the deployment runtime, use the Postgres-backed cache adapter:
248
+
249
+ ```typescript
250
+ export default defineConfig({
251
+ preset: 'vercel',
252
+ adapters: {
253
+ ICachePort: {
254
+ adapter: '@mantajs/adapter-database-pg/cache',
255
+ options: {
256
+ tableName: 'manta_cache',
257
+ },
258
+ },
259
+ },
260
+ })
261
+ ```
262
+
263
+ The Postgres cache reuses the configured `IDatabasePort` connection when bootstrapped by the Manta CLI and creates the cache table automatically by default.
264
+
152
265
  ## Environment variables
153
266
 
154
267
  | Variable | Required | Description |
@@ -156,6 +269,8 @@ Detection: `APP_ENV` > `NODE_ENV` > default `'development'`.
156
269
  | `DATABASE_URL` | Yes | PostgreSQL connection string |
157
270
  | `JWT_SECRET` | Prod only | JWT signing secret (dev: auto-generated) |
158
271
  | `APP_ENV` | No | Force environment (`development` or `production`) |
272
+ | `UPSTASH_REDIS_REST_URL` | Prod when using Upstash cache | Upstash Redis REST endpoint |
273
+ | `UPSTASH_REDIS_REST_TOKEN` | Prod when using Upstash cache | Upstash Redis REST token |
159
274
  | `ANTHROPIC_API_KEY` | No | Enable AI chat in admin dashboard |
160
275
 
161
276
  ## CLI commands
@@ -178,15 +293,21 @@ Detection: `APP_ENV` > `NODE_ENV` > default `'development'`.
178
293
 
179
294
  Starts development server. Auto-performs:
180
295
  1. Load env + config
181
- 2. Generate `.manta/types/` (TypeScript types for autocomplete)
296
+ 2. Generate `.manta/generated.d.ts` (TypeScript types for autocomplete)
182
297
  3. Start Nitro dev server with HMR
183
298
  4. On first request: connect DB, instantiate adapters, discover modules/commands/subscribers/jobs/links/queries
184
299
  5. Auto-create tables in dev mode (no manual migration needed)
185
300
  6. Wire all routes, subscribers, jobs
186
301
 
302
+ ### manta build --preset vercel
303
+
304
+ For Vercel deployments, `manta build --preset vercel` generates the framework-owned SPA rewrites and cron routes in `vercel.json`.
305
+
306
+ If `vercel.json` already exists, Manta preserves custom top-level project settings such as `buildCommand`, `installCommand`, `outputDirectory`, `functions`, `headers`, and other Vercel options. It also preserves custom `redirects`, `rewrites`, and `crons` unless they collide on the same `source` or `path` as a framework-generated entry.
307
+
187
308
  ### manta db:generate
188
309
 
189
- Scans `src/modules/*/models/` and `src/links/` to generate SQL migrations:
310
+ Scans entity definitions from `src/modules/*/entities/` and relation links from `src/links/` to generate SQL migrations:
190
311
 
191
312
  ```bash
192
313
  manta db:generate --name add-blog-post
@@ -212,15 +333,13 @@ manta exec scripts/seed.ts
212
333
  manta exec scripts/seed.ts --dry-run # Rollback after execution (test mode)
213
334
  ```
214
335
 
215
- ## Codegen (.manta/types/)
336
+ ## Codegen (`.manta/generated.d.ts`)
216
337
 
217
338
  On `manta dev` or `manta build`, the framework generates:
218
339
 
219
340
  | File | Content |
220
341
  |------|---------|
221
- | `types.ts` | `MantaEntities` typed step proxy for `step.service.catalog.create()` |
222
- | `app.d.ts` | `MantaAppModules` — typed `app.modules.catalog.listProducts()` |
223
- | `events.d.ts` | `MantaEventName` — union of all known event names |
342
+ | `.manta/generated.d.ts` | `MantaEntities`, `MantaAppModules`, `MantaEventName`, and module augmentation used by IDE autocomplete |
224
343
 
225
344
  These are TypeScript module augmentations. After codegen, `app.modules.*` has full autocomplete in your IDE.
226
345
 
@@ -8,7 +8,7 @@ Every infrastructure concern is abstracted behind a port. Here are all ports you
8
8
 
9
9
  | Port | Responsibility | Dev adapter | Prod adapter |
10
10
  |------|---------------|-------------|--------------|
11
- | `ICachePort` | Key-value cache | InMemoryCacheAdapter | UpstashCacheAdapter |
11
+ | `ICachePort` | Key-value cache | InMemoryCacheAdapter | UpstashCacheAdapter, PostgresCacheAdapter |
12
12
  | `IEventBusPort` | Event pub/sub | InMemoryEventBusAdapter | Upstash Queues |
13
13
  | `IFilePort` | File storage | InMemoryFileAdapter | VercelBlobAdapter |
14
14
  | `ILoggerPort` | Structured logging | TestLogger | PinoLoggerAdapter |
@@ -67,6 +67,25 @@ export class RedisCacheAdapter implements ICachePort {
67
67
  }
68
68
  ```
69
69
 
70
+ ## Built-in durable Postgres cache
71
+
72
+ When Redis/Upstash is not desirable, `@mantajs/adapter-database-pg` exposes a durable cache adapter:
73
+
74
+ ```typescript
75
+ export default defineConfig({
76
+ adapters: {
77
+ ICachePort: {
78
+ adapter: '@mantajs/adapter-database-pg/cache',
79
+ options: {
80
+ tableName: 'manta_cache',
81
+ },
82
+ },
83
+ },
84
+ })
85
+ ```
86
+
87
+ The CLI factory reuses the active `IDatabasePort` Postgres connection. This adapter is appropriate for auth logout/reset/session revocation state where durability matters more than sub-millisecond Redis latency.
88
+
70
89
  ### 2. Create the barrel export
71
90
 
72
91
  ```typescript
@@ -131,8 +150,17 @@ interface IEventBusPort {
131
150
  ### IFilePort
132
151
 
133
152
  ```typescript
153
+ type FileAccess = 'public' | 'private'
154
+
155
+ interface FileUploadResult {
156
+ key: string
157
+ url: string
158
+ access?: FileAccess
159
+ downloadUrl?: string
160
+ }
161
+
134
162
  interface IFilePort {
135
- upload(key: string, data: Buffer | ReadableStream, contentType?: string): Promise<{ key: string; url: string }>
163
+ upload(key: string, data: Buffer | ReadableStream, contentType?: string): Promise<FileUploadResult>
136
164
  delete(key: string | string[]): Promise<void>
137
165
  getPresignedDownloadUrl(key: string): Promise<string>
138
166
  getDownloadStream(key: string): Promise<ReadableStream>
@@ -141,6 +169,28 @@ interface IFilePort {
141
169
  }
142
170
  ```
143
171
 
172
+ #### Vercel Blob access mode
173
+
174
+ `@mantajs/adapter-file-vercel-blob` supports both Vercel Blob access modes:
175
+
176
+ ```typescript
177
+ export default defineConfig({
178
+ adapters: {
179
+ IFilePort: {
180
+ adapter: '@mantajs/adapter-file-vercel-blob',
181
+ options: {
182
+ access: 'private',
183
+ signedUrlExpiresInSeconds: 300,
184
+ },
185
+ },
186
+ },
187
+ })
188
+ ```
189
+
190
+ `access: 'public'` is the default for backward compatibility and should be reserved for public assets. For sensitive admin files, snapshots, invoices, or user content, use a Vercel Blob store created as private and set `access: 'private'`.
191
+
192
+ Private blobs are read through the server SDK in `getDownloadStream()` and `getAsBuffer()`, so the read-write token is never sent to the browser. `getPresignedDownloadUrl()` returns a short-lived signed URL for private blobs. You can also set `MANTA_VERCEL_BLOB_ACCESS=private` or `MANTA_FILE_ACCESS=private` when configuring through environment variables.
193
+
144
194
  ### ILockingPort
145
195
 
146
196
  ```typescript
package/docs/15-hosts.md CHANGED
@@ -17,6 +17,8 @@ Manta ships with a Nitro host (`packages/host-nitro/`). It:
17
17
  - Handles the catch-all route `server/routes/[...].ts`
18
18
  - Passes requests through to Manta's 12-step pipeline
19
19
 
20
+ For serverless and worker production builds, Nitro also receives generated route-level `defineQuery()` handlers under `server/routes/api/{context}/{query}.ts` when the query is compatible with the fast runtime. Those handlers bypass the catch-all route and do not call `bootstrapApp()`. The catch-all remains the fallback for command graphs, query graphs, workflows, custom API routes, auth routes, cron routes, and legacy context setups.
21
+
20
22
  ## Creating a custom host
21
23
 
22
24
  ### Example: Express host
@@ -33,7 +35,7 @@ export async function startExpressHost(options: { port?: number; cwd?: string }
33
35
  // Bootstrap the Manta app (same as manta dev / manta start)
34
36
  const { app, adapter, logger, shutdown } = await bootstrapApp({
35
37
  cwd,
36
- mode: 'production',
38
+ mode: 'prod',
37
39
  })
38
40
 
39
41
  // Create Express app
@@ -84,7 +86,7 @@ let bootstrapped: Awaited<ReturnType<typeof bootstrapApp>> | null = null
84
86
  export async function handler(event: APIGatewayProxyEventV2) {
85
87
  // Cold start: bootstrap once
86
88
  if (!bootstrapped) {
87
- bootstrapped = await bootstrapApp({ cwd: process.cwd(), mode: 'production' })
89
+ bootstrapped = await bootstrapApp({ cwd: process.cwd(), mode: 'prod' })
88
90
  }
89
91
 
90
92
  // Translate Lambda event to Web Request
@@ -117,6 +119,19 @@ A host must:
117
119
 
118
120
  The H3Adapter's `handleRequest(request: Request): Promise<Response>` is the single entry point. It runs the full 12-step pipeline (auth, validation, routing, error handling).
119
121
 
122
+ ## Production bootstrap mode
123
+
124
+ Production hosts must call `bootstrapApp()` with `mode: 'prod'`. Development tooling uses `mode: 'dev'`.
125
+
126
+ The Nitro production build generated by `@mantajs/host-nitro` writes a production bootstrap with `mode: 'prod'` for both manifest-based builds and runtime-discovery fallback builds. This matters because production error handling must not expose development stack traces.
127
+
128
+ For the Node preset, the generated Nitro catch-all route calls the Manta API first, then serves SPA fallbacks from the built static output when the API returns `404` for a `GET` request. This mirrors the generated Vercel routing locally:
129
+
130
+ - `/` redirects to the single SPA mount path when there is one SPA mounted away from `/`
131
+ - `/login`, `/reset-password`, and `/accept-invite` redirect under that mount path
132
+ - SPA subroutes such as `/admin/login` return the built `index.html`
133
+ - `/api/*` and file-extension asset paths are never handled by the SPA fallback
134
+
120
135
  ## What the pipeline provides
121
136
 
122
137
  Every request passing through `adapter.handleRequest()` goes through: