create-questpie 2.0.3 → 2.0.4

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 (35) hide show
  1. package/dist/index.mjs +244 -30
  2. package/package.json +1 -1
  3. package/skills/questpie/AGENTS.md +299 -98
  4. package/skills/questpie/SKILL.md +50 -17
  5. package/skills/questpie/coverage.json +213 -0
  6. package/skills/questpie/references/auth.md +119 -4
  7. package/skills/questpie/references/business-logic.md +126 -56
  8. package/skills/questpie/references/crud-api.md +231 -29
  9. package/skills/questpie/references/data-modeling.md +22 -6
  10. package/skills/questpie/references/extend.md +34 -7
  11. package/skills/questpie/references/field-types.md +14 -2
  12. package/skills/questpie/references/infrastructure-adapters.md +207 -32
  13. package/skills/questpie/references/mcp.md +147 -0
  14. package/skills/questpie/references/multi-tenancy.md +1 -2
  15. package/skills/questpie/references/production.md +218 -53
  16. package/skills/questpie/references/quickstart.md +6 -8
  17. package/skills/questpie/references/rules.md +86 -21
  18. package/skills/questpie/references/sandbox.md +110 -0
  19. package/skills/questpie/references/tanstack-query.md +34 -11
  20. package/skills/questpie/references/type-inference.md +167 -0
  21. package/skills/questpie/references/workflows.md +155 -0
  22. package/skills/questpie-admin/AGENTS.md +47 -40
  23. package/skills/questpie-admin/SKILL.md +46 -39
  24. package/skills/questpie-admin/references/custom-ui.md +1 -1
  25. package/templates/tanstack-start/AGENTS.md +15 -8
  26. package/templates/tanstack-start/CLAUDE.md +12 -5
  27. package/templates/tanstack-start/README.md +7 -6
  28. package/templates/tanstack-start/package.json +1 -0
  29. package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
  30. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
  31. package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
  32. package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
  33. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
  34. package/templates/tanstack-start/src/routes/api/$.ts +1 -2
  35. package/templates/tanstack-start/vite.config.ts +2 -2
