create-questpie 1.0.0 → 2.0.1

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 (43) hide show
  1. package/README.md +20 -12
  2. package/dist/index.mjs +10 -3
  3. package/package.json +21 -21
  4. package/templates/tanstack-start/AGENTS.md +371 -318
  5. package/templates/tanstack-start/CLAUDE.md +84 -52
  6. package/templates/tanstack-start/README.md +65 -52
  7. package/templates/tanstack-start/components.json +20 -20
  8. package/templates/tanstack-start/package.json +6 -0
  9. package/templates/tanstack-start/questpie.config.ts +7 -7
  10. package/templates/tanstack-start/src/lib/auth-client.ts +3 -3
  11. package/templates/tanstack-start/src/lib/client.ts +13 -0
  12. package/templates/tanstack-start/src/lib/env.ts +19 -22
  13. package/templates/tanstack-start/src/lib/query-client.ts +5 -5
  14. package/templates/tanstack-start/src/questpie/admin/.generated/client.ts +13 -0
  15. package/templates/tanstack-start/src/questpie/admin/admin.ts +8 -4
  16. package/templates/tanstack-start/src/questpie/admin/modules.ts +1 -0
  17. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +194 -0
  18. package/templates/tanstack-start/src/questpie/server/.generated/index.ts +201 -0
  19. package/templates/tanstack-start/src/questpie/server/app.ts +10 -52
  20. package/templates/tanstack-start/src/questpie/server/collections/posts.collection.ts +39 -53
  21. package/templates/tanstack-start/src/questpie/server/config/admin.ts +83 -0
  22. package/templates/tanstack-start/src/questpie/server/config/auth.ts +8 -0
  23. package/templates/tanstack-start/src/questpie/server/config/openapi.ts +10 -0
  24. package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +9 -14
  25. package/templates/tanstack-start/src/questpie/server/modules.ts +10 -0
  26. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +20 -0
  27. package/templates/tanstack-start/src/router.tsx +6 -5
  28. package/templates/tanstack-start/src/routes/__root.tsx +11 -9
  29. package/templates/tanstack-start/src/routes/admin/$.tsx +14 -13
  30. package/templates/tanstack-start/src/routes/admin/index.tsx +11 -10
  31. package/templates/tanstack-start/src/routes/admin/login.tsx +11 -10
  32. package/templates/tanstack-start/src/routes/admin.tsx +53 -52
  33. package/templates/tanstack-start/src/routes/api/{cms/$.ts → $.ts} +6 -20
  34. package/templates/tanstack-start/src/styles.css +109 -109
  35. package/templates/tanstack-start/tsconfig.json +27 -25
  36. package/templates/tanstack-start/vite.config.ts +5 -3
  37. package/templates/tanstack-start/src/lib/cms-client.ts +0 -12
  38. package/templates/tanstack-start/src/migrations/index.ts +0 -8
  39. package/templates/tanstack-start/src/questpie/admin/builder.ts +0 -4
  40. package/templates/tanstack-start/src/questpie/server/builder.ts +0 -4
  41. package/templates/tanstack-start/src/questpie/server/dashboard.ts +0 -68
  42. package/templates/tanstack-start/src/questpie/server/rpc.ts +0 -4
  43. package/templates/tanstack-start/src/questpie/server/sidebar.ts +0 -26
@@ -1,13 +1,13 @@
1
1
  # AGENTS.md
2
2
 
3
- Source-of-truth guidance for AI agents working in this QUESTPIE CMS project.
3
+ Source-of-truth guidance for AI agents working in this QUESTPIE project.
4
4
 
5
5
  > **Docs for LLMs**: https://questpie.com/llms.txt (sitemap), https://questpie.com/llms-full.txt (full content)
6
6
 
7
7
  ## Project Overview
8
8
 
9
9
  - **Framework**: TanStack Start (React) + Vite + Nitro (Bun preset)
10
- - **CMS**: QUESTPIE — headless CMS framework with config-driven admin UI
10
+ - **QUESTPIE**: QUESTPIE — application framework with config-driven admin UI
11
11
  - **Database**: PostgreSQL (via Drizzle ORM)
12
12
  - **Styling**: Tailwind CSS v4 + shadcn/ui components
13
13
  - **Auth**: Better Auth (email/password)
@@ -20,7 +20,7 @@ When you need more context about QUESTPIE APIs, consult these resources in order
20
20
 
21
21
  1. **LLMs full docs**: https://questpie.com/llms-full.txt — complete documentation in a single LLM-optimized file
22
22
  2. **Online docs**: https://questpie.com/docs — browsable documentation
23
- 3. **Local API docs**: http://localhost:3000/api/cms/docs — Scalar UI (available when dev server is running)
23
+ 3. **Local API docs**: http://localhost:3000/api/docs — Scalar UI (available when dev server is running)
24
24
 
25
25
  Key documentation pages:
26
26
 
@@ -28,14 +28,14 @@ Key documentation pages:
28
28
  | -------------------------- | ---------------------------------------------------------------- |
29
29
  | Getting Started | https://questpie.com/docs/getting-started |
30
30
  | Project Structure | https://questpie.com/docs/getting-started/project-structure |
31
- | Your First CMS | https://questpie.com/docs/getting-started/your-first-cms |
31
+ | Your First QUESTPIE | https://questpie.com/docs/getting-started/your-first-app |
32
32
  | Architecture Principles | https://questpie.com/docs/mentality |
33
33
  | Field Builder | https://questpie.com/docs/server/field-builder |
