create-top-secret-starter 0.1.1 → 0.2.1

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 (125) hide show
  1. package/index.js +243 -39
  2. package/package.json +2 -2
  3. package/templates/template-api/.env.example +13 -0
  4. package/templates/template-api/CLAUDE.md +32 -0
  5. package/templates/template-api/drizzle/0000_damp_taskmaster.sql +64 -0
  6. package/templates/template-api/drizzle/meta/0000_snapshot.json +469 -0
  7. package/templates/template-api/drizzle/meta/_journal.json +13 -0
  8. package/templates/template-api/drizzle.config.ts +13 -0
  9. package/templates/template-api/package.json +37 -0
  10. package/templates/template-api/src/app.test.ts +119 -0
  11. package/templates/template-api/src/app.ts +26 -0
  12. package/templates/template-api/src/db/auth-schema.ts +92 -0
  13. package/templates/template-api/src/db/index.ts +12 -0
  14. package/templates/template-api/src/db/migrate.ts +11 -0
  15. package/templates/template-api/src/db/schema.ts +21 -0
  16. package/templates/template-api/src/env.ts +30 -0
  17. package/templates/template-api/src/index.ts +38 -0
  18. package/templates/template-api/src/modules/auth/index.ts +6 -0
  19. package/templates/template-api/src/modules/auth/service.ts +53 -0
  20. package/templates/template-api/src/modules/notes/index.ts +38 -0
  21. package/templates/template-api/src/modules/notes/model.ts +23 -0
  22. package/templates/template-api/src/modules/notes/service.ts +36 -0
  23. package/templates/template-api/src/redis.ts +21 -0
  24. package/templates/template-api/tsconfig.json +23 -0
  25. package/templates/template-api/vitest.config.ts +11 -0
  26. package/templates/template-router/.claude/hooks/stop-check.sh +26 -0
  27. package/templates/template-router/.claude/launch.json +11 -0
  28. package/templates/template-router/.claude/settings.json +38 -0
  29. package/templates/template-router/.claude/skills/code-review/SKILL.md +89 -0
  30. package/templates/template-router/.claude/skills/codebase-design/DEEPENING.md +37 -0
  31. package/templates/template-router/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  32. package/templates/template-router/.claude/skills/codebase-design/SKILL.md +114 -0
  33. package/templates/template-router/.claude/skills/diagnosing-bugs/SKILL.md +134 -0
  34. package/templates/template-router/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  35. package/templates/template-router/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
  36. package/templates/template-router/.claude/skills/improve-codebase-architecture/SKILL.md +66 -0
  37. package/templates/template-router/.claude/skills/project-conventions/SKILL.md +140 -0
  38. package/templates/template-router/.claude/skills/react-doctor/AGENTS.md +17 -0
  39. package/templates/template-router/.claude/skills/react-doctor/SKILL.md +21 -0
  40. package/templates/template-router/.mcp.json +12 -0
  41. package/templates/template-router/.oxfmtrc.json +4 -2
  42. package/templates/template-router/.oxlintrc.json +47 -3
  43. package/templates/template-router/CLAUDE.md +59 -0
  44. package/templates/template-router/_gitignore +3 -0
  45. package/templates/template-router/package.json +10 -12
  46. package/templates/template-router/src/api/auth/guards.ts +1 -11
  47. package/templates/template-router/src/api/auth/index.ts +7 -21
  48. package/templates/template-router/src/api/auth/refresh.test.ts +6 -12
  49. package/templates/template-router/src/api/auth/router-bridge.ts +7 -21
  50. package/templates/template-router/src/api/auth/session-store.test.ts +11 -12
  51. package/templates/template-router/src/api/auth/session-store.ts +8 -23
  52. package/templates/template-router/src/api/index.ts +4 -3
  53. package/templates/template-router/src/components/status-page.tsx +1 -4
  54. package/templates/template-router/src/components/ui/field.tsx +18 -23
  55. package/templates/template-router/src/components/ui/sonner.tsx +2 -2
  56. package/templates/template-router/src/env.ts +1 -2
  57. package/templates/template-router/src/index.css +30 -3
  58. package/templates/template-router/src/lib/query-client.test.ts +6 -8
  59. package/templates/template-router/src/lib/query-client.ts +4 -3
  60. package/templates/template-router/src/lib/utils.ts +1 -1
  61. package/templates/template-router/src/main.tsx +1 -4
  62. package/templates/template-router/src/mocks/mock-server.ts +10 -21
  63. package/templates/template-router/src/providers/index.tsx +1 -2
  64. package/templates/template-router/src/providers/theme.test.tsx +7 -4
  65. package/templates/template-router/src/providers/theme.tsx +38 -32
  66. package/templates/template-router/src/router.tsx +7 -7
  67. package/templates/template-router/src/routes/-error.tsx +1 -1
  68. package/templates/template-router/src/routes/-pending.tsx +12 -0
  69. package/templates/template-router/src/routes/__root.tsx +1 -5
  70. package/templates/template-router/src/routes/_authenticated/index.tsx +1 -3
  71. package/templates/template-router/src/routes/sign-in.tsx +7 -12
  72. package/templates/template-router/tsconfig.app.json +2 -1
  73. package/templates/template-router/tsconfig.json +11 -2
  74. package/templates/template-router/tsconfig.node.json +1 -0
  75. package/templates/template-router/vite.config.ts +18 -11
  76. package/templates/template-start/.claude/hooks/stop-check.sh +26 -0
  77. package/templates/template-start/.claude/launch.json +11 -0
  78. package/templates/template-start/.claude/settings.json +38 -0
  79. package/templates/template-start/.claude/skills/code-review/SKILL.md +89 -0
  80. package/templates/template-start/.claude/skills/codebase-design/DEEPENING.md +37 -0
  81. package/templates/template-start/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  82. package/templates/template-start/.claude/skills/codebase-design/SKILL.md +114 -0
  83. package/templates/template-start/.claude/skills/diagnosing-bugs/SKILL.md +134 -0
  84. package/templates/template-start/.claude/skills/diagnosing-bugs/scripts/hitl-loop.template.sh +41 -0
  85. package/templates/template-start/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +129 -0
  86. package/templates/template-start/.claude/skills/improve-codebase-architecture/SKILL.md +66 -0
  87. package/templates/template-start/.claude/skills/project-conventions/SKILL.md +140 -0
  88. package/templates/template-start/.claude/skills/react-doctor/AGENTS.md +17 -0
  89. package/templates/template-start/.claude/skills/react-doctor/SKILL.md +21 -0
  90. package/templates/template-start/.mcp.json +12 -0
  91. package/templates/template-start/.oxfmtrc.json +4 -2
  92. package/templates/template-start/.oxlintrc.json +47 -3
  93. package/templates/template-start/CLAUDE.md +59 -0
  94. package/templates/template-start/_gitignore +3 -0
  95. package/templates/template-start/package.json +10 -12
  96. package/templates/template-start/src/api/auth/guards.ts +2 -4
  97. package/templates/template-start/src/api/auth/index.ts +7 -21
  98. package/templates/template-start/src/api/auth/refresh.test.ts +6 -12
  99. package/templates/template-start/src/api/auth/router-bridge.ts +7 -21
  100. package/templates/template-start/src/api/auth/session-store.test.ts +11 -12
  101. package/templates/template-start/src/api/auth/session-store.ts +3 -7
  102. package/templates/template-start/src/api/index.ts +4 -3
  103. package/templates/template-start/src/components/status-page.tsx +1 -4
  104. package/templates/template-start/src/components/ui/field.tsx +18 -23
  105. package/templates/template-start/src/components/ui/sonner.tsx +2 -2
  106. package/templates/template-start/src/env.ts +1 -2
  107. package/templates/template-start/src/index.css +30 -3
  108. package/templates/template-start/src/lib/query-client.test.ts +6 -8
  109. package/templates/template-start/src/lib/query-client.ts +4 -3
  110. package/templates/template-start/src/lib/utils.ts +1 -1
  111. package/templates/template-start/src/mocks/mock-server.ts +10 -21
  112. package/templates/template-start/src/providers/index.tsx +1 -2
  113. package/templates/template-start/src/providers/theme.test.tsx +7 -4
  114. package/templates/template-start/src/providers/theme.tsx +38 -32
  115. package/templates/template-start/src/router.tsx +8 -8
  116. package/templates/template-start/src/routes/-error.tsx +1 -1
  117. package/templates/template-start/src/routes/-pending.tsx +12 -0
  118. package/templates/template-start/src/routes/__root.tsx +2 -1
  119. package/templates/template-start/src/routes/_authenticated/index.tsx +1 -3
  120. package/templates/template-start/src/routes/sign-in.tsx +7 -12
  121. package/templates/template-start/tsconfig.app.json +2 -1
  122. package/templates/template-start/tsconfig.json +11 -2
  123. package/templates/template-start/tsconfig.node.json +1 -0
  124. package/templates/template-router/src/api/auth/router-bridge.test.ts +0 -74
  125. package/templates/template-start/src/api/auth/router-bridge.test.ts +0 -74