@@ -0,0 +1,147 @@
1
+ # MCP Integration
2
+
3
+ Use `@questpie/mcp` when a QUESTPIE app should expose collections, globals, annotated JSON routes, schemas, or custom tools to Model Context Protocol clients.
4
+
5
+ ## Static Module Pattern
6
+
7
+ MCP is codegen-aware. Keep `modules.ts` static and put options in `config/mcp.ts`.
8
+
9
+ ```ts title="modules.ts"
10
+ import mcpModule from "@questpie/mcp";
11
+
12
+ export default [mcpModule] as const;
13
+ ```
14
+
15
+ ```ts title="config/mcp.ts"
16
+ import { mcpConfig } from "@questpie/mcp";
17
+
18
+ export default mcpConfig({
19
+ crud: {
20
+ defaults: {
21
+ collections: { read: true, write: false, delete: false },
22
+ globals: { read: true, write: false },
23
+ },
24
+ collections: {
25
+ posts: { read: true, write: true },
26
+ users: false,
27
+ },
28
+ globals: {
29
+ siteSettings: { read: true, write: true },
30
+ },
31
+ },
32
+ routes: {
33
+ exposeAnnotated: true,
34
+ },
35
+ });
36
+ ```
37
+
38
+ Do not use `mcpModule(options)`. Runtime options belong in the plugin-discovered config file.
39
+
40
+ `mcpModule` carries its codegen plugin. Do not also add `mcpPlugin()` to `questpie.config.ts` unless you are doing a custom setup that deliberately omits `mcpModule` — double registration duplicates the plugin.
41
+
42
+ ## CRUD Policy
43
+
44
+ Generated collection tools:
45
+
46
+ - `collections.{name}.list`
47
+ - `collections.{name}.count`
48
+ - `collections.{name}.get`
49
+ - `collections.{name}.create`
50
+ - `collections.{name}.update`
51
+ - `collections.{name}.delete`
52
+
53
+ Generated global tools:
54
+
55
+ - `globals.{name}.get`
56
+ - `globals.{name}.update`
57
+
58
+ Policy order:
59
+
60
+ 1. Transport defaults.
61
+ 2. CRUD defaults.
62
+ 3. Per-entity override.
63
+ 4. QUESTPIE access rules execute last and can still deny.
64
+
65
+ HTTP is user mode and read-oriented by default. HTTP cannot be made system mode with config or options. Stdio defaults to trusted system mode unless explicitly lowered to user mode.
66
+
67
+ Use `fields.include` / `fields.exclude` for top-level filtering. It applies to create/update input, CRUD outputs, list docs, global results, and schema resources. Nested relation projection is out of scope for v1.
68
+
69
+ ## Route Tools
70
+
71
+ Only simple JSON routes are auto-converted:
72
+
73
+ - Route has `.schema(...)`.
74
+ - Route is not `.raw()`.
75
+ - Route has `meta.mcp.expose === true`.
76
+ - `routes.exposeAnnotated` is not `false`.
77
+
78
+ ```ts
79
+ route()
80
+ .post()
81
+ .schema(inputSchema)
82
+ .outputSchema(outputSchema)
83
+ .meta({
84
+ title: "Generate report",
85
+ mcp: {
86
+ expose: true,
87
+ name: "reports.generate",
88
+ annotations: { readOnlyHint: true },
89
+ },
90
+ })
91
+ .handler(async ({ input }) => ({ ok: true }));
92
+ ```
93
+
94
+ Routes without path params use the route input schema directly. Routes with params use `{ params, input }`. Route policy keys use the route key, not the overridden tool name.
95
+
96
+ ## Resources
97
+
98
+ Built-in resources:
99
+
100
+ - `questpie://schema/collections`
101
+ - `questpie://schema/collections/{name}`
102
+ - `questpie://schema/globals`
103
+ - `questpie://schema/globals/{name}`
104
+ - `questpie://schema/routes`
105
+ - `questpie://schema/routes/{key}`
106
+
107
+ Resources honor MCP policy and QUESTPIE access visibility. Route resources include input/output JSON Schema when the route has Zod schemas.
108
+
109
+ ## Custom Tools
110
+
111
+ Custom tools live in `mcp-tools/` and are discovered by codegen.
112
+
113
+ ```ts
114
+ import { mcpTool } from "@questpie/mcp";
115
+ import { z } from "zod";
116
+
117
+ export default mcpTool("generate-report", {
118
+ description: "Generate a report.",
119
+ inputSchema: z.object({ period: z.string() }),
120
+ access: ({ session }) => !!session,
121
+ }).handler(async ({ input, ctx }) => ({
122
+ structuredContent: await ctx.services.reports.generate(input),
123
+ }));
124
+ ```
125
+
126
+ Custom tool access is checked during `tools/list` and again during `tools/call`.
127
+
128
+ ## Programmatic Servers
129
+
130
+ Use `createMcpServer(app, { transport: "http", request })` for programmatic HTTP setup. If no `ctx` is passed, the request is preserved through `app.createContext()`.
131
+
132
+ Use `startStdioServer(app)` for trusted stdio integrations:
133
+
134
+ ```ts
135
+ import { app } from "#questpie";
136
+ import { startStdioServer } from "@questpie/mcp/stdio";
137
+
138
+ await startStdioServer(app);
139
+ ```
140
+
141
+ ## Gotchas
142
+
143
+ - Add `mcpModule` to static `modules.ts`, then run codegen.
144
+ - HTTP system mode is intentionally impossible until a future trusted-token design exists.
145
+ - Field filtering is top-level only.
146
+ - Raw routes and unannotated routes are not tools.
147
+ - Custom tool results should use `structuredContent` for machine-readable output.
@@ -278,8 +278,7 @@ For advanced cases, create a request-scoped service that provides a tenant-aware
278
278
 
