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