create-fate 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/bin/create-fate.mjs +412 -0
  4. package/package.json +26 -3
  5. package/templates/fate/README.md +8 -0
  6. package/templates/fate/drizzle/.github/workflows/push.yml +27 -0
  7. package/templates/fate/drizzle/.oxfmtrc.jsonc +23 -0
  8. package/templates/fate/drizzle/.vite-hooks/pre-commit +1 -0
  9. package/templates/fate/drizzle/.vscode/extensions.json +12 -0
  10. package/templates/fate/drizzle/.vscode/settings.json +25 -0
  11. package/templates/fate/drizzle/AGENTS.md +55 -0
  12. package/templates/fate/drizzle/README.md +112 -0
  13. package/templates/fate/drizzle/_gitignore +20 -0
  14. package/templates/fate/drizzle/client/package.json +58 -0
  15. package/templates/fate/drizzle/client/pages/index.tsx +96 -0
  16. package/templates/fate/drizzle/client/pages/layout.tsx +104 -0
  17. package/templates/fate/drizzle/client/pages/login.tsx +107 -0
  18. package/templates/fate/drizzle/client/pages/post/[id].tsx +22 -0
  19. package/templates/fate/drizzle/client/pages/search.tsx +95 -0
  20. package/templates/fate/drizzle/client/public/icon.svg +1 -0
  21. package/templates/fate/drizzle/client/src/App.css +153 -0
  22. package/templates/fate/drizzle/client/src/lib/AvailableLanguages.tsx +5 -0
  23. package/templates/fate/drizzle/client/src/lib/cx.tsx +6 -0
  24. package/templates/fate/drizzle/client/src/lib/env.tsx +5 -0
  25. package/templates/fate/drizzle/client/src/ui/Badge.tsx +31 -0
  26. package/templates/fate/drizzle/client/src/ui/Button.tsx +108 -0
  27. package/templates/fate/drizzle/client/src/ui/Card.tsx +32 -0
  28. package/templates/fate/drizzle/client/src/ui/CommentCard.tsx +75 -0
  29. package/templates/fate/drizzle/client/src/ui/Error.tsx +19 -0
  30. package/templates/fate/drizzle/client/src/ui/H2.tsx +15 -0
  31. package/templates/fate/drizzle/client/src/ui/H3.tsx +20 -0
  32. package/templates/fate/drizzle/client/src/ui/Header.tsx +96 -0
  33. package/templates/fate/drizzle/client/src/ui/Input.tsx +17 -0
  34. package/templates/fate/drizzle/client/src/ui/Link.tsx +11 -0
  35. package/templates/fate/drizzle/client/src/ui/LocaleSwitcher.tsx +52 -0
  36. package/templates/fate/drizzle/client/src/ui/PostCard.tsx +284 -0
  37. package/templates/fate/drizzle/client/src/ui/Section.tsx +23 -0
  38. package/templates/fate/drizzle/client/src/ui/UserCard.tsx +131 -0
  39. package/templates/fate/drizzle/client/src/user/AuthClient.tsx +6 -0
  40. package/templates/fate/drizzle/client/translations/de_DE.json +689 -0
  41. package/templates/fate/drizzle/client/translations/ja_JP.json +689 -0
  42. package/templates/fate/drizzle/client/vite.config.ts +39 -0
  43. package/templates/fate/drizzle/client/void.json +29 -0
  44. package/templates/fate/drizzle/docker-compose.yml +11 -0
  45. package/templates/fate/drizzle/package.json +46 -0
  46. package/templates/fate/drizzle/pnpm-workspace.yaml +36 -0
  47. package/templates/fate/drizzle/public/fate-logo-dark.svg +4 -0
  48. package/templates/fate/drizzle/public/fate-logo.svg +4 -0
  49. package/templates/fate/drizzle/server/.env.example +5 -0
  50. package/templates/fate/drizzle/server/drizzle.config.ts +11 -0
  51. package/templates/fate/drizzle/server/package.json +48 -0
  52. package/templates/fate/drizzle/server/src/__tests__/index.test.tsx +5 -0
  53. package/templates/fate/drizzle/server/src/app.tsx +44 -0
  54. package/templates/fate/drizzle/server/src/drizzle/db.ts +25 -0
  55. package/templates/fate/drizzle/server/src/drizzle/queries.ts +89 -0
  56. package/templates/fate/drizzle/server/src/drizzle/schema.ts +174 -0
  57. package/templates/fate/drizzle/server/src/drizzle/seed.tsx +77 -0
  58. package/templates/fate/drizzle/server/src/drizzle/seedData.ts +205 -0
  59. package/templates/fate/drizzle/server/src/index.tsx +24 -0
  60. package/templates/fate/drizzle/server/src/lib/auth.ts +33 -0
  61. package/templates/fate/drizzle/server/src/lib/env.ts +3 -0
  62. package/templates/fate/drizzle/server/src/lib/origins.ts +18 -0
  63. package/templates/fate/drizzle/server/src/router.ts +15 -0
  64. package/templates/fate/drizzle/server/src/trpc/connection.ts +4 -0
  65. package/templates/fate/drizzle/server/src/trpc/context.ts +22 -0
  66. package/templates/fate/drizzle/server/src/trpc/init.ts +51 -0
  67. package/templates/fate/drizzle/server/src/trpc/routers/comment.ts +146 -0
  68. package/templates/fate/drizzle/server/src/trpc/routers/post.ts +99 -0
  69. package/templates/fate/drizzle/server/src/trpc/routers/user.ts +68 -0
  70. package/templates/fate/drizzle/server/src/trpc/views.ts +75 -0
  71. package/templates/fate/drizzle/server/src/user/SessionUser.tsx +17 -0
  72. package/templates/fate/drizzle/server/vite.config.ts +8 -0
  73. package/templates/fate/drizzle/tsconfig.json +33 -0
  74. package/templates/fate/drizzle/vite.config.ts +71 -0
  75. package/templates/fate/http/.github/workflows/push.yml +27 -0
  76. package/templates/fate/http/.oxfmtrc.jsonc +23 -0
  77. package/templates/fate/http/.vite-hooks/pre-commit +1 -0
  78. package/templates/fate/http/.vscode/extensions.json +12 -0
  79. package/templates/fate/http/.vscode/settings.json +25 -0
  80. package/templates/fate/http/AGENTS.md +55 -0
  81. package/templates/fate/http/README.md +112 -0
  82. package/templates/fate/http/_gitignore +20 -0
  83. package/templates/fate/http/client/package.json +56 -0
  84. package/templates/fate/http/client/pages/index.tsx +96 -0
  85. package/templates/fate/http/client/pages/layout.tsx +89 -0
  86. package/templates/fate/http/client/pages/login.tsx +107 -0
  87. package/templates/fate/http/client/pages/post/[id].tsx +22 -0
  88. package/templates/fate/http/client/pages/search.tsx +95 -0
  89. package/templates/fate/http/client/public/icon.svg +1 -0
  90. package/templates/fate/http/client/src/App.css +153 -0
  91. package/templates/fate/http/client/src/lib/AvailableLanguages.tsx +5 -0
  92. package/templates/fate/http/client/src/lib/cx.tsx +6 -0
  93. package/templates/fate/http/client/src/lib/env.tsx +5 -0
  94. package/templates/fate/http/client/src/ui/Badge.tsx +31 -0
  95. package/templates/fate/http/client/src/ui/Button.tsx +108 -0
  96. package/templates/fate/http/client/src/ui/Card.tsx +32 -0
  97. package/templates/fate/http/client/src/ui/CommentCard.tsx +75 -0
  98. package/templates/fate/http/client/src/ui/Error.tsx +19 -0
  99. package/templates/fate/http/client/src/ui/H2.tsx +15 -0
  100. package/templates/fate/http/client/src/ui/H3.tsx +20 -0
  101. package/templates/fate/http/client/src/ui/Header.tsx +96 -0
  102. package/templates/fate/http/client/src/ui/Input.tsx +17 -0
  103. package/templates/fate/http/client/src/ui/Link.tsx +11 -0
  104. package/templates/fate/http/client/src/ui/LocaleSwitcher.tsx +52 -0
  105. package/templates/fate/http/client/src/ui/PostCard.tsx +281 -0
  106. package/templates/fate/http/client/src/ui/Section.tsx +23 -0
  107. package/templates/fate/http/client/src/ui/UserCard.tsx +131 -0
  108. package/templates/fate/http/client/src/user/AuthClient.tsx +6 -0
  109. package/templates/fate/http/client/translations/de_DE.json +689 -0
  110. package/templates/fate/http/client/translations/ja_JP.json +689 -0
  111. package/templates/fate/http/client/vite.config.ts +40 -0
  112. package/templates/fate/http/client/void.json +29 -0
  113. package/templates/fate/http/docker-compose.yml +11 -0
  114. package/templates/fate/http/package.json +46 -0
  115. package/templates/fate/http/pnpm-workspace.yaml +36 -0
  116. package/templates/fate/http/public/fate-logo-dark.svg +4 -0
  117. package/templates/fate/http/public/fate-logo.svg +4 -0
  118. package/templates/fate/http/server/.env.example +5 -0
  119. package/templates/fate/http/server/drizzle.config.ts +11 -0
  120. package/templates/fate/http/server/package.json +46 -0
  121. package/templates/fate/http/server/src/__tests__/index.test.tsx +5 -0
  122. package/templates/fate/http/server/src/app.tsx +36 -0
  123. package/templates/fate/http/server/src/drizzle/db.ts +25 -0
  124. package/templates/fate/http/server/src/drizzle/queries.ts +89 -0
  125. package/templates/fate/http/server/src/drizzle/schema.ts +174 -0
  126. package/templates/fate/http/server/src/drizzle/seed.tsx +77 -0
  127. package/templates/fate/http/server/src/drizzle/seedData.ts +205 -0
  128. package/templates/fate/http/server/src/fate/context.ts +20 -0
  129. package/templates/fate/http/server/src/fate/server.ts +285 -0
  130. package/templates/fate/http/server/src/fate/views.ts +75 -0
  131. package/templates/fate/http/server/src/index.tsx +24 -0
  132. package/templates/fate/http/server/src/lib/auth.ts +33 -0
  133. package/templates/fate/http/server/src/lib/env.ts +3 -0
  134. package/templates/fate/http/server/src/lib/origins.ts +18 -0
  135. package/templates/fate/http/server/src/router.ts +2 -0
  136. package/templates/fate/http/server/src/user/SessionUser.tsx +17 -0
  137. package/templates/fate/http/server/vite.config.ts +8 -0
  138. package/templates/fate/http/tsconfig.json +33 -0
  139. package/templates/fate/http/vite.config.ts +71 -0
  140. package/templates/fate/prisma/.github/workflows/push.yml +27 -0
  141. package/templates/fate/prisma/.oxfmtrc.jsonc +23 -0
  142. package/templates/fate/prisma/.vite-hooks/pre-commit +1 -0
  143. package/templates/fate/prisma/.vscode/extensions.json +12 -0
  144. package/templates/fate/prisma/.vscode/settings.json +25 -0
  145. package/templates/fate/prisma/AGENTS.md +55 -0
  146. package/templates/fate/prisma/README.md +126 -0
  147. package/templates/fate/prisma/_gitignore +20 -0
  148. package/templates/fate/prisma/client/package.json +58 -0
  149. package/templates/fate/prisma/client/pages/index.tsx +96 -0
  150. package/templates/fate/prisma/client/pages/layout.tsx +94 -0
  151. package/templates/fate/prisma/client/pages/login.tsx +107 -0
  152. package/templates/fate/prisma/client/pages/post/[id].tsx +22 -0
  153. package/templates/fate/prisma/client/pages/search.tsx +95 -0
  154. package/templates/fate/prisma/client/public/icon.svg +1 -0
  155. package/templates/fate/prisma/client/src/App.css +153 -0
  156. package/templates/fate/prisma/client/src/lib/AvailableLanguages.tsx +5 -0
  157. package/templates/fate/prisma/client/src/lib/cx.tsx +6 -0
  158. package/templates/fate/prisma/client/src/lib/env.tsx +5 -0
  159. package/templates/fate/prisma/client/src/ui/Badge.tsx +31 -0
  160. package/templates/fate/prisma/client/src/ui/Button.tsx +108 -0
  161. package/templates/fate/prisma/client/src/ui/Card.tsx +32 -0
  162. package/templates/fate/prisma/client/src/ui/CommentCard.tsx +75 -0
  163. package/templates/fate/prisma/client/src/ui/Error.tsx +19 -0
  164. package/templates/fate/prisma/client/src/ui/H2.tsx +15 -0
  165. package/templates/fate/prisma/client/src/ui/H3.tsx +20 -0
  166. package/templates/fate/prisma/client/src/ui/Header.tsx +96 -0
  167. package/templates/fate/prisma/client/src/ui/Input.tsx +17 -0
  168. package/templates/fate/prisma/client/src/ui/Link.tsx +11 -0
  169. package/templates/fate/prisma/client/src/ui/LocaleSwitcher.tsx +52 -0
  170. package/templates/fate/prisma/client/src/ui/PostCard.tsx +281 -0
  171. package/templates/fate/prisma/client/src/ui/Section.tsx +23 -0
  172. package/templates/fate/prisma/client/src/ui/UserCard.tsx +131 -0
  173. package/templates/fate/prisma/client/src/user/AuthClient.tsx +6 -0
  174. package/templates/fate/prisma/client/translations/de_DE.json +689 -0
  175. package/templates/fate/prisma/client/translations/ja_JP.json +689 -0
  176. package/templates/fate/prisma/client/vite.config.ts +39 -0
  177. package/templates/fate/prisma/client/void.json +29 -0
  178. package/templates/fate/prisma/docker-compose.yml +11 -0
  179. package/templates/fate/prisma/package.json +47 -0
  180. package/templates/fate/prisma/pnpm-workspace.yaml +39 -0
  181. package/templates/fate/prisma/public/fate-logo-dark.svg +4 -0
  182. package/templates/fate/prisma/public/fate-logo.svg +4 -0
  183. package/templates/fate/prisma/server/.env.example +5 -0
  184. package/templates/fate/prisma/server/package.json +42 -0
  185. package/templates/fate/prisma/server/prisma.config.ts +21 -0
  186. package/templates/fate/prisma/server/src/__tests__/index.test.tsx +5 -0
  187. package/templates/fate/prisma/server/src/app.tsx +40 -0
  188. package/templates/fate/prisma/server/src/index.tsx +24 -0
  189. package/templates/fate/prisma/server/src/lib/auth.ts +31 -0
  190. package/templates/fate/prisma/server/src/lib/env.ts +3 -0
  191. package/templates/fate/prisma/server/src/lib/origins.ts +18 -0
  192. package/templates/fate/prisma/server/src/prisma/prisma.ts +9 -0
  193. package/templates/fate/prisma/server/src/prisma/schema.prisma +98 -0
  194. package/templates/fate/prisma/server/src/prisma/seed.tsx +71 -0
  195. package/templates/fate/prisma/server/src/prisma/seedData.ts +205 -0
  196. package/templates/fate/prisma/server/src/router.ts +14 -0
  197. package/templates/fate/prisma/server/src/trpc/connection.ts +4 -0
  198. package/templates/fate/prisma/server/src/trpc/context.ts +22 -0
  199. package/templates/fate/prisma/server/src/trpc/init.ts +8 -0
  200. package/templates/fate/prisma/server/src/trpc/routers/comment.ts +176 -0
  201. package/templates/fate/prisma/server/src/trpc/routers/post.ts +154 -0
  202. package/templates/fate/prisma/server/src/trpc/routers/user.ts +71 -0
  203. package/templates/fate/prisma/server/src/trpc/views.ts +77 -0
  204. package/templates/fate/prisma/server/src/user/SessionUser.tsx +17 -0
  205. package/templates/fate/prisma/server/vite.config.ts +8 -0
  206. package/templates/fate/prisma/tsconfig.json +33 -0
  207. package/templates/fate/prisma/vite.config.ts +72 -0
  208. package/templates/fate/void/.oxfmtrc.jsonc +14 -0
  209. package/templates/fate/void/README.md +55 -0
  210. package/templates/fate/void/auth.ts +22 -0
  211. package/templates/fate/void/db/migrations/20260508073357_initial_schema.sql +133 -0
  212. package/templates/fate/void/db/migrations/20260508120500_seed_void_demo.sql +170 -0
  213. package/templates/fate/void/db/migrations/meta/20260508073357_snapshot.json +868 -0
  214. package/templates/fate/void/db/migrations/meta/_journal.json +20 -0
  215. package/templates/fate/void/db/queries.ts +211 -0
  216. package/templates/fate/void/db/schema.ts +325 -0
  217. package/templates/fate/void/db/seed-auth.ts +31 -0
  218. package/templates/fate/void/db/seed.ts +163 -0
  219. package/templates/fate/void/middleware/01.shared.ts +29 -0
  220. package/templates/fate/void/package.json +65 -0
  221. package/templates/fate/void/pages/category/[id].tsx +23 -0
  222. package/templates/fate/void/pages/index.tsx +153 -0
  223. package/templates/fate/void/pages/layout.tsx +46 -0
  224. package/templates/fate/void/pages/login.tsx +103 -0
  225. package/templates/fate/void/pages/post/[id].tsx +20 -0
  226. package/templates/fate/void/pages/search.tsx +81 -0
  227. package/templates/fate/void/pnpm-workspace.yaml +31 -0
  228. package/templates/fate/void/public/.assetsignore +1 -0
  229. package/templates/fate/void/public/icon.svg +1 -0
  230. package/templates/fate/void/routes/fate-live.ts +8 -0
  231. package/templates/fate/void/routes/fate.ts +8 -0
  232. package/templates/fate/void/seedData.ts +535 -0
  233. package/templates/fate/void/src/App.css +130 -0
  234. package/templates/fate/void/src/fate/__tests__/commentPermissions.test.ts +31 -0
  235. package/templates/fate/void/src/fate/commentPermissions.ts +17 -0
  236. package/templates/fate/void/src/fate/context.ts +15 -0
  237. package/templates/fate/void/src/fate/live.ts +6 -0
  238. package/templates/fate/void/src/fate/server.ts +401 -0
  239. package/templates/fate/void/src/fate/views.ts +164 -0
  240. package/templates/fate/void/src/lib/cx.tsx +6 -0
  241. package/templates/fate/void/src/lib/formatLabel.tsx +6 -0
  242. package/templates/fate/void/src/lib/shared.ts +10 -0
  243. package/templates/fate/void/src/ui/Badge.tsx +31 -0
  244. package/templates/fate/void/src/ui/Button.tsx +92 -0
  245. package/templates/fate/void/src/ui/Card.tsx +29 -0
  246. package/templates/fate/void/src/ui/CategoryCard.tsx +105 -0
  247. package/templates/fate/void/src/ui/CommentCard.tsx +80 -0
  248. package/templates/fate/void/src/ui/CreatePost.tsx +111 -0
  249. package/templates/fate/void/src/ui/Error.tsx +19 -0
  250. package/templates/fate/void/src/ui/EventCard.tsx +152 -0
  251. package/templates/fate/void/src/ui/H2.tsx +15 -0
  252. package/templates/fate/void/src/ui/H3.tsx +20 -0
  253. package/templates/fate/void/src/ui/Header.tsx +96 -0
  254. package/templates/fate/void/src/ui/Input.tsx +33 -0
  255. package/templates/fate/void/src/ui/Link.tsx +11 -0
  256. package/templates/fate/void/src/ui/Number.tsx +5 -0
  257. package/templates/fate/void/src/ui/PostCard.tsx +294 -0
  258. package/templates/fate/void/src/ui/Section.tsx +23 -0
  259. package/templates/fate/void/src/ui/TagBadge.tsx +23 -0
  260. package/templates/fate/void/src/ui/UserCard.tsx +129 -0
  261. package/templates/fate/void/src/user/AuthClient.tsx +1 -0
  262. package/templates/fate/void/src/user/SessionUser.tsx +17 -0
  263. package/templates/fate/void/tsconfig.json +34 -0
  264. package/templates/fate/void/vite.config.ts +83 -0
  265. package/templates/fate/void/void.json +32 -0
  266. package/templates/fate/void/wrangler.jsonc +16 -0
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env NODE_ENV=development node --no-warnings --experimental-specifier-resolution=node --loader ts-node/esm --env-file .env
2
+ import { styleText } from 'node:util';
3
+ import randomEntry from '@nkzw/core/randomEntry.js';
4
+ import { auth } from '../lib/auth.ts';
5
+ import db, { closeDatabase } from './db.ts';
6
+ import { comment, post } from './schema.ts';
7
+ import { comments, posts, users } from './seedData.ts';
8
+
9
+ console.log(styleText('bold', '› Seeding database...'));
10
+
11
+ try {
12
+ console.log(styleText('bold', `Creating users`));
13
+
14
+ for (const data of users) {
15
+ const { user } = await auth.api.createUser({
16
+ body: data,
17
+ });
18
+
19
+ console.log(` Created user ${styleText('blue', user.name)}.`);
20
+ }
21
+
22
+ const seededUsers = await db.query.user.findMany();
23
+ const usersByEmail = new Map(seededUsers.map((user) => [user.email, user]));
24
+
25
+ console.log(styleText('bold', `Seeding posts and comments`));
26
+
27
+ const createdPosts = await Promise.all(
28
+ posts.map(async (postData) => {
29
+ const author = usersByEmail.get(postData.authorEmail);
30
+
31
+ if (!author) {
32
+ throw new Error(`Missing seeded user for ${postData.authorEmail}.`);
33
+ }
34
+
35
+ const [created] = await db
36
+ .insert(post)
37
+ .values({
38
+ authorId: author.id,
39
+ content: postData.content,
40
+ likes: postData.likes,
41
+ title: postData.title,
42
+ })
43
+ .returning();
44
+
45
+ if (!created) {
46
+ throw new Error(`Failed to create post '${postData.title}'.`);
47
+ }
48
+
49
+ return created;
50
+ }),
51
+ );
52
+
53
+ let index = 0;
54
+ for (const content of comments) {
55
+ const post = createdPosts[index % createdPosts.length];
56
+ const author = randomEntry(seededUsers);
57
+
58
+ await db.insert(comment).values({
59
+ authorId: author?.id,
60
+ content,
61
+ postId: post.id,
62
+ });
63
+
64
+ index++;
65
+ }
66
+
67
+ console.log(
68
+ styleText(
69
+ ['green', 'bold'],
70
+ `✓ Created ${createdPosts.length} posts and ${comments.length} comments.`,
71
+ ),
72
+ );
73
+
74
+ console.log(styleText(['green', 'bold'], '✓ Done.'));
75
+ } finally {
76
+ await closeDatabase();
77
+ }
@@ -0,0 +1,205 @@
1
+ export const users = [
2
+ {
3
+ data: {
4
+ username: 'alex',
5
+ },
6
+ email: 'alex@example.com',
7
+ name: 'Alex',
8
+ password: 'password-alex',
9
+ role: 'admin',
10
+ },
11
+ {
12
+ data: {
13
+ username: 'ari',
14
+ },
15
+ email: 'ari@nakazawa.dev',
16
+ name: 'Ari',
17
+ password: 'password-ari',
18
+ },
19
+ {
20
+ data: {
21
+ username: 'dina',
22
+ },
23
+ email: 'dina@nakazawa.dev',
24
+ name: 'Dina',
25
+ password: 'password-dina',
26
+ },
27
+ {
28
+ data: {
29
+ username: 'hana',
30
+ },
31
+ email: 'hana@nakazawa.dev',
32
+ name: 'Hana',
33
+ password: 'password-hana',
34
+ },
35
+ {
36
+ data: {
37
+ username: 'jamal',
38
+ },
39
+ email: 'jamal@nakazawa.dev',
40
+ name: 'Jamal',
41
+ password: 'password-jamal',
42
+ },
43
+ {
44
+ data: {
45
+ username: 'kai',
46
+ },
47
+ email: 'kai@nakazawa.dev',
48
+ name: 'Kai',
49
+ password: 'password-kai',
50
+ },
51
+ {
52
+ data: {
53
+ username: 'lena',
54
+ },
55
+ email: 'lena@nakazawa.dev',
56
+ name: 'Lena',
57
+ password: 'password-lena',
58
+ },
59
+ {
60
+ data: {
61
+ username: 'mika',
62
+ },
63
+ email: 'mika@nakazawa.dev',
64
+ name: 'Mika',
65
+ password: 'password-mika',
66
+ },
67
+ ] as const;
68
+
69
+ export const posts = [
70
+ {
71
+ authorEmail: 'alex@example.com',
72
+ content:
73
+ 'fate keeps component data requirements co-located, then composes them into one request at the screen root. Instead of deciding when every component should fetch, you describe what each component needs. The result feels close to Relay fragments, but it stays in plain TypeScript with no GraphQL schema or query language to adopt.',
74
+ likes: 128,
75
+ title: 'Thinking in views instead of requests',
76
+ },
77
+ {
78
+ authorEmail: 'ari@nakazawa.dev',
79
+ content:
80
+ 'ViewRefs are intentionally small: a typename, an id, and enough metadata for fate to resolve masked data against a view. Passing refs through the tree keeps component props light and lets each component declare the fields it reads. A PostCard can select title and author while a detail route adds comments and counts without creating another hand-written data shape.',
81
+ likes: 93,
82
+ title: 'ViewRefs as the boundary between components',
83
+ },
84
+ {
85
+ authorEmail: 'dina@nakazawa.dev',
86
+ content:
87
+ 'Strict selection is the feature that makes a demo like this stay honest. If a component did not select content, content is not part of its usable data. That protects teams from accidental coupling, makes refactors smaller, and gives code generation tools a predictable surface.',
88
+ likes: 87,
89
+ title: 'Data masking as a team contract',
90
+ },
91
+ {
92
+ authorEmail: 'hana@nakazawa.dev',
93
+ content:
94
+ 'fate stores records by __typename and id, then points roots and lists at those records. That is why liking a post can update every view that selected likes without touching views that only selected title. The cache may know more about an object, but each component only sees the fields its view requested.',
95
+ likes: 111,
96
+ title: 'A normalized cache without manual key math',
97
+ },
98
+ {
99
+ authorEmail: 'jamal@nakazawa.dev',
100
+ content:
101
+ 'The React integration leans on Suspense instead of local loading flags. A root useRequest call can suspend while the composed selection is fetched, and errors bubble to normal error boundaries. The components underneath can stay focused on rendering masked data.',
102
+ likes: 76,
103
+ title: 'Suspense-first screens with useRequest',
104
+ },
105
+ {
106
+ authorEmail: 'kai@nakazawa.dev',
107
+ content:
108
+ 'Actions in fate are exposed for useActionState, and mutations can carry an optimistic object alongside the input. The cache applies the optimistic update immediately, re-renders affected views, and rolls the affected records back if the server rejects the mutation.',
109
+ likes: 82,
110
+ title: 'Optimistic actions that roll back cleanly',
111
+ },
112
+ {
113
+ authorEmail: 'lena@nakazawa.dev',
114
+ content:
115
+ 'The comments under each post use connection-style lists so the UI can load more without inventing a pagination protocol per screen. fate tracks cursor arguments as part of the list state, merges pages into the same connection, and keeps the records normalized.',
116
+ likes: 69,
117
+ title: 'Connection lists for comments and feeds',
118
+ },
119
+ {
120
+ authorEmail: 'mika@nakazawa.dev',
121
+ content:
122
+ 'useLiveView has the same shape as useView, but it subscribes to updates for the selected object through the native live transport. The client keeps one SSE stream open, sends subscribe and unsubscribe control messages, and merges returned records into the same normalized cache.',
123
+ likes: 104,
124
+ title: 'Live views over a single SSE stream',
125
+ },
126
+ {
127
+ authorEmail: 'alex@example.com',
128
+ content:
129
+ 'The launch post introduced fate with tRPC, but the repo now includes a native HTTP transport too. The generated client can point at a fate endpoint, send requests through the protocol package, and use GET and POST routes for live updates.',
130
+ likes: 88,
131
+ title: 'Native HTTP transport after the alpha launch',
132
+ },
133
+ {
134
+ authorEmail: 'ari@nakazawa.dev',
135
+ content:
136
+ 'The server adapters now cover Prisma and Drizzle with the same data view concepts. Sources describe how to resolve records, fields, lists, counts, and computed values, while fate builds the selected shape for the client. The React component model stays the same.',
137
+ likes: 84,
138
+ title: 'Prisma and Drizzle source adapters',
139
+ },
140
+ {
141
+ authorEmail: 'dina@nakazawa.dev',
142
+ content:
143
+ 'The code generator is now optional for CI and custom workflows because the Vite plugin handles the common path during development. The plugin generates the typed client from the server surface, keeps imports fresh, and reduces the number of manual setup steps in a new app.',
144
+ likes: 91,
145
+ title: 'The Vite plugin replaces everyday codegen',
146
+ },
147
+ {
148
+ authorEmail: 'hana@nakazawa.dev',
149
+ content:
150
+ 'The cache tracks retained requests so screens can release data when they unmount and let garbage collection clean up records that are no longer needed. For manual client.request calls, code can retain the descriptor for as long as the work needs it.',
151
+ likes: 58,
152
+ title: 'Garbage collection for request lifetimes',
153
+ },
154
+ {
155
+ authorEmail: 'jamal@nakazawa.dev',
156
+ content:
157
+ 'Stable refs were a small change with a large ergonomic payoff. When a record has not changed, fate can keep ViewRefs stable across renders, which helps memoized components and reduces avoidable updates. Rendering stays tied to the selected fields, not the full server response.',
158
+ likes: 67,
159
+ title: 'Stable refs and smaller rerenders',
160
+ },
161
+ {
162
+ authorEmail: 'kai@nakazawa.dev',
163
+ content:
164
+ 'Migrating from request-centric libraries starts with a mindset shift. Instead of passing server data down as custom prop types, define small views next to the components that render them and compose those views at the route. Mutations and live updates can then update records by object identity.',
165
+ likes: 79,
166
+ title: 'Moving away from request-centric state',
167
+ },
168
+ ] as const;
169
+
170
+ export const comments = [
171
+ 'The view-first framing is the clearest way I have seen to explain why request hooks create so much incidental state.',
172
+ 'Passing ViewRefs through components made the example easier to read than a stack of custom DTO types.',
173
+ 'Strict selection caught a missing field in my test component immediately, which is exactly the kind of failure I want.',
174
+ 'The normalized cache explanation finally connected likes, comments, and detail pages for our team.',
175
+ 'Suspense at the request root keeps the route code small, and the error boundary story feels like normal React.',
176
+ 'The useActionState integration is practical because the form code still looks like React.',
177
+ 'Connection lists are a good fit for this demo because the small home feed forces loadNext to be exercised.',
178
+ 'The single SSE connection model is reassuring; we were worried live views would create one stream per card.',
179
+ 'Native HTTP transport makes adoption easier for teams that like the fate model but are not on tRPC.',
180
+ 'Having Prisma and Drizzle examples side by side makes the server adapter story much more concrete.',
181
+ 'The Vite plugin removing the regular codegen step will help a lot in demos and fresh projects.',
182
+ 'Explicit retain and release behavior answers the question of how long normalized data should live.',
183
+ 'Stable refs should make a visible difference in large feeds with memoized cards.',
184
+ 'The migration checklist is realistic because it starts with one screen instead of a full rewrite.',
185
+ 'Search results and detail pages sharing Post records is a good way to show the cache doing real work.',
186
+ 'Optimistic likes are simple, but they make the rollback behavior easy to test in front of someone.',
187
+ 'The data masking examples should be required reading before anyone adds fields to a shared component.',
188
+ 'I like that the posts use product language instead of pretending this is a generic social network.',
189
+ 'The HTTP transport post answers the biggest question I had after reading the original announcement.',
190
+ 'Garbage collection is easy to ignore until a long-lived app starts leaking records during navigation.',
191
+ 'The stable ref change sounds small, but it explains a lot of the rerender fixes in the history.',
192
+ 'I used the strict selection post to explain why overfetching is not just a network problem.',
193
+ 'The native protocol should make it easier to build examples outside of Hono and tRPC.',
194
+ 'The Vite plugin note clarifies why the generated client is still present without making codegen feel mandatory.',
195
+ 'The migration sequence matches how we would try this inside an existing dashboard.',
196
+ 'The comments list is long enough now to exercise load-more behavior without creating fake lorem ipsum.',
197
+ 'Seeing optimistic actions and live updates use the same normalized cache is the key idea.',
198
+ 'I would link the stable refs note from the performance section of the docs.',
199
+ 'The source adapter post helped our backend team map data views to our existing models.',
200
+ 'The React integration posts make it clear that fate is not trying to replace React state.',
201
+ 'The cache lifetime post is a good reminder to retain manual requests outside React.',
202
+ 'The search route feels more convincing when it can find live views, adapters, and migration content.',
203
+ 'The examples now cover the features added after the announcement instead of stopping at the initial alpha.',
204
+ 'I like that the same records can appear in the feed, search, and detail pages without duplicate cache data.',
205
+ ] as const;
@@ -0,0 +1,24 @@
1
+ import { parseArgs, styleText } from 'node:util';
2
+ import { serve } from '@hono/node-server';
3
+ import parseInteger from '@nkzw/core/parseInteger.js';
4
+ import app from './app.tsx';
5
+
6
+ const {
7
+ values: { port: portArg },
8
+ } = parseArgs({
9
+ options: {
10
+ port: {
11
+ default: '9000',
12
+ short: 'p',
13
+ type: 'string',
14
+ },
15
+ },
16
+ });
17
+
18
+ const port = (portArg && parseInteger(portArg)) || 9000;
19
+
20
+ serve({ fetch: app.fetch, port }, () =>
21
+ console.log(
22
+ `${styleText(['green', 'bold'], ` ➜`)} Server running on port ${styleText('bold', String(port))}.\n`,
23
+ ),
24
+ );
@@ -0,0 +1,33 @@
1
+ import { betterAuth } from 'better-auth';
2
+ import { drizzleAdapter } from 'better-auth/adapters/drizzle';
3
+ import { admin, username } from 'better-auth/plugins';
4
+ import db from '../drizzle/db.ts';
5
+ import schema from '../drizzle/schema.ts';
6
+ import { trustedOrigins } from './origins.ts';
7
+
8
+ export const auth = betterAuth({
9
+ advanced: {
10
+ database: {
11
+ generateId: false,
12
+ },
13
+ },
14
+ database: drizzleAdapter(db, {
15
+ provider: 'pg',
16
+ schema,
17
+ }),
18
+ emailAndPassword: {
19
+ autoSignIn: true,
20
+ enabled: true,
21
+ maxPasswordLength: 128,
22
+ minPasswordLength: 8,
23
+ },
24
+ plugins: [admin(), username()],
25
+ session: {
26
+ cookieCache: {
27
+ enabled: true,
28
+ maxAge: 15 * 24 * 60 * 60,
29
+ },
30
+ },
31
+ telemetry: { enabled: false },
32
+ trustedOrigins,
33
+ });
@@ -0,0 +1,3 @@
1
+ import defineEnv from '@nkzw/define-env';
2
+
3
+ export default defineEnv(['BETTER_AUTH_SECRET', 'CLIENT_DOMAIN', 'DATABASE_URL']);
@@ -0,0 +1,18 @@
1
+ import env from './env.ts';
2
+
3
+ const localDevOriginPattern = /^https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?$/;
4
+
5
+ const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEV;
6
+
7
+ export const clientOrigin = env('CLIENT_DOMAIN');
8
+
9
+ export const trustedOrigins = [
10
+ clientOrigin,
11
+ ...(isDevelopment ? ['http://localhost:*', 'http://127.0.0.1:*'] : []),
12
+ ];
13
+
14
+ export const resolveCorsOrigin = (origin: string) => {
15
+ if (origin === clientOrigin || (isDevelopment && localDevOriginPattern.test(origin))) {
16
+ return origin;
17
+ }
18
+ };
@@ -0,0 +1,15 @@
1
+ import { router } from './trpc/init.ts';
2
+ import { commentRouter } from './trpc/routers/comment.ts';
3
+ import { postRouter } from './trpc/routers/post.ts';
4
+ import { userRouter } from './trpc/routers/user.ts';
5
+
6
+ export const appRouter = router({
7
+ comment: commentRouter,
8
+ post: postRouter,
9
+ user: userRouter,
10
+ });
11
+
12
+ export type AppRouter = typeof appRouter;
13
+
14
+ export { fateServer } from './trpc/init.ts';
15
+ export * from './trpc/views.ts';
@@ -0,0 +1,4 @@
1
+ import { withConnection } from '@nkzw/fate/server';
2
+ import { procedure } from './init.ts';
3
+
4
+ export const createConnectionProcedure = withConnection(procedure);
@@ -0,0 +1,22 @@
1
+ import type { Context } from 'hono';
2
+ import db from '../drizzle/db.ts';
3
+ import { auth } from '../lib/auth.ts';
4
+ import { toSessionUser } from '../user/SessionUser.tsx';
5
+
6
+ type CreateContextOptions = {
7
+ context: Context;
8
+ };
9
+
10
+ export const createContext = async (options?: CreateContextOptions) => {
11
+ const session = options
12
+ ? await auth.api.getSession({ headers: options.context.req.raw.headers })
13
+ : null;
14
+
15
+ return {
16
+ db,
17
+ headers: options ? options.context.req.raw.headers : {},
18
+ sessionUser: session?.user ? toSessionUser(session.user) : null,
19
+ };
20
+ };
21
+
22
+ export type AppContext = Awaited<ReturnType<typeof createContext>>;
@@ -0,0 +1,51 @@
1
+ import { createFateServer, createLiveEventBus } from '@nkzw/fate/server';
2
+ import { createDrizzleFate } from '@nkzw/fate/server/drizzle';
3
+ import { initTRPC } from '@trpc/server';
4
+ import type { Context } from 'hono';
5
+ import db from '../drizzle/db.ts';
6
+ import schema from '../drizzle/schema.ts';
7
+ import type { AppContext } from './context.ts';
8
+ import { Root } from './views.ts';
9
+
10
+ const t = initTRPC.context<AppContext>().create();
11
+
12
+ export const router = t.router;
13
+ export const procedure = t.procedure;
14
+ export const middleware = t.middleware;
15
+ export const live = createLiveEventBus();
16
+
17
+ export const fate = createDrizzleFate<AppContext, typeof procedure>({
18
+ db,
19
+ procedure,
20
+ schema,
21
+ views: Root,
22
+ });
23
+
24
+ export const fateServer = createFateServer<AppContext>({
25
+ context: async ({ adapterContext }) => {
26
+ const { createContext } = await import('./context.ts');
27
+ return createContext({ context: adapterContext as Context });
28
+ },
29
+ live,
30
+ queries: {
31
+ viewer: {
32
+ resolve: ({
33
+ ctx,
34
+ input,
35
+ }: {
36
+ ctx: AppContext;
37
+ input: { args?: Record<string, unknown>; select: Array<string> };
38
+ }) =>
39
+ ctx.sessionUser
40
+ ? fate.resolveById({
41
+ ctx,
42
+ id: ctx.sessionUser.id,
43
+ input,
44
+ view: Root.viewer,
45
+ })
46
+ : null,
47
+ },
48
+ },
49
+ roots: Root,
50
+ sources: fate,
51
+ });
@@ -0,0 +1,146 @@
1
+ import { connectionArgs } from '@nkzw/fate/server';
2
+ import type { DrizzleQueryExtra } from '@nkzw/fate/server/drizzle';
3
+ import { TRPCError } from '@trpc/server';
4
+ import { z } from 'zod';
5
+ import {
6
+ commentSearchWhere,
7
+ createCommentRecord,
8
+ deleteCommentRecord,
9
+ findCommentPostId,
10
+ postExists,
11
+ } from '../../drizzle/queries.ts';
12
+ import { fate, live, procedure, router } from '../init.ts';
13
+ import type { CommentItem } from '../views.ts';
14
+ import { commentDataView, postSummaryDataView } from '../views.ts';
15
+
16
+ export const commentRouter = router({
17
+ ...fate.procedures({
18
+ list: false,
19
+ view: commentDataView,
20
+ }),
21
+ add: procedure
22
+ .input(
23
+ z.object({
24
+ args: connectionArgs,
25
+ content: z.string().min(1, 'Content is required'),
26
+ postId: z.string().min(1, 'Post id is required'),
27
+ select: z.array(z.string()),
28
+ }),
29
+ )
30
+ .mutation(async ({ ctx, input }) => {
31
+ if (!ctx.sessionUser) {
32
+ throw new TRPCError({
33
+ code: 'UNAUTHORIZED',
34
+ message: 'You must be logged in to add a comment',
35
+ });
36
+ }
37
+
38
+ if (!(await postExists(input.postId))) {
39
+ throw new TRPCError({
40
+ code: 'NOT_FOUND',
41
+ message: 'Post not found',
42
+ });
43
+ }
44
+
45
+ const commentId = await createCommentRecord({
46
+ authorId: ctx.sessionUser.id,
47
+ content: input.content,
48
+ postId: input.postId,
49
+ });
50
+
51
+ if (!commentId) {
52
+ throw new TRPCError({
53
+ code: 'INTERNAL_SERVER_ERROR',
54
+ message: 'Failed to create comment.',
55
+ });
56
+ }
57
+
58
+ const result = await fate.resolveById({
59
+ ctx,
60
+ id: commentId,
61
+ input,
62
+ view: commentDataView,
63
+ });
64
+ if (!result) {
65
+ throw new TRPCError({
66
+ code: 'NOT_FOUND',
67
+ message: 'Comment not found',
68
+ });
69
+ }
70
+
71
+ live.connection('Post.comments', { id: input.postId }).appendNode('Comment', commentId);
72
+ live.update('Post', input.postId, { changed: ['commentCount', 'comments'] });
73
+
74
+ return result as CommentItem & { post?: { commentCount: number } };
75
+ }),
76
+ delete: procedure
77
+ .input(
78
+ z.object({
79
+ args: connectionArgs.optional(),
80
+ id: z.string().min(1, 'Comment id is required'),
81
+ select: z.array(z.string()).default([]),
82
+ }),
83
+ )
84
+ .mutation(async ({ ctx, input }) => {
85
+ const postId = await findCommentPostId(input.id);
86
+ if (!postId) {
87
+ throw new TRPCError({
88
+ code: 'NOT_FOUND',
89
+ message: 'Comment not found',
90
+ });
91
+ }
92
+
93
+ const deleted = await deleteCommentRecord(input.id);
94
+ if (!deleted) {
95
+ throw new TRPCError({
96
+ code: 'NOT_FOUND',
97
+ message: 'Comment not found',
98
+ });
99
+ }
100
+
101
+ const post = await fate.resolveById({
102
+ ctx,
103
+ id: postId,
104
+ input: { args: input.args, select: ['commentCount', 'id'] },
105
+ view: postSummaryDataView,
106
+ });
107
+
108
+ live.connection('Post.comments', { id: postId }).deleteEdge('Comment', input.id);
109
+ live.update('Post', postId, { changed: ['commentCount', 'comments'] });
110
+
111
+ return {
112
+ id: input.id,
113
+ post,
114
+ } as CommentItem & { post?: { commentCount: number } };
115
+ }),
116
+
117
+ search: fate.connection({
118
+ input: z.object({
119
+ query: z.string().min(1, 'Search query is required'),
120
+ }),
121
+ query: async ({ ctx, cursor, direction, input, skip, take }) => {
122
+ const query = input.args?.query?.trim();
123
+ if (!query?.length) {
124
+ return [];
125
+ }
126
+
127
+ if (query.length > 1) {
128
+ // Artificial slowdown.
129
+ await new Promise((resolve) => setTimeout(resolve, 500));
130
+ }
131
+
132
+ return fate.resolveConnection({
133
+ ctx,
134
+ cursor,
135
+ direction,
136
+ extra: {
137
+ where: commentSearchWhere(query) as unknown as DrizzleQueryExtra['where'],
138
+ },
139
+ input,
140
+ skip,
141
+ take,
142
+ view: commentDataView,
143
+ });
144
+ },
145
+ }),
146
+ });