34
34
  | Field Types Reference | https://questpie.com/docs/server/field-types |
35
35
  | Collections | https://questpie.com/docs/server/collections |
36
36
  | Globals | https://questpie.com/docs/server/globals |
37
37
  | Relations | https://questpie.com/docs/server/relations |
38
- | RPC (Server Functions) | https://questpie.com/docs/server/rpc |
38
+ | Routes | https://questpie.com/docs/backend/business-logic/routes |
39
39
  | Hooks & Lifecycle | https://questpie.com/docs/server/hooks-and-lifecycle |
40
40
  | Access Control | https://questpie.com/docs/server/access-control |
41
41
  | Reactive Fields | https://questpie.com/docs/server/reactive-fields |
@@ -64,25 +64,31 @@ Key documentation pages:
64
64
  src/
65
65
  questpie/
66
66
  server/ ← WHAT: data contracts and behavior
67
- builder.ts Shared builder: qb = q.use(adminModule)
68
- app.ts Composition root (collections, globals, auth, build)
69
- rpc.ts RPC router instance: r = rpc()
70
- sidebar.ts Admin sidebar configuration
71
- dashboard.ts Admin dashboard configuration
72
- collections/ One file per collection (*.collection.ts)
73
- globals/ One file per global (*.global.ts)
74
- functions/ RPC functions
75
- jobs/ ← Background job definitions
76
- blocks.ts Block definitions (if using blocks)
67
+ questpie.config.ts App config: runtimeConfig({ db, app, ... })
68
+ modules.ts Module dependencies (adminModule, openApiModule, etc.)
69
+ config/ Typed configuration files
70
+ auth.ts authConfig({...}) Better Auth options
71
+ app.ts appConfig({ locale, access, hooks, context })
72
+ admin.ts adminConfig({ sidebar, dashboard, branding, locale })
73
+ openapi.ts openApiConfig({ info, scalar })
74
+ .generated/ Codegen output (app instance + App type)
75
+ index.ts
76
+ collections/ One file per collection (auto-discovered)
77
+ globals/ ← One file per global (auto-discovered)
78
+ routes/ ← Server routes via route() (auto-discovered)
79
+ jobs/ ← Background job definitions (auto-discovered)
80
+ blocks/ ← Block definitions (auto-discovered)
77
81
  admin/ ← HOW: UI rendering concerns
78
- builder.ts Client builder: qa<AppCMS>().use(adminModule)
79
- hooks.ts ← Typed hooks via createTypedHooks<AppCMS>()
82
+ admin.ts Re-exports generated admin config
83
+ hooks.ts ← Typed hooks via createTypedHooks<App>()
84
+ .generated/ ← Codegen output (admin client config)
85
+ client.ts
80
86
  blocks/ ← Block renderers (if using blocks)
81
87
  lib/
82
88
  env.ts ← Type-safe env vars (@t3-oss/env-core + Zod)
83
- cms-client.ts CMS client instance
89
+ client.ts ← client instance
84
90
  routes/
85
- api/cms/$.ts CMS catch-all handler (REST + OpenAPI + auth)
91
+ api/$.ts QUESTPIE catch-all handler (REST + OpenAPI + auth)
86
92
  migrations/ ← Database migrations (generated by CLI)
87
93
  ```
88
94
 
@@ -90,28 +96,33 @@ src/
90
96
 
91
97
  ### Server-First Split
92
98
 
93
- | Directory | Responsibility | Defines |
94
- | ------------------ | --------------------------------- | ---------------------------------- |
95
- | `questpie/server/` | **WHAT** — contracts and behavior | Schema, access, hooks, RPC, jobs |
96
- | `questpie/admin/` | **HOW** — rendering concerns | Branding, locale, custom renderers |
97
- | `routes/` | **Mounting** — HTTP wiring | Route handlers, no business logic |
99
+ | Directory | Responsibility | Defines |
100
+ | ------------------ | --------------------------------- | ----------------------------------- |
101
+ | `questpie/server/` | **WHAT** — contracts and behavior | Schema, access, hooks, routes, jobs |
102
+ | `questpie/admin/` | **HOW** — rendering concerns | Branding, locale, custom renderers |
103
+ | `routes/` | **Mounting** — HTTP wiring | Route handlers, no business logic |
98
104
 
99
105
  ### File Naming Conventions
100
106
 
101
- - Collections: `*.collection.ts` (e.g., `posts.collection.ts`)
102
- - Globals: `*.global.ts` (e.g., `site-settings.global.ts`)
103
- - RPC functions: `*.function.ts` or grouped in `functions/` directory
104
- - Background jobs: grouped in `jobs/` directory
107
+ - Collections: `*.ts` in `collections/` (e.g., `posts.ts`) — named exports
108
+ - Globals: `*.ts` in `globals/` (e.g., `site-settings.ts`) — named exports
109
+ - Routes: `*.ts` in `routes/` (e.g., `get-stats.ts`) — default exports
110
+ - Jobs: `*.ts` in `jobs/` (e.g., `send-email.ts`) — default exports
111
+ - Blocks: `*.ts` in `blocks/` (e.g., `hero.ts`) — named exports
105
112
 
106
113
  ### Key Files
107
114
 
108
- - **`src/questpie/server/builder.ts`** — Creates the shared builder `qb = q.use(adminModule)` used by all collections/globals.
109
- - **`src/questpie/server/app.ts`** — Composition root. Registers collections, globals, sidebar, dashboard, auth, and calls `.build()`. Also exports `appRpc`, `AppCMS`, `AppRpc`.
110
- - **`src/questpie/server/rpc.ts`** — Creates the RPC builder `r = rpc()` used by all server functions.
111
- - **`src/questpie/admin/builder.ts`** — Creates the client builder `admin = qa<AppCMS>().use(adminModule)`.
115
+ - **`src/questpie/server/questpie.config.ts`** — App config: `runtimeConfig({ db, app, ... })`.
116
+ - **`src/questpie/server/modules.ts`** — Module dependencies: `export default [adminModule, openApiModule] as const`.
117
+ - **`src/questpie/server/config/auth.ts`** — Auth config via `authConfig()` factory.
118
+ - **`src/questpie/server/config/admin.ts`** — Admin config (sidebar, dashboard, branding, locale) via `adminConfig()` factory.
119
+ - **`src/questpie/server/config/app.ts`** — *(optional, not scaffolded)* App config (locale, access, hooks, context) via `appConfig()`. Create when needed.
120
+ - **`src/questpie/server/config/openapi.ts`** — OpenAPI config via `openApiConfig()` factory.
121
+ - **`src/questpie/server/.generated/index.ts`** — Codegen output. Exports typed `app` instance and `App` type. Run `bunx questpie generate` to regenerate.
122
+ - **`src/questpie/admin/.generated/client.ts`** — Codegen output: pre-built admin client config. Run `bunx questpie generate` to regenerate.
112
123
  - **`src/lib/env.ts`** — Type-safe env variables via `@t3-oss/env-core`. Add new env vars here with Zod schemas.
113
124
  - **`questpie.config.ts`** — CLI config (migration directory, app reference).
114
- - **`src/routes/api/cms/$.ts`** — CMS API catch-all handler. Serves REST + OpenAPI docs at `/api/cms/docs`.
125
+ - **`src/routes/api/$.ts`** — API catch-all handler. Serves REST + OpenAPI docs at `/api/docs`.
115
126
 
116
127
  ## How To Write Code
117
128
 
@@ -120,181 +131,173 @@ src/
120
131
  Keep the entire builder chain in one file — single source of truth per entity:
121
132
 
122
133
  ```ts
