create-questpie 2.0.4 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/dist/index.mjs +362 -119
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. package/skills/questpie-admin/references/views.md +0 -449
@@ -1,454 +0,0 @@
1
- ---
2
- name: questpie-core-rules
3
- description: QUESTPIE access control hooks validation lifecycle beforeValidate beforeChange afterChange beforeDelete afterDelete access rules field-level row-level secure-by-default Zod schema refinements collection global
4
- - questpie-core
5
- ---
6
-
7
- # QUESTPIE Rules — Access Control, Hooks, Validation
8
-
9
- This skill builds on questpie-core. It covers collection/global access control, lifecycle hooks, and validation — the three rule layers that govern data flow.
10
-
11
- ## Access Control
12
-
13
- Access rules are defined per-collection via `.access()`. Each operation accepts a static `boolean` or a function receiving `AppContext` that returns `boolean` or a where clause (row-level filtering).
14
-
15
- ### Default Behavior
16
-
17
- When no `.access()` is defined, all operations default to `({ session }) => !!session` — **authenticated users only**. You must explicitly set `read: true` for public collections.
18
-
19
- Every operation resolves through the same chain, with no hidden framework grants above your config:
20
-
21
- 1. Collection/global `.access()` rule for that operation
22
- 2. App-level `defaultAccess` (`appConfig({ access })` in `config/app.ts`)
23
- 3. Framework fallback: require session
24
-
25
- A deny-all `defaultAccess` (`{ read: false, create: false, update: false, delete: false }`) closes the entire REST surface — including upload-row listing and schema/meta introspection — until collections opt in.
26
-
27
- ### Collection Access
28
-
29
- ```ts
30
- // collections/posts.collection.ts
31
- import { collection } from "#questpie/factories";
32
-
33
- export default collection("posts")
34
- .fields(({ f }) => ({
35
- title: f.text().label("Title").required(),
36
- content: f.richText().label("Content"),
37
- author: f.relation("user"),
38
- }))
39
- .access({
40
- read: true, // Public read
41
- create: ({ session }) => !!session, // Authenticated
42
- update: ({ session }) => session?.user?.role === "admin", // Admin only
43
- delete: ({ session }) => session?.user?.role === "admin",
44
- });
45
- ```
46
-
47
- ### Operations
48
-
49
- | Operation | When checked |
50
- | ------------ | ---------------------------------------------------------------- |
51
- | `read` | Listing and fetching records |
52
- | `create` | Creating new records |
53
- | `update` | Updating existing records |
54
- | `delete` | Deleting records |
55
- | `transition` | Workflow stage transitions (falls back to `update`) |
56
- | `serve` | Upload file bytes by key (`GET /:collection/files/:key`) |
57
- | `introspect` | Schema/meta routes (`GET /:collection/{schema,meta}`) |
58
-
59
- Two operations have specialized chains:
60
-
61
- - **`serve`** (upload collections): `serve` → explicit collection `read`
62
- (row-aware, `ctx.data` is the upload row) → `defaultAccess.serve` → allow.
63
- `defaultAccess.read` is deliberately NOT consulted — listing rows and
64
- fetching bytes by key are distinct permissions. `visibility: "public"`
65
- means bytes are servable by key; `"private"` files always require the
66
- signed token in addition to any serve rule.
67
- - **`introspect`**: `introspect` → `defaultAccess.introspect` → visible iff
68
- at least one CRUD operation is allowed for the current user. Create-only
69
- public collections keep their validation schema readable; deny-all apps
70
- expose no schemas. Denied requests get 401 (anonymous) or 403
71
- (authenticated).
72
-
73
- `f.upload()` fields populate through the PARENT row's read decision — a
74
- publicly readable gallery shows its assets (with `url`) to anonymous readers
75
- even when the assets collection itself is unlistable. Field-level read rules
76
- on the upload collection still apply inside population.
77
-
78
- ### Global Access
79
-
80
- Globals support `read` and `update` only (singletons have no create/delete):
81
-
82
- ```ts
83
- // globals/site-settings.global.ts
84
- import { global } from "#questpie/factories";
85
-
86
- export default global("siteSettings")
87
- .fields(({ f }) => ({
88
- siteName: f.text().label("Site Name").required(),
89
- logo: f.upload().label("Logo"),
90
- }))
91
- .access({
92
- read: true,
93
- update: ({ session }) => session?.user?.role === "admin",
94
- });
95
- ```
96
-
97
- ### Row-Level Access (AccessWhere)
98
-
99
- Return a where clause object instead of a boolean to restrict operations to matching rows:
100
-
101
- ```ts
102
- .access({
103
- read: true,
104
- update: ({ session }) => {
105
- if (!session) return false;
106
- // Only allow updating own records
107
- return { author: session.user.id };
108
- },
109
- })
110
- ```
111
-
112
- ### Access Function Context
113
-
114
- Access functions receive `AppContext` with these properties:
115
-
116
- | Property | Description |
117
- | ------------- | ------------------------------------------------------------ |
118
- | `session` | Current auth session (null if unauthed) |
119
- | `db` | Database instance |
120
- | `collections` | Typed collection API |
121
- | `request` | Current HTTP `Request` (headers, URL) |
122
- | `data` | The existing row — typed, non-optional in `update`/`delete` rules |
123
- | `input` | Typed insert shape in `create` rules; typed patch in `update` rules |
124
- | _extensions_ | Keys returned by `appConfig({ context })`, flat (see below) |
125
-
126
- `data`/`input` are typed **per operation** by the builder — no casts, no annotations inside the defining collection. For shared rule helpers and every other "I need type X" case, see `references/type-inference.md`.
127
-
128
- ### Derived Request Context in Rules
129
-
130
- `appConfig({ context })` runs once per HTTP request; its result arrives **flat** on every access rule ctx (collections, globals, routes, field access, transitions), typed by inference:
131
-
132
- ```ts
133
- // config/app.ts
134
- export default appConfig({
135
- context: async ({ request }) => ({
136
- workspaceId: request.headers.get("x-workspace") || null,
137
- }),
138
- });
139
-
140
- // collections/projects.ts — destructure flat, narrow before use
141
- .access({
142
- read: ({ workspaceId }) =>
143
- workspaceId ? { workspace: workspaceId } : false,
144
- })
145
- ```
146
-
147
- Extensions are typed `Partial<…>` — absent for non-HTTP contexts (jobs, seeds, system scripts), so rules must handle `undefined`. See `references/multi-tenancy.md` for the full pattern (membership validation, closure memoization, scope UI).
148
-
149
- Access functions may be async. Use `request` for request-scoped checks such as headers, tenant scope, CAPTCHA tokens, or signed public form tokens:
150
-
151
- ```ts
152
- import type { AccessContext } from "questpie";
153
- import { ApiError } from "questpie/errors";
154
- import { isAdminRequest } from "@questpie/admin/shared";
155
-
156
- // AccessContext is the sanctioned shared-helper param — never hand-roll a
157
- // structural ctx type (see references/type-inference.md)
158
- async function canCreatePublicSubmission({ request, session }: AccessContext) {
159
- if (session?.user) return true;
160
- if (request && isAdminRequest(request)) {
161
- throw ApiError.unauthorized();
162
- }
163
-
164
- const token = request?.headers.get("x-captcha-token");
165
- const valid = token ? await verifyCaptchaToken(token) : false;
166
- if (valid) return true;
167
-
168
- throw ApiError.forbidden({
169
- operation: "create",
170
- resource: "public_submissions",
171
- reason: "CAPTCHA verification failed",
172
- });
173
- }
174
-
175
- export default collection("public_submissions")
176
- .fields(({ f }) => ({
177
- message: f.textarea().required(),
178
- }))
179
- .access({
180
- read: false,
181
- create: canCreatePublicSubmission,
182
- });
183
- ```
184
-
185
- For public anti-abuse checks, bypass already authenticated users before requiring a CAPTCHA token. Admin-origin requests should not be asked for CAPTCHA either, but remember that `isAdminRequest()` is a caller-intent signal, not authentication; if an admin-origin request reaches this rule without a session, fail it as unauthorized instead of accepting it.
186
-
187
- Prefer throwing `ApiError.*` from access rules when callers need a specific structured error response. Returning `false` is fine for generic denial, but it produces the default forbidden message.
188
-
189
- ### System Access Mode
190
-
191
- Server-side code can bypass all access checks:
192
-
193
- ```ts
194
- const ctx = await app.createContext({ accessMode: "system" });
195
- const allPosts = await app.collections.posts.find({}, ctx);
196
- ```
197
-
198
- HTTP requests always use session-based access. System mode is for background jobs, seeds, and internal server logic only.
199
-
200
- ## Hooks
201
-
202
- Hooks run logic at specific points in the collection lifecycle. They receive the full typed `AppContext` through context injection.
203
-
204
- ### Lifecycle Order
205
-
206
- For create/update:
207
-
208
- ```text
209
- API Request
210
- |
211
- beforeValidate -- Modify/validate data before schema validation
212
- |
213
- Schema Validation -- Zod validation from field definitions
214
- |
215
- beforeChange -- Transform data before database write
216
- |
217
- Database Write -- Insert or update
218
- |
219
- afterChange -- Side effects after successful write
220
- ```
221
-
222
- For delete:
223
-
224
- ```text
225
- beforeDelete --> Database Delete --> afterDelete
226
- ```
227
-
228
- ### Defining Hooks
229
-
230
- ```ts
231
- // collections/appointments.collection.ts
232
- import { collection } from "#questpie/factories";
233
-
234
- export default collection("appointments")
235
- .fields(({ f }) => ({
236
- customer: f.relation("user"),
237
- barber: f.relation("barbers"),
238
- service: f.relation("services"),
239
- scheduledAt: f.datetime().required(),
240
- status: f.select([
241
- { value: "pending", label: "Pending" },
242
- { value: "confirmed", label: "Confirmed" },
243
- { value: "cancelled", label: "Cancelled" },
244
- ]),
245
- slug: f.text().required().inputOptional(),
246
- name: f.text().required(),
247
- }))
248
- .hooks({
249
- beforeValidate: async (ctx) => {
250
- if (ctx.data.name && !ctx.data.slug) {
251
- ctx.data.slug = slugify(ctx.data.name);
252
- }
253
- },
254
-
255
- beforeChange: async ({ data, operation, original }) => {
256
- if (operation === "create") {
257
- // Set defaults on create
258
- }
259
- if (operation === "update" && original) {
260
- // Compare with original data
261
- }
262
- },
263
-
264
- afterChange: async ({ data, operation, original, queue }) => {
265
- if (operation === "create") {
266
- await queue.sendAppointmentConfirmation.publish({
267
- appointmentId: data.id,
268
- customerId: data.customer,
269
- });
270
- }
271
- if (operation === "update" && data.status === "cancelled") {
272
- await queue.sendAppointmentCancellation.publish({
273
- appointmentId: data.id,
274
- customerId: data.customer,
275
- });
276
- }
277
- },
278
-
279
- beforeDelete: async ({ id }) => {
280
- // Prevent deletion or clean up
281
- },
282
-
283
- afterDelete: async ({ id }) => {
284
- // Clean up related data
285
- },
286
- });
287
- ```
288
-
289
- ### Hook Context Properties
290
-
291
- | Property | Available in | Description |
292
- | ------------- | ----------------------------------------- | -------------------------------- |
293
- | `data` | beforeValidate, beforeChange, afterChange | The record data being written |
294
- | `operation` | beforeChange, afterChange | `"create"` or `"update"` |
295
- | `original` | beforeChange, afterChange (update) | Previous record state |
296
- | `id` | beforeDelete, afterDelete | ID of record being deleted |
297
- | `collections` | All hooks | Typed collection API |
298
- | `globals` | All hooks | Typed globals API |
299
- | `queue` | All hooks | Queue client for publishing jobs |
300
- | `email` | All hooks | Email service |
301
- | `db` | All hooks | Database instance |
302
- | `session` | All hooks | Current auth session |
303
- | `services` | All hooks | Custom services from `services/` |
304
- | _extensions_ | All hooks | `appConfig({ context })` result, flat (HTTP requests only) |
305
-
306
- Derived request context also reaches hooks and any nested code via `getContext<App>()` — including CRUD calls a hook triggers (AsyncLocalStorage carries it):
307
-
308
- ```ts
309
- .hooks({
310
- beforeChange: async ({ data, operation, workspaceId }) => {
311
- if (operation === "create" && workspaceId) data.workspace = workspaceId;
312
- },
313
- })
314
- ```
315
-
316
- ### Context-First Pattern
317
-
318
- All dependencies come through destructuring. No need to import the app instance:
319
-
320
- ```ts
321
- .hooks({
322
- beforeChange: async ({ data, services }) => {
323
- const { blog } = services;
324
- data.slug = blog.generateSlug(data.title);
325
- data.readingTime = blog.computeReadingTime(data.content);
326
- },
327
-
328
- afterChange: async ({ data, operation, original, queue }) => {
329
- if (
330
- operation === "update" &&
331
- original?.status !== "published" &&
332
- data.status === "published"
333
- ) {
334
- await queue.notifyBlogSubscribers.publish({
335
- postId: data.id,
336
- title: data.title,
337
- });
338
- }
339
- },
340
- })
341
- ```
342
-
343
- ## Validation
344
-
345
- QUESTPIE validates at three levels: field constraints, auto-generated Zod schemas, and custom hooks.
346
-
347
- ### Field-Level Constraints
348
-
349
- Built-in constraints on field definitions generate Zod schemas automatically:
350
-
351
- ```ts
352
- .fields(({ f }) => ({
353
- name: f.text(255).required(),
354
- email: f.email().required(),
355
- website: f.url(),
356
- rating: f.number().min(1).max(5),
357
- tags: f.text().array().maxItems(10),
358
- }))
359
- ```
360
-
361
- | Constraint | Fields | Description |
362
- | ----------- | ------------------ | ----------------------- |
363
- | `required` | All | Field must have a value |
364
- | `maxLength` | `text`, `textarea` | Maximum string length |
365
- | `min`/`max` | `number` | Numeric range |
366
- | `maxItems` | `array` | Maximum array length |
367
- | `mimeTypes` | `upload` | Allowed file types |
368
- | `maxSize` | `upload` | Max file size in bytes |
369
-
370
- ### Input Modifier
371
-
372
- The `input` option controls API input behavior for fields computed by hooks:
373
-
374
- ```ts
375
- slug: f.text().required().inputOptional(),
376
- ```
377
-
378
- ### Custom Validation via Hooks
379
-
380
- Use `beforeValidate` to transform data or reject operations:
381
-
382
- ```ts
383
- .hooks({
384
- beforeValidate: async ({ data, operation }) => {
385
- // Transform data before validation
386
- if (operation === "create" && !data.slug) {
387
- data.slug = slugify(data.name);
388
- }
389
- },
390
- })
391
- ```
392
-
393
- To reject an operation, throw an error:
394
-
395
- ```ts
396
- .hooks({
397
- beforeValidate: async ({ data }) => {
398
- if (data.scheduledAt && new Date(data.scheduledAt) < new Date()) {
399
- throw new Error("Cannot schedule appointments in the past");
400
- }
401
- },
402
- })
403
- ```
404
-
405
- ## Common Mistakes
406
-
407
- 1. **HIGH: Forgetting default access is `!!session`.**
408
- Collections without `.access()` require authentication for all operations. For public read access, explicitly set `read: true`.
409
-
410
- 2. **HIGH: Using `accessMode: "system"` in HTTP handlers.**
411
- System mode bypasses all access checks. Only use it for background jobs, seeds, and internal server scripts — never in request handlers.
412
-
413
- 3. **MEDIUM: Mutating `data` in `afterChange` hooks.**
414
- Changes to `data` in `afterChange` are NOT persisted to the database. Only mutations in `beforeValidate` and `beforeChange` are saved.
415
-
416
- 4. **MEDIUM: Not awaiting async access control functions.**
417
- Access functions can be async and must return `boolean` or a where clause object (`AccessWhere`).
418
-
419
- 5. **HIGH: Wrong context usage in access rules.**
420
- Use the destructured `session` parameter from `AppContext`, not a standalone import. Access functions receive `({ session, db, collections })`.
421
-
422
- ## Access Control for Preview Sessions
423
-
424
- Live preview sessions use token-based authentication. When a preview iframe loads, it receives a short-lived preview token that authorizes read access to the collection being previewed.
425
-
426
- ### Key Points
427
-
428
- - Preview tokens are scoped to a specific collection and record — they do not grant broad access.
429
- - Preview does **not** bypass access rules. The token resolves to a session with the same permissions as the user who initiated the preview.
430
- - Access rules (`.access()`) still apply to all data fetched during preview, including prefetched relations and block data.
431
- - Row-level access (AccessWhere) filters are enforced even in preview context — a user cannot preview records they cannot read.
432
-
433
- ### Workflow Published Reads
434
-
435
- For publishable collections that use workflow stages, do not use `read: true` when public client or HTTP access is available. Gate anonymous reads to the published stage:
436
-
437
- ```ts
438
- .access({
439
- read: ({ session, input }) => {
440
- if (session?.user) return true;
441
- return input?.stage === "published";
442
- },
443
- create: ({ session }) => !!session?.user,
444
- update: ({ session }) => !!session?.user,
445
- delete: ({ session }) => !!session?.user,
446
- transition: ({ session }) => !!session?.user,
447
- })
448
- ```
449
-
450
- Public frontend code must pass `stage: "published"`. Preview/draft-mode reads may omit `stage` only when the request has an authorized editor session.
451
-
452
- ### System Access and Preview
453
-
454
- Do not use `accessMode: "system"` to serve preview data. Preview requests should go through normal session-based access, with the preview token resolving to the editor's session. This ensures previewed content respects the same visibility rules as the final published page.
@@ -1,110 +0,0 @@
1
- # Sandboxed Code Execution (ctx.executor)
2
-
3
- Use the executor when an app must run **untrusted or dynamically-authored code** — agent-written scripts, user plugins, knowledge mini-apps — under a default-deny capability model. `ctx.executor.run()` is the primitive (top-level on AppContext; there is no `ctx.sandbox`).
4
-
5
- Unconfigured = disabled: without an `executor` key in `questpie.config.ts`, `ctx.executor.run` throws a clear "not configured" error. An app that never runs dynamic code simply does not configure it.
6
-
7
- ## Two Isolation Modes
8
-
9
- | Mode | Runs in | For |
10
- | --- | --- | --- |
11
- | `"sandboxed"` (default) | fresh, hardened **Deno** subprocess per request (scoped net/import, fs/env/run/ffi denied, memory bound) | untrusted code (user/AI mini-apps) |
12
- | `"trusted"` | in-process (Bun) with a soft timeout | code you already own (code-mode agents, scheduled scripts) |
13
-
14
- Untrusted-by-default: omitting `isolation` means `"sandboxed"`; trusted callers opt in explicitly.
15
-
16
- ## Install And Configure
17
-
18
- The sandboxed adapter comes from the opt-in `@questpie/sandbox` package; the engine is a standalone Deno service your app reaches over HTTP (the app ships no Deno):
19
-
20
- ```bash
21
- bun add @questpie/sandbox
22
- ```
23
-
24
- ```ts
25
- // questpie.config.ts
26
- import { httpSandboxAdapter } from "@questpie/sandbox/adapter";
27
- import { runtimeConfig } from "questpie/app";
28
-
29
- export default runtimeConfig({
30
- executor: {
31
- sandboxed: httpSandboxAdapter({
32
- url: process.env.SANDBOX_URL ?? "http://127.0.0.1:8787",
33
- }),
34
- // TRUSTED internal URL of this app's own broker endpoint — required only
35
- // for the untrusted app-bindings path. NEVER derive from request Host.
36
- brokerUrl: process.env.SANDBOX_BROKER_URL,
37
- // defaultTimeoutMs: 10_000,
38
- },
39
- });
40
- ```
41
-
42
- `executor.trusted` defaults to the built-in in-process adapter — override only to customize.
43
-
44
- ## Running Code
45
-
46
- The guest source must `export default` a function of `input`:
47
-
48
- ```ts
49
- const result = await ctx.executor.run({
50
- source: `export default async function (input) {
51
- const res = await fetch("https://api.example.com/data?since=" + input.since);
52
- const data = await res.json();
53
- return { count: data.length };
54
- }`,
55
- capabilities: {
56
- net: ["api.example.com"], // fetch() egress allowlist
57
- timeoutMs: 5_000,
58
- memoryMb: 128,
59
- },
60
- input: { since: "2026-01-01" },
61
- });
62
- // → { ok: true, output: { count: 42 }, logs: [...], ms: 312 }
63
- ```
64
-
65
- Result shape: `{ ok, output?, logs, error?, timedOut?, ms? }`.
66
-
67
- ## The Capability Model
68
-
69
- Every run declares a manifest; anything not granted is denied (default-deny):
70
-
71
- | Axis | Grants | Enforced by |
72
- | --- | --- | --- |
73
- | `net` | `fetch()` host allowlist (`host[:port]`) | sandbox engine |
74
- | `import` | remote module-import host allowlist (independent of `net`) | sandbox engine |
75
- | `timeoutMs` / `memoryMb` | hard wall-clock / per-guest memory bounds | sandbox engine |
76
- | `files` | read/write path globs into the file store | bindings broker |
77
- | `data.collections` | per-collection verbs (`read`/`create`/`update`/`delete`) | bindings broker |
78
- | `data.globals` / `data.stores` | per-global and per-`document_store`-namespace verbs | bindings broker |
79
- | `services` / `jobs` / `workflows` | allowed service names / enqueueable jobs / triggerable workflows | bindings broker |
80
-
81
- `secrets: Record<string, string>` injects secrets into the guest without embedding them in source.
82
-
83
- ## App Bindings (the `questpie` Proxy)
84
-
85
- A plain `ctx.executor.run` is **compute-only** (plus granted `net`). To let the guest reach app data, the caller passes an `appBindings` target plus `brokerUrl` — the service mints a per-run scoped token, and the guest's `globalThis.questpie` proxy RPCs through a host **broker** that enforces the capability manifest per call and dispatches under a non-privileged principal (never `system`):
86
-
87
- ```ts
88
- // inside the guest source — only the granted surface resolves
89
- const posts = await questpie.collections.posts.find({ limit: 10 });
90
- const file = await questpie.files.read({ path: "company/data/report.json" });
91
- ```
92
-
93
- The broker endpoint is a route the host app mounts (product layers like Autopilot's mini-app runner do this); the guest never imports your app. For trusted in-process runs, `bindings` injects host globals directly instead.
94
-
95
- ## Deployment
96
-
97
- The sandbox engine runs as its own service/container reachable at `SANDBOX_URL`; `brokerUrl` must point at the app's own loopback/internal address (the supervisor is trusted) — never at anything request-derived:
98
-
99
- ```bash
100
- deno run --allow-net --allow-read packages/sandbox/src/sandbox-server.ts
101
- ```
102
-
103
- ## Rules
104
-
105
- - Do not use the executor for trusted first-party logic — routes, jobs, and services are the right tools.
106
- - Never grant `net`/`import` hosts or data verbs a run does not need; capabilities are per-run, not global.
107
- - Never pass `isolation: "trusted"` for code you did not author — there is no sandbox in that mode.
108
- - Source `brokerUrl` from config/env only; a request-derived broker URL lets a spoofed Host exfiltrate the per-run token.
109
-
110
- Full reference: docs page `backend/business-logic/code-execution`.