package/index.js CHANGED
@@ -5,21 +5,28 @@ import { fileURLToPath } from 'node:url'
5
5
  import { parseArgs } from 'node:util'
6
6
  import { cancel, intro, isCancel, outro, select, text } from '@clack/prompts'
7
7
 
8
- // lesson 001: the package manager that launched us sets npm_config_user_agent
8
+ // the package manager that launched us sets npm_config_user_agent
9
9
  const agent = process.env.npm_config_user_agent ?? ''
10
10
  const detectedPm = agent.split('/')[0] || 'npm'
11
11
 
12
12
  const PMS = ['bun', 'pnpm', 'npm']
13
- const ROUTERS = ['router', 'start']
13
+ const ROUTER_LABELS = { router: 'TanStack Router', start: 'TanStack Start' }
14
+ const ROUTERS = Object.keys(ROUTER_LABELS)
14
15
  const NAME_RE = /^[a-z0-9][a-z0-9._-]*$/
16
+ // web-side runtime deps the backend variant adds (the api types come in as a devDep)
17
+ const BACKEND_WEB_DEPS = { '@elysia/eden': '^1.4.10', 'better-auth': '^1.7.2' }
15
18
 
16
- // lesson 002 §6: flags bypass prompts so the CLI works in CI
19
+ // flags bypass prompts so the CLI works in CI
17
20
  const { values: flags, positionals } = parseArgs({
18
21
  args: process.argv.slice(2),
19
22
  allowPositionals: true,
20
23
  options: {
21
24
  pm: { type: 'string' },
22
- router: { type: 'string' }
25
+ router: { type: 'string' },
26
+ backend: { type: 'boolean' },
27
+ 'no-backend': { type: 'boolean' },
28
+ redis: { type: 'boolean' },
29
+ 'no-redis': { type: 'boolean' }
23
30
  }
24
31
  })
