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,147 +0,0 @@
1
- # MCP Integration
2
-
3
- Use `@questpie/mcp` when a QUESTPIE app should expose collections, globals, annotated JSON routes, schemas, or custom tools to Model Context Protocol clients.
4
-
5
- ## Static Module Pattern
6
-
7
- MCP is codegen-aware. Keep `modules.ts` static and put options in `config/mcp.ts`.
8
-
9
- ```ts title="modules.ts"
10
- import mcpModule from "@questpie/mcp";
11
-
12
- export default [mcpModule] as const;
13
- ```
14
-
15
- ```ts title="config/mcp.ts"
16
- import { mcpConfig } from "@questpie/mcp";
17
-
18
- export default mcpConfig({
19
- crud: {
20
- defaults: {
21
- collections: { read: true, write: false, delete: false },
22
- globals: { read: true, write: false },
23
- },
24
- collections: {
25
- posts: { read: true, write: true },
26
- users: false,
27
- },
28
- globals: {
29
- siteSettings: { read: true, write: true },
30
- },
31
- },
32
- routes: {
33
- exposeAnnotated: true,
34
- },
35
- });
36
- ```
37
-
38
- Do not use `mcpModule(options)`. Runtime options belong in the plugin-discovered config file.
39
-
40
- `mcpModule` carries its codegen plugin. Do not also add `mcpPlugin()` to `questpie.config.ts` unless you are doing a custom setup that deliberately omits `mcpModule` — double registration duplicates the plugin.
41
-
42
- ## CRUD Policy
43
-
44
- Generated collection tools:
45
-
46
- - `collections.{name}.list`
47
- - `collections.{name}.count`
48
- - `collections.{name}.get`
49
- - `collections.{name}.create`
50
- - `collections.{name}.update`
51
- - `collections.{name}.delete`
52
-
53
- Generated global tools:
54
-
55
- - `globals.{name}.get`
56
- - `globals.{name}.update`
57
-
58
- Policy order:
59
-
60
- 1. Transport defaults.
61
- 2. CRUD defaults.
62
- 3. Per-entity override.
63
- 4. QUESTPIE access rules execute last and can still deny.
64
-
65
- HTTP is user mode and read-oriented by default. HTTP cannot be made system mode with config or options. Stdio defaults to trusted system mode unless explicitly lowered to user mode.
66
-
67
- Use `fields.include` / `fields.exclude` for top-level filtering. It applies to create/update input, CRUD outputs, list docs, global results, and schema resources. Nested relation projection is out of scope for v1.
68
-
69
- ## Route Tools
70
-
71
- Only simple JSON routes are auto-converted:
72
-
73
- - Route has `.schema(...)`.
74
- - Route is not `.raw()`.
75
- - Route has `meta.mcp.expose === true`.
76
- - `routes.exposeAnnotated` is not `false`.
77
-
78
- ```ts
79
- route()
80
- .post()
81
- .schema(inputSchema)
82
- .outputSchema(outputSchema)
83
- .meta({
84
- title: "Generate report",
85
- mcp: {
86
- expose: true,
87
- name: "reports.generate",
88
- annotations: { readOnlyHint: true },
89
- },
90
- })
91
- .handler(async ({ input }) => ({ ok: true }));
92
- ```
93
-
94
- Routes without path params use the route input schema directly. Routes with params use `{ params, input }`. Route policy keys use the route key, not the overridden tool name.
95
-
96
- ## Resources
97
-
98
- Built-in resources:
99
-
100
- - `questpie://schema/collections`
101
- - `questpie://schema/collections/{name}`
102
- - `questpie://schema/globals`
103
- - `questpie://schema/globals/{name}`
104
- - `questpie://schema/routes`
105
- - `questpie://schema/routes/{key}`
106
-
107
- Resources honor MCP policy and QUESTPIE access visibility. Route resources include input/output JSON Schema when the route has Zod schemas.
108
-
109
- ## Custom Tools
110
-
111
- Custom tools live in `mcp-tools/` and are discovered by codegen.
112
-
113
- ```ts
114
- import { mcpTool } from "@questpie/mcp";
115
- import { z } from "zod";
116
-
117
- export default mcpTool("generate-report", {
118
- description: "Generate a report.",
119
- inputSchema: z.object({ period: z.string() }),
120
- access: ({ session }) => !!session,
121
- }).handler(async ({ input, ctx }) => ({
122
- structuredContent: await ctx.services.reports.generate(input),
123
- }));
124
- ```
125
-
126
- Custom tool access is checked during `tools/list` and again during `tools/call`.
127
-
128
- ## Programmatic Servers
129
-
130
- Use `createMcpServer(app, { transport: "http", request })` for programmatic HTTP setup. If no `ctx` is passed, the request is preserved through `app.createContext()`.
131
-
132
- Use `startStdioServer(app)` for trusted stdio integrations:
133
-
134
- ```ts
135
- import { app } from "#questpie";
136
- import { startStdioServer } from "@questpie/mcp/stdio";
137
-
138
- await startStdioServer(app);
139
- ```
140
-
141
- ## Gotchas
142
-
143
- - Add `mcpModule` to static `modules.ts`, then run codegen.
144
- - HTTP system mode is intentionally impossible until a future trusted-token design exists.
145
- - Field filtering is top-level only.
146
- - Raw routes and unannotated routes are not tools.
147
- - Custom tool results should use `structuredContent` for machine-readable output.
@@ -1,363 +0,0 @@
1
- ---
2
- name: questpie-core-multi-tenancy
3
- description: QUESTPIE multi-tenant scope context resolver header-based tenant isolation ScopeProvider ScopePicker request-scoped services data filtering access control workspace organization property
4
- - questpie-core
5
- - questpie-core-rules
6
- - questpie-core-business-logic
7
- ---
8
-
9
- # QUESTPIE Multi-Tenancy
10
-
11
- QUESTPIE supports multi-tenant applications through a **scope-based** architecture. A "scope" can represent anything: organizations, workspaces, properties, cities, brands — any entity that partitions data.
12
-
13
- The pattern is simple: **HTTP header carries a scope ID, server extracts it into typed context, access rules filter data**.
14
-
15
- ## Architecture Overview
16
-
17
- ```text
18
- Client Server
19
- ────── ──────
20
- ScopeProvider context.ts (file convention)
21
- ↓ stores scopeId ↓ extracts header → typed context
22
- ScopePicker (UI) appConfig({ context }) (types)
23
- ↓ user selects scope ↓ available in every handler
24
- useScopedFetch() access rules / hooks
25
- ↓ injects HTTP header ↓ filter data by scope
26
- fetch("x-selected-city: id") AsyncLocalStorage → getContext()
27
- ```
28
-
29
- ## Step 1: Define the Scope Collection
30
-
31
- Create a collection that represents your tenant entity:
32
-
33
- ```ts
34
- // collections/workspaces.ts
35
- import { collection } from "#questpie/factories";
36
-
37
- export default collection("workspaces").fields(({ f }) => ({
38
- name: f.text().label("Name").required(),
39
- slug: f.text().label("Slug").inputOptional(),
40
- owner: f.relation("user").label("Owner"),
41
- }));
42
- ```
43
-
44
- Other collections reference the scope via a relation:
45
-
46
- ```ts
47
- // collections/projects.ts
48
- import { collection } from "#questpie/factories";
49
-
50
- export default collection("projects").fields(({ f }) => ({
51
- title: f.text().label("Title").required(),
52
- workspace: f.relation("workspaces").label("Workspace").required(),
53
- }));
54
- ```
55
-
56
- ## Step 2: Create the Context Resolver
57
-
58
- The `context.ts` file convention is a singleton that extracts custom properties from each incoming request. Codegen discovers it automatically.
59
-
60
- ```ts
61
- // src/questpie/server/context.ts
62
- import { context } from "#questpie";
63
-
64
- export default context(async ({ request, session, db }) => {
65
- const workspaceId = request.headers.get("x-selected-workspace");
66
-
67
- // Optional: validate that the user has access to this workspace
68
- // if (workspaceId && session?.user) {
69
- // const membership = await db.query.workspaceMembers.findFirst({
70
- // where: and(
71
- // eq(workspaceMembers.workspaceId, workspaceId),
72
- // eq(workspaceMembers.userId, session.user.id),
73
- // ),
74
- // });
75
- // if (!membership) throw new Error("No access to this workspace");
76
- // }
77
-
78
- return {
79
- workspaceId: workspaceId || null,
80
- };
81
- });
82
- ```
83
-
84
- ### Context Resolver Parameters
85
-
86
- | Parameter | Type | Description |
87
- | --------- | --------------------------- | ----------------------------------------------- |
88
- | `request` | `Request` | The incoming HTTP request (Web API) |
89
- | `session` | `{ user, session } \| null` | Resolved auth session (null if unauthenticated) |
90
- | `db` | `Database` | Database client for validation queries |
91
-
92
- The object you return is merged into the request context and becomes available in **every** handler, hook, and access rule.
93
-
94
- ## Step 3: Filter Data with Access Rules
95
-
96
- Use the typed context in access rules and hooks to enforce data isolation:
97
-
98
- ```ts
99
- // collections/projects.ts
100
- import { collection } from "#questpie/factories";
101
-
102
- export default collection("projects")
103
- .fields(({ f }) => ({
104
- title: f.text().label("Title").required(),
105
- workspace: f.relation("workspaces").label("Workspace").required(),
106
- }))
107
- .access({
108
- // Only allow reads when a workspace is selected
109
- read: ({ ctx }) => {
110
- if (!ctx.workspaceId) return false;
111
- return { workspace: ctx.workspaceId };
112
- },
113
- create: ({ ctx }) => !!ctx.workspaceId,
114
- update: ({ ctx }) => {
115
- if (!ctx.workspaceId) return false;
116
- return { workspace: ctx.workspaceId };
117
- },
118
- delete: ({ ctx }) => {
119
- if (!ctx.workspaceId) return false;
120
- return { workspace: ctx.workspaceId };
121
- },
122
- })
123
- .hooks({
124
- // Auto-assign workspace on create
125
- beforeChange: async ({ data, operation, ctx }) => {
126
- if (operation === "create" && ctx.workspaceId) {
127
- data.workspace = ctx.workspaceId;
128
- }
129
- return data;
130
- },
131
- });
132
- ```
133
-
134
- ### Access Rule Return Values
135
-
136
- | Return | Meaning |
137
- | ------------------------------ | ---------------------------------------- |
138
- | `true` | Allow all records |
139
- | `false` | Deny all records |
140
- | `{ field: value }` | Where-clause filter (row-level security) |
141
-
142
- ## Step 5: Set Up the Admin UI
143
-
144
- ### ScopeProvider
145
-
146
- Wrap your admin with `ScopeProvider` to enable scope selection. It manages the selected scope ID and persists it to localStorage.
147
-
148
- ```tsx
149
- // routes/admin/$.tsx
150
- import {
151
- AdminLayout,
152
- AdminRouter,
153
- ScopePicker,
154
- ScopeProvider,
155
- } from "@questpie/admin/client";
156
-
157
- function AdminPage() {
158
- return (
159
- <ScopeProvider
160
- headerName="x-selected-workspace"
161
- storageKey="admin-selected-workspace"
162
- >
163
- <AdminContent />
164
- </ScopeProvider>
165
- );
166
- }
167
- ```
168
-
169
- #### ScopeProvider Props
170
-
171
- | Prop | Type | Required | Description |
172
- | -------------- | ---------------- | -------- | --------------------------------- |
173
- | `headerName` | `string` | Yes | HTTP header name for the scope ID |
174
- | `storageKey` | `string` | No | localStorage key for persistence |
175
- | `defaultScope` | `string \| null` | No | Default scope if none stored |
176
-
177
- ### ScopePicker
178
-
179
- A dropdown for selecting the current scope. Place it in the sidebar:
180
-
181
- ```tsx
182
- function AdminContent() {
183
- return (
184
- <AdminLayout
185
- admin={admin}
186
- basePath="/admin"
187
- slots={{
188
- afterBrand: (
189
- <div className="px-3 py-2 border-b">
190
- <ScopePicker
191
- collection="workspaces"
192
- labelField="name"
193
- placeholder="Select workspace..."
194
- allowClear
195
- clearText="All Workspaces"
196
- compact
197
- />
198
- </div>
199
- ),
200
- }}
201
- >
202
- <AdminRouter basePath="/admin" />
203
- </AdminLayout>
204
- );
205
- }
206
- ```
207
-
208
- #### ScopePicker Props
209
-
210
- | Prop | Type | Default | Description |
211
- | ------------- | ------------------------------ | ------------- | ------------------------------------------ |
212
- | `collection` | `string` | — | Collection to fetch options from |
213
- | `labelField` | `string` | `"name"` | Field to display as label |
214
- | `valueField` | `string` | `"id"` | Field to use as value |
215
- | `options` | `ScopeOption[]` | — | Static options (alternative to collection) |
216
- | `loadOptions` | `() => Promise<ScopeOption[]>` | — | Async options loader |
217
- | `placeholder` | `string` | `"Select..."` | Placeholder text |
218
- | `allowClear` | `boolean` | `false` | Show "All" option to clear scope |
219
- | `clearText` | `string` | `"All"` | Label for the clear option |
220
- | `compact` | `boolean` | `false` | Render smaller (no label) |
221
-
222
- ### Three Data Sources
223
-
224
- ```tsx
225
- // 1. From a collection
226
- <ScopePicker collection="workspaces" labelField="name" />
227
-
228
- // 2. Static options
229
- <ScopePicker options={[
230
- { value: "ws_1", label: "Workspace 1" },
231
- { value: "ws_2", label: "Workspace 2" },
232
- ]} />
233
-
234
- // 3. Async loader
235
- <ScopePicker loadOptions={async () => {
236
- const res = await fetch("/api/my-workspaces");
237
- return res.json();
238
- }} />
239
- ```
240
-
241
- ### useScopedFetch
242
-
243
- When you need to create the API client, use `useScopedFetch()` to automatically inject the scope header into all requests:
244
-
245
- ```tsx
246
- import { useScopedFetch } from "@questpie/admin/client";
247
-
248
- function AdminContent() {
249
- const scopedFetch = useScopedFetch();
250
-
251
- const client = useMemo(
252
- () => createClient<typeof app>({ baseURL: "/api", fetch: scopedFetch }),
253
- [scopedFetch],
254
- );
255
-
256
- return <AdminProvider client={client} />;
257
- }
258
- ```
259
-
260
- ### createScopedFetch (Non-React)
261
-
262
- For use outside React components:
263
-
264
- ```ts
265
- import { createScopedFetch } from "@questpie/admin/client";
266
-
267
- let currentScopeId: string | null = null;
268
-
269
- const scopedFetch = createScopedFetch(
270
- "x-selected-workspace",
271
- () => currentScopeId,
272
- );
273
- ```
274
-
275
- ## Request-Scoped Services
276
-
277
- For advanced cases, create a request-scoped service that provides a tenant-aware database connection:
278
-
279
- ```ts
280
- // services/scoped-db.ts
281
- import { service } from "questpie/services";
282
- export default service({
283
- lifecycle: "request",
284
- deps: ["db", "session"] as const,
285
- create: ({ db, session }) => {
286
- return createScopedDb(db, session?.user?.tenantId);
287
- },
288
- dispose: (scopedDb) => scopedDb.release(),
289
- });
290
- ```
291
-
292
- ## Full Request Flow
293
-
294
- ```text
295
- 1. User selects "Acme Corp" in ScopePicker
296
- 2. ScopeProvider stores scopeId = "ws_123" in state + localStorage
297
- 3. useScopedFetch() creates fetch that adds header: x-selected-workspace: ws_123
298
- 4. Client makes API call → POST /api/collections/projects/find
299
- 5. Server: createAdapterContext() receives Request
300
- 6. Server: context.ts resolver extracts workspaceId = "ws_123" from header
301
- 7. Server: RequestContext created with { workspaceId: "ws_123", session, locale, ... }
302
- 8. Server: runWithContext() stores in AsyncLocalStorage
303
- 9. Server: Access rules evaluate → return { workspace: "ws_123" }
304
- 10. Server: Query filtered to workspace = "ws_123"
305
- 11. Response: Only Acme Corp's projects returned
306
- ```
307
-
308
- ## Common Mistakes
309
-
310
- ### HIGH: Forgetting module augmentation
311
-
312
- Without a `context` function in `appConfig()`, your custom context properties won't be available in handlers. Make sure to define `context` in your `config/app.ts`.
313
-
314
- ### HIGH: Not filtering in access rules
315
-
316
- The context resolver only **extracts** the scope. You must still enforce isolation in `.access()` rules or `.hooks()`. Without access rules, all data is returned regardless of scope.
317
-
318
- ### MEDIUM: Hardcoding header names
319
-
320
- Use the same header name in `ScopeProvider.headerName` and `context.ts`. A mismatch means the server never sees the scope ID.
321
-
322
- ```ts
323
- // These MUST match:
324
- // Client:
325
- <ScopeProvider headerName="x-selected-workspace" />
326
-
327
- // Server (context.ts):
328
- request.headers.get("x-selected-workspace")
329
- ```
330
-
331
- ### MEDIUM: Not validating scope access
332
-
333
- In production, validate that the authenticated user actually belongs to the selected scope. Otherwise any user can access any scope by sending the header manually.
334
-
335
- ```ts
336
- export default context(async ({ request, session, db }) => {
337
- const workspaceId = request.headers.get("x-selected-workspace");
338
-
339
- if (workspaceId && session?.user) {
340
- const isMember = await db.query.workspaceMembers.findFirst({
341
- where: and(
342
- eq(workspaceMembers.workspaceId, workspaceId),
343
- eq(workspaceMembers.userId, session.user.id),
344
- ),
345
- });
346
- if (!isMember) {
347
- throw new Error("Unauthorized access to workspace");
348
- }
349
- }
350
-
351
- return { workspaceId: workspaceId || null };
352
- });
353
- ```
354
-
355
- ## Reference Example
356
-
357
- See the **city-portal** example for a complete working implementation:
358
-
359
- ```text
360
- examples/city-portal/
361
- src/questpie/server/context.ts # Context resolver (x-selected-city header)
362
- src/routes/admin/$.tsx # Admin with ScopeProvider + ScopePicker
363
- ```