123
- // src/questpie/server/collections/posts.collection.ts
124
- import { qb } from "@/questpie/server/builder";
125
-
126
- export const posts = qb
127
- .collection("posts")
128
- .fields((f) => ({
129
- title: f.text({ label: "Title", required: true }),
130
- slug: f.slug({ label: "Slug", from: "title" }),
131
- content: f.richText({ label: "Content" }),
132
- published: f.boolean({ label: "Published", default: false }),
133
- category: f.select({ label: "Category", options: ["news", "blog", "tutorial"] }),
134
- author: f.relation({ label: "Author", to: "users" }),
135
- image: f.upload({ label: "Cover Image" }),
136
- }))
137
- .title(({ f }) => f.title)
138
- .admin(({ c }) => ({
139
- label: "Posts",
140
- icon: c.icon("ph:article"),
141
- }))
142
- .access({
143
- read: true,
144
- create: ({ session }) => !!session,
145
- update: ({ session }) => !!session,
146
- delete: ({ session }) => session?.user?.role === "admin",
147
- })
148
- .hooks({
149
- beforeCreate: [async ({ data, ctx }) => { /* ... */ return data; }],
150
- })
151
- .list(({ v }) => v.table({}))
152
- .form(({ v, f }) =>
153
- v.form({
154
- sidebar: { position: "right", fields: [f.slug, f.published, f.category] },
155
- fields: [f.title, f.content, f.author, f.image],
156
- })
157
- );
134
+ // src/questpie/server/collections/posts.ts
135
+ import { collection } from "#questpie/factories";
136
+
137
+ export const posts = collection("posts")
138
+ .fields(({ f }) => ({
139
+ title: f.text(255).label("Title").required(),
140
+ slug: f
141
+ .text(255)
142
+ .label("Slug")
143
+ .required()
144
+ .inputOptional()
145
+ .admin({
146
+ compute: {
147
+ handler: ({ data, prev }) => {
148
+ /* slugify title */
149
+ },
150
+ deps: ({ data }) => [data.title],
151
+ debounce: 300,
152
+ },
153
+ }),
154
+ content: f.richText().label("Content"),
155
+ published: f.boolean().label("Published").default(false),
156
+ category: f
157
+ .select([
158
+ { value: "news", label: "News" },
159
+ { value: "blog", label: "Blog" },
160
+ { value: "tutorial", label: "Tutorial" },
161
+ ])
162
+ .label("Category")
163
+ author: f.relation("users").label("Author"),
164
+ image: f.upload().label("Cover Image"),
165
+ }))
166
+ .title(({ f }) => f.title)
167
+ .admin(({ c }) => ({
168
+ label: "Posts",
169
+ icon: c.icon("ph:article"),
170
+ }))
171
+ .access({
172
+ read: true,
173
+ create: ({ session }) => !!session,
174
+ update: ({ session }) => !!session,
175
+ delete: ({ session }) => session?.user?.role === "admin",
176
+ })
177
+ .hooks({
178
+ beforeCreate: [
179
+ async ({ data, ctx }) => {
180
+ /* ... */ return data;
181
+ },
182
+ ],
183
+ })
184
+ .list(({ v }) => v.collectionTable({}))
185
+ .form(({ v, f }) =>
186
+ v.collectionForm({
187
+ sidebar: { position: "right", fields: [f.slug, f.published, f.category] },
188
+ fields: [f.title, f.content, f.author, f.image],
189
+ }),
190
+ );
158
191
  ```
159
192
 
160
- Then register it:
161
- 1. Export from `src/questpie/server/collections/index.ts`
162
- 2. Add to `.collections({ ..., posts })` in `app.ts`
163
- 3. Add to sidebar in `sidebar.ts`
164
- 4. Run `bun questpie migrate:create` to generate migration
193
+ Then (preferred):
194
+
195
+ 1. Use `bun questpie add collection <name>` to scaffold files
196
+ 2. Codegen runs automatically
197
+ 3. Run `bun questpie migrate:create` to generate migration
198
+
199
+ Manual workflow (if you create files yourself):
200
+
201
+ 1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
202
+ 2. Run `bun questpie migrate:create` to generate migration
203
+
204
+ Collections are auto-discovered by codegen — no manual registration needed.
165
205
 
166
206
  ### Available Field Types
167
207
 
168
- `text`, `number`, `boolean`, `date`, `dateTime`, `select`, `multiSelect`, `relation`, `upload`, `richText`, `json`, `slug`, `email`, `url`, `password`, `color`, `textarea`
208
+ **Core:** `text`, `number`, `boolean`, `date`, `datetime`, `time`, `select`, `relation`, `upload`, `object`, `json`, `from`, `email`, `url`, `textarea`.
209
+ **Admin module:** `richText`, `blocks` (provided by `@questpie/admin`)
169
210
 
170
211
  ### Creating a Global
171
212
 
172
213
  ```ts