279
279
  ```ts
280
280
  // services/scoped-db.ts
281
- import { service } from "questpie";
282
-
281
+ import { service } from "questpie/services";
283
282
  export default service({
284
283
  lifecycle: "request",
285
284
  deps: ["db", "session"] as const,
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: questpie-core/production
3
3
  description:
4
- QUESTPIE production deployment authentication better-auth OAuth database PostgreSQL Drizzle storage S3 Flydrive queue pg-boss jobs realtime SSE pgNotify Redis migrations email SMTP KV key-value logger Pino OpenAPI Docker environment variables adapters infrastructure
4
+ QUESTPIE production deployment authentication better-auth OAuth database PostgreSQL Drizzle storage S3 Files SDK queue pg-boss jobs realtime SSE pgNotify Redis migrations email SMTP KV key-value logger Pino OpenAPI Docker environment variables adapters infrastructure
5
5
  - questpie-core
6
6
  ---
7
7
 
@@ -11,15 +11,40 @@ This skill builds on questpie-core.
11
11
 
12
12
  QUESTPIE uses an adapter-based architecture for all infrastructure. Development defaults work out of the box; production requires explicit adapter configuration in `questpie.config.ts`.
13
13
 
14
- | Service | Dev Default | Production Adapter |
15
- | -------- | --------------------- | ------------------------------------- |
16
- | Database | PostgreSQL (local) | PostgreSQL (remote, SSL) |
17
- | Storage | Local filesystem | S3-compatible (`s3` driver) |
18
- | Queue | None (jobs skip) | pg-boss (`pgBossAdapter`) |
19
- | Realtime | pgNotify | Redis Streams (`redisStreamsAdapter`) |
20
- | Email | Console (logs output) | SMTP (`SmtpAdapter`) |
21
- | KV Store | In-memory | Redis (`"redis"` adapter) |
22
- | Logger | Pino (console) | Pino (structured JSON) |
14
+ | Service | Dev Default | Production Adapter |
15
+ | -------- | --------------------- | ----------------------------------------------- |
16
+ | Database | PostgreSQL (local) | PostgreSQL (remote, SSL) |
17
+ | Storage | Local filesystem | Files SDK provider adapter (`s3`, `r2`, etc.) |
18
+ | Queue | None (jobs skip) | pg-boss (`pgBossAdapter`) |
19
+ | Realtime | pgNotify | Redis Streams (`redisStreamsAdapter`) |
20
+ | Email | Console (logs output) | SMTP (`SmtpAdapter`) |
21
+ | KV Store | In-memory | Redis (`redisKVAdapter`) |
22
+ | Logger | Pino (console) | Pino (structured JSON) |
23
+
24
+ ## Environment
25
+
26
+ Declare every env var once in `env.ts` (beside `questpie.config.ts`) — schema-validated at boot, typed everywhere. Never use raw `process.env.X` / `process.env.X!` in app code. Full reference: `references/env.md`.
27
+
28
+ ```ts
29
+ // src/questpie/server/env.ts
30
+ import { env } from "questpie/env";
31
+ import { z } from "zod";
32
+
33
+ export default env({
34
+ server: {
35
+ DATABASE_URL: z.url(),
36
+ BETTER_AUTH_SECRET: z.string().min(32),
37
+ S3_BUCKET: z.string().optional(),
38
+ S3_REGION: z.string().optional(),
39
+ S3_ACCESS_KEY: z.string().optional(),
40
+ S3_SECRET_KEY: z.string().optional(),
41
+ SMTP_HOST: z.string().optional(),
42
+ REDIS_URL: z.url().optional(),
43
+ },
44
+ });
45
+ ```
46
+
47
+ All snippets below assume `import env from "./env"` at the top of `questpie.config.ts`.
23
48
 
24
49
  ## Authentication
25
50
 
@@ -27,16 +52,18 @@ QUESTPIE uses [Better Auth](https://www.better-auth.com/). Configure via `config
27
52
 
28
53
  ```ts
29
54
  // src/questpie/server/config/auth.ts
30
- import { authConfig } from "questpie";
55
+ import { authConfig } from "questpie/app";
56
+
57
+ import env from "../env";
31
58
 
32
59
  export default authConfig({
33
60
  emailAndPassword: {
34
61
  enabled: true,
35
62
  requireEmailVerification: false,
36
63
  },
37
- baseURL: process.env.APP_URL || "http://localhost:3000",
64
+ baseURL: env.APP_URL ?? "http://localhost:3000",
38
65
  basePath: "/api/auth",
39
- secret: process.env.BETTER_AUTH_SECRET || "change-me",
66
+ secret: env.BETTER_AUTH_SECRET,
40
67
  });
41
68
  ```
