create-questpie 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/index.mjs +362 -119
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. package/skills/questpie-admin/references/views.md +0 -449
@@ -1,640 +0,0 @@
1
- ---
2
- name: questpie-core/production
3
- description:
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
- - questpie-core
6
- ---
7
-
8
- This skill builds on questpie-core.
9
-
10
- ## Overview
11
-
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
-
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`.
48
-
49
- ## Authentication
50
-
51
- QUESTPIE uses [Better Auth](https://www.better-auth.com/). Configure via `config/auth.ts`:
52
-
53
- ```ts
54
- // src/questpie/server/config/auth.ts
55
- import { authConfig } from "questpie/app";
56
-
57
- import env from "../env";
58
-
59
- export default authConfig({
60
- emailAndPassword: {
61
- enabled: true,
62
- requireEmailVerification: false,
63
- },
64
- baseURL: env.APP_URL ?? "http://localhost:3000",
65
- basePath: "/api/auth",
66
- secret: env.BETTER_AUTH_SECRET,
67
- });
68
- ```
69
-
70
- ### Auth Options
71
-
72
- | Option | Type | Description |
73
- | ------------------------------------------- | --------- | --------------------------------------------------- |
74
- | `emailAndPassword.enabled` | `boolean` | Enable email/password login |
75
- | `emailAndPassword.requireEmailVerification` | `boolean` | Require email verification |
76
- | `baseURL` | `string` | App public URL |
77
- | `basePath` | `string` | Auth API path prefix |
78
- | `secret` | `string` | Session signing secret (min 32 chars in production) |
79
-
80
- ### Session in Handlers
81
-
82
- Access the current session in functions, hooks, and access rules:
83
-
84
- ```ts
85
- handler: async ({ session }) => {
86
- if (!session) throw new Error("Not authenticated");
87
- const user = session.user;
88
- // user.id, user.email, user.name
89
- };
90
- ```
91
-
92
- ### Access Control with Session
93
-
94
- ```ts
95
- .access({
96
- read: true,
97
- create: ({ session }) => !!session,
98
- update: ({ session }) => (session?.user as any)?.role === "admin",
99
- delete: ({ session }) => (session?.user as any)?.role === "admin",
100
- })
101
- ```
102
-
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.
125
-
126
- ## Database
127
-
128
- PostgreSQL with Drizzle ORM. Schema is generated from your collection and global definitions.
129
-
130
- ```ts
131
- export default runtimeConfig({
132
- db: {
133
- url: env.DATABASE_URL,
134
- },
135
- });
136
- ```
137
-
138
- Raw access via `db` context, indexes via `.indexes()`. See `references/infrastructure-adapters.md` for field-to-column mapping and full details.
139
-
140
- ## Migrations
141
-
142
- ### Development: Push
143
-
144
- Sync schema directly without migration files:
145
-
146
- ```bash
147
- bunx questpie push
148
- ```
149
-
150
- ### Production: Migration Files
151
-
152
- ```bash
153
- # Generate migration from schema diff
154
- bunx questpie migrate:generate
155
-
156
- # Run pending migrations
157
- bunx questpie migrate:up
158
-
159
- # Rollback last migration
160
- bunx questpie migrate:down
161
-
162
- # Drop everything and re-run (DESTRUCTIVE -- dev only)
163
- bunx questpie migrate:fresh
164
-
165
- # Reset migration tracking
166
- bunx questpie migrate:reset
167
- ```
168
-
169
- Configure migration and seed directories in `questpie.config.ts` under `cli.migrations.directory` and `cli.seeds.directory`. Run seeds with `bunx questpie seed`.
170
-
171
- ## Storage
172
-
173
- QUESTPIE uses [Files SDK](https://files-sdk.dev/) for file storage.
174
-
175
- ### Local (Development Default)
176
-
177
- ```ts
178
- export default runtimeConfig({
179
- storage: {
180
- basePath: "/api",
181
- },
182
- });
183
- ```
184
-
185
- ### S3 (Production)
186
-
187
- ```ts
188
- import { s3 } from "files-sdk/s3";
189
-
190
- export default runtimeConfig({
191
- storage: {
192
- basePath: "/api",
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,
218
- }),
219
- },
220
- });
221
- ```
222
-
223
- ### Upload Fields
224
-
225
- ```ts
226
- avatar: f.upload({
227
- to: "assets",
228
- mimeTypes: ["image/*"],
229
- maxSize: 5_000_000,
230
- }),
231
- ```
232
-
233
- ## Queue
234
-
235
- Background job processing with [pg-boss](https://github.com/timgit/pg-boss). Jobs stored in PostgreSQL.
236
-
237
- ```ts
238
- import { runtimeConfig } from "questpie/app";
239
- import { pgBossAdapter } from "questpie/adapters/pg-boss";
240
-
241
- export default runtimeConfig({
242
- queue: {
243
- adapter: pgBossAdapter({
244
- connectionString: env.DATABASE_URL,
245
- }),
246
- },
247
- });
248
- ```
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
-
254
- ### Publishing Jobs
255
-
256
- From hooks, functions, or other jobs:
257
-
258
- ```ts
259
- handler: async ({ queue }) => {
260
- await queue.sendAppointmentConfirmation.publish({
261
- appointmentId: "abc",
262
- customerId: "def",
263
- });
264
- };
265
- ```
266
-
267
- The `queue` object is fully typed -- autocompletion shows all registered jobs and their payload schemas.
268
-
269
- ## Realtime
270
-
271
- SSE-based live updates via `POST /realtime` multiplexed endpoint.
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
-
275
- ### pgNotify (Single Instance)
276
-
277
- ```ts
278
- import { runtimeConfig } from "questpie/app";
279
- import { pgNotifyAdapter } from "questpie/adapters/pg-notify";
280
-
281
- export default runtimeConfig({
282
- realtime: {
283
- adapter: pgNotifyAdapter({
284
- connectionString: env.DATABASE_URL,
285
- }),
286
- },
287
- });
288
- ```
289
-
290
- ### Redis Streams (Multi-Instance)
291
-
292
- Required for horizontal scaling:
293
-
294
- ```ts
295
- import { runtimeConfig } from "questpie/app";
296
- import { redisStreamsAdapter } from "questpie/adapters/redis-streams";
297
-
298
- export default runtimeConfig({
299
- realtime: {
300
- adapter: redisStreamsAdapter({
301
- url: env.REDIS_URL,
302
- }),
303
- },
304
- });
305
- ```
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
-
353
- ## Email
354
-
355
- Transactional email with typed templates. Two adapters: `SmtpAdapter` (production) and `ConsoleAdapter` (development).
356
-
357
- ```ts
358
- import { runtimeConfig } from "questpie/app";
359
- import { ConsoleAdapter } from "questpie/adapters/console";
360
- import { SmtpAdapter } from "questpie/adapters/smtp";
361
-
362
- export default runtimeConfig({
363
- email: {
364
- adapter:
365
- env.NODE_ENV === "development"
366
- ? new ConsoleAdapter({ logHtml: false })
367
- : new SmtpAdapter({
368
- transport: { host: env.SMTP_HOST, port: 587, secure: true },
369
- }),
370
- },
371
- });
372
- ```
373
-
374
- Templates go in `emails/` directory using the `email()` factory. Send via `email.sendTemplate()` in handlers. See `references/infrastructure-adapters.md` for template examples.
375
-
376
- ## Search
377
-
378
- PostgreSQL full-text search. Mark collections as searchable:
379
-
380
- ```ts
381
- .searchable(["title", "body", "tags"])
382
- ```
383
-
384
- Client usage:
385
-
386
- ```ts
387
- const results = await client.search.search({
388
- query: "haircut styles",
389
- collections: ["posts", "services"],
390
- limit: 20,
391
- });
392
- ```
393
-
394
- ## KV Store
395
-
396
- ### Redis
397
-
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
-
408
- export default runtimeConfig({
409
- kv: {
410
- adapter: redisKVAdapter({ client: getRedis, keyPrefix: "my-app:" }),
411
- defaultTtl: 3600,
412
- },
413
- });
414
- ```
415
-
416
- ### In-Memory Default
417
-
418
- ```ts
419
- kv: {
420
- defaultTtl: 3600,
421
- }
422
- ```
423
-
424
- ### Usage
425
-
426
- ```ts
427
- handler: async ({ kv }) => {
428
- await kv.set("key", "value", 3600);
429
- const value = await kv.get("key");
430
- await kv.delete("key");
431
- };
432
- ```
433
-
434
- ## Logger
435
-
436
- Structured logging with [Pino](https://getpino.io):
437
-
438
- ```ts
439
- handler: async ({ logger }) => {
440
- logger.info("Processing booking");
441
- logger.error({ err: error }, "Booking failed");
442
- logger.debug({ barberId, serviceId }, "Checking availability");
443
- };
444
- ```
445
-
446
- Log levels: `trace`, `debug`, `info`, `warn`, `error`, `fatal`.
447
-
448
- Structured data goes as the first argument:
449
-
450
- ```ts
451
- logger.info({ appointmentId: "abc", action: "created" }, "Appointment created");
452
- ```
453
-
454
- ## OpenAPI
455
-
456
- Auto-generate OpenAPI 3.1 spec with `@questpie/openapi`. Install with `bun add @questpie/openapi`, add `openApiModule` to `modules.ts`, configure it in `config/openapi.ts` with `openApiConfig({ info: { title: "My API", version: "1.0.0" } })`, then run `bunx questpie generate`. Serves spec at `/api/openapi.json` and Scalar docs at `/api/docs`. See `references/infrastructure-adapters.md` for full options.
457
-
458
- ## Deployment
459
-
460
- ### Docker
461
-
462
- ```dockerfile
463
- FROM oven/bun:1 AS base
464
- WORKDIR /app
465
-
466
- FROM base AS build
467
- COPY package.json bun.lockb ./
468
- RUN bun install --frozen-lockfile
469
- COPY . .
470
- RUN bunx questpie generate
471
- RUN bun run build
472
-
473
- FROM base AS production
474
- COPY --from=build /app/.output /app/.output
475
- EXPOSE 3000
476
- CMD ["bun", "run", ".output/server/index.mjs"]
477
- ```
478
-
479
- ### Environment Variables
480
-
481
- | Variable | Required | Description |
482
- | ----------------------------------- | -------- | ------------------------------------- |
483
- | `DATABASE_URL` | Yes | PostgreSQL connection string |
484
- | `APP_URL` | Yes | Public URL of the application |
485
- | `APP_SECRET` / `BETTER_AUTH_SECRET` | Yes | Session signing secret (min 32 chars) |
486
- | `SMTP_HOST` | No | Email SMTP host |
487
- | `SMTP_PORT` | No | Email SMTP port |
488
- | `REDIS_URL` | No | Redis URL (for KV, realtime) |
489
- | `S3_BUCKET` | No | S3 bucket name |
490
- | `S3_REGION` | No | S3 region |
491
- | `S3_ACCESS_KEY` | No | S3 access key |
492
- | `S3_SECRET_KEY` | No | S3 secret key |
493
-
494
- ### Production Checklist
495
-
496
- - Set strong `APP_SECRET` (min 32 characters)
497
- - Use production `DATABASE_URL` with SSL
498
- - Run `bunx questpie migrate:up` before deploying
499
- - Configure SMTP for transactional email
500
- - Set `APP_URL` to your public domain
501
- - Enable HTTPS
502
- - Configure S3 or persistent storage for uploads
503
- - Use `redisStreamsAdapter` if running multiple instances
504
- - Set up health checks
505
-
506
- ### Health Check
507
-
508
- ```ts
509
- // routes/health.ts
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 }) => {
518
- await db.execute(sql`SELECT 1`);
519
- return Response.json({ status: "ok" });
520
- });
521
- ```
522
-
523
- ## Common Mistakes
524
-
525
- ### CRITICAL: Missing BETTER_AUTH_SECRET in production
526
-
527
- Without a strong secret, sessions can be forged. The default `"change-me"` is for development only.
528
-
529
- ```ts
530
- // WRONG -- in production
531
- secret: "change-me";
532
-
533
- // CORRECT -- declared in env.ts as z.string().min(32), validated at boot
534
- secret: env.BETTER_AUTH_SECRET;
535
- ```
536
-
537
- ### HIGH: Not running migrations after schema changes
538
-
539
- When you add, remove, or change collection fields, the database schema must be updated. Without migrations, queries fail or return stale data.
540
-
541
- ```bash
542
- # After changing any collection fields:
543
- bunx questpie migrate:generate # create migration file
544
- bunx questpie migrate:up # apply to database
545
-
546
- # Or in development:
547
- bunx questpie push # direct schema sync (no migration file)
548
- ```
549
-
550
- ### HIGH: Using local storage in production without persistent volume
551
-
552
- The local storage adapter writes to the filesystem. In containerized deployments, files are lost when the container restarts.
553
-
554
- ```ts
555
- // WRONG -- files lost on container restart
556
- storage: { basePath: "/api" }
557
-
558
- // CORRECT -- persistent S3 storage
559
- import { s3 } from "files-sdk/s3";
560
-
561
- storage: {
562
- basePath: "/api",
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
- },
570
- }),
571
- }
572
- ```
573
-
574
- ### MEDIUM: Missing queue adapter for background jobs
575
-
576
- Without pg-boss configured, job `.publish()` calls silently do nothing. Jobs defined in `jobs/` will never run.
577
-
578
- ```ts
579
- // REQUIRED for jobs to actually execute
580
- queue: {
581
- adapter: pgBossAdapter({
582
- connectionString: env.DATABASE_URL,
583
- }),
584
- }
585
- ```
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
-
616
- ## Realtime and Live Preview
617
-
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.
619
-
620
- For the default **same-tab preview**, realtime is NOT involved. Current same-tab preview uses `postMessage` for refresh/focus messages between the editor and the iframe.
621
-
622
- | Preview mode | Transport | Requires realtime adapter? |
623
- | ------------------- | -------------- | -------------------------- |
624
- | Same-tab (default) | `postMessage` | No |
625
- | Detached tab | SSE / realtime | Yes |
626
- | Shared / multi-user | SSE / realtime | Yes |
627
-
628
- If your app only uses same-tab preview (the default), you do not need to configure a realtime adapter for preview purposes. Configure it when you need detached preview, multi-user collaboration, or other realtime features (live notifications, presence, etc.).
629
-
630
- ### MEDIUM: Missing APP_URL environment variable
631
-
632
- Auth callbacks, email links, and storage URLs all depend on `APP_URL`. Without it, OAuth redirects break and email links point to `localhost`.
633
-
634
- ```bash
635
- # WRONG
636
- # APP_URL not set
637
-
638
- # CORRECT
639
- APP_URL=https://myapp.example.com
640
- ```