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.
- package/README.md +81 -0
- package/dist/index.mjs +284 -0
- package/package.json +44 -0
- package/templates/tanstack-start/AGENTS.md +563 -0
- package/templates/tanstack-start/CLAUDE.md +105 -0
- package/templates/tanstack-start/Dockerfile +23 -0
- package/templates/tanstack-start/README.md +94 -0
- package/templates/tanstack-start/components.json +22 -0
- package/templates/tanstack-start/docker-compose.yml +20 -0
- package/templates/tanstack-start/env.example +16 -0
- package/templates/tanstack-start/gitignore +12 -0
- package/templates/tanstack-start/package.json +43 -0
- package/templates/tanstack-start/questpie.config.ts +12 -0
- package/templates/tanstack-start/src/admin.css +4 -0
- package/templates/tanstack-start/src/lib/auth-client.ts +12 -0
- package/templates/tanstack-start/src/lib/cms-client.ts +12 -0
- package/templates/tanstack-start/src/lib/env.ts +27 -0
- package/templates/tanstack-start/src/lib/query-client.ts +9 -0
- package/templates/tanstack-start/src/migrations/index.ts +8 -0
- package/templates/tanstack-start/src/questpie/admin/admin.ts +5 -0
- package/templates/tanstack-start/src/questpie/admin/builder.ts +4 -0
- package/templates/tanstack-start/src/questpie/server/app.ts +52 -0
- package/templates/tanstack-start/src/questpie/server/builder.ts +4 -0
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -0
- package/templates/tanstack-start/src/questpie/server/collections/posts.collection.ts +72 -0
- package/templates/tanstack-start/src/questpie/server/dashboard.ts +68 -0
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -0
- package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +24 -0
- package/templates/tanstack-start/src/questpie/server/rpc.ts +4 -0
- package/templates/tanstack-start/src/questpie/server/sidebar.ts +26 -0
- package/templates/tanstack-start/src/router.tsx +10 -0
- package/templates/tanstack-start/src/routes/__root.tsx +16 -0
- package/templates/tanstack-start/src/routes/admin/$.tsx +21 -0
- package/templates/tanstack-start/src/routes/admin/index.tsx +18 -0
- package/templates/tanstack-start/src/routes/admin/login.tsx +17 -0
- package/templates/tanstack-start/src/routes/admin.tsx +68 -0
- package/templates/tanstack-start/src/routes/api/cms/$.ts +45 -0
- package/templates/tanstack-start/src/styles.css +125 -0
- package/templates/tanstack-start/tsconfig.json +27 -0
- 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"]
|