173
- // src/questpie/server/globals/site-settings.global.ts
174
- import { qb } from "@/questpie/server/builder";
175
-
176
- export const siteSettings = qb
177
- .global("site_settings")
178
- .fields((f) => ({
179
- siteName: f.text({ label: "Site Name", required: true }),
180
- description: f.textarea({ label: "Description" }),
181
- logo: f.upload({ label: "Logo" }),
182
- maintenanceMode: f.boolean({ label: "Maintenance Mode", default: false }),
183
- }))
184
- .admin(({ c }) => ({ label: "Site Settings", icon: c.icon("ph:gear") }))
185
- .form(({ v, f }) => v.form({
186
- fields: [f.siteName, f.description, f.logo, f.maintenanceMode],
187
- }));
214
+ // src/questpie/server/globals/site-settings.ts
215
+ import { global } from "#questpie/factories";
216
+
217
+ export const siteSettings = global("siteSettings")
218
+ .fields(({ f }) => ({
219
+ siteName: f.text(255).label("Site Name").required(),
220
+ description: f.textarea().label("Description"),
221
+ logo: f.upload().label("Logo"),
222
+ maintenanceMode: f.boolean().label("Maintenance Mode").default(false),
223
+ }))
224
+ .admin(({ c }) => ({ label: "Site Settings", icon: c.icon("ph:gear") }))
225
+ .form(({ v, f }) =>
226
+ v.globalForm({
227
+ fields: [f.siteName, f.description, f.logo, f.maintenanceMode],
228
+ }),
229
+ );
188
230
  ```
189
231
 
190
- Then register it:
191
- 1. Export from `src/questpie/server/globals/index.ts`
192
- 2. Add to `.globals({ ..., siteSettings })` in `app.ts`
193
- 3. Add to sidebar in `sidebar.ts`
194
- 4. Run `bun questpie migrate:create`
232
+ Then (preferred):
195
233
 
196
- ### Creating an RPC Function (End-to-End Type-Safe)
234
+ 1. Use `bun questpie add global <name>` to scaffold files
235
+ 2. Codegen runs automatically
236
+ 3. Run `bun questpie migrate:create`
197
237
 
198
- QUESTPIE provides standalone RPC `cms` and `appRpc` are two independent instances, no circular dependency.
238
+ Manual workflow (if you create files yourself):
199
239
 
200
- **How typing works:**
201
- ```ts
202
- // rpc.ts — standalone RPC builder
203
- import { rpc } from "questpie";
204
- export const r = rpc();
205
- ```
240
+ 1. Run `bunx questpie generate` to regenerate `.generated/index.ts`
241
+ 2. Run `bun questpie migrate:create`
206
242
 
207
- ```ts
208
- // app.ts — imports r (runtime), exports cms and appRpc separately
209
- import { r } from "./rpc.js";
243
+ Globals are auto-discovered by codegen — no manual registration needed.
210
244
 
211
- export const cms = qb.collections({...}).build({...});
212
- export const appRpc = r.router({ ...adminRpc, myFn });
245
+ ### Creating a Server Route (End-to-End Type-Safe)
213
246
 
214
- export type AppCMS = typeof cms;
215
- export type AppRpc = typeof appRpc;
216
- ```
247
+ Routes are defined as standalone files in `routes/` and auto-discovered by codegen.
217
248
 
218
- **Step 1 — Define a function:**
249
+ **Step 1 — Define a route:**
219
250
 
220
251
  ```ts
221
- // src/questpie/server/functions/get-stats.function.ts
222
- import { r } from "@/questpie/server/rpc";
252
+ // src/questpie/server/routes/get-stats.ts
253
+ import { route } from "questpie";
223
254
  import { z } from "zod";
224
255
 