25
32
 
@@ -38,18 +45,24 @@ function validateName(v) {
38
45
 
39
46
  intro('create-top-secret-starter')
40
47
 
41
- let name = positionals[0]
42
- if (name && validateName(name)) {
48
+ // `.` / `./` scaffold into the current directory; the package name is then the
49
+ // directory's basename
50
+ const target =
51
+ positionals.at(0) ??
52
+ must(
53
+ await text({
54
+ message: 'Project name (or . for the current directory)?',
55
+ placeholder: 'my-ai-app',
56
+ validate: v => (v ? undefined : 'Project name is required')
57
+ })
58
+ )
59
+ const targetDir = path.resolve(process.cwd(), target)
60
+ const inPlace = targetDir === process.cwd()
61
+ const name = path.basename(targetDir)
62
+ if (validateName(name)) {
43
63
  cancel(`Invalid project name "${name}": ${validateName(name)}`)
44
64
  process.exit(1)
45
65
  }
46
- name ??= must(
47
- await text({
48
- message: 'Project name?',
49
- placeholder: 'my-ai-app',
50
- validate: validateName
51
- })
52
- )
53
66
 
54
67
  let pm = flags.pm
55
68
  if (pm && !PMS.includes(pm)) {
@@ -73,45 +86,236 @@ router ??= must(
73
86
  await select({
74
87
  message: 'Routing?',
75
88
  options: [
76
- { value: 'router', label: 'TanStack Router', hint: 'SPA' },
77
- { value: 'start', label: 'TanStack Start', hint: 'SSR, full-stack' }
89
+ { value: 'router', label: ROUTER_LABELS.router, hint: 'SPA' },
90
+ { value: 'start', label: ROUTER_LABELS.start, hint: 'SSR, full-stack' }
78
91
  ]
79
92
  })
80
93
  )
81
94
 
82
- const targetDir = path.resolve(process.cwd(), name)
83
- if (fs.existsSync(targetDir) && fs.readdirSync(targetDir).length > 0) {
84
- cancel(`Directory "${name}" already exists and is not empty.`)
95
+ // Elysia runs on Bun, so the backend is only offered when bun is the package manager.
96
+ const flagPair = (on, off) => {
97
+ if (on) return true
98
+ if (off) return false
99
+ return undefined
100
+ }
101
+
102
+ let backend = flagPair(flags.backend, flags['no-backend'])
103
+ if (backend && pm !== 'bun') {
104
+ cancel('The backend (Elysia) requires bun: pass --pm bun.')
105
+ process.exit(1)
106
+ }
107
+ if (backend === undefined && pm === 'bun') {
108
+ backend = must(
109
+ await select({
110
+ message: 'Backend?',
111
+ options: [
112
+ { value: false, label: 'None', hint: 'SPA only, mock API in dev' },
113
+ { value: true, label: 'Elysia + Drizzle + Better Auth', hint: 'Postgres, bun workspace' }
114
+ ]
115
+ })
116
+ )
117
+ }
118
+ backend ??= false
119
+
120
+ // Redis (Bun's built-in client) as Better Auth's secondary storage: sessions and
121
+ // rate-limit counters survive restarts and are shared across instances.
122
+ let redis = flagPair(flags.redis, flags['no-redis'])
123
+ if (redis && !backend) {
124
+ cancel('--redis requires the backend: pass --backend.')
85
125
  process.exit(1)
86
126
  }
127
+ if (redis === undefined && backend) {
128
+ redis = must(
129
+ await select({
130
+ message: 'Redis?',
131
+ options: [
132
+ { value: false, label: 'None', hint: 'sessions in Postgres, rate limit per process' },
133
+ { value: true, label: 'Redis', hint: 'Better Auth secondary storage, bun.redis' }
134
+ ]
135
+ })
136
+ )
137
+ }
138
+ redis ??= false
87
139
 
88
- // lesson 003 §4: variant A one full template per router choice
89
- const templateDir = path.join(
90
- path.dirname(fileURLToPath(import.meta.url)),
91
- 'templates',
92
- `template-${router}`
93
- )
140
+ // a fresh `git init` is fine, anything else would be overwritten
141
+ const existing = fs.existsSync(targetDir)
142
+ ? fs.readdirSync(targetDir).filter(entry => entry !== '.git')
143
+ : []
144
+ if (existing.length > 0) {
145
+ cancel(`Directory "${target}" is not empty.`)
146
+ process.exit(1)
147
+ }
148
+
149
+ // one full template per router choice
150
+ const cliRoot = path.dirname(fileURLToPath(import.meta.url))
151
+ const templateDir = path.join(cliRoot, 'templates', `template-${router}`)
94
152
  if (!fs.existsSync(templateDir)) {
95
- cancel(`The ${router === 'start' ? 'TanStack Start' : router} template is not available yet.`)
153
+ cancel(`The ${ROUTER_LABELS[router]} template is not available yet.`)
96
154
  process.exit(1)
97
155
  }
98
- fs.cpSync(templateDir, targetDir, { recursive: true })
156
+ // npm strips .gitignore from packages, so it ships as _gitignore
157
+ const restoreGitignore = dir => {
158
+ const src = path.join(dir, '_gitignore')
159
+ if (fs.existsSync(src)) fs.renameSync(src, path.join(dir, '.gitignore'))
160
+ }
161
+ const patchFile = (file, fn) => fs.writeFileSync(file, fn(fs.readFileSync(file, 'utf8')))
162
+ // The api source keeps Redis-only code between `redis:start` / `redis:end` marker
163
+ // comments, so one source tree serves both answers to the Redis prompt.
164
+ const REDIS_MARKER = /^[ \t]*(\/\/|#|<!--) redis:(start|end)( -->)?\n/gm
165
+ const REDIS_BLOCK =
166
+ /^[ \t]*(\/\/|#|<!--) redis:start( -->)?\n[\s\S]*?^[ \t]*(\/\/|#|<!--) redis:end( -->)?\n/gm
167
+ // each removal site is marked so the comma repair below stays local to it
168
+ const HOLE = '__REDIS_HOLE__'
169
+
170
+ const applyRedisMarkers = (file, keep) =>
171
+ patchFile(file, source =>
172
+ source
173
+ .replace(keep ? REDIS_MARKER : REDIS_BLOCK, HOLE)
174
+ // dropping the last entry of an object or call leaves a dangling comma
175
+ .replace(new RegExp(`,(\\s*)${HOLE}(\\s*[)}\\]])`, 'g'), '$1$2')
176
+ .replaceAll(HOLE, '')
177
+ .replace(/\n{3,}/g, '\n\n')
178
+ )
179
+
180
+ // walk rather than a hardcoded list: marked code may live in any api file
181
+ const filesWithRedisMarkers = dir =>
182
+ fs
183
+ .readdirSync(dir, { recursive: true, withFileTypes: true })
184
+ .filter(entry => entry.isFile())
185
+ .map(entry => path.join(entry.parentPath, entry.name))
186
+ .filter(file => fs.readFileSync(file, 'utf8').includes('redis:start'))
187
+
188
+ const editJson = (file, fn) => {
189
+ const value = JSON.parse(fs.readFileSync(file, 'utf8'))
190
+ fn(value)
191
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n')
192
+ }
193
+
194
+ const webDir = backend ? path.join(targetDir, 'apps', 'web') : targetDir
195
+ fs.cpSync(templateDir, webDir, { recursive: true })
196
+
197
+ // The backend variant is the flat template plus an overlay: files the Eden/Better
198
+ // Auth stack replaces are deleted, the rest is copied over, and the workspace root
199
+ // is laid down around it.
200
+ const applyBackendOverlay = () => {
201
+ const overlay = path.join(cliRoot, 'backend-overlay')
202
+
203
+ for (const file of JSON.parse(fs.readFileSync(path.join(overlay, 'remove.json'), 'utf8'))) {
204
+ fs.rmSync(path.join(webDir, file), { recursive: true, force: true })
205
+ }
206
+ fs.cpSync(path.join(overlay, 'web'), webDir, { recursive: true })
207
+ fs.cpSync(path.join(overlay, `web-${router}`), webDir, { recursive: true })
208
+ // one .gitignore, at the workspace root
209
+ fs.rmSync(path.join(webDir, '_gitignore'), { force: true })
210
+
211
+ // dev proxy: same origin for the session cookie
212
+ patchFile(path.join(webDir, 'vite.config.ts'), source =>
213
+ source.replace(
214
+ 'export default defineConfig({\n',
215
+ "export default defineConfig({\n server: { proxy: { '/api': 'http://localhost:3000' } },\n"
216
+ )
217
+ )
218
+
219
+ const templatePkg = JSON.parse(fs.readFileSync(path.join(webDir, 'package.json'), 'utf8'))
220
+ editJson(path.join(webDir, 'package.json'), pkg => {
221
+ pkg.name = '@repo/web'
222
+ delete pkg.dependencies.ky
223
+ Object.assign(pkg.dependencies, BACKEND_WEB_DEPS)
224
+ pkg.dependencies = Object.fromEntries(
225
+ Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b))
226
+ )
227
+ // type-only import (Eden); as a runtime dep it would drag elysia/drizzle into
228
+ // the web app's production dependency graph
229
+ pkg.devDependencies['@repo/api'] = 'workspace:*'
230
+ pkg.devDependencies = Object.fromEntries(
231
+ Object.entries(pkg.devDependencies).sort(([a], [b]) => a.localeCompare(b))
232
+ )
233
+ // lint and format run once, from the workspace root
234
+ for (const script of ['lint', 'lint:fix', 'format', 'format:check']) delete pkg.scripts[script]
235
+ delete pkg.trustedDependencies
236
+ })
237
+
238
+ const apiDir = path.join(targetDir, 'apps', 'api')
239
+ fs.cpSync(path.join(cliRoot, 'templates', 'template-api'), apiDir, { recursive: true })
240
+ if (!redis) fs.rmSync(path.join(apiDir, 'src', 'redis.ts'))
241
+ for (const file of filesWithRedisMarkers(apiDir)) applyRedisMarkers(file, redis)
242
+ // an unbalanced marker would otherwise ship Redis code into a --no-redis project
243
+ const leftover = filesWithRedisMarkers(apiDir)
244
+ if (leftover.length > 0) {
245
+ cancel(`Unbalanced redis: markers in ${leftover.join(', ')} - this is a template bug.`)
246
+ process.exit(1)
247
+ }
248
+ fs.cpSync(path.join(overlay, 'root'), targetDir, { recursive: true })
249
+ restoreGitignore(targetDir)
250
+
251
+ // Claude Code config belongs to the workspace root, not the web app
252
+ for (const file of ['.claude', '.mcp.json', 'CLAUDE.md']) {
253
+ fs.renameSync(path.join(webDir, file), path.join(targetDir, file))
254
+ }
255
+ patchFile(
256
+ path.join(targetDir, 'CLAUDE.md'),
257
+ source =>
258
+ source
259
+ .replace('`@/` → `src/`.', '`@/` → `apps/web/src/`.')
260
+ .replaceAll('`src/', '`apps/web/src/') +
261
+ '\n## Backend\n\n`apps/api` (Elysia + Drizzle + Better Auth) has its own `CLAUDE.md`. The web app talks to it through the Eden client; the Vite dev proxy keeps the session cookie first-party.\n'
262
+ )
263
+
264
+ for (const file of ['.oxfmtrc.json', '.oxlintrc.json']) {
265
+ const dest = path.join(targetDir, file)
266
+ fs.renameSync(path.join(webDir, file), dest)
267
+ // textual edit: re-serialising would break the oxfmt style the file itself enforces
268
+ patchFile(dest, source => source.replaceAll('"src/', '"apps/web/src/'))
269
+ }
270
+ editJson(path.join(targetDir, 'package.json'), pkg => {
271
+ pkg.name = name
272
+ pkg.devDependencies.oxfmt = templatePkg.devDependencies.oxfmt
273
+ pkg.devDependencies.oxlint = templatePkg.devDependencies.oxlint
274
+ })
275
+
276
+ for (const file of [
277
+ 'README.md',
278
+ path.join('apps', 'web', 'src', 'routes', '_authenticated', 'index.tsx')
279
+ ]) {
280
+ patchFile(path.join(targetDir, file), source => source.replaceAll('PLACEHOLDER', name))
281
+ }
282
+ }
99
283
 
100
- // lesson 003 §2: npm strips .gitignore from packages, so it ships as _gitignore
101
- const renameFiles = { _gitignore: '.gitignore' }
102
- for (const [from, to] of Object.entries(renameFiles)) {
103
- const src = path.join(targetDir, from)
104
- if (fs.existsSync(src)) fs.renameSync(src, path.join(targetDir, to))
284
+ if (backend) {
285
+ applyBackendOverlay()
286
+ } else {
287
+ restoreGitignore(targetDir)
288
+ editJson(path.join(targetDir, 'package.json'), pkg => {
289
+ pkg.name = name
290
+ })
291
+ // textual edits: re-serialising would break the oxfmt style the project enforces
292
+ if (pm !== 'bun') {
293
+ patchFile(path.join(targetDir, '.claude', 'launch.json'), source =>
294
+ source.replace('"runtimeExecutable": "bun"', `"runtimeExecutable": "${pm}"`)
295
+ )
296
+ // MCP servers start through the pm's package runner
297
+ patchFile(path.join(targetDir, '.mcp.json'), source =>
298
+ pm === 'pnpm'
299
+ ? source
300
+ .replaceAll('"command": "bunx"', '"command": "pnpm"')
301
+ .replaceAll('"args": [', '"args": ["dlx", ')
302
+ : source.replaceAll('"command": "bunx"', '"command": "npx"')
303
+ )
304
+ }
105
305
  }
106
306
 
107
- const pkgPath = path.join(targetDir, 'package.json')
108
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
109
- pkg.name = name
110
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
307
+ const label = `${ROUTER_LABELS[router]}${backend ? ' + Elysia/Drizzle/Better Auth' : ''}${redis ? ' + Redis' : ''}`
308
+ const steps = backend
309
+ ? [
310
+ ...(inPlace ? [] : [`cd ${target}`]),
311
+ 'bun install',
312
+ `cp apps/api/.env.example apps/api/.env # set DATABASE_URL${redis ? ', REDIS_URL' : ''}, BETTER_AUTH_SECRET`,
313
+ 'bun run db:migrate',
314
+ 'bun run dev'
315
+ ]
316
+ : [...(inPlace ? [] : [`cd ${target}`]), `${pm} install`, `${pm} run dev`]
111
317
 
112
- outro(`✔ ${name} created (${router === 'start' ? 'TanStack Start' : 'TanStack Router'})
318
+ outro(`✔ ${name} created (${label})
113
319
 
114
320
  Next steps:
115
- cd ${name}
116
- ${pm} install
117
- ${pm} run dev`)
321
+ ${steps.map(s => ` ${s}`).join('\n')}`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-top-secret-starter",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Scaffold a Vite + React 19 + TypeScript + shadcn starter with TanStack Router",
5
5
  "keywords": [
6
6
  "create",
@@ -28,6 +28,6 @@
28
28
  "@clack/prompts": "^1.7.0"
29
29
  },
30
30
  "engines": {
31
- "node": ">=20"
31
+ "node": ">=20.12"
32
32
  }
33
33
  }
@@ -0,0 +1,13 @@
1
+ DATABASE_URL=postgres://user:password@host:5432/dbname
2
+ # openssl rand -base64 32
3
+ BETTER_AUTH_SECRET=
4
+ # public origin the browser talks to — the Vite dev server when proxying /api;
5
+ # https in production (also enables secure cookies)
6
+ BETTER_AUTH_URL=http://localhost:5173
7
+ PORT=3000
8
+ # comma-separated extra origins; leave empty when the SPA is served from the same origin
9
+ CORS_ORIGINS=
10
+ # redis:start
11
+ # optional: sessions + rate-limit counters in Redis instead of Postgres/memory
12
+ REDIS_URL=redis://localhost:6379
13
+ # redis:end
@@ -0,0 +1,32 @@
1
+ # apps/api
2
+
3
+ Elysia + Drizzle (Postgres via postgres.js) + Better Auth. Bun workspace member;
4
+ root `verify` covers it. Tests run on PGlite with the committed migrations — no
5
+ server needed.
6
+
7
+ - Feature layout per [Elysia best practices](https://elysiajs.com/essential/best-practice):
8
+ `src/modules/<feature>/{index,service,model}.ts` — controller / plain static-class
9
+ service / `t` schemas. Services never touch `Context`; one Elysia instance is one
10
+ controller.
11
+ - `src/app.ts` is the Eden boundary: no Bun globals, no `process.env`, deps injected
12
+ via `createApp`.
13
+ - Wire values are JSON-shaped (`createdAt` is an ISO string, not `Date`) so Eden's
14
+ types match what the client receives.
15
+ - `src/db/auth-schema.ts` is hand-maintained in the better-auth **1.7** shape
16
+ (`account.issuer`). `@better-auth/cli generate` lags the library — don't use it.
17
+ - Elysia's `status(204)` works under Bun but 500s under Node (vitest); return
18
+ `new Response(null, { status: 204 })` so tests pass.
19
+ - Schema change → `bun run db:generate`, commit `drizzle/`.
20
+
21
+ <!-- redis:start -->
22
+
23
+ - `REDIS_URL` is optional: when set, `src/redis.ts` (Bun's built-in `RedisClient`)
24
+ becomes Better Auth's `secondaryStorage` for sessions and rate limiting.
25
+
26
+ <!-- redis:end -->
27
+
28
+ - Deploy target is Railway (Railpack, no Dockerfile): service root `apps/api`,
29
+ `start` runs migrations then serves. `BETTER_AUTH_URL` must be the public https
30
+ origin — Better Auth derives the secure cookie flag from it.
31
+ - `@repo/api` is a **dev**Dependency of the web app: the import is type-only, and as
32
+ a runtime dep it drags elysia/drizzle into the web production graph.
@@ -0,0 +1,64 @@
1
+ CREATE TABLE "note" (
2
+ "id" text PRIMARY KEY NOT NULL,
3
+ "title" text NOT NULL,
4
+ "user_id" text NOT NULL,
5
+ "created_at" timestamp DEFAULT now() NOT NULL
6
+ );
7
+ --> statement-breakpoint
8
+ CREATE TABLE "account" (
9
+ "id" text PRIMARY KEY NOT NULL,
10
+ "issuer" text NOT NULL,
11
+ "account_id" text NOT NULL,
12
+ "provider_id" text NOT NULL,
13
+ "user_id" text NOT NULL,
14
+ "access_token" text,
15
+ "refresh_token" text,
16
+ "id_token" text,
17
+ "access_token_expires_at" timestamp,
18
+ "refresh_token_expires_at" timestamp,
19
+ "scope" text,
20
+ "password" text,
21
+ "created_at" timestamp DEFAULT now() NOT NULL,
22
+ "updated_at" timestamp NOT NULL
23
+ );
24
+ --> statement-breakpoint
25
+ CREATE TABLE "session" (
26
+ "id" text PRIMARY KEY NOT NULL,
27
+ "expires_at" timestamp NOT NULL,
28
+ "token" text NOT NULL,
29
+ "created_at" timestamp DEFAULT now() NOT NULL,
30
+ "updated_at" timestamp NOT NULL,
31
+ "ip_address" text,
32
+ "user_agent" text,
33
+ "user_id" text NOT NULL,
34
+ CONSTRAINT "session_token_unique" UNIQUE("token")
35
+ );
36
+ --> statement-breakpoint
37
+ CREATE TABLE "user" (
38
+ "id" text PRIMARY KEY NOT NULL,
39
+ "name" text NOT NULL,
40
+ "email" text NOT NULL,
41
+ "email_verified" boolean DEFAULT false NOT NULL,
42
+ "image" text,
43
+ "created_at" timestamp DEFAULT now() NOT NULL,
44
+ "updated_at" timestamp DEFAULT now() NOT NULL,
45
+ CONSTRAINT "user_email_unique" UNIQUE("email")
46
+ );
47
+ --> statement-breakpoint
48
+ CREATE TABLE "verification" (
49
+ "id" text PRIMARY KEY NOT NULL,
50
+ "identifier" text NOT NULL,
51
+ "value" text NOT NULL,
52
+ "expires_at" timestamp NOT NULL,
53
+ "created_at" timestamp DEFAULT now() NOT NULL,
54
+ "updated_at" timestamp DEFAULT now() NOT NULL
55
+ );
56
+ --> statement-breakpoint
57
+ ALTER TABLE "note" ADD CONSTRAINT "note_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
58
+ ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
59
+ ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
60
+ CREATE INDEX "note_userId_idx" ON "note" USING btree ("user_id");--> statement-breakpoint
61
+ CREATE UNIQUE INDEX "account_issuer_accountId_uidx" ON "account" USING btree ("issuer","account_id");--> statement-breakpoint
62
+ CREATE INDEX "account_userId_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
63
+ CREATE INDEX "session_userId_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
64
+ CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");