create-questpie 1.0.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 (40) hide show
  1. package/README.md +81 -0
  2. package/dist/index.mjs +284 -0
  3. package/package.json +44 -0
  4. package/templates/tanstack-start/AGENTS.md +563 -0
  5. package/templates/tanstack-start/CLAUDE.md +105 -0
  6. package/templates/tanstack-start/Dockerfile +23 -0
  7. package/templates/tanstack-start/README.md +94 -0
  8. package/templates/tanstack-start/components.json +22 -0
  9. package/templates/tanstack-start/docker-compose.yml +20 -0
  10. package/templates/tanstack-start/env.example +16 -0
  11. package/templates/tanstack-start/gitignore +12 -0
  12. package/templates/tanstack-start/package.json +43 -0
  13. package/templates/tanstack-start/questpie.config.ts +12 -0
  14. package/templates/tanstack-start/src/admin.css +4 -0
  15. package/templates/tanstack-start/src/lib/auth-client.ts +12 -0
  16. package/templates/tanstack-start/src/lib/cms-client.ts +12 -0
  17. package/templates/tanstack-start/src/lib/env.ts +27 -0
  18. package/templates/tanstack-start/src/lib/query-client.ts +9 -0
  19. package/templates/tanstack-start/src/migrations/index.ts +8 -0
  20. package/templates/tanstack-start/src/questpie/admin/admin.ts +5 -0
  21. package/templates/tanstack-start/src/questpie/admin/builder.ts +4 -0
  22. package/templates/tanstack-start/src/questpie/server/app.ts +52 -0
  23. package/templates/tanstack-start/src/questpie/server/builder.ts +4 -0
  24. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -0
  25. package/templates/tanstack-start/src/questpie/server/collections/posts.collection.ts +72 -0
  26. package/templates/tanstack-start/src/questpie/server/dashboard.ts +68 -0
  27. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -0
  28. package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +24 -0
  29. package/templates/tanstack-start/src/questpie/server/rpc.ts +4 -0
  30. package/templates/tanstack-start/src/questpie/server/sidebar.ts +26 -0
  31. package/templates/tanstack-start/src/router.tsx +10 -0
  32. package/templates/tanstack-start/src/routes/__root.tsx +16 -0
  33. package/templates/tanstack-start/src/routes/admin/$.tsx +21 -0
  34. package/templates/tanstack-start/src/routes/admin/index.tsx +18 -0
  35. package/templates/tanstack-start/src/routes/admin/login.tsx +17 -0
  36. package/templates/tanstack-start/src/routes/admin.tsx +68 -0
  37. package/templates/tanstack-start/src/routes/api/cms/$.ts +45 -0
  38. package/templates/tanstack-start/src/styles.css +125 -0
  39. package/templates/tanstack-start/tsconfig.json +27 -0
  40. package/templates/tanstack-start/vite.config.ts +26 -0