42
69
 
@@ -73,7 +100,28 @@ handler: async ({ session }) => {
73
100
  })
74
101
  ```
75
102
 
76
- The `adminModule` provides a built-in `user` collection for storing user accounts.
103
+ The `adminModule` provides the canonical Better Auth `user` collection for storing user accounts. That contract includes `user.role` (`admin` or `user`), which built-in admin setup and login guards depend on. Do not replace `collection("user")` from scratch in apps that use `adminModule`; merge `starterModule.collections.user` and extend it instead.
104
+
105
+ ### Locking Down the REST Surface
106
+
107
+ Deny-all is actually deny-all — there are no implicit framework grants above
108
+ `defaultAccess`:
109
+
110
+ ```ts title="config/app.ts"
111
+ export default appConfig({
112
+ access: { read: false, create: false, update: false, delete: false },
113
+ });
114
+ ```
115
+
116
+ With this config, anonymous and authenticated callers get nothing unless a
117
+ collection opts in via `.access()`: no row listing (including
118
+ public-visibility upload collections like `assets`), and no schema/meta
119
+ introspection (gated by the same access system — visible iff at least one
120
+ operation is allowed, overridable with the `introspect` access kind). Public
121
+ upload files still serve by key (`GET /:collection/files/:key`) because
122
+ `visibility: "public"` declares the BYTES public — override with the `serve`
123
+ access kind. Do not wrap schema/meta routes in custom auth middleware; use
124
+ `introspect` rules instead.
77
125
 
78
126
  ## Database
79
127
 
@@ -82,7 +130,7 @@ PostgreSQL with Drizzle ORM. Schema is generated from your collection and global
82
130
  ```ts
83
131
  export default runtimeConfig({
84
132
  db: {
85
- url: process.env.DATABASE_URL || "postgres://localhost/myapp",
133
+ url: env.DATABASE_URL,
86
134
  },
87
135
  });
88
136
  ```
@@ -122,7 +170,7 @@ Configure migration and seed directories in `questpie.config.ts` under `cli.migr
122
170
 
123
171
  ## Storage
124
172
 
