@percepta/create 3.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 (138) hide show
  1. package/README.md +93 -0
  2. package/dist/chunk-GEVZERMP.js +108 -0
  3. package/dist/chunk-R4FWPE4A.js +49 -0
  4. package/dist/chunk-WMJT7CB5.js +57 -0
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +974 -0
  7. package/dist/init-Z4VGBHAK.js +96 -0
  8. package/dist/status-MITGDLTT.js +76 -0
  9. package/dist/sync-J4SFZHDX.js +136 -0
  10. package/dist/upstream-AQI7P4EU.js +144 -0
  11. package/package.json +58 -0
  12. package/template-versions.json +4 -0
  13. package/templates/library/README.md +30 -0
  14. package/templates/library/eslint.config.js +10 -0
  15. package/templates/library/gitignore.template +18 -0
  16. package/templates/library/package.json.template +29 -0
  17. package/templates/library/src/index.ts +9 -0
  18. package/templates/library/tsconfig.json +19 -0
  19. package/templates/monorepo/README.md +41 -0
  20. package/templates/monorepo/eslint.config.js +10 -0
  21. package/templates/monorepo/gitignore.template +31 -0
  22. package/templates/monorepo/npmrc.template +4 -0
  23. package/templates/monorepo/package.json.template +25 -0
  24. package/templates/monorepo/packages/.gitkeep +0 -0
  25. package/templates/monorepo/pnpm-workspace.yaml +2 -0
  26. package/templates/monorepo/tsconfig.json +16 -0
  27. package/templates/webapp/.claude/commands/sync.md +19 -0
  28. package/templates/webapp/.claude/commands/upstream.md +17 -0
  29. package/templates/webapp/.dockerignore +59 -0
  30. package/templates/webapp/.gitattributes +1 -0
  31. package/templates/webapp/.github/workflows/__APP_NAME__-ryvn-release.yaml +114 -0
  32. package/templates/webapp/.github/workflows/__APP_NAME__-terraform.yml +28 -0
  33. package/templates/webapp/.github/workflows/ci.yml +149 -0
  34. package/templates/webapp/.node-version +2 -0
  35. package/templates/webapp/.prettierrc.mjs +5 -0
  36. package/templates/webapp/AGENTS.md +240 -0
  37. package/templates/webapp/Dockerfile +64 -0
  38. package/templates/webapp/README.md +200 -0
  39. package/templates/webapp/agent-skills/database.md +140 -0
  40. package/templates/webapp/agent-skills/deploy.md +94 -0
  41. package/templates/webapp/agent-skills/inngest.md +147 -0
  42. package/templates/webapp/agent-skills/langfuse.md +117 -0
  43. package/templates/webapp/agent-skills/oneshot.md +216 -0
  44. package/templates/webapp/agent-skills/ryvn.md +25 -0
  45. package/templates/webapp/deploy/README.md +39 -0
  46. package/templates/webapp/deploy/ryvn/__APP_NAME__.service.yaml +11 -0
  47. package/templates/webapp/deploy/ryvn/environments/percepta-test/installations/__APP_NAME__.env.percepta-test.serviceinstallation.yaml +121 -0
  48. package/templates/webapp/docker-compose.yml +19 -0
  49. package/templates/webapp/drizzle.config.ts +30 -0
  50. package/templates/webapp/env.example.template +44 -0
  51. package/templates/webapp/eslint.config.mjs +52 -0
  52. package/templates/webapp/gitignore.template +53 -0
  53. package/templates/webapp/next.config.ts +8 -0
  54. package/templates/webapp/npmrc.template +4 -0
  55. package/templates/webapp/package.json.template +122 -0
  56. package/templates/webapp/postcss.config.mjs +5 -0
  57. package/templates/webapp/scripts/create-user.ts +47 -0
  58. package/templates/webapp/scripts/migrate.ts +18 -0
  59. package/templates/webapp/scripts/seed.ts +62 -0
  60. package/templates/webapp/scripts/setup-database.ts +57 -0
  61. package/templates/webapp/scripts/setup-readonly-user.ts +193 -0
  62. package/templates/webapp/scripts/start.sh +52 -0
  63. package/templates/webapp/src/app/(app)/layout.tsx +21 -0
  64. package/templates/webapp/src/app/(app)/page.tsx +30 -0
  65. package/templates/webapp/src/app/(auth)/auth/signin/CredentialsSignInForm.tsx +103 -0
  66. package/templates/webapp/src/app/(auth)/auth/signin/page.tsx +30 -0
  67. package/templates/webapp/src/app/(auth)/layout.tsx +15 -0
  68. package/templates/webapp/src/app/api/auth/[...all]/route.ts +4 -0
  69. package/templates/webapp/src/app/api/healthz/route.ts +10 -0
  70. package/templates/webapp/src/app/api/inngest/route.ts +31 -0
  71. package/templates/webapp/src/app/api/readyz/route.ts +31 -0
  72. package/templates/webapp/src/app/api/trpc/[trpc]/route.ts +21 -0
  73. package/templates/webapp/src/app/favicon.ico +0 -0
  74. package/templates/webapp/src/app/global-error.tsx +27 -0
  75. package/templates/webapp/src/app/layout.tsx +18 -0
  76. package/templates/webapp/src/components/FaroProvider.tsx +37 -0
  77. package/templates/webapp/src/components/Header.tsx +70 -0
  78. package/templates/webapp/src/components/Providers.tsx +45 -0
  79. package/templates/webapp/src/components/form/FormItem.tsx +82 -0
  80. package/templates/webapp/src/config/clientEnvConfig.ts +11 -0
  81. package/templates/webapp/src/config/getEnvConfig.ts +62 -0
  82. package/templates/webapp/src/config/isDev.ts +7 -0
  83. package/templates/webapp/src/drizzle/db.ts +28 -0
  84. package/templates/webapp/src/drizzle/migrations/0000_eager_grandmaster.sql +57 -0
  85. package/templates/webapp/src/drizzle/migrations/meta/0000_snapshot.json +376 -0
  86. package/templates/webapp/src/drizzle/migrations/meta/_journal.json +13 -0
  87. package/templates/webapp/src/drizzle/schema/auth/accounts.ts +33 -0
  88. package/templates/webapp/src/drizzle/schema/auth/sessions.ts +25 -0
  89. package/templates/webapp/src/drizzle/schema/auth/users.ts +38 -0
  90. package/templates/webapp/src/drizzle/schema/auth/verifications.ts +19 -0
  91. package/templates/webapp/src/drizzle/schema/index.ts +4 -0
  92. package/templates/webapp/src/drizzle/schema/utils/jsonbFromZod.ts +25 -0
  93. package/templates/webapp/src/instrumentation.ts +35 -0
  94. package/templates/webapp/src/lib/auth/index.ts +85 -0
  95. package/templates/webapp/src/lib/auth-client.ts +6 -0
  96. package/templates/webapp/src/lib/trpc.ts +15 -0
  97. package/templates/webapp/src/server/api/root.ts +5 -0
  98. package/templates/webapp/src/server/trpc.ts +61 -0
  99. package/templates/webapp/src/services/AuthContextService.ts +63 -0
  100. package/templates/webapp/src/services/DatabaseService.ts +54 -0
  101. package/templates/webapp/src/services/inngest/InngestFunctionCollection.ts +5 -0
  102. package/templates/webapp/src/services/inngest/InngestService.ts +71 -0
  103. package/templates/webapp/src/services/inngest/events/AppEvents.ts +34 -0
  104. package/templates/webapp/src/services/inngest/events/payloads/ExampleEventPayload.ts +14 -0
  105. package/templates/webapp/src/services/langfuse/LangfuseService.ts +80 -0
  106. package/templates/webapp/src/services/logger/AppLogger.ts +61 -0
  107. package/templates/webapp/src/services/logger/withRequestContext.ts +27 -0
  108. package/templates/webapp/src/services/observability/initFaro.ts +22 -0
  109. package/templates/webapp/src/startup-checks.ts +32 -0
  110. package/templates/webapp/src/styles/globals.css +27 -0
  111. package/templates/webapp/src/utils/__tests__/cn.test.ts +20 -0
  112. package/templates/webapp/src/utils/cn.ts +6 -0
  113. package/templates/webapp/src/utils/syncInngestApp.ts +62 -0
  114. package/templates/webapp/terraform/README.md +147 -0
  115. package/templates/webapp/terraform/deploy.sh +97 -0
  116. package/templates/webapp/terraform/main.tf +101 -0
  117. package/templates/webapp/terraform/modules/cloudtrail/main.tf +27 -0
  118. package/templates/webapp/terraform/modules/cloudtrail/outputs.tf +10 -0
  119. package/templates/webapp/terraform/modules/cloudtrail/variables.tf +15 -0
  120. package/templates/webapp/terraform/modules/networking/main.tf +118 -0
  121. package/templates/webapp/terraform/modules/networking/outputs.tf +38 -0
  122. package/templates/webapp/terraform/modules/networking/variables.tf +24 -0
  123. package/templates/webapp/terraform/modules/rds/main.tf +227 -0
  124. package/templates/webapp/terraform/modules/rds/outputs.tf +73 -0
  125. package/templates/webapp/terraform/modules/rds/variables.tf +61 -0
  126. package/templates/webapp/terraform/modules/s3-logging/main.tf +148 -0
  127. package/templates/webapp/terraform/modules/s3-logging/outputs.tf +10 -0
  128. package/templates/webapp/terraform/modules/s3-logging/variables.tf +16 -0
  129. package/templates/webapp/terraform/modules/secrets/main.tf +39 -0
  130. package/templates/webapp/terraform/modules/secrets/outputs.tf +9 -0
  131. package/templates/webapp/terraform/modules/secrets/variables.tf +51 -0
  132. package/templates/webapp/terraform/outputs.tf +102 -0
  133. package/templates/webapp/terraform/providers.tf +32 -0
  134. package/templates/webapp/terraform/terraform.tfvars.example +65 -0
  135. package/templates/webapp/terraform/variables.tf +129 -0
  136. package/templates/webapp/tsconfig.json +14 -0
  137. package/templates/webapp/vitest.config.ts +9 -0
  138. package/templates/webapp/vitest.setup.ts +5 -0
