create-aron-app 0.1.7 → 0.1.10

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 (169) hide show
  1. package/.cursor/worktrees.json +3 -0
  2. package/README.md +24 -31
  3. package/dist/index.js +38 -49
  4. package/package.json +3 -7
  5. package/templates/.cursor/rules/backend.mdc +112 -0
  6. package/templates/.cursor/rules/coding_standards.mdc +85 -4
  7. package/templates/.cursor/rules/frontend_architecture.mdc +334 -0
  8. package/templates/.env.example +6 -0
  9. package/templates/apps/{react-router → web}/.react-router/types/+routes.ts +11 -6
  10. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/[id].ts +5 -2
  11. package/templates/apps/{react-router/.react-router/types/src/routes/(dashboard)/todos → web/.react-router/types/src/routes/(dashboard)/(todos)}/+types/index.ts +5 -2
  12. package/templates/apps/web/.react-router/types/src/routes/(dashboard)/(todos)/+types/layout.ts +65 -0
  13. package/templates/apps/{react-router → web}/project.json +11 -4
  14. package/templates/apps/{react-router → web}/react-router.config.ts +1 -1
  15. package/templates/apps/web/src/libs/convex_query_client.ts +11 -0
  16. package/templates/apps/web/src/libs/react_query_client.ts +17 -0
  17. package/templates/apps/web/src/libs/server/auth.ts +32 -0
  18. package/templates/apps/web/src/libs/server/protected.ts +17 -0
  19. package/templates/apps/web/src/providers/api_auth_provider.tsx +26 -0
  20. package/templates/apps/web/src/providers/global_provider.tsx +28 -0
  21. package/templates/apps/web/src/providers/navigation_loading_bar_provider.tsx +72 -0
  22. package/templates/apps/web/src/root.tsx +68 -0
  23. package/templates/apps/web/src/routes/(dashboard)/(todos)/[id].tsx +33 -0
  24. package/templates/apps/web/src/routes/(dashboard)/(todos)/index.tsx +26 -0
  25. package/templates/apps/web/src/routes/(dashboard)/(todos)/layout.tsx +9 -0
  26. package/templates/apps/{react-router → web}/src/routes/(dashboard)/index.tsx +3 -2
  27. package/templates/apps/web/src/routes/(dashboard)/layout.tsx +20 -0
  28. package/templates/apps/{react-router → web}/src/routes.ts +4 -2
  29. package/templates/apps/{react-router → web}/src/surfaces/sidebar/install.tsx +1 -5
  30. package/templates/apps/{react-router → web}/src/surfaces/sidebar/layout.tsx +24 -15
  31. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos.tsx +1 -1
  32. package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/all_todos_controller.ts +1 -1
  33. package/templates/apps/web/src/surfaces/todos/all_todos/bootstrap.ts +21 -0
  34. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/bootstrap.ts +4 -5
  35. package/templates/apps/{nextjs → web}/src/surfaces/todos/single_todo/header/create.tsx +4 -6
  36. package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo_controller.ts +1 -1
  37. package/templates/components.json +20 -0
  38. package/templates/gitignore +60 -0
  39. package/templates/nx.json +0 -11
  40. package/templates/package.json +2 -3
  41. package/templates/shared/assets/src/styles/global.css +14 -8
  42. package/templates/shared/ui/src/base/collapsible.tsx +31 -0
  43. package/templates/shared/ui/src/base/hover-card.tsx +42 -0
  44. package/templates/shared/ui/src/base/input-group.tsx +168 -0
  45. package/templates/shared/ui/src/base/panel.tsx +93 -0
  46. package/templates/shared/ui/src/hooks/use_mobile.tsx +1 -1
  47. package/templates/shared/ui/src/hooks/use_query_params.tsx +6 -7
  48. package/templates/shared/utils/src/convex.ts +2 -1
  49. package/templates/tsconfig.base.json +2 -4
  50. package/templates/.cursor/commands/builder.md +0 -0
  51. package/templates/.cursor/rules/api_architecture.mdc +0 -262
  52. package/templates/.cursor/rules/convex_rules.mdc +0 -331
  53. package/templates/.cursor/rules/frontend_architecture_core.mdc +0 -495
  54. package/templates/.cursor/rules/frontend_architecture_nextjs.mdc +0 -458
  55. package/templates/.cursor/rules/frontend_architecture_reactrouter.mdc +0 -473
  56. package/templates/.github/workflows/ci.yml +0 -29
  57. package/templates/apps/api/tsconfig.json +0 -23
  58. package/templates/apps/nextjs/.env.example +0 -10
  59. package/templates/apps/nextjs/index.d.ts +0 -6
  60. package/templates/apps/nextjs/next-env.d.ts +0 -5
  61. package/templates/apps/nextjs/next.config.js +0 -22
  62. package/templates/apps/nextjs/postcss.config.js +0 -17
  63. package/templates/apps/nextjs/project.json +0 -22
  64. package/templates/apps/nextjs/src/app/(auth)/layout.tsx +0 -21
  65. package/templates/apps/nextjs/src/app/(auth)/not-allowed/page.tsx +0 -23
  66. package/templates/apps/nextjs/src/app/(auth)/sign-in/[[...sign-in]]/page.tsx +0 -15
  67. package/templates/apps/nextjs/src/app/(dashboard)/layout.tsx +0 -22
  68. package/templates/apps/nextjs/src/app/(dashboard)/page.tsx +0 -12
  69. package/templates/apps/nextjs/src/app/(dashboard)/todos/[id]/page.tsx +0 -26
  70. package/templates/apps/nextjs/src/app/(dashboard)/todos/page.tsx +0 -19
  71. package/templates/apps/nextjs/src/app/app.css +0 -3
  72. package/templates/apps/nextjs/src/app/layout.tsx +0 -26
  73. package/templates/apps/nextjs/src/middleware.ts +0 -18
  74. package/templates/apps/nextjs/src/providers/convex_provider.tsx +0 -44
  75. package/templates/apps/nextjs/src/surfaces/home/home.tsx +0 -27
  76. package/templates/apps/nextjs/src/surfaces/home/layout.tsx +0 -44
  77. package/templates/apps/nextjs/src/surfaces/home/main/create.tsx +0 -34
  78. package/templates/apps/nextjs/src/surfaces/sidebar/install.tsx +0 -23
  79. package/templates/apps/nextjs/src/surfaces/sidebar/layout.tsx +0 -118
  80. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/create.tsx +0 -19
  81. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_config.ts +0 -22
  82. package/templates/apps/nextjs/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -25
  83. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  84. package/templates/apps/nextjs/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -33
  85. package/templates/apps/nextjs/src/surfaces/sidebar/sidebar.tsx +0 -23
  86. package/templates/apps/nextjs/src/surfaces/sidebar/ui/sidebar_nav_link.tsx +0 -39
  87. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/create.tsx +0 -28
  88. package/templates/apps/nextjs/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -42
  89. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos.tsx +0 -29
  90. package/templates/apps/nextjs/src/surfaces/todos/all_todos/all_todos_controller.ts +0 -61
  91. package/templates/apps/nextjs/src/surfaces/todos/all_todos/bootstrap.ts +0 -21
  92. package/templates/apps/nextjs/src/surfaces/todos/all_todos/header/create.tsx +0 -23
  93. package/templates/apps/nextjs/src/surfaces/todos/all_todos/install.tsx +0 -23
  94. package/templates/apps/nextjs/src/surfaces/todos/all_todos/layout.tsx +0 -44
  95. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/create.tsx +0 -49
  96. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/main.tsx +0 -70
  97. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -56
  98. package/templates/apps/nextjs/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -99
  99. package/templates/apps/nextjs/src/surfaces/todos/single_todo/bootstrap.ts +0 -32
  100. package/templates/apps/nextjs/src/surfaces/todos/single_todo/header/header.tsx +0 -22
  101. package/templates/apps/nextjs/src/surfaces/todos/single_todo/install.tsx +0 -27
  102. package/templates/apps/nextjs/src/surfaces/todos/single_todo/layout.tsx +0 -55
  103. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/create.tsx +0 -38
  104. package/templates/apps/nextjs/src/surfaces/todos/single_todo/main/main.tsx +0 -49
  105. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo.tsx +0 -29
  106. package/templates/apps/nextjs/src/surfaces/todos/single_todo/single_todo_controller.ts +0 -13
  107. package/templates/apps/nextjs/src/utils/auth.ts +0 -18
  108. package/templates/apps/nextjs/src/utils/convex.ts +0 -11
  109. package/templates/apps/nextjs/src/utils/font.ts +0 -9
  110. package/templates/apps/nextjs/tsconfig.json +0 -42
  111. package/templates/apps/react-router/src/providers/api_auth_provider.tsx +0 -40
  112. package/templates/apps/react-router/src/root.tsx +0 -37
  113. package/templates/apps/react-router/src/routes/(dashboard)/layout.tsx +0 -37
  114. package/templates/apps/react-router/src/routes/(dashboard)/todos/[id].tsx +0 -19
  115. package/templates/apps/react-router/src/routes/(dashboard)/todos/index.tsx +0 -19
  116. package/templates/apps/react-router/src/surfaces/home/bootstrap.ts +0 -9
  117. package/templates/apps/react-router/src/surfaces/home/install.tsx +0 -17
  118. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/create.tsx +0 -21
  119. package/templates/apps/react-router/src/surfaces/sidebar/nav_secondary/nav_secondary.tsx +0 -31
  120. package/templates/apps/react-router/src/surfaces/todos/all_todos/bootstrap.ts +0 -18
  121. package/templates/apps/react-router/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -11
  122. package/templates/apps/react-router/src/surfaces/todos/single_todo/header/create.tsx +0 -32
  123. package/templates/apps/react-router/tsconfig.json +0 -20
  124. package/templates/biome.json +0 -121
  125. package/templates/bun.lock +0 -3187
  126. package/templates/emails/tsconfig.json +0 -5
  127. package/templates/shared/assets/tsconfig.json +0 -5
  128. package/templates/shared/ui/tsconfig.json +0 -8
  129. package/templates/shared/utils/tsconfig.json +0 -5
  130. /package/templates/apps/{react-router → web}/.env.example +0 -0
  131. /package/templates/apps/{react-router → web}/.react-router/types/+future.ts +0 -0
  132. /package/templates/apps/{react-router → web}/.react-router/types/+server-build.d.ts +0 -0
  133. /package/templates/apps/{react-router → web}/.react-router/types/src/+types/root.ts +0 -0
  134. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/+types/layout.ts +0 -0
  135. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(auth)/sign-in/+types/index.ts +0 -0
  136. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/index.ts +0 -0
  137. /package/templates/apps/{react-router → web}/.react-router/types/src/routes/(dashboard)/+types/layout.ts +0 -0
  138. /package/templates/apps/{react-router → web}/postcss.config.js +0 -0
  139. /package/templates/apps/{react-router → web}/public/favicon.ico +0 -0
  140. /package/templates/apps/{react-router → web}/src/app.css +0 -0
  141. /package/templates/apps/{react-router → web}/src/components/error_boundary.tsx +0 -0
  142. /package/templates/apps/{react-router → web}/src/routes/(auth)/layout.tsx +0 -0
  143. /package/templates/apps/{react-router → web}/src/routes/(auth)/sign-in/index.tsx +0 -0
  144. /package/templates/apps/{nextjs → web}/src/surfaces/home/bootstrap.ts +0 -0
  145. /package/templates/apps/{react-router → web}/src/surfaces/home/home.tsx +0 -0
  146. /package/templates/apps/{nextjs → web}/src/surfaces/home/install.tsx +0 -0
  147. /package/templates/apps/{react-router → web}/src/surfaces/home/layout.tsx +0 -0
  148. /package/templates/apps/{react-router → web}/src/surfaces/home/main/create.tsx +0 -0
  149. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/create.tsx +0 -0
  150. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/nav_main/nav_main.tsx +0 -0
  151. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/sidebar.tsx +0 -0
  152. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/create.tsx +0 -0
  153. /package/templates/apps/{react-router → web}/src/surfaces/sidebar/user_menu/user_menu.tsx +0 -0
  154. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/header/create.tsx +0 -0
  155. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/install.tsx +0 -0
  156. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/layout.tsx +0 -0
  157. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/create.tsx +0 -0
  158. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/main.tsx +0 -0
  159. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/create.tsx +0 -0
  160. /package/templates/apps/{react-router → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/new_todo_sheet.tsx +0 -0
  161. /package/templates/apps/{nextjs → web}/src/surfaces/todos/all_todos/main/new_todo_sheet/schema.ts +0 -0
  162. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/header/header.tsx +0 -0
  163. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/install.tsx +0 -0
  164. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/layout.tsx +0 -0
  165. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/create.tsx +0 -0
  166. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/main/main.tsx +0 -0
  167. /package/templates/apps/{react-router → web}/src/surfaces/todos/single_todo/single_todo.tsx +0 -0
  168. /package/templates/apps/{react-router → web}/vite.config.ts +0 -0
  169. /package/templates/emails/{welcome_email.tsx → src/welcome_email.tsx} +0 -0
@@ -22,6 +22,22 @@ globs: **/*.ts,**/*.tsx
22
22
  - Use singular for single items (`todo`, `entity`) and plural for collections (`todos`, `entities`)
23
23
  - Destructure to extract IDs: `const { entityId, ...updates } = args;`
24
24
 
25
+ ### Zod Schemas
26
+
27
+ The inferred type and the schema constant must share the same PascalCase name. Declare the type **above** the const. This applies to both API and web code:
28
+
29
+ ```typescript
30
+ // Good
31
+ export type CreateConversationInput = z.infer<typeof CreateConversationInput>;
32
+ export const CreateConversationInput = z.object({
33
+ title: z.string().min(1).max(120),
34
+ });
35
+
36
+ // Bad — mismatched names, type below const
37
+ export const createConversationSchema = z.object({ title: z.string() });
38
+ export type CreateConversationInput = z.infer<typeof createConversationSchema>;
39
+ ```
40
+
25
41
  ## Code Style
26
42
 
27
43
  ### Formatting
@@ -30,6 +46,73 @@ globs: **/*.ts,**/*.tsx
30
46
  - 2-space indentation
31
47
  - Trailing commas in multi-line arrays and objects
32
48
 
49
+ ### Class Spacing
50
+
51
+ Each member of a class (property, accessor, or method) must be separated by a blank line. Decorators go on their own line above the member. Never inline a method body — always expand to a full block:
52
+
53
+ ```typescript
54
+ // Good
55
+ export class FooController {
56
+ @observable.ref
57
+ accessor isOpen = false;
58
+
59
+ @observable.ref
60
+ accessor selectedId: string | undefined = undefined;
61
+
62
+ @computed
63
+ get label() {
64
+ return this.isOpen ? "Close" : "Open";
65
+ }
66
+
67
+ @action
68
+ open(id: string) {
69
+ this.selectedId = id;
70
+ this.isOpen = true;
71
+ }
72
+ }
73
+
74
+ // Bad — inlined bodies, missing blank lines, stacked decorator
75
+ export class FooController {
76
+ @observable.ref accessor isOpen = false;
77
+ @observable.ref accessor selectedId: string | undefined = undefined;
78
+ @computed get label() { return this.isOpen ? "Close" : "Open"; }
79
+ @action open(id: string) { this.selectedId = id; this.isOpen = true; }
80
+ }
81
+ ```
82
+
83
+ ### No Expression Nesting
84
+
85
+ Never pass an awaited call or a complex expression directly as an argument. Extract to a named variable first:
86
+
87
+ ```typescript
88
+ // Good
89
+ const user = await getUser(userId);
90
+ await sendWelcomeEmail(user);
91
+
92
+ // Bad
93
+ await sendWelcomeEmail(await getUser(userId));
94
+ ```
95
+
96
+ ### Block Statements
97
+
98
+ Always use block bodies (`{ }`). This is enforced by Biome (`useBlockStatements`). No single-line `if`/`throw`, no expression-body arrow callbacks:
99
+
100
+ ```typescript
101
+ // Good
102
+ if (!conversation) {
103
+ throw new Error("Not found");
104
+ }
105
+
106
+ onSubmit={() => {
107
+ controller.submit({ id: bootstrap.id });
108
+ }}
109
+
110
+ // Bad
111
+ if (!conversation) throw new Error("Not found");
112
+
113
+ onSubmit={() => controller.submit({ id: bootstrap.id })}
114
+ ```
115
+
33
116
  ### Conditional Logic
34
117
 
35
118
  Prefer concise conditionals — avoid unnecessary intermediate variables:
@@ -38,25 +121,23 @@ Prefer concise conditionals — avoid unnecessary intermediate variables:
38
121
  // Good
39
122
  const count = entity.limit ?? 0;
40
123
  if (count <= 0) {
41
- throw new ConvexError("Limit reached");
124
+ throw new Error("Limit reached");
42
125
  }
43
126
 
44
127
  // Bad
45
128
  const remaining = entity.limit ?? 0;
46
129
  if (!entity.limit || entity.limit <= 0) {
47
- throw new ConvexError("Limit reached");
130
+ throw new Error("Limit reached");
48
131
  }
49
132
  ```
50
133
 
51
134
  ### Async / Await
52
135
  - Always `await` async operations — never fire-and-forget unless intentional
53
- - Chain operations when it improves readability
54
136
  - Use `Promise.all()` for independent parallel operations
55
137
 
56
138
  ### Validation
57
139
  - Validate business rules before any database operation
58
140
  - Centralise reusable validation in `@/utils/`
59
- - Throw `ConvexError` with clear, actionable messages
60
141
  - Keep CRUD files thin — validation logic belongs in utils
61
142
 
62
143
  ## Testing
@@ -0,0 +1,334 @@
1
+ ---
2
+ description: Web frontend — surface architecture (MobX + React Router + Convex + Clerk SSR). Layers, routing, data loading, Convex/React Query wiring, and component conventions.
3
+ globs: templates/apps/web/**/*.tsx,templates/apps/web/**/*.ts,apps/web/**/*.tsx,apps/web/**/*.ts
4
+ alwaysApply: false
5
+ ---
6
+
7
+ # Frontend architecture — React Router + Convex
8
+
9
+ Audience: engineers working on `apps/web` (or `templates/apps/web` in this repo).
10
+
11
+ This stack uses **surfaces** (vertical slices), **MobX** for UI state, **Convex** for data, and **SSR loaders** with `ConvexHttpClient` + Clerk. An older **v2** ruleset documented a Hono BFF + `api` client; this project does **not** use that pattern — use Convex queries/mutations and the paths below instead.
12
+
13
+ ---
14
+
15
+ ## Philosophy
16
+
17
+ 1. **Display components are dumb** — props in, JSX out. No data fetching, no surface state, no controller imports.
18
+ 2. **`create.tsx` factories are smart closures** — they close over controller/bootstrap at wiring time and return an `observer()` component. This is the only layer that bridges MobX and React hooks.
19
+ 3. **Controllers are the brain** — a MobX class owns cross-section UI state (dialogs, selection). No React imports.
20
+
21
+ **Why explicit wiring instead of React Context?** Context hides the dependency graph. Here, access is traceable: surface → `install` → `create` → display. No surprise re-renders or ambiguous state ownership.
22
+
23
+ **Why not add Zustand?** MobX plus optional `GlobalProvider` already cover reactive UI state; Convex plus TanStack Query cover server state — avoid a third paradigm.
24
+
25
+ ---
26
+
27
+ ## Layer stack
28
+
29
+ ```
30
+ routes/.../[page].tsx → server loader + clientLoader; passes loaderData as bootstrap
31
+ bootstrap.ts → ConvexHttpClient query only (SSR); no browser APIs
32
+ [feature].tsx → surface entry: useRef install guard, useMemo(createLayout)
33
+ install.tsx → instantiate controller; dynamic import() of section creates
34
+ layout.tsx → LayoutController + observer shell + skeleton slots
35
+ [feature]_controller.ts → MobX — shared UI state; may call convex.mutation
36
+ [section]_presenter.ts → MobX — section-scoped logic (optional)
37
+ [section]/create.tsx → factory → observer; useQuery + convexQuery + initialData
38
+ [section]/[section].tsx → pure display (`useFormContext` exception for forms)
39
+ ui/ → dumb components scoped to this surface
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Folder and naming
45
+
46
+ ```
47
+ src/surfaces/todos/
48
+ all_todos/ # list surface
49
+ single_todo/ # id-scoped surface
50
+ ```
51
+
52
+ - File names: **snake_case**
53
+ - Factory/install args: `Opts` suffix — `InstallSingleTodoOpts`
54
+ - Component props: `Props` suffix — `MainProps`
55
+ - List surfaces: **`All`** prefix — `AllTodos`
56
+ - Id surfaces: **`Single`** prefix — `SingleTodo`
57
+ - CRUD UI folders: `new_`, `edit_`, `delete_` — never `create_` (conflicts with `create.tsx` pattern)
58
+ - Display component names must **not** repeat the parent path — inside `single_todo/header/`, export `Header`, not `SingleTodoHeader`
59
+ - Imports: **`@/...` only** — no relative imports within features
60
+
61
+ ---
62
+
63
+ ## Setup — MobX (Stage 3)
64
+
65
+ Requires TypeScript 5+ with `experimentalDecorators: false` and Babel decorators in Vite:
66
+
67
+ ```json
68
+ { "compilerOptions": { "experimentalDecorators": false } }
69
+ ```
70
+
71
+ ```typescript
72
+ react({ babel: { plugins: [["@babel/plugin-proposal-decorators", { version: "2023-05" }]] } })
73
+ ```
74
+
75
+ - Use `accessor` on `@observable` fields
76
+ - Prefer `@action` methods — mutations after `await` inside an `@action async` are safe without `runInAction`
77
+ - Use `@observable.ref` for component slot fields on layout controllers
78
+
79
+ ---
80
+
81
+ ## Setup — Convex and React Query
82
+
83
+ **Convex React client** (browser, `expectAuth: true`) and **ConvexQueryClient** live in `@/web/libs/convex_query_client`:
84
+
85
+ ```typescript
86
+ import { convex, convexQueryClient } from "@/web/libs/convex_query_client";
87
+ ```
88
+
89
+ **TanStack Query client** is `@/web/libs/react_query_client` — it connects `convexQueryClient` and sets default `queryFn`. Use this singleton anywhere you need a `QueryClient` on the client.
90
+
91
+ **Presenters and controllers** call mutations outside React via the same `convex` singleton:
92
+
93
+ ```typescript
94
+ import { convex } from "@/web/libs/convex_query_client";
95
+ import { api } from "@/api/_generated/api";
96
+
97
+ await convex.mutation(api.todos.crud.updateTodo, args);
98
+ ```
99
+
100
+ **Creates** use `useQuery` with `convexQuery` from `@convex-dev/react-query` and **`initialData` from bootstrap** so the first paint matches SSR.
101
+
102
+ ---
103
+
104
+ ## Global state (three tiers)
105
+
106
+ | Tier | Mechanism | When |
107
+ |------|-----------|------|
108
+ | Module `stores/` | Plain module variable | One-shot, non-reactive handoff across navigation |
109
+ | `GlobalProvider` | React context + `useGlobal()` | Reactive state across `<Outlet />` (use sparingly) |
110
+ | MobX controller | `[feature]_controller.ts` | All UI state inside a surface |
111
+
112
+ Module store rules: consume-once helpers, one concern per file, document lifetime at top, never for persistence alone.
113
+
114
+ ---
115
+
116
+ ## Routing and auth
117
+
118
+ **Route config** (`src/routes.ts`) uses route groups `(auth)` and `(dashboard)`. URLs come from `layout`/`prefix`/`route`, not folder names.
119
+
120
+ Example shape (adjust files to match your app):
121
+
122
+ ```typescript
123
+ import { index, layout, prefix, type RouteConfig, route } from "@react-router/dev/routes";
124
+
125
+ export default [
126
+ layout("./routes/(auth)/layout.tsx", [route("/sign-in/*", "./routes/(auth)/sign-in/index.tsx")]),
127
+ layout("./routes/(dashboard)/layout.tsx", [
128
+ index("./routes/(dashboard)/index.tsx"),
129
+ ...prefix("todos", [
130
+ layout("./routes/(dashboard)/(todos)/layout.tsx", [
131
+ index("./routes/(dashboard)/(todos)/index.tsx"),
132
+ route(":id", "./routes/(dashboard)/(todos)/[id].tsx"),
133
+ ]),
134
+ ]),
135
+ ]),
136
+ ] satisfies RouteConfig;
137
+ ```
138
+
139
+ **Middleware** (`root.tsx`): Clerk + a protected-route middleware enforce auth for non-public paths before loaders run.
140
+
141
+ **`root.tsx`**: providers (`ApiAuthProvider`, `GlobalProvider`, …) + `<Outlet />` — no feature sidebar here.
142
+
143
+ **Dashboard layout**: shell only (e.g. `SidebarProvider`, `Sidebar`, `Outlet`).
144
+
145
+ **Router types**: import `Route` from `./+types/...` adjacent to the route file (or the generated types path your tsconfig uses).
146
+
147
+ ---
148
+
149
+ ## SSR data loading (loaders + bootstrap)
150
+
151
+ Flow:
152
+
153
+ 1. **Server** — route `loader` builds an authenticated `ConvexHttpClient` and runs bootstrap.
154
+ 2. **Client** — `clientLoader` returns `serverLoader()` only — no second fetch.
155
+ 3. **Hydration** — `useQuery({ ...convexQuery(...), initialData: bootstrap.* })` so the live subscription takes over.
156
+
157
+ ### Auth helper — `src/libs/server/auth.ts`
158
+
159
+ Single place for loader-time Convex HTTP client + redirect if unauthenticated:
160
+
161
+ ```typescript
162
+ import { getAuth } from "@clerk/react-router/server";
163
+ import { ConvexHttpClient } from "convex/browser";
164
+ import type { LoaderFunctionArgs } from "react-router";
165
+ import { redirect } from "react-router";
166
+
167
+ export const createConvexClient = async (args: LoaderFunctionArgs): Promise<ConvexHttpClient> => {
168
+ const auth = await getAuth(args);
169
+ if (!auth.isAuthenticated) {
170
+ throw redirect(`/sign-in?redirect_url=${encodeURIComponent(args.request.url)}`);
171
+ }
172
+ const token = await auth.getToken({ template: "convex" });
173
+ const client = new ConvexHttpClient(process.env.VITE_CONVEX_URL!);
174
+ if (token) {
175
+ client.setAuth(token);
176
+ }
177
+ return client;
178
+ };
179
+ ```
180
+
181
+ Use `getToken({ template: "convex" })` (not the default session token). In server loaders use `process.env.VITE_CONVEX_URL` (not `import.meta.env`).
182
+
183
+ ### Bootstrap — `surfaces/.../bootstrap.ts`
184
+
185
+ - Accept `{ client: ConvexHttpClient }` from the route loader
186
+ - Use `client.query(api...)` only — no `ConvexReactClient`, `convexQuery`, or `ensureQueryData` in bootstrap
187
+ - May `throw redirect(...)` for invalid params
188
+
189
+ ### Route module
190
+
191
+ ```typescript
192
+ export async function loader(args: Route.LoaderArgs) {
193
+ const client = await createConvexClient(args);
194
+ return bootstrapAllTodos({ client });
195
+ }
196
+
197
+ export async function clientLoader({ serverLoader }: Route.ClientLoaderArgs) {
198
+ return serverLoader();
199
+ }
200
+
201
+ export default function Page({ loaderData: bootstrap }: Route.ComponentProps) {
202
+ return <AllTodos bootstrap={bootstrap} />;
203
+ }
204
+ ```
205
+
206
+ ### Client `create.tsx`
207
+
208
+ ```typescript
209
+ const { data: todos } = useQuery({
210
+ ...convexQuery(api.todos.crud.listTodos, {}),
211
+ initialData: bootstrap.todos as never,
212
+ });
213
+ ```
214
+
215
+ Spread `convexQuery` first, then `initialData`. The `as never` cast is often needed for serialized vs local types.
216
+
217
+ ### Anti-patterns
218
+
219
+ | Wrong | Right |
220
+ |-------|--------|
221
+ | `clientLoader` calling `ensureQueryData(convexQuery(...))` for SSR | Server `loader` + `clientLoader` → `serverLoader()` |
222
+ | Building `ConvexHttpClient` inside bootstrap | Receive `client` from `createConvexClient` |
223
+ | Duplicating Clerk redirects in every bootstrap | Centralize in `createConvexClient` / middleware |
224
+
225
+ ---
226
+
227
+ ## Shell vs pages
228
+
229
+ - **Layout-mounted** (e.g. sidebar): `useEffect(() => installSidebar(...), [])` once.
230
+ - **Route-mounted** pages: synchronous **`useRef` install guard** on first render (avoids empty flash; StrictMode-safe).
231
+
232
+ ---
233
+
234
+ ## Surface entry (`[feature].tsx`)
235
+
236
+ - Receives **`bootstrap` from the route** — never fetch the primary payload inside the surface
237
+ - `useMemo(() => createLayout(), [])` once
238
+ - `useRef` guard calls `installFeature({ layout, bootstrap, navigate })` synchronously on first run
239
+ - Obtain `useNavigate` (and similar) here and **pass into `install`**, not inside `install` itself
240
+
241
+ ---
242
+
243
+ ## `install.tsx`
244
+
245
+ - **New `Controller()` here** — exactly once; shared across all `create` closures
246
+ - Use dynamic `import()` per section for code splitting
247
+ - Plain function — not a hook, not a component
248
+ - Do not use React Context to replace explicit controller passing
249
+
250
+ ---
251
+
252
+ ## `layout.tsx`
253
+
254
+ - `LayoutController` holds `@observable.ref` slots for `ComponentType`
255
+ - `@action` setters for slots; observer shell renders skeletons until slots load
256
+
257
+ ---
258
+
259
+ ## Controllers and presenters
260
+
261
+ - **Controller**: cross-section UI state; `convex.mutation` / `convex.query` when needed; **no React**
262
+ - **Presenter**: one section’s logic; same rules — **no React**
263
+ - Prefer **`@action async`** without extra `runInAction` for Stage 3 decorators when using MobX’s supported pattern
264
+
265
+ ---
266
+
267
+ ## `create.tsx`
268
+
269
+ - Call child factories **outside** the returned `observer()` so identities stay stable
270
+ - Inside `observer`: `useQuery`, `useForm`, etc.
271
+ - Pass **`initialData: bootstrap.\*`** for the same Convex function used in SSR
272
+ - No `useMutation` for routine writes — use `convex.mutation` in controller/presenter and pass callbacks
273
+ - No `useState` for feature UI state — controller/presenter own that
274
+ - Inline JSX handlers: **block or multi-line** — not one-liner `onClick={() => foo()}`
275
+
276
+ ---
277
+
278
+ ## Display components
279
+
280
+ - Props + callbacks only; **no** `Doc<"todos">` / raw Convex types — import **`Todo`**, **`TodoId`** from `@/api/todos/types` (or the entity’s `types.ts`)
281
+ - Never import **`Ent<...>`** on the frontend
282
+ - **Forms**: `useForm` + `<FormProvider>` in `create.tsx`; display uses **`useFormContext()`** — never pass `UseFormReturn` as a prop
283
+ - **Thin UIs** can live inline in `create.tsx` without a separate display file
284
+ - Arrow functions, named `Props` type, explicit `return (...)` for JSX
285
+
286
+ ---
287
+
288
+ ## Schema (`schema.ts`)
289
+
290
+ Same PascalCase name for type and Zod value (see `coding_standards.mdc`):
291
+
292
+ ```typescript
293
+ export type NewTodoSchema = z.infer<typeof NewTodoSchema>;
294
+ export const NewTodoSchema = z.object({
295
+ title: z.string().min(1, "Title is required"),
296
+ });
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Factory opts vs observer props
302
+
303
+ - **Opts** — known when the factory runs (`controller`, `bootstrap`, child components created by parent). Closed over.
304
+ - **Props** on the observer — per-render / per-item values (`todoId`, list row props).
305
+
306
+ Component slots are **`ComponentType`**, not `ReactNode`.
307
+
308
+ ---
309
+
310
+ ## DI tree
311
+
312
+ Nested surfaces (sheets, dialogs) live **under** the section that constructs them so dependencies stay a subtree of that section’s `create.tsx`.
313
+
314
+ ---
315
+
316
+ ## Next.js variant
317
+
318
+ Adapt loader and client-entry patterns to your framework’s routing and data APIs — layer names and surface rules stay the same.
319
+
320
+ ---
321
+
322
+ ## Quick reference
323
+
324
+ | File | Hooks? | Controller |
325
+ |------|--------|------------|
326
+ | Route module | No in loader | No |
327
+ | `bootstrap.ts` | No | No |
328
+ | `[feature].tsx` | `useRef`, `useMemo`, router | No — passes to install |
329
+ | `install.tsx` | No | **Instantiates** |
330
+ | `layout.tsx` | No | No |
331
+ | `*_controller.ts` | No (class) | — |
332
+ | `*_presenter.ts` | No (class) | — |
333
+ | `create.tsx` | Yes, inside observer | Receives via closure |
334
+ | Display | `useFormContext` only | No |
@@ -0,0 +1,6 @@
1
+ CONVEX_DEPLOYMENT=
2
+ CLERK_SECRET_KEY=
3
+ VITE_CONVEX_URL=
4
+ VITE_CONVEX_SITE_URL=
5
+ VITE_CLERK_PUBLISHABLE_KEY=
6
+
@@ -50,12 +50,16 @@ type RouteFiles = {
50
50
  id: "routes/(dashboard)/index";
51
51
  page: "/";
52
52
  };
53
- "./routes/(dashboard)/todos/index.tsx": {
54
- id: "routes/(dashboard)/todos/index";
53
+ "./routes/(dashboard)/(todos)/layout.tsx": {
54
+ id: "routes/(dashboard)/(todos)/layout";
55
+ page: "/todos" | "/todos/:id";
56
+ };
57
+ "./routes/(dashboard)/(todos)/index.tsx": {
58
+ id: "routes/(dashboard)/(todos)/index";
55
59
  page: "/todos";
56
60
  };
57
- "./routes/(dashboard)/todos/[id].tsx": {
58
- id: "routes/(dashboard)/todos/[id]";
61
+ "./routes/(dashboard)/(todos)/[id].tsx": {
62
+ id: "routes/(dashboard)/(todos)/[id]";
59
63
  page: "/todos/:id";
60
64
  };
61
65
  };
@@ -66,6 +70,7 @@ type RouteModules = {
66
70
  "routes/(auth)/sign-in/index": typeof import("./src/./routes/(auth)/sign-in/index.tsx");
67
71
  "routes/(dashboard)/layout": typeof import("./src/./routes/(dashboard)/layout.tsx");
68
72
  "routes/(dashboard)/index": typeof import("./src/./routes/(dashboard)/index.tsx");
69
- "routes/(dashboard)/todos/index": typeof import("./src/./routes/(dashboard)/todos/index.tsx");
70
- "routes/(dashboard)/todos/[id]": typeof import("./src/./routes/(dashboard)/todos/[id].tsx");
73
+ "routes/(dashboard)/(todos)/layout": typeof import("./src/./routes/(dashboard)/(todos)/layout.tsx");
74
+ "routes/(dashboard)/(todos)/index": typeof import("./src/./routes/(dashboard)/(todos)/index.tsx");
75
+ "routes/(dashboard)/(todos)/[id]": typeof import("./src/./routes/(dashboard)/(todos)/[id].tsx");
71
76
  };
@@ -5,7 +5,7 @@ import type { GetInfo, GetAnnotations } from "react-router/internal";
5
5
  type Module = typeof import("../[id].js")
6
6
 
7
7
  type Info = GetInfo<{
8
- file: "./routes/(dashboard)/todos/[id].tsx",
8
+ file: "./routes/(dashboard)/(todos)/[id].tsx",
9
9
  module: Module
10
10
  }>
11
11
 
@@ -16,7 +16,10 @@ type Matches = [{
16
16
  id: "routes/(dashboard)/layout";
17
17
  module: typeof import("../../layout.js");
18
18
  }, {
19
- id: "routes/(dashboard)/todos/[id]";
19
+ id: "routes/(dashboard)/(todos)/layout";
20
+ module: typeof import("../layout.js");
21
+ }, {
22
+ id: "routes/(dashboard)/(todos)/[id]";
20
23
  module: typeof import("../[id].js");
21
24
  }];
22
25
 
@@ -5,7 +5,7 @@ import type { GetInfo, GetAnnotations } from "react-router/internal";
5
5
  type Module = typeof import("../index.js")
6
6
 
7
7
  type Info = GetInfo<{
8
- file: "./routes/(dashboard)/todos/index.tsx",
8
+ file: "./routes/(dashboard)/(todos)/index.tsx",
9
9
  module: Module
10
10
  }>
11
11
 
@@ -16,7 +16,10 @@ type Matches = [{
16
16
  id: "routes/(dashboard)/layout";
17
17
  module: typeof import("../../layout.js");
18
18
  }, {
19
- id: "routes/(dashboard)/todos/index";
19
+ id: "routes/(dashboard)/(todos)/layout";
20
+ module: typeof import("../layout.js");
21
+ }, {
22
+ id: "routes/(dashboard)/(todos)/index";
20
23
  module: typeof import("../index.js");
21
24
  }];
22
25
 
@@ -0,0 +1,65 @@
1
+ // Generated by React Router
2
+
3
+ import type { GetInfo, GetAnnotations } from "react-router/internal";
4
+
5
+ type Module = typeof import("../layout.js")
6
+
7
+ type Info = GetInfo<{
8
+ file: "./routes/(dashboard)/(todos)/layout.tsx",
9
+ module: Module
10
+ }>
11
+
12
+ type Matches = [{
13
+ id: "root";
14
+ module: typeof import("../../../../root.js");
15
+ }, {
16
+ id: "routes/(dashboard)/layout";
17
+ module: typeof import("../../layout.js");
18
+ }, {
19
+ id: "routes/(dashboard)/(todos)/layout";
20
+ module: typeof import("../layout.js");
21
+ }];
22
+
23
+ type Annotations = GetAnnotations<Info & { module: Module, matches: Matches }, false>;
24
+
25
+ export namespace Route {
26
+ // links
27
+ export type LinkDescriptors = Annotations["LinkDescriptors"];
28
+ export type LinksFunction = Annotations["LinksFunction"];
29
+
30
+ // meta
31
+ export type MetaArgs = Annotations["MetaArgs"];
32
+ export type MetaDescriptors = Annotations["MetaDescriptors"];
33
+ export type MetaFunction = Annotations["MetaFunction"];
34
+
35
+ // headers
36
+ export type HeadersArgs = Annotations["HeadersArgs"];
37
+ export type HeadersFunction = Annotations["HeadersFunction"];
38
+
39
+ // middleware
40
+ export type MiddlewareFunction = Annotations["MiddlewareFunction"];
41
+
42
+ // clientMiddleware
43
+ export type ClientMiddlewareFunction = Annotations["ClientMiddlewareFunction"];
44
+
45
+ // loader
46
+ export type LoaderArgs = Annotations["LoaderArgs"];
47
+
48
+ // clientLoader
49
+ export type ClientLoaderArgs = Annotations["ClientLoaderArgs"];
50
+
51
+ // action
52
+ export type ActionArgs = Annotations["ActionArgs"];
53
+
54
+ // clientAction
55
+ export type ClientActionArgs = Annotations["ClientActionArgs"];
56
+
57
+ // HydrateFallback
58
+ export type HydrateFallbackProps = Annotations["HydrateFallbackProps"];
59
+
60
+ // Component
61
+ export type ComponentProps = Annotations["ComponentProps"];
62
+
63
+ // ErrorBoundary
64
+ export type ErrorBoundaryProps = Annotations["ErrorBoundaryProps"];
65
+ }
@@ -1,21 +1,28 @@
1
1
  {
2
- "name": "react-router",
2
+ "name": "web",
3
3
  "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
- "sourceRoot": "apps/react-router",
4
+ "sourceRoot": "apps/web",
5
5
  "projectType": "application",
6
6
  "tags": [],
7
7
  "targets": {
8
+ "typecheck": {
9
+ "executor": "nx:run-commands",
10
+ "options": {
11
+ "cwd": "apps/web",
12
+ "command": "bunx react-router typecheck"
13
+ }
14
+ },
8
15
  "dev": {
9
16
  "executor": "nx:run-commands",
10
17
  "options": {
11
- "cwd": "apps/react-router",
18
+ "cwd": "apps/web",
12
19
  "command": "bunx react-router dev --port 3000"
13
20
  }
14
21
  },
15
22
  "build": {
16
23
  "executor": "nx:run-commands",
17
24
  "options": {
18
- "cwd": "apps/react-router",
25
+ "cwd": "apps/web",
19
26
  "command": "bunx react-router build"
20
27
  }
21
28
  }
@@ -2,7 +2,7 @@ import type { Config } from "@react-router/dev/config";
2
2
 
3
3
  export default {
4
4
  appDirectory: "src",
5
- ssr: false,
5
+ ssr: true,
6
6
  future: {
7
7
  v8_middleware: true,
8
8
  },
@@ -0,0 +1,11 @@
1
+ import { ConvexQueryClient } from "@convex-dev/react-query";
2
+ import { ConvexReactClient } from "convex/react";
3
+
4
+ const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string, {
5
+ expectAuth: true,
6
+ logger: true,
7
+ });
8
+
9
+ const convexQueryClient = new ConvexQueryClient(convex);
10
+
11
+ export { convex, convexQueryClient };
@@ -0,0 +1,17 @@
1
+ import { QueryClient } from "@tanstack/react-query";
2
+
3
+ import { convexQueryClient } from "@/web/libs/convex_query_client";
4
+
5
+ const reactQueryClient = new QueryClient({
6
+ defaultOptions: {
7
+ queries: {
8
+ queryFn: convexQueryClient.queryFn(),
9
+ gcTime: 0,
10
+ staleTime: 0,
11
+ },
12
+ },
13
+ });
14
+
15
+ convexQueryClient.connect(reactQueryClient);
16
+
17
+ export { reactQueryClient };