225
- export const getStats = r.fn({
226
- schema: z.object({
227
- period: z.enum(["day", "week", "month"]),
228
- }),
229
- handler: async ({ input, app }) => {
230
- // input: { period: "day" | "week" | "month" } — typed from Zod schema
231
- // app: fully typed CMS instance with autocomplete
232
- const count = await app.api.collections.posts.count({});
233
- return { totalPosts: count, period: input.period };
234
- },
235
- });
256
+ export default route()
257
+ .post()
258
+ .schema(
259
+ z.object({
260
+ period: z.enum(["day", "week", "month"]),
261
+ }),
262
+ )
263
+ .handler(async ({ input, collections }) => {
264
+ // input: typed from Zod schema; collections, db, session, etc. from AppContext
265
+ const count = await collections.posts.count({});
266
+ return { totalPosts: count, period: input.period };
267
+ });
236
268
  ```
237
269
 
238
- **Step 2 — Register in `app.ts`:**
239
-
240
- ```ts
241
- import { getStats } from "./functions/get-stats.function.js";
270
+ **Step 2 — Run codegen:**
242
271
 
243
- export const appRpc = r.router({
244
- ...adminRpc,
245
- getStats,
246
- });
272
+ ```bash
273
+ bunx questpie generate
247
274
  ```
248
275
 
249
- **Step 3 Call from client (fully typed):**
250
-
251
- ```ts
252
- import { client } from "@/lib/cms-client";
253
-
254
- const result = await client.rpc.getStats({ period: "week" });
255
- // result: { totalPosts: number, period: string }
256
- ```
276
+ The route is auto-discovered and available at `/api/get-stats`.
257
277
 
258
278
  **With access control:**
259
279
 
260
280
  ```ts
261
- export const adminOnlyFn = r.fn({
262
- access: ({ session }) => session?.user?.role === "admin",
263
- schema: z.object({ ... }),
264
- handler: async ({ input, app }) => { ... },
265
- });
281
+ export default route()
282
+ .post()
283
+ .schema(z.object({ ... }))
284
+ .handler(async ({ input, session }) => {
285
+ if (session?.user?.role !== "admin") throw new Error("Forbidden");
286
+ return { ok: true };
287
+ });
266
288
  ```
267
289
 
268
290
  **With TanStack Query:**
269
291
 
270
292
  ```ts
271
- import { useQuery, useMutation } from "@tanstack/react-query";
293
+ import { useQuery } from "@tanstack/react-query";
272
294
 
273
295
  const { data } = useQuery({
274
- queryKey: ["stats", period],
275
- queryFn: () => client.rpc.getStats({ period }),
276
- });
277
-
278
- const mutation = useMutation({
279
- mutationFn: (input) => client.rpc.createSomething(input),
296
+ queryKey: ["stats", period],
297
+ queryFn: () => client.routes.getStats({ period }),
280
298
  });
281
299
  ```
282
300
 
283
- **Type flow:**
284
-
285
- ```
286
- rpc() → r.fn() handlers get typed `app`
287
-
288
- r.fn({ schema, handler }) → RpcProcedureDefinition<TInput, TOutput>
289
-
290
- r.router({ myFn }) → AppRpc type (preserves all function types)
291
-
292
- createClient<AppCMS, AppRpc>(...) → client.rpc is fully typed
293
-
294
- client.rpc.myFn(input) → Input: compile-time + runtime (Zod) validation
295
- → Output: inferred from handler return type
296
- ```
297
-
298
301
  ### Blocks (Page Builder)
299
302
 
300
303
  Blocks are content building units for page builders and rich content areas.
@@ -302,128 +305,80 @@ Blocks are content building units for page builders and rich content areas.
302
305
  **Simple block (no data fetching):**
303
306
 
304
307
  ```ts
305
- // src/questpie/server/blocks.ts
306
- import { qb } from "./builder";
307
-
308
- const heroBlock = qb
309
- .block("hero")
310
- .admin(({ c }) => ({
311
- label: "Hero Section",
312
- icon: c.icon("ph:image"),
313
- category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
314
- }))
315
- .fields((f) => ({
316
- title: f.text({ label: "Title", required: true }),
317
- subtitle: f.textarea({ label: "Subtitle" }),
318
- backgroundImage: f.upload({ label: "Background Image" }),
319
- ctaText: f.text({ label: "CTA Text" }),
320
- ctaLink: f.text({ label: "CTA Link" }),
321
- }))
322
- .prefetch({ with: { backgroundImage: true } }); // expand upload to full URL
323
-
324
- export const blocks = { hero: heroBlock };
308
+ // src/questpie/server/blocks/hero.ts
309
+ import { block } from "#questpie/factories";
310
+
311
+ export const heroBlock = block("hero")
312
+ .admin(({ c }) => ({
313
+ label: "Hero Section",
314
+ icon: c.icon("ph:image"),
315
+ category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
316
+ }))
317
+ .fields(({ f }) => ({
318
+ title: f.text(255).label("Title").required(),
319
+ subtitle: f.textarea().label("Subtitle"),
320
+ backgroundImage: f.upload().label("Background Image"),
321
+ ctaText: f.text(255).label("CTA Text"),
322
+ ctaLink: f.text(255).label("CTA Link"),
323
+ }))
324
+ .prefetch({ with: { backgroundImage: true } }); // expand upload to full URL
325
325
  ```
326
326
 
327
327
  **Block with dynamic data fetching (prefetch):**
328
328
 
