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.
- package/dist/index.mjs +362 -119
- package/package.json +2 -3
- package/templates/elysia/AGENTS.md +56 -0
- package/templates/elysia/CLAUDE.md +39 -0
- package/templates/elysia/Dockerfile +24 -0
- package/templates/elysia/README.md +148 -0
- package/templates/elysia/docker/init-extensions.sql +11 -0
- package/templates/elysia/docker-compose.yml +21 -0
- package/templates/elysia/env.example +16 -0
- package/templates/elysia/gitignore +6 -0
- package/templates/elysia/package.json +47 -0
- package/templates/elysia/questpie.config.ts +12 -0
- package/templates/elysia/src/index.ts +21 -0
- package/templates/elysia/src/lib/auth-client.ts +32 -0
- package/templates/elysia/src/lib/client.ts +13 -0
- package/templates/elysia/src/lib/env.ts +24 -0
- package/templates/elysia/src/lib/query-client.ts +18 -0
- package/templates/elysia/src/lib/query.ts +18 -0
- package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
- package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/elysia/src/questpie/server/app.ts +10 -0
- package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
- package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
- package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
- package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
- package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/elysia/src/questpie/server/modules.ts +8 -0
- package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
- package/templates/elysia/tsconfig.json +28 -0
- package/templates/hono/AGENTS.md +56 -0
- package/templates/hono/CLAUDE.md +39 -0
- package/templates/hono/Dockerfile +24 -0
- package/templates/hono/README.md +148 -0
- package/templates/hono/docker/init-extensions.sql +11 -0
- package/templates/hono/docker-compose.yml +21 -0
- package/templates/hono/env.example +16 -0
- package/templates/hono/gitignore +6 -0
- package/templates/hono/package.json +47 -0
- package/templates/hono/questpie.config.ts +12 -0
- package/templates/hono/src/index.ts +30 -0
- package/templates/hono/src/lib/auth-client.ts +32 -0
- package/templates/hono/src/lib/client.ts +13 -0
- package/templates/hono/src/lib/env.ts +24 -0
- package/templates/hono/src/lib/query-client.ts +18 -0
- package/templates/hono/src/lib/query.ts +18 -0
- package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
- package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/hono/src/questpie/server/app.ts +10 -0
- package/templates/hono/src/questpie/server/collections/index.ts +1 -0
- package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/hono/src/questpie/server/config/auth.ts +8 -0
- package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
- package/templates/hono/src/questpie/server/globals/index.ts +1 -0
- package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/hono/src/questpie/server/modules.ts +8 -0
- package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
- package/templates/hono/tsconfig.json +28 -0
- package/templates/next/AGENTS.md +55 -0
- package/templates/next/CLAUDE.md +39 -0
- package/templates/next/Dockerfile +25 -0
- package/templates/next/README.md +148 -0
- package/templates/next/components.json +22 -0
- package/templates/next/docker/init-extensions.sql +11 -0
- package/templates/next/docker-compose.yml +21 -0
- package/templates/next/env.example +16 -0
- package/templates/next/gitignore +10 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.ts +20 -0
- package/templates/next/package.json +54 -0
- package/templates/next/postcss.config.mjs +8 -0
- package/templates/next/public/.gitkeep +0 -0
- package/templates/next/questpie.config.ts +12 -0
- package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
- package/templates/next/src/app/admin/admin.css +4 -0
- package/templates/next/src/app/admin/layout.tsx +63 -0
- package/templates/next/src/app/api/[...all]/route.ts +24 -0
- package/templates/next/src/app/layout.tsx +24 -0
- package/templates/next/src/app/not-found.tsx +18 -0
- package/templates/next/src/app/page.tsx +74 -0
- package/templates/next/src/app/providers.tsx +11 -0
- package/templates/next/src/lib/auth-client.ts +12 -0
- package/templates/next/src/lib/client.ts +13 -0
- package/templates/next/src/lib/env.ts +24 -0
- package/templates/next/src/lib/query-client.ts +18 -0
- package/templates/next/src/lib/query.ts +18 -0
- package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/next/src/questpie/admin/admin.ts +9 -0
- package/templates/next/src/questpie/admin/modules.ts +3 -0
- package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
- package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
- package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
- package/templates/next/src/questpie/server/.generated/index.ts +139 -0
- package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
- package/templates/next/src/questpie/server/app.ts +10 -0
- package/templates/next/src/questpie/server/collections/index.ts +1 -0
- package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
- package/templates/next/src/questpie/server/config/admin.ts +80 -0
- package/templates/next/src/questpie/server/config/auth.ts +8 -0
- package/templates/next/src/questpie/server/config/openapi.ts +10 -0
- package/templates/next/src/questpie/server/globals/index.ts +1 -0
- package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
- package/templates/next/src/questpie/server/modules.ts +9 -0
- package/templates/next/src/questpie/server/questpie.config.ts +21 -0
- package/templates/next/src/styles.css +125 -0
- package/templates/next/tsconfig.json +37 -0
- package/templates/tanstack-start/AGENTS.md +35 -607
- package/templates/tanstack-start/CLAUDE.md +26 -134
- package/templates/tanstack-start/README.md +13 -1
- package/templates/tanstack-start/docker/init-extensions.sql +11 -0
- package/templates/tanstack-start/docker-compose.yml +1 -0
- package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
- package/templates/tanstack-start/src/lib/client.ts +1 -1
- package/templates/tanstack-start/src/lib/query.ts +18 -0
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
- package/templates/tanstack-start/src/routes/__root.tsx +31 -1
- package/templates/tanstack-start/src/routes/api/$.ts +1 -1
- package/templates/tanstack-start/src/routes/index.tsx +97 -0
- package/skills/questpie/AGENTS.md +0 -2871
- package/skills/questpie/SKILL.md +0 -293
- package/skills/questpie/coverage.json +0 -213
- package/skills/questpie/references/auth.md +0 -236
- package/skills/questpie/references/business-logic.md +0 -620
- package/skills/questpie/references/codegen-plugin-api.md +0 -382
- package/skills/questpie/references/crud-api.md +0 -580
- package/skills/questpie/references/data-modeling.md +0 -509
- package/skills/questpie/references/extend.md +0 -584
- package/skills/questpie/references/field-types.md +0 -398
- package/skills/questpie/references/infrastructure-adapters.md +0 -720
- package/skills/questpie/references/mcp.md +0 -147
- package/skills/questpie/references/multi-tenancy.md +0 -363
- package/skills/questpie/references/production.md +0 -640
- package/skills/questpie/references/query-operators.md +0 -125
- package/skills/questpie/references/quickstart.md +0 -562
- package/skills/questpie/references/rules.md +0 -454
- package/skills/questpie/references/sandbox.md +0 -110
- package/skills/questpie/references/tanstack-query.md +0 -543
- package/skills/questpie/references/type-inference.md +0 -167
- package/skills/questpie/references/workflows.md +0 -155
- package/skills/questpie-admin/AGENTS.md +0 -1515
- package/skills/questpie-admin/SKILL.md +0 -443
- package/skills/questpie-admin/references/blocks.md +0 -331
- package/skills/questpie-admin/references/custom-ui.md +0 -305
- 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
|
-
```
|