create-questpie 2.0.2 → 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 (40) hide show
  1. package/dist/index.mjs +244 -30
  2. package/package.json +1 -1
  3. package/skills/questpie/AGENTS.md +310 -103
  4. package/skills/questpie/SKILL.md +196 -84
  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 +26 -6
  10. package/skills/questpie/references/extend.md +98 -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 +31 -18
  17. package/skills/questpie/references/rules.md +140 -13
  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 +141 -68
  23. package/skills/questpie-admin/SKILL.md +96 -63
  24. package/skills/questpie-admin/references/blocks.md +28 -4
  25. package/skills/questpie-admin/references/custom-ui.md +1 -1
  26. package/skills/questpie-admin/references/views.md +21 -5
  27. package/templates/tanstack-start/AGENTS.md +15 -8
  28. package/templates/tanstack-start/CLAUDE.md +12 -5
  29. package/templates/tanstack-start/README.md +7 -6
  30. package/templates/tanstack-start/package.json +1 -0
  31. package/templates/tanstack-start/src/lib/query-client.ts +10 -1
  32. package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
  33. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
  34. package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
  35. package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
  36. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
  37. package/templates/tanstack-start/src/routes/admin/$.tsx +12 -1
  38. package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
  39. package/templates/tanstack-start/src/routes/api/$.ts +1 -2
  40. package/templates/tanstack-start/vite.config.ts +2 -2
@@ -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" });
@@ -494,7 +492,7 @@ bun dev
494
492
 
495
493
  ## 12. Live Preview (Optional)
496
494
 
497
- Add split-screen live preview to any collection with `.preview()`. The current same-tab flow refreshes the preview iframe after save/autosave and supports field focus over `postMessage`.
495
+ Add split-screen live preview to any collection with `.preview()`. Live Preview uses the existing admin `FormView`, Preview button, `LivePreviewMode`, and iframe. Do not introduce a second default form view or parallel preview API names.
498
496
 
499
497
  ### Add Preview to a Collection
500
498
 
@@ -518,32 +516,47 @@ export default collection("pages")
518
516
 
519
517
  ### Add Preview Support to the Frontend Page
520
518
 
519
+ Frontend checklist:
520
+
521
+ 1. Call `useCollectionPreview({ initialData, onRefresh })`.
522
+ 2. Wrap the rendered output in `PreviewProvider`.
523
+ 3. Render from `preview.data`, not directly from loader data.
524
+ 4. Wrap editable scalar text in `PreviewField`.
525
+ 5. Render blocks with `BlockRenderer` when the page uses `f.blocks()`.
526
+
521
527
  ```tsx
522
528
  import {
529
+ BlockRenderer,
523
530
  PreviewField,
524
531
  PreviewProvider,
525
532
  useCollectionPreview,
526
533
  } from "@questpie/admin/client";
534
+ import admin from "@/questpie/admin/.generated/client";
527
535
 
528
- function PageView({ initialData }) {
536
+ function PageView({ page }) {
529
537
  const router = useRouter();
530
538
  const preview = useCollectionPreview({
531
- initialData,
539
+ initialData: page,
532
540
  onRefresh: () => router.invalidate(),
533
541
  });
534
542
 
535
543
  return (
536
- <PreviewProvider
537
- isPreviewMode={preview.isPreviewMode}
538
- focusedField={preview.focusedField}
539
- onFieldClick={preview.handleFieldClick}
540
- >
541
- <PreviewField field="title" as="h1">
544
+ <PreviewProvider preview={preview}>
545
+ <PreviewField field="title" editable="text" as="h1">
542
546
  {preview.data.title}
543
547
  </PreviewField>
548
+ <BlockRenderer
549
+ content={preview.data.content}
550
+ data={preview.data.content?._data}
551
+ renderers={admin.blocks}
552
+ selectedBlockId={preview.selectedBlockId}
553
+ onBlockClick={
554
+ preview.isPreviewMode ? preview.handleBlockClick : undefined
555
+ }
556
+ />
544
557
  </PreviewProvider>
545
558
  );
546
559
  }
547
560
  ```
548
561
 
549
- The `"hybrid"` strategy is recommended as the default it applies field patches instantly via `postMessage` while reconciling derived data (slugs, relations) through the server.
562
+ The form remains authoritative. Save, autosave, Cmd+S, history, workflow, locks, and actions stay in the existing form lifecycle.