125
- QUESTPIE uses [Flydrive](https://flydrive.dev/) for file storage.
173
+ QUESTPIE uses [Files SDK](https://files-sdk.dev/) for file storage.
126
174
 
127
175
  ### Local (Development Default)
128
176
 
@@ -137,16 +185,36 @@ export default runtimeConfig({
137
185
  ### S3 (Production)
138
186
 
139
187
  ```ts
140
- import { S3Driver } from "flydrive/drivers/s3";
188
+ import { s3 } from "files-sdk/s3";
141
189
 
142
190
  export default runtimeConfig({
143
191
  storage: {
144
192
  basePath: "/api",
145
- driver: new S3Driver({
146
- bucket: process.env.S3_BUCKET,
147
- region: process.env.S3_REGION,
148
- accessKeyId: process.env.S3_ACCESS_KEY,
149
- secretAccessKey: process.env.S3_SECRET_KEY,
193
+ adapter: s3({
194
+ bucket: env.S3_BUCKET,
195
+ region: env.S3_REGION,
196
+ credentials: {
197
+ accessKeyId: env.S3_ACCESS_KEY,
198
+ secretAccessKey: env.S3_SECRET_KEY,
199
+ },
200
+ }),
201
+ },
202
+ });
203
+ ```
204
+
205
+ ### Cloudflare R2 (Production)
206
+
207
+ ```ts
208
+ import { r2 } from "files-sdk/r2";
209
+
210
+ export default runtimeConfig({
211
+ storage: {
212
+ basePath: "/api",
213
+ adapter: r2({
214
+ bucket: env.R2_BUCKET,
215
+ accountId: env.R2_ACCOUNT_ID,
216
+ accessKeyId: env.R2_ACCESS_KEY_ID,
217
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY,
150
218
  }),
151
219
  },
152
220
  });
@@ -167,17 +235,22 @@ avatar: f.upload({
167
235
  Background job processing with [pg-boss](https://github.com/timgit/pg-boss). Jobs stored in PostgreSQL.
168
236
 
169
237
  ```ts
170
- import { pgBossAdapter, runtimeConfig } from "questpie";
238
+ import { runtimeConfig } from "questpie/app";
239
+ import { pgBossAdapter } from "questpie/adapters/pg-boss";
171
240
 
172
241
  export default runtimeConfig({
173
242
  queue: {
174
243
  adapter: pgBossAdapter({
175
- connectionString: process.env.DATABASE_URL,
244
+ connectionString: env.DATABASE_URL,
176
245
  }),
177
246
  },
178
247
  });
179
248
  ```
180
249
 
250
+ > **Warning:** pg-boss uses `LISTEN/NOTIFY` internally. Do not point `connectionString` at a PgBouncer in transaction pool mode — jobs will queue but never wake the worker, so they fire only on the polling fallback (slow, sometimes never). See "PgBouncer Compatibility" for the full adapter matrix and routing options. Use a direct PG connection or `cloudflareQueuesAdapter`.
251
+
252
+ On Cloudflare Workers, queue processing is push-based. Configure `cloudflareQueuesAdapter` from `questpie/adapters/cloudflare` and export the Worker through `createCloudflareWorkerHandlers`; do not run `app.queue.listen()` in a Worker.
253
+
181
254
  ### Publishing Jobs
182
255
 
183
256
  From hooks, functions, or other jobs:
@@ -197,15 +270,18 @@ The `queue` object is fully typed -- autocompletion shows all registered jobs an
197
270
 
198
271
  SSE-based live updates via `POST /realtime` multiplexed endpoint.
199
272
 
273
+ > **Warning:** `pgNotifyAdapter` requires a direct PG connection (or PgBouncer in `session` mode). Behind PgBouncer transaction pooling, `LISTEN` is silently dropped and clients fall back to polling — events never fan out. See "PgBouncer Compatibility" below.
274
+
200
275
  ### pgNotify (Single Instance)
201
276
 
202
277
  ```ts
203
- import { pgNotifyAdapter, runtimeConfig } from "questpie";
278
+ import { runtimeConfig } from "questpie/app";
279
+ import { pgNotifyAdapter } from "questpie/adapters/pg-notify";
204
280
 
205
281
  export default runtimeConfig({
206
282
  realtime: {
207
283
  adapter: pgNotifyAdapter({
208
- connectionString: process.env.DATABASE_URL,
284
+ connectionString: env.DATABASE_URL,
209
285
  }),
210
286
  },
211
287
  });
@@ -216,31 +292,80 @@ export default runtimeConfig({
216
292
  Required for horizontal scaling:
217
293
 
218
294
  ```ts
219
- import { redisStreamsAdapter } from "questpie";
295
+ import { runtimeConfig } from "questpie/app";
296
+ import { redisStreamsAdapter } from "questpie/adapters/redis-streams";
220
297
 
221
298
  export default runtimeConfig({
222
299
  realtime: {
223
300
  adapter: redisStreamsAdapter({
224
- url: process.env.REDIS_URL,
301
+ url: env.REDIS_URL,
225
302
  }),
226
303
  },
227
304
  });
228
305
  ```
229
306
 
307
+ > **Warning:** `pgNotifyAdapter` and `pgBossAdapter` both rely on PostgreSQL `LISTEN/NOTIFY`. They will silently fail behind PgBouncer in transaction pool mode. See "PgBouncer Compatibility" below.
308
+
309
+ ### SSE Keepalive & Timeouts
310
+
311
+ The `POST /realtime` SSE stream sends a `ping` every **8s** by default (`realtime.keepAliveIntervalMs`). Every layer between browser and server must tolerate at least that idle window, or subscriptions die and reconnect in a loop:
312
+
313
+ | Layer | Setting | Recommendation |
314
+ | -------------------------- | ---------------------------------- | ------------------------------------------------------------------------- |
315
+ | Bun (`Bun.serve`) | `idleTimeout` (default 10s) | Default ping survives it; set `idleTimeout: 30` for headroom |
316
+ | nginx | `proxy_read_timeout` (default 60s) | Keep >= 60s; disable SSE response buffering (`proxy_buffering off`) |
317
+ | Load balancers (ALB, etc.) | idle timeout (often 60s) | Keep above `keepAliveIntervalMs` |
318
+ | Serverless platforms | response buffering / max duration | SSE needs streaming responses; buffered platforms break realtime entirely |
319
+
320
+ ```ts
321
+ // Bun server entry — the app owns Bun.serve options, not the framework
322
+ export default {
323
+ port: 3000,
324
+ idleTimeout: 30, // seconds
325
+ fetch: server.fetch,
326
+ };
327
+ ```
328
+
329
+ ## PgBouncer Compatibility
330
+
331
+ PgBouncer in `transaction` pool mode reassigns sessions per-transaction, so persistent listeners are impossible. Once `LISTEN` returns, the connection is handed to a different client and notifications are dropped. This breaks any feature that depends on session-bound state.
332
+
333
+ Bun SQL (`new SQL({ url })`) already pools connections internally. In single-instance and small-replica deployments, PgBouncer adds nothing on top of it. PgBouncer only earns its keep when you have 20+ replicas, run on serverless with cold-start churn, or share infra with non-Bun consumers.
334
+
335
+ ### Adapter Compatibility Matrix
336
+
337
+ | Adapter | Direct PG | PgBouncer (transaction) | PgBouncer (session) |
338
+ | -------------------------------- | --------- | --------------------------------- | --------------------------- |
339
+ | `pgNotifyAdapter` (realtime) | works | broken — listens silently dropped | works (pooling neutralized) |
340
+ | `pgBossAdapter` (queue) | works | broken — LISTEN/NOTIFY required | works (pooling neutralized) |
341
+ | Drizzle queries via Bun SQL | works | works | works |
342
+ | `redisStreamsAdapter` (realtime) | n/a | n/a | n/a |
343
+
344
+ Prepared statements also break under transaction pooling. If you must use it, ensure your driver disables prepared statements end-to-end.
345
+
346
+ ### DB Connection Routing
347
+
348
+ - **Default and recommended:** direct connection to the PG primary. Bun SQL pools internally; you do not need PgBouncer.
349
+ - **If you must use PgBouncer:** put it in `session` pool mode for any process that runs `pgBossAdapter` or `pgNotifyAdapter`. Session mode pins one server connection per client, which neutralizes pooling but keeps `LISTEN/NOTIFY` working.
350
+ - **Split topology:** route web traffic through PgBouncer (transaction mode) and run workers (pgBoss, pgNotify) on a direct connection. This works, but realtime fired from web handlers still routes through the same `QUESTPIE_DB`, so realtime in web fails. In practice, going direct everywhere is simpler.
351
+ - **TODO / current limitation:** the framework reads a single `QUESTPIE_DB` env var. There is no built-in split between a pooled URL and a direct URL for LISTEN consumers. Track this if you need a mixed topology.
352
+
230
353
  ## Email
231
354
 
232
355
  Transactional email with typed templates. Two adapters: `SmtpAdapter` (production) and `ConsoleAdapter` (development).
233
356
 
234
357
  ```ts
235
- import { SmtpAdapter, ConsoleAdapter, runtimeConfig } from "questpie";
358
+ import { runtimeConfig } from "questpie/app";
359
+ import { ConsoleAdapter } from "questpie/adapters/console";
360
+ import { SmtpAdapter } from "questpie/adapters/smtp";
236
361
 
237
362
  export default runtimeConfig({
238
363
  email: {
239
364
  adapter:
240
- process.env.NODE_ENV === "development"
365
+ env.NODE_ENV === "development"
241
366
  ? new ConsoleAdapter({ logHtml: false })
242
367
  : new SmtpAdapter({
243
- transport: { host: process.env.SMTP_HOST, port: 587, secure: true },
368
+ transport: { host: env.SMTP_HOST, port: 587, secure: true },
244
369
  }),
245
370
  },
246
371
  });
@@ -268,12 +393,21 @@ const results = await client.search.search({
268
393
 
269
394
  ## KV Store
270
395
 
271
- ### Custom Adapter
396
+ ### Redis
272
397
 
273
398
  ```ts
399
+ import { createClient } from "redis";
400
+ import { redisKVAdapter } from "questpie/adapters/redis-kv";
401
+
402
+ async function getRedis() {
403
+ const redis = createClient({ url: env.REDIS_URL });
404
+ await redis.connect();
405
+ return redis;
406
+ }
407
+
274
408
  export default runtimeConfig({
275
409
  kv: {
276
- adapter: myKvAdapter,
410
+ adapter: redisKVAdapter({ client: getRedis, keyPrefix: "my-app:" }),
277
411
  defaultTtl: 3600,
278
412
  },
279
413
  });
@@ -291,7 +425,7 @@ kv: {
291
425
 
292
426
  ```ts
293
427
  handler: async ({ kv }) => {
294
- await kv.set("key", "value", { ttl: 3600 });
428
+ await kv.set("key", "value", 3600);
295
429
  const value = await kv.get("key");
296
430
  await kv.delete("key");
297
431
  };
@@ -373,17 +507,17 @@ CMD ["bun", "run", ".output/server/index.mjs"]
373
507
 
374
508
  ```ts
375
509
  // routes/health.ts
376
- import { route } from "questpie";
377
-
378
- export default route({
379
- method: "GET",
380
- handler: async ({ db }) => {
510
+ import { sql } from "questpie/drizzle";
511
+ import { route } from "questpie/services";
512
+
513
+ export default route()
514
+ .get()
515
+ .raw()
516
+ .access(true)
517
+ .handler(async ({ db }) => {
381
518
  await db.execute(sql`SELECT 1`);
382
- return new Response(JSON.stringify({ status: "ok" }), {
383
- headers: { "Content-Type": "application/json" },
384
- });
385
- },
386
- });
519
+ return Response.json({ status: "ok" });
520
+ });
387
521
  ```
388
522
 
389
523
  ## Common Mistakes
@@ -396,8 +530,8 @@ Without a strong secret, sessions can be forged. The default `"change-me"` is fo
396
530
  // WRONG -- in production
397
531
  secret: "change-me";
398
532
 
399
- // CORRECT -- strong random secret from environment
400
- secret: process.env.BETTER_AUTH_SECRET; // min 32 chars
533
+ // CORRECT -- declared in env.ts as z.string().min(32), validated at boot
534
+ secret: env.BETTER_AUTH_SECRET;
401
535
  ```
402
536
 
403
537
  ### HIGH: Not running migrations after schema changes
@@ -422,15 +556,17 @@ The local storage adapter writes to the filesystem. In containerized deployments
422
556
  storage: { basePath: "/api" }
423
557
 
424
558
  // CORRECT -- persistent S3 storage
425
- import { S3Driver } from "flydrive/drivers/s3";
559
+ import { s3 } from "files-sdk/s3";
426
560
 
427
561
  storage: {
428
562
  basePath: "/api",
429
- driver: new S3Driver({
430
- bucket: process.env.S3_BUCKET,
431
- region: process.env.S3_REGION,
432
- accessKeyId: process.env.S3_ACCESS_KEY,
433
- secretAccessKey: process.env.S3_SECRET_KEY,
563
+ adapter: s3({
564
+ bucket: env.S3_BUCKET,
565
+ region: env.S3_REGION,
566
+ credentials: {
567
+ accessKeyId: env.S3_ACCESS_KEY,
568
+ secretAccessKey: env.S3_SECRET_KEY,
569
+ },
434
570
  }),
435
571
  }
436
572
  ```
@@ -443,11 +579,40 @@ Without pg-boss configured, job `.publish()` calls silently do nothing. Jobs def
443
579
  // REQUIRED for jobs to actually execute
444
580
  queue: {
445
581
  adapter: pgBossAdapter({
446
- connectionString: process.env.DATABASE_URL,
582
+ connectionString: env.DATABASE_URL,
447
583
  }),
448
584
  }
449
585
  ```
450
586
 
587
+ ### HIGH: PgBouncer transaction pool with pgNotify/pgBoss
588
+
589
+ Pointing `QUESTPIE_DB` at a PgBouncer in transaction pool mode silently breaks `pgNotifyAdapter` and `pgBossAdapter`. Both rely on PostgreSQL `LISTEN/NOTIFY`, which requires a persistent session. PgBouncer transaction pooling reassigns the session per-transaction, so the listener is dropped right after `LISTEN` returns.
590
+
591
+ Symptoms:
592
+
593
+ - Realtime: SSE clients connect but never receive events; UI silently falls back to polling, or never refreshes
594
+ - Queue: jobs sit in the table; workers process them only on the polling tick (delayed by seconds to minutes), or never wake at all
595
+
596
+ Fix:
597
+
598
+ ```ts
599
+ // WRONG -- QUESTPIE_DB points at PgBouncer (transaction mode)
600
+ realtime: {
601
+ adapter: pgNotifyAdapter({ connectionString: env.QUESTPIE_DB });
602
+ }
603
+ queue: {
604
+ adapter: pgBossAdapter({ connectionString: env.QUESTPIE_DB });
605
+ }
606
+
607
+ // CORRECT -- direct PG connection (Bun SQL pools internally)
608
+ // Or switch to redisStreamsAdapter for realtime in multi-instance deployments
609
+ realtime: {
610
+ adapter: redisStreamsAdapter({ url: env.REDIS_URL });
611
+ }
612
+ ```
613
+
614
+ If your infra mandates PgBouncer, use `session` pool mode for processes that run pgBoss or pgNotify. See "PgBouncer Compatibility" for the full matrix.
615
+
451
616
  ## Realtime and Live Preview
452
617
 
453
618
  The realtime adapter (`pgNotifyAdapter` or `redisStreamsAdapter`) is relevant for **detached or shared preview sessions** — when the preview runs in a separate browser tab, or multiple collaborators view the same preview.
@@ -101,8 +101,7 @@ Only hyphens are camelized in factory args; underscores are preserved (`global("
101
101
 
102
102
  ```ts
103
103
  // src/questpie/server/questpie.config.ts
104
- import { runtimeConfig } from "questpie";
105
-
104
+ import { runtimeConfig } from "questpie/app";
106
105
  export default runtimeConfig({
107
106
  app: {
108
107
  url: process.env.APP_URL || "http://localhost:3000",
@@ -162,7 +161,7 @@ Core: `text`, `number`, `boolean`, `date`, `datetime`, `time`, `select`, `relati
162
161
 
163
162
  ```ts
164
163
  // src/questpie/server/routes/get-overdue-tasks.ts
165
- import { route } from "questpie";
164
+ import { route } from "questpie/services";
166
165
  import z from "zod";
167
166
 
168
167
  export default route()
@@ -239,7 +238,7 @@ bunx questpie migrate:fresh
239
238
  ```ts
240
239
  // src/routes/api/$.ts
241
240
  import { createAPIFileRoute } from "@tanstack/react-start/api";
242
- import { createFetchHandler } from "questpie";
241
+ import { createFetchHandler } from "questpie/http";
243
242
  import { app } from "#questpie";
244
243
 
245
244
  const handler = createFetchHandler(app, { basePath: "/api" });
@@ -285,7 +284,7 @@ bun add @questpie/admin
285
284
 
286
285
  ```ts
287
286
  // src/questpie/server/modules.ts
288
- import { adminModule } from "@questpie/admin/server";
287
+ import { adminModule } from "@questpie/admin/modules/admin";
289
288
 
290
289
  export default [adminModule] as const;
291
290
  ```
@@ -443,8 +442,7 @@ export { default } from "./src/questpie/server/questpie.config";
443
442
 
444
443
  ```ts
445
444
  // src/questpie/server/questpie.config.ts
446
- import { runtimeConfig } from "questpie";
447
-
445
+ import { runtimeConfig } from "questpie/app";
448
446
  export default runtimeConfig({
449
447
  app: { url: process.env.APP_URL || "http://localhost:3000" },
450
448
  db: { url: process.env.DATABASE_URL! },
@@ -469,7 +467,7 @@ export default collection("posts").fields(({ f }) => ({
469
467
  ```ts
470
468
  // src/routes/api/$.ts
471
469
  import { createAPIFileRoute } from "@tanstack/react-start/api";
472
- import { createFetchHandler } from "questpie";
470
+ import { createFetchHandler } from "questpie/http";
473
471
  import { app } from "#questpie";
474
472
 
475
473
  const handler = createFetchHandler(app, { basePath: "/api" });