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.
- package/dist/index.mjs +244 -30
- package/package.json +1 -1
- package/skills/questpie/AGENTS.md +310 -103
- package/skills/questpie/SKILL.md +196 -84
- package/skills/questpie/coverage.json +213 -0
- package/skills/questpie/references/auth.md +119 -4
- package/skills/questpie/references/business-logic.md +126 -56
- package/skills/questpie/references/crud-api.md +231 -29
- package/skills/questpie/references/data-modeling.md +26 -6
- package/skills/questpie/references/extend.md +98 -7
- package/skills/questpie/references/field-types.md +14 -2
- package/skills/questpie/references/infrastructure-adapters.md +207 -32
- package/skills/questpie/references/mcp.md +147 -0
- package/skills/questpie/references/multi-tenancy.md +1 -2
- package/skills/questpie/references/production.md +218 -53
- package/skills/questpie/references/quickstart.md +31 -18
- package/skills/questpie/references/rules.md +140 -13
- package/skills/questpie/references/sandbox.md +110 -0
- package/skills/questpie/references/tanstack-query.md +34 -11
- package/skills/questpie/references/type-inference.md +167 -0
- package/skills/questpie/references/workflows.md +155 -0
- package/skills/questpie-admin/AGENTS.md +141 -68
- package/skills/questpie-admin/SKILL.md +96 -63
- package/skills/questpie-admin/references/blocks.md +28 -4
- package/skills/questpie-admin/references/custom-ui.md +1 -1
- package/skills/questpie-admin/references/views.md +21 -5
- package/templates/tanstack-start/AGENTS.md +15 -8
- package/templates/tanstack-start/CLAUDE.md +12 -5
- package/templates/tanstack-start/README.md +7 -6
- package/templates/tanstack-start/package.json +1 -0
- package/templates/tanstack-start/src/lib/query-client.ts +10 -1
- package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
- package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +2 -1
- package/templates/tanstack-start/src/routes/admin/$.tsx +12 -1
- package/templates/tanstack-start/src/routes/admin/index.tsx +12 -5
- package/templates/tanstack-start/src/routes/api/$.ts +1 -2
- 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
|
|
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 |
|
|
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 (`
|
|
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:
|
|
64
|
+
baseURL: env.APP_URL ?? "http://localhost:3000",
|
|
38
65
|
basePath: "/api/auth",
|
|
39
|
-
secret:
|
|
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
|
|
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:
|
|
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 [
|
|
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 {
|
|
188
|
+
import { s3 } from "files-sdk/s3";
|
|
141
189
|
|
|
142
190
|
export default runtimeConfig({
|
|
143
191
|
storage: {
|
|
144
192
|
basePath: "/api",
|
|
145
|
-
|
|
146
|
-
bucket:
|
|
147
|
-
region:
|
|
148
|
-
|
|
149
|
-
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
365
|
+
env.NODE_ENV === "development"
|
|
241
366
|
? new ConsoleAdapter({ logHtml: false })
|
|
242
367
|
: new SmtpAdapter({
|
|
243
|
-
transport: { host:
|
|
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
|
-
###
|
|
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:
|
|
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",
|
|
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 {
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
|
383
|
-
|
|
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 --
|
|
400
|
-
secret:
|
|
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 {
|
|
559
|
+
import { s3 } from "files-sdk/s3";
|
|
426
560
|
|
|
427
561
|
storage: {
|
|
428
562
|
basePath: "/api",
|
|
429
|
-
|
|
430
|
-
bucket:
|
|
431
|
-
region:
|
|
432
|
-
|
|
433
|
-
|
|
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:
|
|
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/
|
|
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()`.
|
|
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({
|
|
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
|
-
|
|
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
|
|
562
|
+
The form remains authoritative. Save, autosave, Cmd+S, history, workflow, locks, and actions stay in the existing form lifecycle.
|