appflare 0.2.24 → 0.2.26

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 (138) hide show
  1. package/Documentation.md +758 -758
  2. package/cli/commands/index.ts +238 -238
  3. package/cli/generate.ts +178 -178
  4. package/cli/index.ts +120 -120
  5. package/cli/load-config.ts +184 -184
  6. package/cli/schema-compiler.ts +1183 -1183
  7. package/cli/templates/auth/README.md +156 -156
  8. package/cli/templates/auth/config.ts +61 -61
  9. package/cli/templates/auth/route-config.ts +1 -1
  10. package/cli/templates/auth/route-handler.ts +1 -1
  11. package/cli/templates/auth/route-request-utils.ts +5 -5
  12. package/cli/templates/auth/route.config.ts +18 -18
  13. package/cli/templates/auth/route.handler.ts +18 -18
  14. package/cli/templates/auth/route.request-utils.ts +55 -55
  15. package/cli/templates/auth/route.ts +14 -14
  16. package/cli/templates/core/README.md +266 -266
  17. package/cli/templates/core/app-creation.ts +19 -19
  18. package/cli/templates/core/client/appflare.ts +112 -112
  19. package/cli/templates/core/client/handlers/index.ts +748 -749
  20. package/cli/templates/core/client/handlers.ts +1 -1
  21. package/cli/templates/core/client/index.ts +7 -7
  22. package/cli/templates/core/client/storage.ts +180 -180
  23. package/cli/templates/core/client/types.ts +184 -184
  24. package/cli/templates/core/client-modules/appflare.ts +1 -1
  25. package/cli/templates/core/client-modules/handlers.ts +1 -1
  26. package/cli/templates/core/client-modules/index.ts +1 -1
  27. package/cli/templates/core/client-modules/storage.ts +1 -1
  28. package/cli/templates/core/client-modules/types.ts +1 -1
  29. package/cli/templates/core/client.artifacts.ts +39 -39
  30. package/cli/templates/core/client.ts +4 -4
  31. package/cli/templates/core/drizzle.ts +15 -15
  32. package/cli/templates/core/export.ts +14 -14
  33. package/cli/templates/core/handlers.route.ts +24 -24
  34. package/cli/templates/core/handlers.ts +1 -1
  35. package/cli/templates/core/imports.ts +9 -9
  36. package/cli/templates/core/server.ts +38 -38
  37. package/cli/templates/core/types.ts +6 -6
  38. package/cli/templates/core/wrangler.ts +109 -109
  39. package/cli/templates/dashboard/builders/functions/index.ts +17 -17
  40. package/cli/templates/dashboard/builders/functions/render-page/header.ts +20 -20
  41. package/cli/templates/dashboard/builders/functions/render-page/index.ts +33 -33
  42. package/cli/templates/dashboard/builders/functions/render-page/request-panel.ts +171 -171
  43. package/cli/templates/dashboard/builders/functions/render-page/result-panel.ts +85 -85
  44. package/cli/templates/dashboard/builders/functions/render-page/scripts.ts +554 -554
  45. package/cli/templates/dashboard/builders/navigation.ts +122 -122
  46. package/cli/templates/dashboard/builders/storage/index.ts +13 -13
  47. package/cli/templates/dashboard/builders/storage/routes/create-directory-route.ts +29 -29
  48. package/cli/templates/dashboard/builders/storage/routes/delete-route.ts +18 -18
  49. package/cli/templates/dashboard/builders/storage/routes/download-route.ts +23 -23
  50. package/cli/templates/dashboard/builders/storage/routes/index.ts +22 -22
  51. package/cli/templates/dashboard/builders/storage/routes/list-route.ts +25 -25
  52. package/cli/templates/dashboard/builders/storage/routes/preview-route.ts +21 -21
  53. package/cli/templates/dashboard/builders/storage/routes/upload-route.ts +21 -21
  54. package/cli/templates/dashboard/builders/storage/runtime/helpers.ts +72 -72
  55. package/cli/templates/dashboard/builders/storage/runtime/storage-page.ts +130 -130
  56. package/cli/templates/dashboard/builders/table-routes/common/drawer-panel.ts +27 -27
  57. package/cli/templates/dashboard/builders/table-routes/common/pagination.ts +30 -30
  58. package/cli/templates/dashboard/builders/table-routes/common/search-bar.ts +23 -23
  59. package/cli/templates/dashboard/builders/table-routes/fragments.ts +217 -217
  60. package/cli/templates/dashboard/builders/table-routes/helpers.ts +45 -45
  61. package/cli/templates/dashboard/builders/table-routes/index.ts +8 -8
  62. package/cli/templates/dashboard/builders/table-routes/table/actions-cell.ts +71 -71
  63. package/cli/templates/dashboard/builders/table-routes/table/get-route.ts +291 -291
  64. package/cli/templates/dashboard/builders/table-routes/table/index.ts +80 -80
  65. package/cli/templates/dashboard/builders/table-routes/table/post-routes.ts +163 -163
  66. package/cli/templates/dashboard/builders/table-routes/table-route.ts +7 -7
  67. package/cli/templates/dashboard/builders/table-routes/users/get-route.ts +69 -69
  68. package/cli/templates/dashboard/builders/table-routes/users/html/modals.ts +57 -57
  69. package/cli/templates/dashboard/builders/table-routes/users/html/page.ts +27 -27
  70. package/cli/templates/dashboard/builders/table-routes/users/html/table.ts +128 -128
  71. package/cli/templates/dashboard/builders/table-routes/users/index.ts +32 -32
  72. package/cli/templates/dashboard/builders/table-routes/users/post-routes.ts +150 -150
  73. package/cli/templates/dashboard/builders/table-routes/users/redirect.ts +14 -14
  74. package/cli/templates/dashboard/builders/table-routes/users-route.ts +10 -10
  75. package/cli/templates/dashboard/components/dashboard-home.ts +23 -23
  76. package/cli/templates/dashboard/components/layout.ts +388 -388
  77. package/cli/templates/dashboard/components/login-page.ts +65 -65
  78. package/cli/templates/dashboard/index.ts +61 -61
  79. package/cli/templates/dashboard/types.ts +9 -9
  80. package/cli/templates/handlers/README.md +353 -353
  81. package/cli/templates/handlers/auth.ts +37 -37
  82. package/cli/templates/handlers/execution.ts +42 -42
  83. package/cli/templates/handlers/generators/context/context-creation.ts +101 -101
  84. package/cli/templates/handlers/generators/context/error-helpers.ts +11 -11
  85. package/cli/templates/handlers/generators/context/scheduler.ts +24 -24
  86. package/cli/templates/handlers/generators/context/storage-api.ts +134 -112
  87. package/cli/templates/handlers/generators/context/storage-helpers.ts +59 -59
  88. package/cli/templates/handlers/generators/context/types.ts +18 -18
  89. package/cli/templates/handlers/generators/context.ts +43 -43
  90. package/cli/templates/handlers/generators/execution.ts +15 -15
  91. package/cli/templates/handlers/generators/handlers.ts +13 -13
  92. package/cli/templates/handlers/generators/registration/modules/cron.ts +26 -26
  93. package/cli/templates/handlers/generators/registration/modules/realtime/auth.ts +75 -75
  94. package/cli/templates/handlers/generators/registration/modules/realtime/durable-object.ts +144 -144
  95. package/cli/templates/handlers/generators/registration/modules/realtime/index.ts +14 -14
  96. package/cli/templates/handlers/generators/registration/modules/realtime/publisher.ts +102 -102
  97. package/cli/templates/handlers/generators/registration/modules/realtime/routes.ts +164 -164
  98. package/cli/templates/handlers/generators/registration/modules/realtime/types.ts +30 -30
  99. package/cli/templates/handlers/generators/registration/modules/realtime/utils.ts +516 -516
  100. package/cli/templates/handlers/generators/registration/modules/scheduler.ts +56 -56
  101. package/cli/templates/handlers/generators/registration/modules/storage.ts +196 -194
  102. package/cli/templates/handlers/generators/registration/sections.ts +210 -210
  103. package/cli/templates/handlers/generators/types/context.ts +68 -66
  104. package/cli/templates/handlers/generators/types/core.ts +106 -106
  105. package/cli/templates/handlers/generators/types/operations.ts +135 -135
  106. package/cli/templates/handlers/generators/types/query-definitions/filter-and-where-types.ts +259 -259
  107. package/cli/templates/handlers/generators/types/query-definitions/query-api-types.ts +135 -135
  108. package/cli/templates/handlers/generators/types/query-definitions/query-helper-functions.ts +1031 -1031
  109. package/cli/templates/handlers/generators/types/query-definitions/schema-and-table-types.ts +246 -246
  110. package/cli/templates/handlers/generators/types/query-definitions.ts +13 -13
  111. package/cli/templates/handlers/generators/types/query-runtime/handled-error.ts +13 -13
  112. package/cli/templates/handlers/generators/types/query-runtime/runtime-aggregate-and-footer.ts +174 -174
  113. package/cli/templates/handlers/generators/types/query-runtime/runtime-read.ts +121 -121
  114. package/cli/templates/handlers/generators/types/query-runtime/runtime-setup.ts +45 -45
  115. package/cli/templates/handlers/generators/types/query-runtime/runtime-write.ts +676 -676
  116. package/cli/templates/handlers/generators/types/query-runtime.ts +15 -15
  117. package/cli/templates/handlers/index.ts +43 -43
  118. package/cli/templates/handlers/operations.ts +116 -116
  119. package/cli/templates/handlers/registration.ts +91 -83
  120. package/cli/templates/handlers/types.ts +15 -15
  121. package/cli/templates/handlers/utils.ts +48 -48
  122. package/cli/types.ts +110 -110
  123. package/cli/utils/handler-discovery.ts +466 -466
  124. package/cli/utils/json-utils.ts +24 -24
  125. package/cli/utils/path-utils.ts +19 -19
  126. package/cli/utils/schema-discovery.ts +399 -399
  127. package/dist/cli/index.js +61 -28
  128. package/dist/cli/index.mjs +61 -28
  129. package/index.ts +18 -18
  130. package/package.json +58 -58
  131. package/react/index.ts +5 -5
  132. package/react/use-infinite-query.ts +252 -252
  133. package/react/use-mutation.ts +89 -89
  134. package/react/use-query.ts +207 -207
  135. package/schema.ts +415 -415
  136. package/test-better-auth-hash.ts +2 -2
  137. package/tsconfig.json +6 -6
  138. package/tsup.config.ts +82 -82