329
329
  ```ts
330
- const teamBlock = qb
331
- .block("team")
332
- .admin(({ c }) => ({
333
- label: "Team",
334
- icon: c.icon("ph:users"),
335
- category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
336
- }))
337
- .fields((f) => ({
338
- title: f.text({ label: "Title" }),
339
- limit: f.number({ label: "Number to Show", default: 4 }),
340
- }))
341
- .prefetch(async ({ values, ctx }) => {
342
- const res = await ctx.app.api.collections.members.find({
343
- limit: values.limit || 4,
344
- where: { isActive: true },
345
- with: { avatar: true },
346
- });
347
- return { members: res.docs };
348
- });
330
+ const teamBlock = block("team")
331
+ .admin(({ c }) => ({
332
+ label: "Team",
333
+ icon: c.icon("ph:users"),
334
+ category: { label: "Sections", icon: c.icon("ph:layout"), order: 1 },
335
+ }))
336
+ .fields(({ f }) => ({
337
+ title: f.text(255).label("Title"),
338
+ limit: f.number().label("Number to Show").default(4),
339
+ }))
340
+ .prefetch(async ({ values, ctx }) => {
341
+ const res = await ctx.collections.members.find({
342
+ limit: values.limit || 4,
343
+ where: { isActive: true },
344
+ with: { avatar: true },
345
+ });
346
+ return { members: res.docs };
347
+ });
349
348
  ```
350
349
 
351
- **Register blocks in `app.ts`:**
352
-
353
- ```ts
354
- import { blocks } from "./blocks";
355
-
356
- export const cms = qb
357
- .collections({ ... })
358
- .blocks(blocks) // ← register blocks
359
- .build({ ... });
360
- ```
350
+ Blocks in `blocks/` are auto-discovered by codegen. No manual registration needed.
361
351
 
362
352
  **Use blocks in a collection's richText field:**
363
353
 
364
354
  ```ts
365
- content: f.richText({
366
- label: "Content",
367
- blocks: [heroBlock, teamBlock],
368
- })
355
+ content: f.richText().label("Content").blocks([heroBlock, teamBlock]);
369
356
  ```
370
357
 
371
- #### Blocks & Circular Dependencies (BaseCMS Pattern)
372
-
373
- When blocks use `.prefetch()` with functional handlers that need typed access to `ctx.app` (e.g., `ctx.app.api.collections.posts.find(...)`), you hit a circular dependency:
374
-
375
- - `app.ts` imports `blocks.ts` (to register blocks)
376
- - `blocks.ts` wants to import `AppCMS` from `app.ts` (for typed prefetch)
377
- - **Circular!**
358
+ #### Blocks & Circular Dependencies
378
359
 
379
- **The workaround: split into `baseCms` and final `cms`:**
360
+ Block prefetch handlers receive `ctx` with fully typed `collections` and `globals` via `AppContext` augmentation. Use `ctx.collections.*` directly — no app import needed:
380
361
 
381
362
  ```ts
382
- // app.ts
383
- import { blocks } from "./blocks";
384
-
385
- // Step 1: Build everything EXCEPT blocks
386
- export const baseCms = qb
387
- .collections({ posts, pages })
388
- .globals({ siteSettings })
389
- .auth({ ... });
390
-
391
- // Step 2: Export the base type — blocks import THIS (not AppCMS)
392
- export type BaseCMS = (typeof baseCms)["$inferCms"];
393
-
394
- // Step 3: Add blocks and build
395
- export const cms = baseCms.blocks(blocks).build({ ... });
396
-
397
- export type AppCMS = typeof cms;
363
+ // blocks/latest-posts.ts
364
+ import { block } from "#questpie/factories";
365
+
366
+ export const latestPostsBlock = block("latest-posts")
367
+ .fields(({ f }) => ({
368
+ count: f.number().label("Number of Posts").default(3),
369
+ }))
370
+ .prefetch(async ({ values, ctx }) => {
371
+ const res = await ctx.collections.posts.find({
372
+ limit: values.count || 3,
373
+ where: { published: true },
374
+ });
375
+ return { posts: res.docs };
376
+ });
398
377
  ```
399
378
 
400
- ```ts
401
- // blocks.ts — imports BaseCMS (not AppCMS) to avoid circular dependency
402
- import { typedApp, type Where } from "questpie";
403
- import type { BaseCMS } from "./app";
404
-
405
- const latestPostsBlock = qb
406
- .block("latest-posts")
407
- .fields((f) => ({
408
- count: f.number({ label: "Number of Posts", default: 3 }),
409
- }))
410
- .prefetch(async ({ values, ctx }) => {
411
- // Use typedApp<BaseCMS> for typed access without circular import
412
- const cms = typedApp<BaseCMS>(ctx.app);
413
- const res = await cms.api.collections.posts.find({
414
- limit: values.count || 3,
415
- where: { published: true },
416
- orderBy: { createdAt: "desc" },
417
- });
418
- return { posts: res.docs };
419
- });
420
- ```
379
+ Do NOT import `app` from `#questpie` inside block files — these are imported BY `.generated/index.ts`, creating circular dependencies. Use the `ctx` parameters instead.
421
380
 
422
- **Key points:**
423
- - `BaseCMS` has the same collections/globals as `AppCMS` — blocks just aren't part of the type yet
424
- - `typedApp<BaseCMS>(ctx.app)` casts the untyped `ctx.app` to the typed CMS API
425
- - This is a known limitation; we're working on a more ergonomic solution
426
- - If your blocks only use declarative prefetch (`{ with: { field: true } }`), you don't need this pattern at all — it's only needed for functional prefetch that calls `ctx.app.api.*`
381
+ If your blocks only use declarative prefetch (`{ with: { field: true } }`), you don't need a function at all.
427
382
 
428
383
  ### Reactive Fields
429
384
 