@@ -0,0 +1,563 @@
1
+ # AGENTS.md
2
+
3
+ Source-of-truth guidance for AI agents working in this QUESTPIE CMS project.
4
+
5
+ > **Docs for LLMs**: https://questpie.com/llms.txt (sitemap), https://questpie.com/llms-full.txt (full content)
6
+
7
+ ## Project Overview
8
+
9
+ - **Framework**: TanStack Start (React) + Vite + Nitro (Bun preset)
10
+ - **CMS**: QUESTPIE — headless CMS framework with config-driven admin UI
11
+ - **Database**: PostgreSQL (via Drizzle ORM)
12
+ - **Styling**: Tailwind CSS v4 + shadcn/ui components
13
+ - **Auth**: Better Auth (email/password)
14
+ - **Package manager**: Bun
15
+ - **Validation**: Zod v4 (NOT v3)
16
+
17
+ ## Documentation & Resources
18
+
19
+ When you need more context about QUESTPIE APIs, consult these resources in order:
20
+
21
+ 1. **LLMs full docs**: https://questpie.com/llms-full.txt — complete documentation in a single LLM-optimized file
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)
24
+
25
+ Key documentation pages:
26
+
27
+ | Topic | URL |
28
+ | -------------------------- | ---------------------------------------------------------------- |
29
+ | Getting Started | https://questpie.com/docs/getting-started |
30
+ | Project Structure | https://questpie.com/docs/getting-started/project-structure |
31
+ | Your First CMS | https://questpie.com/docs/getting-started/your-first-cms |
32
+ | Architecture Principles | https://questpie.com/docs/mentality |
33
+ | Field Builder | https://questpie.com/docs/server/field-builder |
34
+ | Field Types Reference | https://questpie.com/docs/server/field-types |
35
+ | Collections | https://questpie.com/docs/server/collections |
36
+ | Globals | https://questpie.com/docs/server/globals |
37
+ | Relations | https://questpie.com/docs/server/relations |
38
+ | RPC (Server Functions) | https://questpie.com/docs/server/rpc |
39
+ | Hooks & Lifecycle | https://questpie.com/docs/server/hooks-and-lifecycle |
40
+ | Access Control | https://questpie.com/docs/server/access-control |
41
+ | Reactive Fields | https://questpie.com/docs/server/reactive-fields |
42
+ | Validation | https://questpie.com/docs/server/validation |
43
+ | Localization | https://questpie.com/docs/server/localization |
44
+ | Modules & Extensions | https://questpie.com/docs/server/modules-and-extensions |
45
+ | Admin Architecture | https://questpie.com/docs/admin |
46
+ | Client Builder (qa) | https://questpie.com/docs/admin/client-builder-qa |
47
+ | Component Registry | https://questpie.com/docs/admin/component-registry |
48
+ | View Registry | https://questpie.com/docs/admin/view-registry-list-and-form |
49
+ | Actions System | https://questpie.com/docs/admin/actions-system |
50
+ | Blocks System | https://questpie.com/docs/admin/blocks-system |
51
+ | Dashboard & Sidebar | https://questpie.com/docs/admin/dashboard-sidebar-branding |
52
+ | TanStack Query Integration | https://questpie.com/docs/client/tanstack-query |
53
+ | OpenAPI | https://questpie.com/docs/client/openapi |
54
+ | Authentication | https://questpie.com/docs/infrastructure/authentication |
55
+ | Database & Migrations | https://questpie.com/docs/infrastructure/database-and-migrations |
56
+ | Queue & Jobs | https://questpie.com/docs/infrastructure/queue-and-jobs |
57
+ | Storage | https://questpie.com/docs/infrastructure/storage |
58
+ | Email | https://questpie.com/docs/infrastructure/email |
59
+ | Realtime | https://questpie.com/docs/infrastructure/realtime |
60
+
61
+ ## Project Structure
62
+
63
+ ```
64
+ src/
65
+ questpie/
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)
77
+ admin/ ← HOW: UI rendering concerns
78
+ builder.ts ← Client builder: qa<AppCMS>().use(adminModule)
79
+ hooks.ts ← Typed hooks via createTypedHooks<AppCMS>()
80
+ blocks/ ← Block renderers (if using blocks)
81
+ lib/
82
+ env.ts ← Type-safe env vars (@t3-oss/env-core + Zod)
83
+ cms-client.ts ← CMS client instance
84
+ routes/
85
+ api/cms/$.ts ← CMS catch-all handler (REST + OpenAPI + auth)
86
+ migrations/ ← Database migrations (generated by CLI)
87
+ ```
88
+
89
+ ## Architecture Rules
90
+
91
+ ### Server-First Split
92
+
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 |
98
+
99
+ ### File Naming Conventions
100
+
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
105
+
106
+ ### Key Files
107
+
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)`.
112
+ - **`src/lib/env.ts`** — Type-safe env variables via `@t3-oss/env-core`. Add new env vars here with Zod schemas.
113
+ - **`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`.
115
+
116
+ ## How To Write Code
117
+
118
+ ### Creating a Collection
119
+
120
+ Keep the entire builder chain in one file — single source of truth per entity:
121
+
122
+ ```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
+ );
158
+ ```
159
+
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
165
+
166
+ ### Available Field Types
167
+
168
+ `text`, `number`, `boolean`, `date`, `dateTime`, `select`, `multiSelect`, `relation`, `upload`, `richText`, `json`, `slug`, `email`, `url`, `password`, `color`, `textarea`
169
+
170
+ ### Creating a Global
171
+
172
+ ```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
+ }));
188
+ ```
189
+
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`
195
+
196
+ ### Creating an RPC Function (End-to-End Type-Safe)
197
+
198
+ QUESTPIE provides standalone RPC — `cms` and `appRpc` are two independent instances, no circular dependency.
199
+
200
+ **How typing works:**
201
+ ```ts
202
+ // rpc.ts — standalone RPC builder
203
+ import { rpc } from "questpie";
204
+ export const r = rpc();
205
+ ```
206
+
207
+ ```ts
208
+ // app.ts — imports r (runtime), exports cms and appRpc separately
209
+ import { r } from "./rpc.js";
210
+
211
+ export const cms = qb.collections({...}).build({...});
212
+ export const appRpc = r.router({ ...adminRpc, myFn });
213
+
214
+ export type AppCMS = typeof cms;
215
+ export type AppRpc = typeof appRpc;
216
+ ```
217
+
218
+ **Step 1 — Define a function:**
219
+
220
+ ```ts
221
+ // src/questpie/server/functions/get-stats.function.ts
222
+ import { r } from "@/questpie/server/rpc";
223
+ import { z } from "zod";
224
+
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
+ });
236
+ ```
237
+
238
+ **Step 2 — Register in `app.ts`:**
239
+
240
+ ```ts
241
+ import { getStats } from "./functions/get-stats.function.js";
242
+
243
+ export const appRpc = r.router({
244
+ ...adminRpc,
245
+ getStats,
246
+ });
247
+ ```
248
+
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
+ ```
257
+
258
+ **With access control:**
259
+
260
+ ```ts
261
+ export const adminOnlyFn = r.fn({
262
+ access: ({ session }) => session?.user?.role === "admin",
263
+ schema: z.object({ ... }),
264
+ handler: async ({ input, app }) => { ... },
265
+ });
266
+ ```
267
+
268
+ **With TanStack Query:**
269
+
270
+ ```ts
271
+ import { useQuery, useMutation } from "@tanstack/react-query";
272
+
273
+ 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),
280
+ });
281
+ ```
282
+
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
+ ### Blocks (Page Builder)
299
+
300
+ Blocks are content building units for page builders and rich content areas.
301
+
302
+ **Simple block (no data fetching):**
303
+
304
+ ```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 };
325
+ ```
326
+
327
+ **Block with dynamic data fetching (prefetch):**
328
+
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
+ });
349
+ ```
350
+
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
+ ```
361
+
362
+ **Use blocks in a collection's richText field:**
363
+
364
+ ```ts
365
+ content: f.richText({
366
+ label: "Content",
367
+ blocks: [heroBlock, teamBlock],
368
+ })
369
+ ```
370
+
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!**
378
+
379
+ **The workaround: split into `baseCms` and final `cms`:**
380
+
381
+ ```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;
398
+ ```
399
+
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
+ ```
421
+
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.*`
427
+
428
+ ### Reactive Fields
429
+
430
+ Fields support reactive behaviors in `meta.admin`:
431
+
432
+ - **`hidden`**: Conditionally hide — `({ data }: { data: Record<string, any> }) => !data.isPublished`
433
+ - **`readOnly`**: Make read-only based on conditions
434
+ - **`disabled`**: Disable conditionally
435
+ - **`compute`**: Auto-compute values — `{ handler, deps, debounce }`
436
+
437
+ All reactive handlers run **server-side** with access to `ctx.db`, `ctx.user`, `ctx.req`.
438
+
439
+ ```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
+ })
468
+ ```
469
+
470
+ ### Admin Configuration (Client-Side)
471
+
472
+ ```ts
473
+ // src/questpie/admin/builder.ts
474
+ import { adminModule, qa } from "@questpie/admin/client";
475
+ import type { AppCMS } from "@/questpie/server/cms";
476
+
477
+ export const admin = qa<AppCMS>().use(adminModule);
478
+ ```
479
+
480
+ ```ts
481
+ // src/questpie/admin/hooks.ts
482
+ import { createTypedHooks } from "@questpie/admin/client";
483
+ import type { AppCMS } from "../server/cms";
484
+
485
+ export const {
486
+ useCollectionList, useCollectionCount, useCollectionItem,
487
+ useCollectionCreate, useCollectionUpdate, useCollectionDelete,
488
+ useGlobal, useGlobalUpdate,
489
+ } = createTypedHooks<AppCMS>();
490
+ ```
491
+
492
+ ### CMS Route Handler
493
+
494
+ ```ts
495
+ // src/routes/api/cms/$.ts
496
+ import { createFetchHandler } from "questpie";
497
+ import { withOpenApi } from "@questpie/openapi";
498
+ import { appRpc, cms } from "~/questpie/server/cms";
499
+
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
+ );
504
+ ```
505
+
506
+ ### Icons
507
+
508
+ Use `@iconify/react` with Phosphor icon set:
509
+ - Prefix: `ph:` (e.g., `ph:house`, `ph:article`, `ph:gear`)
510
+ - Weight variants: `-bold`, `-fill`, `-duotone`, `-light`, `-thin`
511
+ - Regular weight = no suffix (default)
512
+ - Naming: PascalCase → kebab-case (e.g., `CaretDown` → `ph:caret-down`)
513
+ - In server/admin config, use `c.icon("ph:icon-name")`
514
+
515
+ ## Environment Variables
516
+
517
+ Type-safe via `@t3-oss/env-core` in `src/lib/env.ts`. All env vars must be:
518
+ 1. Declared with Zod schema in `env.ts`
519
+ 2. Accessed via `env.VAR_NAME` (not `process.env.VAR_NAME`)
520
+
521
+ Required:
522
+ - `DATABASE_URL` — PostgreSQL connection string
523
+
524
+ Optional (with defaults):
525
+ - `APP_URL` — Application URL (default: `http://localhost:3000`)
526
+ - `BETTER_AUTH_SECRET` — Auth secret key
527
+ - `MAIL_ADAPTER` — `console` or `smtp`
528
+
529
+ ## Commands
530
+
531
+ ```bash
532
+ bun dev # Start dev server
533
+ bun build # Build for production
534
+ bun start # Start production server
535
+ bun questpie migrate # Run database migrations
536
+ bun questpie migrate:create # Create new migration
537
+ docker compose up -d # Start PostgreSQL
538
+ ```
539
+
540
+ ## Critical Dependencies
541
+
542
+ Always use these exact versions — check `package.json` before upgrading:
543
+
544
+ | Package | Version | Notes |
545
+ | ---------------- | ------- | -------------------- |
546
+ | `zod` | `^4.x` | **v4 ONLY** — not v3 |
547
+ | `drizzle-orm` | `beta` | Specific beta build |
548
+ | `react` | `^19.x` | React 19 |
549
+ | `tailwindcss` | `^4.x` | Tailwind CSS v4 |
550
+ | `@base-ui/react` | `^1.x` | NOT @radix-ui |
551
+
552
+ ## Anti-Patterns
553
+
554
+ - **Schema rules in client code** — Validation, access control, and hooks belong on the server.
555
+ - **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.
557
+ - **Hardcoding view components** — Use the registry pattern for custom views.
558
+ - **Using `process.env` directly** — Use the `env` object from `src/lib/env.ts`.
559
+ - **Using Zod v3 API** — This project uses Zod v4. Use `z.object()` etc. from `zod` (v4).
560
+ - **Using `asChild` prop** — This project uses `@base-ui/react`, not Radix. Use `render` prop instead.
561
+ - **Using Radix UI or Lucide icons** — Use `@base-ui/react` and `@iconify/react` with `ph:` prefix.
562
+ - **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).
@@ -0,0 +1,105 @@
1
+ # CLAUDE.md
2
+
3
+ This is a [QUESTPIE CMS](https://questpie.com) project scaffolded with `create-questpie`.
4
+
5
+ ## Quick Reference
6
+
7
+ | Command | Purpose |
8
+ | ----------------------------- | ---------------------------- |
9
+ | `bun dev` | Start dev server (port 3000) |
10
+ | `bun build` | Build for production |
11
+ | `bun start` | Start production server |
12
+ | `bun questpie migrate` | Run database migrations |
13
+ | `bun questpie migrate:create` | Generate a new migration |
14
+ | `docker compose up -d` | Start PostgreSQL |
15
+
16
+ ## Project Architecture
17
+
18
+ This project follows QUESTPIE's **server-first** philosophy:
19
+ - **Server** defines WHAT (schema, validation, access, hooks, jobs)
20
+ - **Client** defines HOW (rendering, themes, custom components)
21
+
22
+ ```
23
+ src/questpie/
24
+ server/ ← WHAT: data contracts and behavior
25
+ app.ts ← Main composition root (collections, globals, auth, build)
26
+ builder.ts ← Shared builder: qb = q.use(adminModule)
27
+ rpc.ts ← RPC router instance
28
+ sidebar.ts ← Admin sidebar configuration
29
+ dashboard.ts ← Admin dashboard configuration
30
+ collections/ ← One file per collection (*.collection.ts)
31
+ globals/ ← One file per global (*.global.ts)
32
+ admin/ ← HOW: UI rendering concerns
33
+ admin.ts ← Client builder: qa<AppCMS>().use(adminModule)
34
+ builder.ts ← Client-side builder instance
35
+ ```
36
+
37
+ ## Key Files
38
+
39
+ - **`src/questpie/server/app.ts`** — The composition root. Register collections, globals, sidebar, dashboard, auth, and call `.build()`.
40
+ - **`src/lib/env.ts`** — Type-safe env variables via `@t3-oss/env-core`. Add new env vars here with Zod schemas.
41
+ - **`questpie.config.ts`** — CLI config (migration directory, app reference).
42
+ - **`src/routes/api/cms/$.ts`** — CMS API catch-all handler. Serves REST + OpenAPI docs at `/api/cms/docs`.
43
+
44
+ ## Environment Variables
45
+
46
+ Defined in `src/lib/env.ts` with runtime validation. See `.env.example` for all available variables.
47
+
48
+ Required:
49
+ - `DATABASE_URL` — PostgreSQL connection string
50
+
51
+ Optional (with defaults):
52
+ - `APP_URL` — Application URL (default: `http://localhost:3000`)
53
+ - `BETTER_AUTH_SECRET` — Auth secret key
54
+ - `MAIL_ADAPTER` — `console` or `smtp`
55
+
56
+ ## Common Tasks
57
+
58
+ ### Add a new collection
59
+
60
+ 1. Create `src/questpie/server/collections/my-thing.collection.ts`
61
+ 2. Export from `src/questpie/server/collections/index.ts`
62
+ 3. Register in `src/questpie/server/app.ts` → `.collections({ posts, myThing })`
63
+ 4. Add to sidebar in `src/questpie/server/sidebar.ts`
64
+ 5. Run `bun questpie migrate:create` to generate migration
65
+
66
+ ### Add a new global
67
+
68
+ 1. Create `src/questpie/server/globals/my-global.global.ts`
69
+ 2. Export from `src/questpie/server/globals/index.ts`
70
+ 3. Register in `src/questpie/server/app.ts` → `.globals({ siteSettings, myGlobal })`
71
+ 4. Add to sidebar in `src/questpie/server/sidebar.ts`
72
+ 5. Run `bun questpie migrate:create`
73
+
74
+ ### Add an RPC function (end-to-end type-safe)
75
+
76
+ `rpc.ts` uses `rpc<AppCMS>()` — a type-only import from `app.ts` (erased at runtime, no circular dependency). This gives you fully typed `app` in all handlers.
77
+
78
+ 1. Create `src/questpie/server/functions/my-function.ts`:
79
+ ```ts
80
+ import { r } from "@/questpie/server/rpc";
81
+ import { z } from "zod";
82
+
83
+ export const myFunction = r.fn({
84
+ schema: z.object({ id: z.string() }),
85
+ handler: async ({ input, app }) => {
86
+ // input: { id: string } — typed from Zod schema
87
+ // app: AppCMS — fully typed, autocomplete works
88
+ return { name: "result" };
89
+ },
90
+ });
91
+ ```
92
+ 2. Register in `app.ts` → `appRpc = r.router({ ...adminRpc, myFunction })`
93
+ 3. Call from client (fully typed):
94
+ ```ts
95
+ const result = await client.rpc.myFunction({ id: "123" });
96
+ // result: { name: string } — inferred from handler return type
97
+ ```
98
+
99
+ See AGENTS.md for detailed RPC type flow, access control, and TanStack Query integration.
100
+
101
+ ## Documentation
102
+
103
+ - **QUESTPIE Docs**: https://questpie.com/docs
104
+ - **Getting Started**: https://questpie.com/docs/getting-started
105
+ - **API Reference (local)**: http://localhost:3000/api/cms/docs (Scalar UI, available when dev server is running)
@@ -0,0 +1,23 @@
1
+ FROM oven/bun:1.3-alpine AS base
2
+
3
+ WORKDIR /app
4
+
5
+ # Install dependencies
6
+ FROM base AS deps
7
+ COPY package.json bun.lock* ./
8
+ RUN bun install --frozen-lockfile
9
+
10
+ # Build application
11
+ FROM base AS builder
12
+ COPY --from=deps /app/node_modules ./node_modules
13
+ COPY . .
14
+ RUN bun run build
15
+
16
+ # Production image
17
+ FROM base AS runner
18
+ ENV NODE_ENV=production
19
+ ENV PORT=3000
20
+ COPY --from=builder /app/.output ./.output
21
+ COPY --from=builder /app/package.json ./package.json
22
+ EXPOSE 3000
23
+ CMD ["bun", "run", ".output/server/index.mjs"]