package/Documentation.md CHANGED
@@ -1,758 +1,758 @@
1
- # Appflare Documentation
2
-
3
- This guide explains how to build backend handlers, run schema and database migrations, and consume your generated Appflare client in frontend apps (both plain TypeScript/JavaScript and React).
4
-
5
- All examples are aligned with the current workspace structure and APIs.
6
-
7
- ---
8
-
9
- ## 1) How Appflare works (high level)
10
-
11
- Appflare follows a generate-first workflow:
12
-
13
- 1. You write backend schema and handlers in your backend package.
14
- 2. You run the Appflare CLI.
15
- 3. Appflare generates runtime artifacts under your backend out directory (for example `_generated`).
16
- 4. Frontend imports the generated client and calls typed query/mutation routes.
17
-
18
- In this workspace, the backend uses:
19
-
20
- - `packages/backend/appflare.config.ts`
21
- - `packages/backend/schema.ts`
22
- - `packages/backend/src/**` for handlers
23
- - Generated output in `packages/backend/_generated/**`
24
-
25
- ---
26
-
27
- ## 2) Required backend files
28
-
29
- ### 2.1 Appflare config
30
-
31
- Your main config is `packages/backend/appflare.config.ts`.
32
-
33
- Important fields:
34
-
35
- - `scanDir`: where handlers are discovered (currently `./src`)
36
- - `outDir`: where generated artifacts are written (currently `./_generated`)
37
- - `schemaDsl.entry`: schema entry file (currently `./schema.ts`)
38
- - `schema`: schema files used by drizzle generation
39
- - `database`, `kv`, `r2`, `auth`, `scheduler`: runtime binding and feature configuration
40
- - `wranglerOverrides`: final Worker deployment settings
41
-
42
- ### 2.2 Schema
43
-
44
- Schema is defined with `schema`, `table`, and `v` from Appflare.
45
-
46
- Example source: `packages/backend/schema.ts`.
47
-
48
- ### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
49
-
50
- - `v.one("target")` creates a single-reference relation and infers a local FK field.
51
- - `v.many("target")` is inverse one-to-many and infers an FK on the target table.
52
- - `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
53
-
54
- Many-to-many example:
55
-
56
- ```ts
57
- export const schemas = schema({
58
- pets: table({
59
- id: v.uuid(),
60
- trips: v.manyToMany("trips"),
61
- }),
62
- trips: table({
63
- id: v.uuid(),
64
- pets: v.manyToMany("pets"),
65
- }),
66
- });
67
- ```
68
-
69
- Default behavior for `v.manyToMany`:
70
-
71
- - Generates one deterministic junction table per pair.
72
- - Junction rows are pure links (two FK columns, no payload columns).
73
- - Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
74
-
75
- ---
76
-
77
- ## 3) How to create handlers
78
-
79
- Handlers are created with generated helpers:
80
-
81
- - `query(...)` for read endpoints
82
- - `mutation(...)` for write endpoints
83
-
84
- Import them from your generated handlers module:
85
-
86
- ```ts
87
- import { query, mutation } from "../_generated/handlers";
88
- ```
89
-
90
- ### 3.1 Query handler example
91
-
92
- ```ts
93
- import { query } from "../../_generated/handlers";
94
- import * as z from "zod";
95
-
96
- export const getUserProfile = query({
97
- args: {
98
- userId: z.string(),
99
- },
100
- handler: async (ctx, args) => {
101
- const user = await ctx.db.users.findFirst({
102
- where: { id: args.userId },
103
- });
104
-
105
- if (!user) {
106
- ctx.error(404, "User not found", { userId: args.userId });
107
- }
108
-
109
- return user;
110
- },
111
- });
112
- ```
113
-
114
- ### 3.1.1 More query examples (basic to advanced)
115
-
116
- #### A) Search + relation include + limit
117
-
118
- ```ts
119
- import { query } from "../../_generated/handlers";
120
- import * as z from "zod";
121
-
122
- export const searchPosts = query({
123
- args: {
124
- search: z.string().optional(),
125
- ownerId: z.string().optional(),
126
- limit: z.number().int().min(1).max(100).default(25),
127
- },
128
- handler: async (ctx, args) => {
129
- return ctx.db.posts.findMany({
130
- where: {
131
- title: {
132
- regex: args.search ?? "",
133
- $options: "i",
134
- },
135
- ...(args.ownerId ? { ownerId: args.ownerId } : {}),
136
- },
137
- with: {
138
- owner: true,
139
- comments: true,
140
- },
141
- limit: args.limit,
142
- });
143
- },
144
- });
145
- ```
146
-
147
- #### B) Cursor/pagination-style query
148
-
149
- ```ts
150
- import { query } from "../../_generated/handlers";
151
- import * as z from "zod";
152
-
153
- export const listPostsPage = query({
154
- args: {
155
- cursor: z.number().int().optional(),
156
- pageSize: z.number().int().min(1).max(50).default(20),
157
- },
158
- handler: async (ctx, args) => {
159
- const rows = await ctx.db.posts.findMany({
160
- where: args.cursor
161
- ? {
162
- id: {
163
- gt: args.cursor,
164
- },
165
- }
166
- : {},
167
- limit: args.pageSize,
168
- with: {
169
- owner: true,
170
- },
171
- });
172
-
173
- const nextCursor = rows.length > 0 ? rows[rows.length - 1]?.id : undefined;
174
-
175
- return {
176
- rows,
177
- nextCursor,
178
- hasMore: rows.length === args.pageSize,
179
- };
180
- },
181
- });
182
- ```
183
-
184
- #### C) Aggregate-heavy query (`count`, `avg`, relation path)
185
-
186
- ```ts
187
- import { query } from "../../_generated/handlers";
188
- import * as z from "zod";
189
-
190
- export const getPostStats = query({
191
- args: {
192
- ownerId: z.string().optional(),
193
- },
194
- handler: async (ctx, args) => {
195
- const totalPosts = await ctx.db.posts.count({
196
- where: args.ownerId ? { ownerId: args.ownerId } : {},
197
- });
198
-
199
- const uniqueOwners = await ctx.db.posts.count({
200
- field: "ownerId",
201
- distinct: true,
202
- });
203
-
204
- const averagePostId = await ctx.db.posts.avg({
205
- field: "id",
206
- });
207
-
208
- const averageCommentId = await ctx.db.posts.avg({
209
- field: "comments.id",
210
- with: {
211
- comments: {
212
- where: {
213
- id: {
214
- gte: 10000,
215
- },
216
- },
217
- },
218
- },
219
- });
220
-
221
- return {
222
- totalPosts,
223
- uniqueOwners,
224
- averagePostId,
225
- averageCommentId,
226
- };
227
- },
228
- });
229
- ```
230
-
231
- #### D) Geo query (`geoWithin`) + filter composition
232
-
233
- ```ts
234
- import { query } from "../../_generated/handlers";
235
- import * as z from "zod";
236
-
237
- export const nearbyPlaygroundItems = query({
238
- args: {
239
- latitude: z.number(),
240
- longitude: z.number(),
241
- radiusMeters: z.number().positive().default(5000),
242
- },
243
- handler: async (ctx, args) => {
244
- const rows = await ctx.db.queryPlayground.findMany({
245
- where: {
246
- geoWithin: {
247
- $geometry: {
248
- latitude: args.latitude,
249
- longitude: args.longitude,
250
- },
251
- latitudeField: "latitude",
252
- longitudeField: "longitude",
253
- $gte: 0,
254
- $lt: args.radiusMeters,
255
- },
256
- isActive: {
257
- eq: true,
258
- },
259
- },
260
- limit: 100,
261
- });
262
-
263
- return {
264
- count: rows.length,
265
- rows,
266
- };
267
- },
268
- });
269
- ```
270
-
271
- #### E) Complex production-style query (similar to `db-features`)
272
-
273
- ```ts
274
- import { query } from "../../_generated/handlers";
275
- import * as z from "zod";
276
-
277
- export const queryDashboardData = query({
278
- args: {
279
- userId: z.string().optional(),
280
- search: z.string().optional(),
281
- },
282
- handler: async (ctx, args) => {
283
- const posts = await ctx.db.posts.findMany({
284
- where: {
285
- ownerId: args.userId,
286
- title: {
287
- regex: args.search ?? "test",
288
- $options: "i",
289
- },
290
- id: { gt: 0 },
291
- },
292
- with: {
293
- comments: true,
294
- owner: true,
295
- },
296
- limit: 25,
297
- });
298
-
299
- const postsWithCommentStats = await ctx.db.posts.findMany({
300
- with: {
301
- comments: {
302
- _count: true,
303
- _avg: { id: true },
304
- },
305
- },
306
- limit: 10,
307
- });
308
-
309
- const postsTotal = await ctx.db.posts.count({
310
- where: { id: { $gte: 1 } },
311
- });
312
-
313
- return {
314
- posts,
315
- postsTotal,
316
- postsWithCommentStats,
317
- };
318
- },
319
- });
320
- ```
321
-
322
- ### 3.1.2 Query design tips for complex handlers
323
-
324
- - Keep args schema strict (defaults, min/max, optional fields).
325
- - Return stable shapes (avoid switching response shape by condition).
326
- - Start with one root query and compose aggregates/relations progressively.
327
- - Prefer server-side filtering in `where` instead of filtering on frontend.
328
- - For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
329
-
330
- ### 3.2 Mutation handler example
331
-
332
- ```ts
333
- import { mutation } from "../../_generated/handlers";
334
- import * as z from "zod";
335
-
336
- export const createPost = mutation({
337
- args: {
338
- title: z.string().min(1),
339
- slug: z.string().min(1),
340
- },
341
- handler: async (ctx, args) => {
342
- const inserted = await ctx.db.posts.insert({
343
- values: {
344
- title: args.title,
345
- slug: args.slug,
346
- ownerId: "some-user-id",
347
- },
348
- });
349
-
350
- return { created: inserted.length };
351
- },
352
- });
353
- ```
354
-
355
- ### 3.3 Handler file placement
356
-
357
- Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
358
-
359
- - `packages/backend/src/test.ts`
360
- - `packages/backend/src/queries/db-features.ts`
361
- - `packages/backend/src/mutations/db-features.ts`
362
- - `packages/backend/src/bun/test.ts`
363
-
364
- Generated client route names follow directory + file + export naming. For example:
365
-
366
- - query from `src/test.ts` export `getTest` becomes `appflare.queries.test.getTest`
367
- - mutation from `src/mutations/db-features.ts` export `testMutationFeatures` becomes `appflare.mutations["db-features"].testMutationFeatures`
368
-
369
- ### 3.4 Context utilities available in handlers
370
-
371
- Inside handlers, you commonly use:
372
-
373
- - `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
374
- - aggregate helpers like `count` and `avg`
375
- - `ctx.error(status, message, details)` for typed failures
376
-
377
- See real examples in:
378
-
379
- - `packages/backend/src/queries/db-features.ts`
380
- - `packages/backend/src/mutations/db-features.ts`
381
-
382
- ---
383
-
384
- ## 4) Generate artifacts
385
-
386
- From backend package:
387
-
388
- ```bash
389
- cd packages/backend
390
- bun ../appflare/cli dev
391
- ```
392
-
393
- Or via scripts in `packages/backend/package.json`:
394
-
395
- ```bash
396
- bun run build
397
- ```
398
-
399
- What gets generated (core set):
400
-
401
- - `_generated/server.ts`
402
- - `_generated/client.ts`
403
- - `_generated/auth.config.ts`
404
- - `_generated/drizzle.config.ts`
405
- - `_generated/handlers.ts`
406
- - `_generated/handlers.context.ts`
407
- - `_generated/handlers.execution.ts`
408
- - `_generated/handlers.routes.ts`
409
- - `_generated/client/**`
410
-
411
- ### Watch mode
412
-
413
- To regenerate on file changes:
414
-
415
- ```bash
416
- cd packages/backend
417
- bun ../appflare/cli dev --watch
418
- ```
419
-
420
- ---
421
-
422
- ## 5) How to migrate database schema
423
-
424
- Appflare migration flow wraps two steps:
425
-
426
- 1. Generate drizzle migrations
427
- 2. Apply to D1 via Wrangler
428
-
429
- ### 5.1 Standard migrate
430
-
431
- ```bash
432
- cd packages/backend
433
- bun ../appflare/cli migrate
434
- ```
435
-
436
- Or script:
437
-
438
- ```bash
439
- bun run migrate
440
- ```
441
-
442
- ### 5.2 Choose target environment
443
-
444
- Use exactly one of these flags:
445
-
446
- - `--local`
447
- - `--remote`
448
- - `--preview`
449
-
450
- Examples:
451
-
452
- ```bash
453
- bun ../appflare/cli migrate --local
454
- bun ../appflare/cli migrate --remote
455
- bun ../appflare/cli migrate --preview
456
- ```
457
-
458
- ### 5.3 Typical change workflow
459
-
460
- 1. Update `schema.ts`.
461
- 2. Regenerate artifacts:
462
- - `bun ../appflare/cli dev`
463
- 3. Run migration:
464
- - `bun ../appflare/cli migrate --local` (or remote/preview)
465
- 4. Verify app behavior in `wrangler dev`.
466
-
467
- ---
468
-
469
- ## 6) Frontend usage (plain TypeScript/JavaScript)
470
-
471
- Use the generated backend client directly.
472
-
473
- ### 6.1 Create client instance
474
-
475
- ```ts
476
- import { Appflare } from "appflare-backend/_generated/client";
477
-
478
- const appflare = new Appflare({
479
- endpoint: "http://127.0.0.1:8787",
480
- wsEndpoint: "ws://127.0.0.1:8787",
481
- onGetAuthToken: async () => localStorage.getItem("appflare-auth-token") ?? "",
482
- onSetAuthToken: async (token) => {
483
- localStorage.setItem("appflare-auth-token", token);
484
- },
485
- });
486
- ```
487
-
488
- ### 6.2 Run a query
489
-
490
- ```ts
491
- const result = await appflare.queries.test.getTest.run({ id: "test" });
492
-
493
- if (result.error) {
494
- console.error(result.error.status, result.error.message);
495
- } else {
496
- console.log(result.data);
497
- }
498
- ```
499
-
500
- ### 6.2.1 More frontend query call examples
501
-
502
- #### A) Query with filters
503
-
504
- ```ts
505
- const result = await appflare.queries["db-features"].testQueryFeatures.run({
506
- search: "test",
507
- userId: "as3xNgfPVzrooSuSwn1ZSEKNA92Cjp4V",
508
- });
509
-
510
- if (!result.error) {
511
- console.log(result.data.postsCount, result.data.uniqueOwnerCount);
512
- }
513
- ```
514
-
515
- #### B) Query with request options
516
-
517
- ```ts
518
- const result = await appflare.queries.test.getTest.run(
519
- { id: "test" },
520
- {
521
- headers: {
522
- "x-trace-id": crypto.randomUUID(),
523
- },
524
- },
525
- );
526
- ```
527
-
528
- #### C) Query with realtime and explicit auth token
529
-
530
- ```ts
531
- const sub = appflare.queries.test.getTest.subscribe({
532
- args: { id: "test" },
533
- authToken: "token-from-auth-flow",
534
- onChange: (data) => {
535
- console.log("fresh data", data);
536
- },
537
- });
538
-
539
- setTimeout(() => sub.remove(), 30000);
540
- ```
541
-
542
- ### 6.3 Run a mutation
543
-
544
- ```ts
545
- const result = await appflare.mutations.test.newTest.run({});
546
-
547
- if (result.error) {
548
- console.error(result.error.message);
549
- } else {
550
- console.log(result.data);
551
- }
552
- ```
553
-
554
- ### 6.4 Realtime subscribe to a query
555
-
556
- ```ts
557
- const sub = appflare.queries.test.getTest.subscribe({
558
- args: { id: "test" },
559
- onChange: (data, event) => {
560
- console.log("update", event.payload.queryName, data);
561
- },
562
- onError: (error) => {
563
- console.error("subscription error", error);
564
- },
565
- });
566
-
567
- // later
568
- sub.remove();
569
- ```
570
-
571
- ---
572
-
573
- ## 7) How to use with React
574
-
575
- Appflare ships React hooks in `appflare/react`:
576
-
577
- - `useQuery`
578
- - `useInfiniteQuery`
579
- - `useMutation`
580
-
581
- These are thin wrappers around TanStack Query.
582
-
583
- ### 7.1 Setup requirements
584
-
585
- Install peer requirements in your frontend app:
586
-
587
- ```bash
588
- bun add @tanstack/react-query react
589
- ```
590
-
591
- Wrap app with `QueryClientProvider`.
592
-
593
- ```tsx
594
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
595
-
596
- const queryClient = new QueryClient();
597
-
598
- export function Providers({ children }: { children: React.ReactNode }) {
599
- return (
600
- <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
601
- );
602
- }
603
- ```
604
-
605
- ### 7.2 React query usage
606
-
607
- ```tsx
608
- import { useQuery } from "appflare/react";
609
- import { appflare } from "./appflare-client";
610
-
611
- export function TestScreen() {
612
- const query = useQuery(
613
- appflare.queries.test.getTest,
614
- { id: "test" },
615
- {
616
- realtime: { enabled: true },
617
- },
618
- );
619
-
620
- if (query.isLoading) return <div>Loading...</div>;
621
- if (query.error) return <div>{query.error.message}</div>;
622
-
623
- return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
624
- }
625
- ```
626
-
627
- ### 7.3 React mutation usage
628
-
629
- ```tsx
630
- import { useMutation } from "appflare/react";
631
- import { appflare } from "./appflare-client";
632
-
633
- export function CreatePostButton() {
634
- const mutation = useMutation(appflare.mutations.test.newTest, {
635
- onSuccess: (data) => console.log("created", data),
636
- });
637
-
638
- return (
639
- <button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
640
- Create
641
- </button>
642
- );
643
- }
644
- ```
645
-
646
- ### 7.4 React infinite query usage
647
-
648
- ```tsx
649
- import { useInfiniteQuery } from "appflare/react";
650
- import { appflare } from "./appflare-client";
651
-
652
- const result = useInfiniteQuery(
653
- appflare.queries.test.getTest,
654
- { id: "test" },
655
- {
656
- pageParamToArgs: (baseArgs, page) => ({ ...baseArgs, page }),
657
- queryOptions: {
658
- initialPageParam: 1,
659
- getNextPageParam: (lastPage, pages) => pages.length + 1,
660
- },
661
- },
662
- );
663
- ```
664
-
665
- ### 7.5 Realtime with React hooks
666
-
667
- Both `useQuery` and `useInfiniteQuery` support:
668
-
669
- ```ts
670
- realtime: {
671
- enabled: true,
672
- authToken: "optional-token",
673
- requestOptions: { headers: { "x-custom": "1" } },
674
- onChange: (data, update) => {},
675
- onError: (error) => {},
676
- }
677
- ```
678
-
679
- When enabled, hooks subscribe via generated query `.subscribe(...)` and keep query cache updated automatically.
680
-
681
- ---
682
-
683
- ## 8) Frontend app helper pattern
684
-
685
- A good pattern is to keep one shared client factory in a single file.
686
-
687
- Example in this workspace:
688
-
689
- - `apps/app/lib/appflare.ts`
690
-
691
- This file centralizes:
692
-
693
- - endpoint/wsEndpoint selection (web/mobile)
694
- - token storage and retrieval
695
- - exported hook wrappers
696
-
697
- ---
698
-
699
- ## 9) Common commands cheat sheet
700
-
701
- From `packages/backend`:
702
-
703
- ```bash
704
- # Generate once
705
- bun ../appflare/cli build -c appflare.config.ts
706
-
707
- # Generate in dev mode
708
- bun ../appflare/cli dev -c appflare.config.ts
709
-
710
- # Generate + watch
711
- bun ../appflare/cli dev -c appflare.config.ts --watch
712
-
713
- # Migrate local D1
714
- bun ../appflare/cli migrate -c appflare.config.ts --local
715
-
716
- # Migrate remote D1
717
- bun ../appflare/cli migrate -c appflare.config.ts --remote
718
- ```
719
-
720
- Or use backend scripts:
721
-
722
- ```bash
723
- bun run build
724
- bun run dev
725
- bun run migrate
726
- ```
727
-
728
- ---
729
-
730
- ## 10) Troubleshooting
731
-
732
- ### Generated client has missing routes
733
-
734
- - Ensure handler file is under `scanDir`.
735
- - Ensure export uses `query(...)` or `mutation(...)`.
736
- - Run `bun ../appflare/cli dev` again.
737
-
738
- ### Realtime not receiving updates
739
-
740
- - Ensure query has `.subscribe` in generated client.
741
- - Ensure `wsEndpoint` is set correctly.
742
- - Ensure valid auth token is available if your runtime requires auth.
743
-
744
- ### Migration fails
745
-
746
- - Confirm database values in `appflare.config.ts`.
747
- - Use one environment flag only (`--local`, `--remote`, or `--preview`).
748
- - Re-run generation before migration if schema changed.
749
-
750
- ---
751
-
752
- ## 11) Recommended development loop
753
-
754
- 1. Edit schema and handlers.
755
- 2. Run generator (`dev` or `dev --watch`).
756
- 3. Run migrations.
757
- 4. Start backend (`wrangler dev`).
758
- 5. Use generated client in frontend and iterate.
1
+ # Appflare Documentation
2
+
3
+ This guide explains how to build backend handlers, run schema and database migrations, and consume your generated Appflare client in frontend apps (both plain TypeScript/JavaScript and React).
4
+
5
+ All examples are aligned with the current workspace structure and APIs.
6
+
7
+ ---
8
+
9
+ ## 1) How Appflare works (high level)
10
+
11
+ Appflare follows a generate-first workflow:
12
+
13
+ 1. You write backend schema and handlers in your backend package.
14
+ 2. You run the Appflare CLI.
15
+ 3. Appflare generates runtime artifacts under your backend out directory (for example `_generated`).
16
+ 4. Frontend imports the generated client and calls typed query/mutation routes.
17
+
18
+ In this workspace, the backend uses:
19
+
20
+ - `packages/backend/appflare.config.ts`
21
+ - `packages/backend/schema.ts`
22
+ - `packages/backend/src/**` for handlers
23
+ - Generated output in `packages/backend/_generated/**`
24
+
25
+ ---
26
+
27
+ ## 2) Required backend files
28
+
29
+ ### 2.1 Appflare config
30
+
31
+ Your main config is `packages/backend/appflare.config.ts`.
32
+
33
+ Important fields:
34
+
35
+ - `scanDir`: where handlers are discovered (currently `./src`)
36
+ - `outDir`: where generated artifacts are written (currently `./_generated`)
37
+ - `schemaDsl.entry`: schema entry file (currently `./schema.ts`)
38
+ - `schema`: schema files used by drizzle generation
39
+ - `database`, `kv`, `r2`, `auth`, `scheduler`: runtime binding and feature configuration
40
+ - `wranglerOverrides`: final Worker deployment settings
41
+
42
+ ### 2.2 Schema
43
+
44
+ Schema is defined with `schema`, `table`, and `v` from Appflare.
45
+
46
+ Example source: `packages/backend/schema.ts`.
47
+
48
+ ### 2.3 Relation helpers (`v.one`, `v.many`, `v.manyToMany`)
49
+
50
+ - `v.one("target")` creates a single-reference relation and infers a local FK field.
51
+ - `v.many("target")` is inverse one-to-many and infers an FK on the target table.
52
+ - `v.manyToMany("target")` creates a many-to-many relation by synthesizing a junction table.
53
+
54
+ Many-to-many example:
55
+
56
+ ```ts
57
+ export const schemas = schema({
58
+ pets: table({
59
+ id: v.uuid(),
60
+ trips: v.manyToMany("trips"),
61
+ }),
62
+ trips: table({
63
+ id: v.uuid(),
64
+ pets: v.manyToMany("pets"),
65
+ }),
66
+ });
67
+ ```
68
+
69
+ Default behavior for `v.manyToMany`:
70
+
71
+ - Generates one deterministic junction table per pair.
72
+ - Junction rows are pure links (two FK columns, no payload columns).
73
+ - Reciprocal declarations must agree on options (`junctionTable`, field names, FK actions), otherwise generation throws a conflict error.
74
+
75
+ ---
76
+
77
+ ## 3) How to create handlers
78
+
79
+ Handlers are created with generated helpers:
80
+
81
+ - `query(...)` for read endpoints
82
+ - `mutation(...)` for write endpoints
83
+
84
+ Import them from your generated handlers module:
85
+
86
+ ```ts
87
+ import { query, mutation } from "../_generated/handlers";
88
+ ```
89
+
90
+ ### 3.1 Query handler example
91
+
92
+ ```ts
93
+ import { query } from "../../_generated/handlers";
94
+ import * as z from "zod";
95
+
96
+ export const getUserProfile = query({
97
+ args: {
98
+ userId: z.string(),
99
+ },
100
+ handler: async (ctx, args) => {
101
+ const user = await ctx.db.users.findFirst({
102
+ where: { id: args.userId },
103
+ });
104
+
105
+ if (!user) {
106
+ ctx.error(404, "User not found", { userId: args.userId });
107
+ }
108
+
109
+ return user;
110
+ },
111
+ });
112
+ ```
113
+
114
+ ### 3.1.1 More query examples (basic to advanced)
115
+
116
+ #### A) Search + relation include + limit
117
+
118
+ ```ts
119
+ import { query } from "../../_generated/handlers";
120
+ import * as z from "zod";
121
+
122
+ export const searchPosts = query({
123
+ args: {
124
+ search: z.string().optional(),
125
+ ownerId: z.string().optional(),
126
+ limit: z.number().int().min(1).max(100).default(25),
127
+ },
128
+ handler: async (ctx, args) => {
129
+ return ctx.db.posts.findMany({
130
+ where: {
131
+ title: {
132
+ regex: args.search ?? "",
133
+ $options: "i",
134
+ },
135
+ ...(args.ownerId ? { ownerId: args.ownerId } : {}),
136
+ },
137
+ with: {
138
+ owner: true,
139
+ comments: true,
140
+ },
141
+ limit: args.limit,
142
+ });
143
+ },
144
+ });
145
+ ```
146
+
147
+ #### B) Cursor/pagination-style query
148
+
149
+ ```ts
150
+ import { query } from "../../_generated/handlers";
151
+ import * as z from "zod";
152
+
153
+ export const listPostsPage = query({
154
+ args: {
155
+ cursor: z.number().int().optional(),
156
+ pageSize: z.number().int().min(1).max(50).default(20),
157
+ },
158
+ handler: async (ctx, args) => {
159
+ const rows = await ctx.db.posts.findMany({
160
+ where: args.cursor
161
+ ? {
162
+ id: {
163
+ gt: args.cursor,
164
+ },
165
+ }
166
+ : {},
167
+ limit: args.pageSize,
168
+ with: {
169
+ owner: true,
170
+ },
171
+ });
172
+
173
+ const nextCursor = rows.length > 0 ? rows[rows.length - 1]?.id : undefined;
174
+
175
+ return {
176
+ rows,
177
+ nextCursor,
178
+ hasMore: rows.length === args.pageSize,
179
+ };
180
+ },
181
+ });
182
+ ```
183
+
184
+ #### C) Aggregate-heavy query (`count`, `avg`, relation path)
185
+
186
+ ```ts
187
+ import { query } from "../../_generated/handlers";
188
+ import * as z from "zod";
189
+
190
+ export const getPostStats = query({
191
+ args: {
192
+ ownerId: z.string().optional(),
193
+ },
194
+ handler: async (ctx, args) => {
195
+ const totalPosts = await ctx.db.posts.count({
196
+ where: args.ownerId ? { ownerId: args.ownerId } : {},
197
+ });
198
+
199
+ const uniqueOwners = await ctx.db.posts.count({
200
+ field: "ownerId",
201
+ distinct: true,
202
+ });
203
+
204
+ const averagePostId = await ctx.db.posts.avg({
205
+ field: "id",
206
+ });
207
+
208
+ const averageCommentId = await ctx.db.posts.avg({
209
+ field: "comments.id",
210
+ with: {
211
+ comments: {
212
+ where: {
213
+ id: {
214
+ gte: 10000,
215
+ },
216
+ },
217
+ },
218
+ },
219
+ });
220
+
221
+ return {
222
+ totalPosts,
223
+ uniqueOwners,
224
+ averagePostId,
225
+ averageCommentId,
226
+ };
227
+ },
228
+ });
229
+ ```
230
+
231
+ #### D) Geo query (`geoWithin`) + filter composition
232
+
233
+ ```ts
234
+ import { query } from "../../_generated/handlers";
235
+ import * as z from "zod";
236
+
237
+ export const nearbyPlaygroundItems = query({
238
+ args: {
239
+ latitude: z.number(),
240
+ longitude: z.number(),
241
+ radiusMeters: z.number().positive().default(5000),
242
+ },
243
+ handler: async (ctx, args) => {
244
+ const rows = await ctx.db.queryPlayground.findMany({
245
+ where: {
246
+ geoWithin: {
247
+ $geometry: {
248
+ latitude: args.latitude,
249
+ longitude: args.longitude,
250
+ },
251
+ latitudeField: "latitude",
252
+ longitudeField: "longitude",
253
+ $gte: 0,
254
+ $lt: args.radiusMeters,
255
+ },
256
+ isActive: {
257
+ eq: true,
258
+ },
259
+ },
260
+ limit: 100,
261
+ });
262
+
263
+ return {
264
+ count: rows.length,
265
+ rows,
266
+ };
267
+ },
268
+ });
269
+ ```
270
+
271
+ #### E) Complex production-style query (similar to `db-features`)
272
+
273
+ ```ts
274
+ import { query } from "../../_generated/handlers";
275
+ import * as z from "zod";
276
+
277
+ export const queryDashboardData = query({
278
+ args: {
279
+ userId: z.string().optional(),
280
+ search: z.string().optional(),
281
+ },
282
+ handler: async (ctx, args) => {
283
+ const posts = await ctx.db.posts.findMany({
284
+ where: {
285
+ ownerId: args.userId,
286
+ title: {
287
+ regex: args.search ?? "test",
288
+ $options: "i",
289
+ },
290
+ id: { gt: 0 },
291
+ },
292
+ with: {
293
+ comments: true,
294
+ owner: true,
295
+ },
296
+ limit: 25,
297
+ });
298
+
299
+ const postsWithCommentStats = await ctx.db.posts.findMany({
300
+ with: {
301
+ comments: {
302
+ _count: true,
303
+ _avg: { id: true },
304
+ },
305
+ },
306
+ limit: 10,
307
+ });
308
+
309
+ const postsTotal = await ctx.db.posts.count({
310
+ where: { id: { $gte: 1 } },
311
+ });
312
+
313
+ return {
314
+ posts,
315
+ postsTotal,
316
+ postsWithCommentStats,
317
+ };
318
+ },
319
+ });
320
+ ```
321
+
322
+ ### 3.1.2 Query design tips for complex handlers
323
+
324
+ - Keep args schema strict (defaults, min/max, optional fields).
325
+ - Return stable shapes (avoid switching response shape by condition).
326
+ - Start with one root query and compose aggregates/relations progressively.
327
+ - Prefer server-side filtering in `where` instead of filtering on frontend.
328
+ - For heavy queries, add `limit`, cursor args, and response metadata (`nextCursor`, `hasMore`).
329
+
330
+ ### 3.2 Mutation handler example
331
+
332
+ ```ts
333
+ import { mutation } from "../../_generated/handlers";
334
+ import * as z from "zod";
335
+
336
+ export const createPost = mutation({
337
+ args: {
338
+ title: z.string().min(1),
339
+ slug: z.string().min(1),
340
+ },
341
+ handler: async (ctx, args) => {
342
+ const inserted = await ctx.db.posts.insert({
343
+ values: {
344
+ title: args.title,
345
+ slug: args.slug,
346
+ ownerId: "some-user-id",
347
+ },
348
+ });
349
+
350
+ return { created: inserted.length };
351
+ },
352
+ });
353
+ ```
354
+
355
+ ### 3.3 Handler file placement
356
+
357
+ Put handlers under `packages/backend/src` (including nested directories). Example patterns already used in this repo:
358
+
359
+ - `packages/backend/src/test.ts`
360
+ - `packages/backend/src/queries/db-features.ts`
361
+ - `packages/backend/src/mutations/db-features.ts`
362
+ - `packages/backend/src/bun/test.ts`
363
+
364
+ Generated client route names follow directory + file + export naming. For example:
365
+
366
+ - query from `src/test.ts` export `getTest` becomes `appflare.queries.test.getTest`
367
+ - mutation from `src/mutations/db-features.ts` export `testMutationFeatures` becomes `appflare.mutations["db-features"].testMutationFeatures`
368
+
369
+ ### 3.4 Context utilities available in handlers
370
+
371
+ Inside handlers, you commonly use:
372
+
373
+ - `ctx.db.<table>.findMany/findFirst/insert/update/upsert/delete`
374
+ - aggregate helpers like `count` and `avg`
375
+ - `ctx.error(status, message, details)` for typed failures
376
+
377
+ See real examples in:
378
+
379
+ - `packages/backend/src/queries/db-features.ts`
380
+ - `packages/backend/src/mutations/db-features.ts`
381
+
382
+ ---
383
+
384
+ ## 4) Generate artifacts
385
+
386
+ From backend package:
387
+
388
+ ```bash
389
+ cd packages/backend
390
+ bun ../appflare/cli dev
391
+ ```
392
+
393
+ Or via scripts in `packages/backend/package.json`:
394
+
395
+ ```bash
396
+ bun run build
397
+ ```
398
+
399
+ What gets generated (core set):
400
+
401
+ - `_generated/server.ts`
402
+ - `_generated/client.ts`
403
+ - `_generated/auth.config.ts`
404
+ - `_generated/drizzle.config.ts`
405
+ - `_generated/handlers.ts`
406
+ - `_generated/handlers.context.ts`
407
+ - `_generated/handlers.execution.ts`
408
+ - `_generated/handlers.routes.ts`
409
+ - `_generated/client/**`
410
+
411
+ ### Watch mode
412
+
413
+ To regenerate on file changes:
414
+
415
+ ```bash
416
+ cd packages/backend
417
+ bun ../appflare/cli dev --watch
418
+ ```
419
+
420
+ ---
421
+
422
+ ## 5) How to migrate database schema
423
+
424
+ Appflare migration flow wraps two steps:
425
+
426
+ 1. Generate drizzle migrations
427
+ 2. Apply to D1 via Wrangler
428
+
429
+ ### 5.1 Standard migrate
430
+
431
+ ```bash
432
+ cd packages/backend
433
+ bun ../appflare/cli migrate
434
+ ```
435
+
436
+ Or script:
437
+
438
+ ```bash
439
+ bun run migrate
440
+ ```
441
+
442
+ ### 5.2 Choose target environment
443
+
444
+ Use exactly one of these flags:
445
+
446
+ - `--local`
447
+ - `--remote`
448
+ - `--preview`
449
+
450
+ Examples:
451
+
452
+ ```bash
453
+ bun ../appflare/cli migrate --local
454
+ bun ../appflare/cli migrate --remote
455
+ bun ../appflare/cli migrate --preview
456
+ ```
457
+
458
+ ### 5.3 Typical change workflow
459
+
460
+ 1. Update `schema.ts`.
461
+ 2. Regenerate artifacts:
462
+ - `bun ../appflare/cli dev`
463
+ 3. Run migration:
464
+ - `bun ../appflare/cli migrate --local` (or remote/preview)
465
+ 4. Verify app behavior in `wrangler dev`.
466
+
467
+ ---
468
+
469
+ ## 6) Frontend usage (plain TypeScript/JavaScript)
470
+
471
+ Use the generated backend client directly.
472
+
473
+ ### 6.1 Create client instance
474
+
475
+ ```ts
476
+ import { Appflare } from "appflare-backend/_generated/client";
477
+
478
+ const appflare = new Appflare({
479
+ endpoint: "http://127.0.0.1:8787",
480
+ wsEndpoint: "ws://127.0.0.1:8787",
481
+ onGetAuthToken: async () => localStorage.getItem("appflare-auth-token") ?? "",
482
+ onSetAuthToken: async (token) => {
483
+ localStorage.setItem("appflare-auth-token", token);
484
+ },
485
+ });
486
+ ```
487
+
488
+ ### 6.2 Run a query
489
+
490
+ ```ts
491
+ const result = await appflare.queries.test.getTest.run({ id: "test" });
492
+
493
+ if (result.error) {
494
+ console.error(result.error.status, result.error.message);
495
+ } else {
496
+ console.log(result.data);
497
+ }
498
+ ```
499
+
500
+ ### 6.2.1 More frontend query call examples
501
+
502
+ #### A) Query with filters
503
+
504
+ ```ts
505
+ const result = await appflare.queries["db-features"].testQueryFeatures.run({
506
+ search: "test",
507
+ userId: "as3xNgfPVzrooSuSwn1ZSEKNA92Cjp4V",
508
+ });
509
+
510
+ if (!result.error) {
511
+ console.log(result.data.postsCount, result.data.uniqueOwnerCount);
512
+ }
513
+ ```
514
+
515
+ #### B) Query with request options
516
+
517
+ ```ts
518
+ const result = await appflare.queries.test.getTest.run(
519
+ { id: "test" },
520
+ {
521
+ headers: {
522
+ "x-trace-id": crypto.randomUUID(),
523
+ },
524
+ },
525
+ );
526
+ ```
527
+
528
+ #### C) Query with realtime and explicit auth token
529
+
530
+ ```ts
531
+ const sub = appflare.queries.test.getTest.subscribe({
532
+ args: { id: "test" },
533
+ authToken: "token-from-auth-flow",
534
+ onChange: (data) => {
535
+ console.log("fresh data", data);
536
+ },
537
+ });
538
+
539
+ setTimeout(() => sub.remove(), 30000);
540
+ ```
541
+
542
+ ### 6.3 Run a mutation
543
+
544
+ ```ts
545
+ const result = await appflare.mutations.test.newTest.run({});
546
+
547
+ if (result.error) {
548
+ console.error(result.error.message);
549
+ } else {
550
+ console.log(result.data);
551
+ }
552
+ ```
553
+
554
+ ### 6.4 Realtime subscribe to a query
555
+
556
+ ```ts
557
+ const sub = appflare.queries.test.getTest.subscribe({
558
+ args: { id: "test" },
559
+ onChange: (data, event) => {
560
+ console.log("update", event.payload.queryName, data);
561
+ },
562
+ onError: (error) => {
563
+ console.error("subscription error", error);
564
+ },
565
+ });
566
+
567
+ // later
568
+ sub.remove();
569
+ ```
570
+
571
+ ---
572
+
573
+ ## 7) How to use with React
574
+
575
+ Appflare ships React hooks in `appflare/react`:
576
+
577
+ - `useQuery`
578
+ - `useInfiniteQuery`
579
+ - `useMutation`
580
+
581
+ These are thin wrappers around TanStack Query.
582
+
583
+ ### 7.1 Setup requirements
584
+
585
+ Install peer requirements in your frontend app:
586
+
587
+ ```bash
588
+ bun add @tanstack/react-query react
589
+ ```
590
+
591
+ Wrap app with `QueryClientProvider`.
592
+
593
+ ```tsx
594
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
595
+
596
+ const queryClient = new QueryClient();
597
+
598
+ export function Providers({ children }: { children: React.ReactNode }) {
599
+ return (
600
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
601
+ );
602
+ }
603
+ ```
604
+
605
+ ### 7.2 React query usage
606
+
607
+ ```tsx
608
+ import { useQuery } from "appflare/react";
609
+ import { appflare } from "./appflare-client";
610
+
611
+ export function TestScreen() {
612
+ const query = useQuery(
613
+ appflare.queries.test.getTest,
614
+ { id: "test" },
615
+ {
616
+ realtime: { enabled: true },
617
+ },
618
+ );
619
+
620
+ if (query.isLoading) return <div>Loading...</div>;
621
+ if (query.error) return <div>{query.error.message}</div>;
622
+
623
+ return <pre>{JSON.stringify(query.data, null, 2)}</pre>;
624
+ }
625
+ ```
626
+
627
+ ### 7.3 React mutation usage
628
+
629
+ ```tsx
630
+ import { useMutation } from "appflare/react";
631
+ import { appflare } from "./appflare-client";
632
+
633
+ export function CreatePostButton() {
634
+ const mutation = useMutation(appflare.mutations.test.newTest, {
635
+ onSuccess: (data) => console.log("created", data),
636
+ });
637
+
638
+ return (
639
+ <button onClick={() => mutation.mutate()} disabled={mutation.isPending}>
640
+ Create
641
+ </button>
642
+ );
643
+ }
644
+ ```
645
+
646
+ ### 7.4 React infinite query usage
647
+
648
+ ```tsx
649
+ import { useInfiniteQuery } from "appflare/react";
650
+ import { appflare } from "./appflare-client";
651
+
652
+ const result = useInfiniteQuery(
653
+ appflare.queries.test.getTest,
654
+ { id: "test" },
655
+ {
656
+ pageParamToArgs: (baseArgs, page) => ({ ...baseArgs, page }),
657
+ queryOptions: {
658
+ initialPageParam: 1,
659
+ getNextPageParam: (lastPage, pages) => pages.length + 1,
660
+ },
661
+ },
662
+ );
663
+ ```
664
+
665
+ ### 7.5 Realtime with React hooks
666
+
667
+ Both `useQuery` and `useInfiniteQuery` support:
668
+
669
+ ```ts
670
+ realtime: {
671
+ enabled: true,
672
+ authToken: "optional-token",
673
+ requestOptions: { headers: { "x-custom": "1" } },
674
+ onChange: (data, update) => {},
675
+ onError: (error) => {},
676
+ }
677
+ ```
678
+
679
+ When enabled, hooks subscribe via generated query `.subscribe(...)` and keep query cache updated automatically.
680
+
681
+ ---
682
+
683
+ ## 8) Frontend app helper pattern
684
+
685
+ A good pattern is to keep one shared client factory in a single file.
686
+
687
+ Example in this workspace:
688
+
689
+ - `apps/app/lib/appflare.ts`
690
+
691
+ This file centralizes:
692
+
693
+ - endpoint/wsEndpoint selection (web/mobile)
694
+ - token storage and retrieval
695
+ - exported hook wrappers
696
+
697
+ ---
698
+
699
+ ## 9) Common commands cheat sheet
700
+
701
+ From `packages/backend`:
702
+
703
+ ```bash
704
+ # Generate once
705
+ bun ../appflare/cli build -c appflare.config.ts
706
+
707
+ # Generate in dev mode
708
+ bun ../appflare/cli dev -c appflare.config.ts
709
+
710
+ # Generate + watch
711
+ bun ../appflare/cli dev -c appflare.config.ts --watch
712
+
713
+ # Migrate local D1
714
+ bun ../appflare/cli migrate -c appflare.config.ts --local
715
+
716
+ # Migrate remote D1
717
+ bun ../appflare/cli migrate -c appflare.config.ts --remote
718
+ ```
719
+
720
+ Or use backend scripts:
721
+
722
+ ```bash
723
+ bun run build
724
+ bun run dev
725
+ bun run migrate
726
+ ```
727
+
728
+ ---
729
+
730
+ ## 10) Troubleshooting
731
+
732
+ ### Generated client has missing routes
733
+
734
+ - Ensure handler file is under `scanDir`.
735
+ - Ensure export uses `query(...)` or `mutation(...)`.
736
+ - Run `bun ../appflare/cli dev` again.
737
+
738
+ ### Realtime not receiving updates
739
+
740
+ - Ensure query has `.subscribe` in generated client.
741
+ - Ensure `wsEndpoint` is set correctly.
742
+ - Ensure valid auth token is available if your runtime requires auth.
743
+
744
+ ### Migration fails
745
+
746
+ - Confirm database values in `appflare.config.ts`.
747
+ - Use one environment flag only (`--local`, `--remote`, or `--preview`).
748
+ - Re-run generation before migration if schema changed.
749
+
750
+ ---
751
+
752
+ ## 11) Recommended development loop
753
+
754
+ 1. Edit schema and handlers.
755
+ 2. Run generator (`dev` or `dev --watch`).
756
+ 3. Run migrations.
757
+ 4. Start backend (`wrangler dev`).
758
+ 5. Use generated client in frontend and iterate.