@@ -437,75 +392,83 @@ Fields support reactive behaviors in `meta.admin`:
437
392
  All reactive handlers run **server-side** with access to `ctx.db`, `ctx.user`, `ctx.req`.
438
393
 
439
394
  ```ts
440
- fields: (f) => ({
441
- country: f.relation({ to: "countries", label: "Country" }),
442
- city: f.relation({
443
- to: "cities",
444
- label: "City",
445
- options: {
446
- handler: async ({ data, search, ctx }) => {
447
- const cities = await ctx.db.query.cities.findMany({
448
- where: { countryId: data.country },
449
- });
450
- return { options: cities.map((c) => ({ value: c.id, label: c.name })) };
451
- },
452
- deps: ({ data }) => [data.country],
453
- },
454
- }),
455
- status: f.select({
456
- label: "Status",
457
- options: ["draft", "published", "archived"],
458
- }),
459
- publishedAt: f.dateTime({
460
- label: "Published At",
461
- meta: {
462
- admin: {
463
- hidden: ({ data }: { data: Record<string, any> }) => data.status !== "published",
464
- },
465
- },
466
- }),
467
- })
395
+ fields: ({ f }) => ({
396
+ country: f.relation("countries").label("Country"),
397
+ city: f
398
+ .relation("cities")
399
+ .label("City")
400
+ .admin({
401
+ options: {
402
+ handler: async ({ data, search, ctx }) => {
403
+ const cities = await ctx.db.query.cities.findMany({
404
+ where: { countryId: data.country },
405
+ });
406
+ return {
407
+ options: cities.map((c) => ({ value: c.id, label: c.name })),
408
+ };
409
+ },
410
+ deps: ({ data }) => [data.country],
411
+ },
412
+ }),
413
+ status: f
414
+ .select([
415
+ { value: "draft", label: "Draft" },
416
+ { value: "published", label: "Published" },
417
+ { value: "archived", label: "Archived" },
418
+ ])
419
+ .label("Status")
420
+ publishedAt: f
421
+ .datetime()
422
+ .label("Published At")
423
+ .admin({
424
+ hidden: ({ data }) => data.status !== "published",
425
+ }),
426
+ });
468
427
  ```
469
428
 
470
429
  ### Admin Configuration (Client-Side)
471
430
 
472
- ```ts
473
- // src/questpie/admin/builder.ts
474
- import { adminModule, qa } from "@questpie/admin/client";
475
- import type { AppCMS } from "@/questpie/server/cms";
431
+ The admin client config is auto-generated by codegen into `admin/.generated/client.ts`.
432
+ No manual builder setup is needed. Run `bunx questpie generate` to regenerate.
476
433
 
477
- export const admin = qa<AppCMS>().use(adminModule);
434
+ ```ts
435
+ // src/questpie/admin/admin.ts (re-export for convenience)
436
+ export { default as admin } from "./.generated/client";
478
437
  ```
479
438
 
480
439
  ```ts
481
440
  // src/questpie/admin/hooks.ts
482
441
  import { createTypedHooks } from "@questpie/admin/client";
483
- import type { AppCMS } from "../server/cms";
442
+ import type { App } from "#questpie";
484
443
 
485
444
  export const {
486
- useCollectionList, useCollectionCount, useCollectionItem,
487
- useCollectionCreate, useCollectionUpdate, useCollectionDelete,
488
- useGlobal, useGlobalUpdate,
489
- } = createTypedHooks<AppCMS>();
445
+ useCollectionList,
446
+ useCollectionCount,
447
+ useCollectionItem,
448
+ useCollectionCreate,
449
+ useCollectionUpdate,
450
+ useCollectionDelete,
451
+ useGlobal,
452
+ useGlobalUpdate,
453
+ } = createTypedHooks<App>();
490
454
  ```
491
455
 
492
- ### CMS Route Handler
456
+ ### QUESTPIE Route Handler
493
457
 
494
458
  ```ts
495
- // src/routes/api/cms/$.ts
459
+ // src/routes/api/$.ts
496
460
  import { createFetchHandler } from "questpie";
497
- import { withOpenApi } from "@questpie/openapi";
498
- import { appRpc, cms } from "~/questpie/server/cms";
461
+ import { app } from "#questpie";
499
462
 
500
- const handler = withOpenApi(
501
- createFetchHandler(cms, { basePath: "/api/cms", rpc: appRpc }),
502
- { cms, rpc: appRpc, basePath: "/api/cms", info: { title: "My API", version: "1.0.0" } },
503
- );
463
+ const handler = createFetchHandler(app, { basePath: "/api" });
504
464
  ```
505
465
 
466
+ OpenAPI is registered as a module in `src/questpie/server/modules.ts` via `openApiModule` — no wrapper needed in the route handler.
467
+
506
468
  ### Icons
507
469
 
508
470
  Use `@iconify/react` with Phosphor icon set:
471
+
509
472
  - Prefix: `ph:` (e.g., `ph:house`, `ph:article`, `ph:gear`)
510
473
  - Weight variants: `-bold`, `-fill`, `-duotone`, `-light`, `-thin`
511
474
  - Regular weight = no suffix (default)
@@ -515,13 +478,16 @@ Use `@iconify/react` with Phosphor icon set:
515
478
  ## Environment Variables
516
479
 
517
480
  Type-safe via `@t3-oss/env-core` in `src/lib/env.ts`. All env vars must be:
481
+
518
482
  1. Declared with Zod schema in `env.ts`
519
483
  2. Accessed via `env.VAR_NAME` (not `process.env.VAR_NAME`)
520
484
 
521
485
  Required:
486
+
522
487
  - `DATABASE_URL` — PostgreSQL connection string
523
488
 
524
489
  Optional (with defaults):
490
+
525
491
  - `APP_URL` — Application URL (default: `http://localhost:3000`)