@@ -0,0 +1,376 @@
1
+ {
2
+ "id": "33e835dd-70ef-42a5-8790-b8ea2038db7a",
3
+ "prevId": "00000000-0000-0000-0000-000000000000",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "public.account": {
8
+ "name": "account",
9
+ "schema": "",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "text",
14
+ "primaryKey": true,
15
+ "notNull": true
16
+ },
17
+ "user_id": {
18
+ "name": "user_id",
19
+ "type": "uuid",
20
+ "primaryKey": false,
21
+ "notNull": true
22
+ },
23
+ "account_id": {
24
+ "name": "account_id",
25
+ "type": "text",
26
+ "primaryKey": false,
27
+ "notNull": true
28
+ },
29
+ "provider_id": {
30
+ "name": "provider_id",
31
+ "type": "text",
32
+ "primaryKey": false,
33
+ "notNull": true
34
+ },
35
+ "access_token": {
36
+ "name": "access_token",
37
+ "type": "text",
38
+ "primaryKey": false,
39
+ "notNull": false
40
+ },
41
+ "refresh_token": {
42
+ "name": "refresh_token",
43
+ "type": "text",
44
+ "primaryKey": false,
45
+ "notNull": false
46
+ },
47
+ "expires_at": {
48
+ "name": "expires_at",
49
+ "type": "integer",
50
+ "primaryKey": false,
51
+ "notNull": false
52
+ },
53
+ "access_token_expires_at": {
54
+ "name": "access_token_expires_at",
55
+ "type": "timestamp",
56
+ "primaryKey": false,
57
+ "notNull": false
58
+ },
59
+ "refresh_token_expires_at": {
60
+ "name": "refresh_token_expires_at",
61
+ "type": "timestamp",
62
+ "primaryKey": false,
63
+ "notNull": false
64
+ },
65
+ "scope": {
66
+ "name": "scope",
67
+ "type": "text",
68
+ "primaryKey": false,
69
+ "notNull": false
70
+ },
71
+ "id_token": {
72
+ "name": "id_token",
73
+ "type": "text",
74
+ "primaryKey": false,
75
+ "notNull": false
76
+ },
77
+ "password": {
78
+ "name": "password",
79
+ "type": "text",
80
+ "primaryKey": false,
81
+ "notNull": false
82
+ },
83
+ "created_at": {
84
+ "name": "created_at",
85
+ "type": "timestamp",
86
+ "primaryKey": false,
87
+ "notNull": true
88
+ },
89
+ "updated_at": {
90
+ "name": "updated_at",
91
+ "type": "timestamp",
92
+ "primaryKey": false,
93
+ "notNull": true
94
+ }
95
+ },
96
+ "indexes": {},
97
+ "foreignKeys": {
98
+ "account_user_id_users_id_fk": {
99
+ "name": "account_user_id_users_id_fk",
100
+ "tableFrom": "account",
101
+ "tableTo": "users",
102
+ "columnsFrom": [
103
+ "user_id"
104
+ ],
105
+ "columnsTo": [
106
+ "id"
107
+ ],
108
+ "onDelete": "cascade",
109
+ "onUpdate": "no action"
110
+ }
111
+ },
112
+ "compositePrimaryKeys": {},
113
+ "uniqueConstraints": {},
114
+ "policies": {},
115
+ "checkConstraints": {},
116
+ "isRLSEnabled": false
117
+ },
118
+ "public.session": {
119
+ "name": "session",
120
+ "schema": "",
121
+ "columns": {
122
+ "id": {
123
+ "name": "id",
124
+ "type": "text",
125
+ "primaryKey": true,
126
+ "notNull": true
127
+ },
128
+ "user_id": {
129
+ "name": "user_id",
130
+ "type": "uuid",
131
+ "primaryKey": false,
132
+ "notNull": true
133
+ },
134
+ "token": {
135
+ "name": "token",
136
+ "type": "text",
137
+ "primaryKey": false,
138
+ "notNull": true
139
+ },
140
+ "expires_at": {
141
+ "name": "expires_at",
142
+ "type": "timestamp",
143
+ "primaryKey": false,
144
+ "notNull": true
145
+ },
146
+ "ip_address": {
147
+ "name": "ip_address",
148
+ "type": "text",
149
+ "primaryKey": false,
150
+ "notNull": false
151
+ },
152
+ "user_agent": {
153
+ "name": "user_agent",
154
+ "type": "text",
155
+ "primaryKey": false,
156
+ "notNull": false
157
+ },
158
+ "impersonated_by": {
159
+ "name": "impersonated_by",
160
+ "type": "text",
161
+ "primaryKey": false,
162
+ "notNull": false
163
+ },
164
+ "created_at": {
165
+ "name": "created_at",
166
+ "type": "timestamp",
167
+ "primaryKey": false,
168
+ "notNull": true
169
+ },
170
+ "updated_at": {
171
+ "name": "updated_at",
172
+ "type": "timestamp",
173
+ "primaryKey": false,
174
+ "notNull": true
175
+ }
176
+ },
177
+ "indexes": {},
178
+ "foreignKeys": {
179
+ "session_user_id_users_id_fk": {
180
+ "name": "session_user_id_users_id_fk",
181
+ "tableFrom": "session",
182
+ "tableTo": "users",
183
+ "columnsFrom": [
184
+ "user_id"
185
+ ],
186
+ "columnsTo": [
187
+ "id"
188
+ ],
189
+ "onDelete": "cascade",
190
+ "onUpdate": "no action"
191
+ }
192
+ },
193
+ "compositePrimaryKeys": {},
194
+ "uniqueConstraints": {
195
+ "session_token_unique": {
196
+ "name": "session_token_unique",
197
+ "nullsNotDistinct": false,
198
+ "columns": [
199
+ "token"
200
+ ]
201
+ }
202
+ },
203
+ "policies": {},
204
+ "checkConstraints": {},
205
+ "isRLSEnabled": false
206
+ },
207
+ "public.users": {
208
+ "name": "users",
209
+ "schema": "",
210
+ "columns": {
211
+ "id": {
212
+ "name": "id",
213
+ "type": "uuid",
214
+ "primaryKey": true,
215
+ "notNull": true
216
+ },
217
+ "name": {
218
+ "name": "name",
219
+ "type": "text",
220
+ "primaryKey": false,
221
+ "notNull": true
222
+ },
223
+ "email": {
224
+ "name": "email",
225
+ "type": "text",
226
+ "primaryKey": false,
227
+ "notNull": true
228
+ },
229
+ "email_verified": {
230
+ "name": "email_verified",
231
+ "type": "boolean",
232
+ "primaryKey": false,
233
+ "notNull": true,
234
+ "default": false
235
+ },
236
+ "image": {
237
+ "name": "image",
238
+ "type": "text",
239
+ "primaryKey": false,
240
+ "notNull": false
241
+ },
242
+ "role": {
243
+ "name": "role",
244
+ "type": "text",
245
+ "primaryKey": false,
246
+ "notNull": true,
247
+ "default": "'user'"
248
+ },
249
+ "banned": {
250
+ "name": "banned",
251
+ "type": "boolean",
252
+ "primaryKey": false,
253
+ "notNull": false,
254
+ "default": false
255
+ },
256
+ "ban_reason": {
257
+ "name": "ban_reason",
258
+ "type": "text",
259
+ "primaryKey": false,
260
+ "notNull": false
261
+ },
262
+ "ban_expires": {
263
+ "name": "ban_expires",
264
+ "type": "timestamp",
265
+ "primaryKey": false,
266
+ "notNull": false
267
+ },
268
+ "created_at": {
269
+ "name": "created_at",
270
+ "type": "timestamp",
271
+ "primaryKey": false,
272
+ "notNull": true,
273
+ "default": "now()"
274
+ },
275
+ "updated_at": {
276
+ "name": "updated_at",
277
+ "type": "timestamp",
278
+ "primaryKey": false,
279
+ "notNull": true,
280
+ "default": "now()"
281
+ }
282
+ },
283
+ "indexes": {
284
+ "lower_email_index": {
285
+ "name": "lower_email_index",
286
+ "columns": [
287
+ {
288
+ "expression": "lower(\"email\")",
289
+ "asc": true,
290
+ "isExpression": true,
291
+ "nulls": "last"
292
+ }
293
+ ],
294
+ "isUnique": true,
295
+ "concurrently": false,
296
+ "method": "btree",
297
+ "with": {}
298
+ }
299
+ },
300
+ "foreignKeys": {},
301
+ "compositePrimaryKeys": {},
302
+ "uniqueConstraints": {
303
+ "users_email_unique": {
304
+ "name": "users_email_unique",
305
+ "nullsNotDistinct": false,
306
+ "columns": [
307
+ "email"
308
+ ]
309
+ }
310
+ },
311
+ "policies": {},
312
+ "checkConstraints": {},
313
+ "isRLSEnabled": false
314
+ },
315
+ "public.verification": {
316
+ "name": "verification",
317
+ "schema": "",
318
+ "columns": {
319
+ "id": {
320
+ "name": "id",
321
+ "type": "text",
322
+ "primaryKey": true,
323
+ "notNull": true
324
+ },
325
+ "identifier": {
326
+ "name": "identifier",
327
+ "type": "text",
328
+ "primaryKey": false,
329
+ "notNull": true
330
+ },
331
+ "value": {
332
+ "name": "value",
333
+ "type": "text",
334
+ "primaryKey": false,
335
+ "notNull": true
336
+ },
337
+ "expires_at": {
338
+ "name": "expires_at",
339
+ "type": "timestamp",
340
+ "primaryKey": false,
341
+ "notNull": true
342
+ },
343
+ "created_at": {
344
+ "name": "created_at",
345
+ "type": "timestamp",
346
+ "primaryKey": false,
347
+ "notNull": false
348
+ },
349
+ "updated_at": {
350
+ "name": "updated_at",
351
+ "type": "timestamp",
352
+ "primaryKey": false,
353
+ "notNull": false
354
+ }
355
+ },
356
+ "indexes": {},
357
+ "foreignKeys": {},
358
+ "compositePrimaryKeys": {},
359
+ "uniqueConstraints": {},
360
+ "policies": {},
361
+ "checkConstraints": {},
362
+ "isRLSEnabled": false
363
+ }
364
+ },
365
+ "enums": {},
366
+ "schemas": {},
367
+ "sequences": {},
368
+ "roles": {},
369
+ "policies": {},
370
+ "views": {},
371
+ "_meta": {
372
+ "columns": {},
373
+ "schemas": {},
374
+ "tables": {}
375
+ }
376
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "7",
3
+ "dialect": "postgresql",
4
+ "entries": [
5
+ {
6
+ "idx": 0,
7
+ "version": "7",
8
+ "when": 1776438047648,
9
+ "tag": "0000_eager_grandmaster",
10
+ "breakpoints": true
11
+ }
12
+ ]
13
+ }
@@ -0,0 +1,33 @@
1
+ import { integer, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
2
+ import { users } from "./users";
3
+
4
+ /**
5
+ * Better Auth account table.
6
+ * Stores OAuth provider links and credential password hashes.
7
+ * @see https://better-auth.com/docs/concepts/database
8
+ */
9
+ export const accounts = pgTable("account", {
10
+ id: text("id")
11
+ .$defaultFn(() => crypto.randomUUID())
12
+ .primaryKey(),
13
+ userId: uuid("user_id")
14
+ .notNull()
15
+ .references(() => users.id, { onDelete: "cascade" }),
16
+ accountId: text("account_id").notNull(),
17
+ providerId: text("provider_id").notNull(),
18
+ accessToken: text("access_token"),
19
+ refreshToken: text("refresh_token"),
20
+ expiresAt: integer("expires_at"),
21
+ accessTokenExpiresAt: timestamp("access_token_expires_at", { mode: "date" }),
22
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at", {
23
+ mode: "date",
24
+ }),
25
+ scope: text("scope"),
26
+ idToken: text("id_token"),
27
+ password: text("password"),
28
+ createdAt: timestamp("created_at", { mode: "date" }).notNull(),
29
+ updatedAt: timestamp("updated_at", { mode: "date" }).notNull(),
30
+ });
31
+
32
+ export type Account = typeof accounts.$inferSelect;
33
+ export type NewAccount = typeof accounts.$inferInsert;
@@ -0,0 +1,25 @@
1
+ import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
2
+ import { users } from "./users";
3
+
4
+ /**
5
+ * Better Auth session table.
6
+ * @see https://better-auth.com/docs/concepts/database
7
+ */
8
+ export const sessions = pgTable("session", {
9
+ id: text("id")
10
+ .$defaultFn(() => crypto.randomUUID())
11
+ .primaryKey(),
12
+ userId: uuid("user_id")
13
+ .notNull()
14
+ .references(() => users.id, { onDelete: "cascade" }),
15
+ token: text("token").notNull().unique(),
16
+ expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
17
+ ipAddress: text("ip_address"),
18
+ userAgent: text("user_agent"),
19
+ impersonatedBy: text("impersonated_by"),
20
+ createdAt: timestamp("created_at", { mode: "date" }).notNull(),
21
+ updatedAt: timestamp("updated_at", { mode: "date" }).notNull(),
22
+ });
23
+
24
+ export type Session = typeof sessions.$inferSelect;
25
+ export type NewSession = typeof sessions.$inferInsert;
@@ -0,0 +1,38 @@
1
+ import { sql } from "drizzle-orm";
2
+ import {
3
+ boolean,
4
+ pgTable,
5
+ text,
6
+ timestamp,
7
+ uniqueIndex,
8
+ uuid,
9
+ } from "drizzle-orm/pg-core";
10
+
11
+ /**
12
+ * Users table for Better Auth.
13
+ * @see https://better-auth.com/docs/concepts/database
14
+ */
15
+ export const users = pgTable(
16
+ "users",
17
+ {
18
+ id: uuid("id")
19
+ .$defaultFn(() => crypto.randomUUID())
20
+ .primaryKey(),
21
+ name: text("name").notNull(),
22
+ email: text("email").notNull().unique(),
23
+ emailVerified: boolean("email_verified").notNull().default(false),
24
+ image: text("image"),
25
+ role: text("role").notNull().default("user"),
26
+ banned: boolean("banned").default(false),
27
+ banReason: text("ban_reason"),
28
+ banExpires: timestamp("ban_expires", { mode: "date" }),
29
+ createdAt: timestamp("created_at").defaultNow().notNull(),
30
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
31
+ },
32
+ (table) => ({
33
+ emailIndex: uniqueIndex("lower_email_index").on(sql`lower(${table.email})`),
34
+ }),
35
+ );
36
+
37
+ export type User = typeof users.$inferSelect;
38
+ export type NewUser = typeof users.$inferInsert;
@@ -0,0 +1,19 @@
1
+ import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
2
+
3
+ /**
4
+ * Better Auth verification table.
5
+ * @see https://better-auth.com/docs/concepts/database
6
+ */
7
+ export const verifications = pgTable("verification", {
8
+ id: text("id")
9
+ .$defaultFn(() => crypto.randomUUID())
10
+ .primaryKey(),
11
+ identifier: text("identifier").notNull(),
12
+ value: text("value").notNull(),
13
+ expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
14
+ createdAt: timestamp("created_at", { mode: "date" }),
15
+ updatedAt: timestamp("updated_at", { mode: "date" }),
16
+ });
17
+
18
+ export type Verification = typeof verifications.$inferSelect;
19
+ export type NewVerification = typeof verifications.$inferInsert;
@@ -0,0 +1,4 @@
1
+ export { accounts } from "./auth/accounts";
2
+ export { sessions } from "./auth/sessions";
3
+ export { users } from "./auth/users";
4
+ export { verifications } from "./auth/verifications";
@@ -0,0 +1,25 @@
1
+ import { customType } from "drizzle-orm/pg-core";
2
+ import { type z } from "zod";
3
+
4
+ export function jsonbFromZod<TValue>(schema: z.Schema<TValue>): ReturnType<
5
+ typeof customType<{
6
+ data: TValue;
7
+ driverData: string;
8
+ }>
9
+ > {
10
+ return customType<{
11
+ data: TValue;
12
+ driverData: string;
13
+ }>({
14
+ dataType() {
15
+ return "jsonb";
16
+ },
17
+ toDriver(value) {
18
+ return JSON.stringify(schema.parse(value));
19
+ },
20
+ fromDriver(raw) {
21
+ const parsed = schema.parse(raw);
22
+ return parsed;
23
+ },
24
+ });
25
+ }
@@ -0,0 +1,35 @@
1
+ import { LangfuseSpanProcessor } from "@langfuse/otel";
2
+ import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
3
+ import { NodeSDK } from "@opentelemetry/sdk-node";
4
+ import { compact } from "lodash-es";
5
+ import { getEnvConfig } from "./config/getEnvConfig";
6
+ import { getLogger } from "./services/logger/AppLogger";
7
+
8
+ function getSpanProcessor(): LangfuseSpanProcessor | undefined {
9
+ const {
10
+ LANGFUSE_BASE_URL: baseUrl,
11
+ LANGFUSE_PUBLIC_KEY: publicKey,
12
+ LANGFUSE_SECRET_KEY: secretKey,
13
+ } = getEnvConfig();
14
+ if (baseUrl == null) {
15
+ getLogger().debug(
16
+ undefined,
17
+ "No Langfuse base URL found. Skipping Langfuse OpenTelemetry.",
18
+ );
19
+ return undefined;
20
+ }
21
+
22
+ getLogger().debug(undefined, "Registering Langfuse OpenTelemetry.");
23
+ return new LangfuseSpanProcessor({
24
+ baseUrl,
25
+ publicKey,
26
+ secretKey,
27
+ });
28
+ }
29
+
30
+ const sdk = new NodeSDK({
31
+ spanProcessors: compact([getSpanProcessor()]),
32
+ instrumentations: [getNodeAutoInstrumentations()],
33
+ });
34
+
35
+ sdk.start();
@@ -0,0 +1,85 @@
1
+ import { betterAuth } from "better-auth";
2
+ import { drizzleAdapter } from "better-auth/adapters/drizzle";
3
+ import { admin } from "better-auth/plugins";
4
+ import { db } from "../../drizzle/db";
5
+ import { accounts } from "../../drizzle/schema/auth/accounts";
6
+ import { sessions } from "../../drizzle/schema/auth/sessions";
7
+ import { users } from "../../drizzle/schema/auth/users";
8
+ import { verifications } from "../../drizzle/schema/auth/verifications";
9
+ import { getEnvConfig } from "../../config/getEnvConfig";
10
+ import { getLogger } from "../../services/logger/AppLogger";
11
+
12
+ // eslint-disable-next-line n/no-process-env -- detecting Next.js build phase
13
+ const isBuildPhase = process.env.NEXT_PHASE === "phase-production-build";
14
+
15
+ function getSecret(): string {
16
+ if (isBuildPhase) {
17
+ return "build-placeholder-not-used-at-runtime";
18
+ }
19
+
20
+ const secret = getEnvConfig().BETTER_AUTH_SECRET;
21
+ if (secret == null) {
22
+ throw new Error(
23
+ "BETTER_AUTH_SECRET is required. Set it in your environment variables.",
24
+ );
25
+ }
26
+ return secret;
27
+ }
28
+
29
+ function createAuth() {
30
+ const config = getEnvConfig();
31
+
32
+ return betterAuth({
33
+ baseURL: config.BETTER_AUTH_URL,
34
+ secret: getSecret(),
35
+ database: drizzleAdapter(db, {
36
+ provider: "pg",
37
+ schema: {
38
+ user: users,
39
+ session: sessions,
40
+ account: accounts,
41
+ verification: verifications,
42
+ },
43
+ }),
44
+ emailAndPassword: {
45
+ enabled: true,
46
+ },
47
+ plugins: [admin()],
48
+ advanced: {
49
+ database: {
50
+ generateId: false,
51
+ },
52
+ },
53
+ logger: {
54
+ disabled: false,
55
+ },
56
+ });
57
+ }
58
+
59
+ type Auth = ReturnType<typeof createAuth>;
60
+
61
+ let _auth: Auth | undefined;
62
+
63
+ function getAuth(): Auth {
64
+ if (_auth == null) {
65
+ _auth = createAuth();
66
+ getLogger().info(undefined, "Better Auth initialized");
67
+ }
68
+ return _auth;
69
+ }
70
+
71
+ /**
72
+ * Lazy proxy that defers auth initialization to first access (runtime),
73
+ * avoiding build-time evaluation of environment variables.
74
+ */
75
+ export const auth = new Proxy({} as Auth, {
76
+ get(_target, prop, receiver) {
77
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
78
+ return Reflect.get(getAuth(), prop, receiver);
79
+ },
80
+ has(_target, prop) {
81
+ return Reflect.has(getAuth(), prop);
82
+ },
83
+ });
84
+
85
+ export type BetterAuthSession = typeof auth.$Infer.Session;
@@ -0,0 +1,6 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+ import { adminClient } from "better-auth/client/plugins";
3
+
4
+ export const authClient = createAuthClient({
5
+ plugins: [adminClient()],
6
+ });
@@ -0,0 +1,15 @@
1
+ import { createTRPCClient, httpBatchLink } from "@trpc/client";
2
+ import { createTRPCContext } from "@trpc/tanstack-react-query";
3
+ import superjson from "superjson";
4
+ import { type AppRouter } from "../server/api/root";
5
+
6
+ export const trpcClient = createTRPCClient<AppRouter>({
7
+ links: [
8
+ httpBatchLink({
9
+ url: "/api/trpc",
10
+ transformer: superjson,
11
+ }),
12
+ ],
13
+ });
14
+
15
+ export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
@@ -0,0 +1,5 @@
1
+ import { router } from "../trpc";
2
+
3
+ export const appRouter = router({});
4
+
5
+ export type AppRouter = typeof appRouter;