526
492
  - `BETTER_AUTH_SECRET` — Auth secret key
527
493
  - `MAIL_ADAPTER` — `console` or `smtp`
@@ -553,11 +519,98 @@ Always use these exact versions — check `package.json` before upgrading:
553
519
 
554
520
  - **Schema rules in client code** — Validation, access control, and hooks belong on the server.
555
521
  - **Splitting a collection across files** — Keep the full `.collection().fields().admin().list().form()` chain in one file.
556
- - **Business logic in route handlers** — Routes only mount handlers. Logic goes in RPC functions, hooks, or jobs.
522
+ - **Business logic in route handlers** — Mounting files should only mount handlers. Business logic goes in server routes, hooks, or jobs.
557
523
  - **Hardcoding view components** — Use the registry pattern for custom views.
558
524
  - **Using `process.env` directly** — Use the `env` object from `src/lib/env.ts`.
559
525
  - **Using Zod v3 API** — This project uses Zod v4. Use `z.object()` etc. from `zod` (v4).
560
526
  - **Using `asChild` prop** — This project uses `@base-ui/react`, not Radix. Use `render` prop instead.
561
527
  - **Using Radix UI or Lucide icons** — Use `@base-ui/react` and `@iconify/react` with `ph:` prefix.
562
528
  - **Adding UI config to database schema** — Admin UI config is UI-only, defined in builder chain.
563
- - **Importing `AppCMS` in `blocks.ts`**Use `BaseCMS` pattern to avoid circular dependencies (see Blocks section).
529
+ - **Importing `app` from `#questpie` in blocks/collections/hooks**Files inside `collections/`, `globals/`, `routes/`, `hooks/`, `blocks/` are imported BY `.generated/index.ts`, so importing from it back creates circular dependencies. Use the `ctx` parameters instead.
530
+
531
+ ## Live Preview
532
+
533
+ QUESTPIE supports live preview with a split-screen editor. The current implementation refreshes the preview iframe after save/autosave and uses `postMessage` for field/block focus sync.
534
+
535
+ ### Key Principles
536
+
537
+ - **Same-tab preview** = iframe refresh after save/autosave plus `postMessage` focus events
538
+ - **Frontend hook** = `useCollectionPreview({ initialData, onRefresh })`
539
+ - **Field focus** = `PreviewProvider` + `PreviewField`
540
+ - **Block field paths** = `BlockScopeProvider` + `PreviewField`
541
+
542
+ ### Server Config
543
+
544
+ Add `.preview()` to a collection to enable the split-screen editor:
545
+
546
+ ```ts
547
+ // src/questpie/server/collections/pages.ts
548
+ import { collection } from "#questpie/factories";
549
+
550
+ export const pages = collection("pages")
551
+ .fields(({ f }) => ({
552
+ title: f.text(255).label("Title").required(),
553
+ slug: f.text(255).label("Slug").required(),
554
+ content: f.blocks().label("Content"),
555
+ }))
556
+ .preview({
557
+ enabled: true,
558
+ position: "right",
559
+ defaultWidth: 50,
560
+ url: ({ record }) => {
561
+ const slug = record.slug as string;
562
+ return slug === "home" ? "/?preview=true" : `/${slug}?preview=true`;
563
+ },
564
+ });
565
+ ```
566
+
567
+ ### Frontend Integration
568
+
569
+ Use `useCollectionPreview` in your frontend page components:
570
+
571
+ ```tsx
572
+ // src/routes/[slug].tsx
573
+ import {
574
+ PreviewField,
575
+ PreviewProvider,
576
+ useCollectionPreview,
577
+ } from "@questpie/admin/client";
578
+
579
+ function PageComponent({ initialData }) {
580
+ const router = useRouter();
581
+ const preview = useCollectionPreview({
582
+ initialData,
583
+ onRefresh: () => router.invalidate(),
584
+ });
585
+
586
+ return (
587
+ <PreviewProvider
588
+ isPreviewMode={preview.isPreviewMode}
589
+ focusedField={preview.focusedField}
590
+ onFieldClick={preview.handleFieldClick}
591
+ >
592
+ <PreviewField field="title" as="h1">
593
+ {preview.data.title}
594
+ </PreviewField>
595
+ </PreviewProvider>
596
+ );
597
+ }
598
+ ```
599
+
600
+ #### Protocol
601
+
602
+ The implemented preview messages are simple `postMessage` events:
603
+
604
+ | Field | Description |
605
+ | ----------------- | --------------------------------------- |
606
+ | `PREVIEW_READY` | Preview iframe tells admin it is ready |
607
+ | `PREVIEW_REFRESH` | Admin asks iframe to refresh data |
608
+ | `FIELD_CLICKED` | Preview asks admin to focus a field |
609
+ | `BLOCK_CLICKED` | Preview asks admin to select a block |
610
+ | `FOCUS_FIELD` | Admin asks preview to highlight a field |
611
+ | `SELECT_BLOCK` | Admin asks preview to highlight a block |
612
+
613
+ ### Anti-Patterns (Preview)
614
+
615
+ - **Using V2-only APIs in this template** — `useQuestpiePreview`, `PreviewRoot`, and `PreviewBlock` are not exported yet.
616
+ - **Importing `app` inside previewed collection/block files** — use handler `ctx` values to avoid generated-app cycles.