create-restack-pattern 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/dist/index.js +240 -67
  2. package/package.json +14 -8
  3. package/template/{DEVELOPMENT.md → base/DEVELOPMENT.md} +73 -62
  4. package/template/{backend → base/backend}/package.json +13 -10
  5. package/template/{backend → base/backend}/src/config/env.ts +2 -2
  6. package/template/{backend → base/backend}/src/domains/auth/auth.model.ts +5 -3
  7. package/template/{backend → base/backend}/src/domains/auth/auth.repo.ts +23 -5
  8. package/template/{backend → base/backend}/src/domains/auth/auth.routes.ts +77 -22
  9. package/template/{backend → base/backend}/src/domains/auth/auth.service.test.ts +51 -19
  10. package/template/{backend → base/backend}/src/domains/auth/auth.service.ts +15 -10
  11. package/template/{backend → base/backend}/src/domains/auth/auth.types.ts +1 -1
  12. package/template/{backend → base/backend}/src/domains/user/user.model.ts +2 -1
  13. package/template/{backend → base/backend}/src/domains/user/user.repo.ts +3 -2
  14. package/template/base/backend/src/domains/user/user.routes.test.ts +127 -0
  15. package/template/{backend → base/backend}/src/domains/user/user.routes.ts +19 -9
  16. package/template/{backend → base/backend}/src/domains/user/user.service.ts +8 -2
  17. package/template/{backend → base/backend}/src/hono-app.ts +6 -11
  18. package/template/base/backend/src/infra/middlewares/require-auth.test.ts +91 -0
  19. package/template/base/backend/src/infra/middlewares/require-auth.ts +63 -0
  20. package/template/base/backend/src/infra/middlewares/require-role.test.ts +66 -0
  21. package/template/base/backend/src/infra/middlewares/require-role.ts +34 -0
  22. package/template/{backend → base/backend}/src/utils/logger.ts +7 -10
  23. package/template/base/backend/src/utils/uuid.test.ts +37 -0
  24. package/template/{backend → base/backend}/tsconfig.json +4 -0
  25. package/template/base/frontend/AGENTS.md +11 -0
  26. package/template/{frontend → base/frontend}/app/(auth)/login/page.tsx +1 -1
  27. package/template/{frontend → base/frontend}/app/(auth)/register/page.tsx +1 -1
  28. package/template/{frontend → base/frontend}/app/globals.css +1 -1
  29. package/template/{frontend → base/frontend}/app/layout.tsx +9 -12
  30. package/template/{frontend → base/frontend}/app/page.tsx +2 -2
  31. package/template/{frontend → base/frontend}/app/profile/page.tsx +3 -3
  32. package/template/{frontend → base/frontend}/components/ui/button.tsx +2 -2
  33. package/template/{frontend → base/frontend}/components/ui/card.tsx +8 -23
  34. package/template/{frontend → base/frontend}/components/ui/input.tsx +2 -2
  35. package/template/{frontend → base/frontend}/components/ui/label.tsx +3 -6
  36. package/template/{frontend → base/frontend}/domains/auth/components/login-form.tsx +10 -3
  37. package/template/{frontend → base/frontend}/domains/auth/components/logout-button.tsx +2 -2
  38. package/template/{frontend → base/frontend}/domains/auth/components/register-form.tsx +10 -3
  39. package/template/{frontend → base/frontend}/domains/auth/hooks/use-login.ts +2 -2
  40. package/template/{frontend → base/frontend}/domains/auth/hooks/use-register.ts +6 -3
  41. package/template/{frontend → base/frontend}/domains/auth/services/auth.api.ts +5 -7
  42. package/template/{frontend → base/frontend}/domains/user/components/profile-card.tsx +2 -4
  43. package/template/{frontend → base/frontend}/domains/user/services/user.api.ts +2 -3
  44. package/template/base/frontend/env.ts +12 -0
  45. package/template/{frontend → base/frontend}/hooks/use-auth-mutation.ts +6 -3
  46. package/template/{frontend → base/frontend}/lib/api-client.ts +40 -5
  47. package/template/{frontend → base/frontend}/lib/utils.ts +1 -1
  48. package/template/base/frontend/next.config.ts +11 -0
  49. package/template/{frontend → base/frontend}/package.json +15 -14
  50. package/template/{frontend → base/frontend}/tsconfig.json +4 -0
  51. package/template/{gitignore → base/gitignore} +15 -0
  52. package/template/base/package.json +25 -0
  53. package/template/{packages → base/packages}/shared/package.json +1 -1
  54. package/template/{packages → base/packages}/shared/src/index.ts +1 -0
  55. package/template/{packages → base/packages}/shared/src/schemas/auth.schema.ts +8 -0
  56. package/template/{packages → base/packages}/shared/src/schemas/user.schema.ts +1 -0
  57. package/template/base/packages/shared/src/utils/uuid.ts +40 -0
  58. package/template/{packages → base/packages}/shared/tsconfig.json +26 -0
  59. package/template/base/pnpm-workspace.yaml +12 -0
  60. package/template/overlays/db-postgres/backend/.env.example +6 -0
  61. package/template/overlays/db-postgres/backend/drizzle.config.ts +11 -0
  62. package/template/overlays/db-postgres/backend/scripts/db-check.js +27 -0
  63. package/template/overlays/db-postgres/backend/src/infra/db/index.ts +6 -0
  64. package/template/overlays/db-sqlite/backend/.env.example +6 -0
  65. package/template/overlays/db-sqlite/backend/drizzle.config.ts +11 -0
  66. package/template/overlays/db-sqlite/backend/scripts/db-check.js +20 -0
  67. package/template/overlays/db-sqlite/backend/src/infra/db/index.ts +8 -0
  68. package/template/overlays/id-serial/packages/shared/src/schemas/user.schema.ts +14 -0
  69. package/template/overlays/id-uuid/packages/shared/src/schemas/user.schema.ts +14 -0
  70. package/template/overlays/models/postgres-serial/backend/src/domains/auth/auth.model.ts +12 -0
  71. package/template/overlays/models/postgres-serial/backend/src/domains/user/user.model.ts +10 -0
  72. package/template/overlays/models/postgres-uuid/backend/src/domains/auth/auth.model.ts +15 -0
  73. package/template/overlays/models/postgres-uuid/backend/src/domains/user/user.model.ts +13 -0
  74. package/template/overlays/models/sqlite-serial/backend/src/domains/auth/auth.model.ts +14 -0
  75. package/template/overlays/models/sqlite-serial/backend/src/domains/user/user.model.ts +12 -0
  76. package/template/overlays/models/sqlite-uuid/backend/src/domains/auth/auth.model.ts +17 -0
  77. package/template/overlays/models/sqlite-uuid/backend/src/domains/user/user.model.ts +15 -0
  78. package/template/backend/src/domains/user/user.routes.test.ts +0 -82
  79. package/template/backend/src/infra/middlewares/require-auth.test.ts +0 -51
  80. package/template/backend/src/infra/middlewares/require-auth.ts +0 -36
  81. package/template/frontend/AGENTS.md +0 -5
  82. package/template/frontend/next.config.ts +0 -7
  83. package/template/package.json +0 -17
  84. package/template/pnpm-workspace.yaml +0 -4
  85. package/template/{backend → base/backend}/.env.example +0 -0
  86. package/template/{backend → base/backend}/README.md +0 -0
  87. package/template/{backend → base/backend}/api/index.ts +0 -0
  88. package/template/{backend → base/backend}/drizzle/0000_grey_lethal_legion.sql +0 -0
  89. package/template/{backend → base/backend}/drizzle/meta/0000_snapshot.json +0 -0
  90. package/template/{backend → base/backend}/drizzle/meta/_journal.json +0 -0
  91. package/template/{backend → base/backend}/drizzle.config.ts +0 -0
  92. package/template/{backend → base/backend}/gitignore +0 -0
  93. package/template/{backend → base/backend}/public/.gitkeep +0 -0
  94. package/template/{backend → base/backend}/scripts/db-check.js +0 -0
  95. package/template/{backend → base/backend}/src/config/.gitkeep +0 -0
  96. package/template/{backend → base/backend}/src/domains/auth/.gitkeep +0 -0
  97. package/template/{backend → base/backend}/src/domains/user/.gitkeep +0 -0
  98. package/template/{backend → base/backend}/src/index.ts +1 -1
  99. package/template/{backend → base/backend}/src/infra/db/.gitkeep +0 -0
  100. package/template/{backend → base/backend}/src/infra/db/index.ts +0 -0
  101. package/template/{backend → base/backend}/src/infra/middlewares/.gitkeep +0 -0
  102. package/template/{backend → base/backend}/vercel.json +0 -0
  103. package/template/{frontend → base/frontend}/.env.example +0 -0
  104. package/template/{frontend → base/frontend}/CLAUDE.md +0 -0
  105. package/template/{frontend → base/frontend}/README.md +0 -0
  106. package/template/{frontend → base/frontend}/app/dashboard/page.tsx +2 -2
  107. package/template/{frontend → base/frontend}/app/icon.png +0 -0
  108. package/template/{frontend → base/frontend}/components/require-auth.tsx +1 -1
  109. package/template/{frontend → base/frontend}/components.json +0 -0
  110. package/template/{frontend → base/frontend}/domains/auth/components/.gitkeep +0 -0
  111. package/template/{frontend → base/frontend}/domains/auth/hooks/.gitkeep +0 -0
  112. package/template/{frontend → base/frontend}/domains/auth/services/.gitkeep +0 -0
  113. package/template/{frontend → base/frontend}/domains/user/components/.gitkeep +0 -0
  114. package/template/{frontend → base/frontend}/domains/user/hooks/use-profile.ts +2 -2
  115. /package/template/{frontend → base/frontend}/eslint.config.mjs +0 -0
  116. /package/template/{frontend → base/frontend}/gitignore +0 -0
  117. /package/template/{frontend → base/frontend}/postcss.config.mjs +0 -0
  118. /package/template/{frontend → base/frontend}/public/favicon.ico +0 -0
  119. /package/template/{frontend → base/frontend}/public/logo.png +0 -0
package/dist/index.js CHANGED
@@ -1,20 +1,24 @@
1
1
  #!/usr/bin/env node
2
- import { cpSync, existsSync, readFileSync, writeFileSync, readdirSync, renameSync } from "node:fs";
3
2
  import { execSync } from "node:child_process";
4
- import { createInterface } from "node:readline/promises";
5
- import { fileURLToPath } from "node:url";
3
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import * as p from "@clack/prompts";
7
+ import color from "picocolors";
7
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
9
  const templateDir = path.resolve(__dirname, "..", "template");
9
- async function promptProjectName() {
10
- const rl = createInterface({ input: process.stdin, output: process.stdout });
11
- const answer = await rl.question("Project name: ");
12
- rl.close();
13
- return answer.trim();
14
- }
15
10
  function isValidProjectName(name) {
16
11
  return /^[a-z0-9-_]+$/i.test(name);
17
12
  }
13
+ function isCommandAvailable(command) {
14
+ try {
15
+ execSync(`${command} --version`, { stdio: "ignore" });
16
+ return true;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
18
22
  // Undoes sync-template.mjs's .gitignore -> gitignore rename (npm strips
19
23
  // files literally named .gitignore from the published tarball).
20
24
  function renameGitignoreFilesBack(dir) {
@@ -26,85 +30,254 @@ function renameGitignoreFilesBack(dir) {
26
30
  renameSync(full, path.join(dir, ".gitignore"));
27
31
  }
28
32
  }
29
- function isPnpmInstalled() {
30
- try {
31
- execSync("pnpm --version", { stdio: "ignore" });
32
- return true;
33
- }
34
- catch {
35
- return false;
36
- }
37
- }
38
33
  async function main() {
39
- if (!isPnpmInstalled()) {
40
- console.error("\n\x1b[31mError: pnpm is not installed.\x1b[0m");
41
- console.error("This template is configured as a pnpm workspace monorepo and requires pnpm to run.");
42
- console.error("Please install pnpm before continuing:");
43
- console.error(" npm install -g pnpm");
44
- console.error("Or visit https://pnpm.io for more installation options.\n");
45
- process.exit(1);
34
+ if (process.argv.includes("--help") || process.argv.includes("-h")) {
35
+ console.log(`
36
+ Usage: create-restack-pattern [project-name]
37
+
38
+ Options:
39
+ -h, --help Show help message
40
+ -v, --version Show CLI version
41
+
42
+ Examples:
43
+ pnpm create restack-pattern my-saas-app
44
+ npm create restack-pattern@latest
45
+ bun create restack-pattern@latest
46
+ `);
47
+ process.exit(0);
46
48
  }
47
- const argName = process.argv[2];
48
- const projectName = argName ?? (await promptProjectName());
49
- if (!projectName || !isValidProjectName(projectName)) {
50
- console.error("Invalid project name. Use letters, numbers, hyphens, or underscores only.");
51
- process.exit(1);
49
+ if (process.argv.includes("--version") || process.argv.includes("-v")) {
50
+ const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8"));
51
+ console.log(pkg.version);
52
+ process.exit(0);
53
+ }
54
+ console.log();
55
+ p.intro(color.bgCyan(color.black(" create-restack-pattern ")));
56
+ let projectName = process.argv[2];
57
+ if (!projectName || projectName.startsWith("-")) {
58
+ const nameInput = await p.text({
59
+ message: "What is your project named?",
60
+ placeholder: "my-restack-app",
61
+ defaultValue: "my-restack-app",
62
+ validate: (value) => {
63
+ if (!value)
64
+ return "Project name cannot be empty";
65
+ if (!isValidProjectName(value)) {
66
+ return "Project name may only contain letters, numbers, hyphens, and underscores";
67
+ }
68
+ if (existsSync(path.resolve(process.cwd(), value))) {
69
+ return `Directory "${value}" already exists in the current directory`;
70
+ }
71
+ },
72
+ });
73
+ if (p.isCancel(nameInput)) {
74
+ p.cancel("Operation cancelled.");
75
+ process.exit(0);
76
+ }
77
+ projectName = nameInput;
52
78
  }
53
79
  const targetDir = path.resolve(process.cwd(), projectName);
54
80
  if (existsSync(targetDir)) {
55
- console.error(`Directory "${projectName}" already exists.`);
81
+ p.cancel(color.red(`Error: Directory "${projectName}" already exists.`));
56
82
  process.exit(1);
57
83
  }
58
- console.log(`Scaffolding into ${targetDir} ...`);
59
- cpSync(templateDir, targetDir, { recursive: true });
84
+ const database = (await p.select({
85
+ message: "Select your database:",
86
+ options: [
87
+ {
88
+ value: "postgres",
89
+ label: "PostgreSQL",
90
+ hint: "Neon, Supabase, Docker, AWS RDS (Recommended for SaaS)",
91
+ },
92
+ {
93
+ value: "sqlite",
94
+ label: "SQLite",
95
+ hint: "Zero-config local file, Turso, Cloudflare D1",
96
+ },
97
+ ],
98
+ initialValue: "postgres",
99
+ }));
100
+ if (p.isCancel(database)) {
101
+ p.cancel("Operation cancelled.");
102
+ process.exit(0);
103
+ }
104
+ const idStrategy = (await p.select({
105
+ message: "Select primary key strategy:",
106
+ options: [
107
+ {
108
+ value: "uuid",
109
+ label: "UUIDv7",
110
+ hint: "Time-sortable, unguessable, secure against enumeration (Recommended)",
111
+ },
112
+ {
113
+ value: "serial",
114
+ label: "Serial Integer",
115
+ hint: "Auto-incrementing 1, 2, 3... (Simple relational lookups)",
116
+ },
117
+ ],
118
+ initialValue: "uuid",
119
+ }));
120
+ if (p.isCancel(idStrategy)) {
121
+ p.cancel("Operation cancelled.");
122
+ process.exit(0);
123
+ }
124
+ const packageManager = (await p.select({
125
+ message: "Select package manager:",
126
+ options: [
127
+ {
128
+ value: "pnpm",
129
+ label: "pnpm",
130
+ hint: "Fast, disk-efficient workspace monorepo (Recommended)",
131
+ },
132
+ {
133
+ value: "npm",
134
+ label: "npm",
135
+ hint: "Default Node.js package manager",
136
+ },
137
+ {
138
+ value: "bun",
139
+ label: "bun",
140
+ hint: "Fast all-in-one JavaScript runtime & toolkit",
141
+ },
142
+ ],
143
+ initialValue: "pnpm",
144
+ }));
145
+ if (p.isCancel(packageManager)) {
146
+ p.cancel("Operation cancelled.");
147
+ process.exit(0);
148
+ }
149
+ const s = p.spinner();
150
+ // 1. Scaffold files
151
+ s.start(`Scaffolding ${projectName} (${database} + ${idStrategy}) ...`);
152
+ const baseDir = path.join(templateDir, "base");
153
+ const overlaysDir = path.join(templateDir, "overlays");
154
+ // Copy base template
155
+ cpSync(baseDir, targetDir, { recursive: true });
156
+ // Overlay database configuration
157
+ const dbOverlayDir = path.join(overlaysDir, `db-${database}`);
158
+ if (existsSync(dbOverlayDir)) {
159
+ cpSync(dbOverlayDir, targetDir, { recursive: true });
160
+ }
161
+ // Overlay models
162
+ const modelOverlayDir = path.join(overlaysDir, "models", `${database}-${idStrategy}`);
163
+ if (existsSync(modelOverlayDir)) {
164
+ cpSync(modelOverlayDir, targetDir, { recursive: true });
165
+ }
166
+ // Overlay shared schema
167
+ const idOverlayDir = path.join(overlaysDir, `id-${idStrategy}`);
168
+ if (existsSync(idOverlayDir)) {
169
+ cpSync(idOverlayDir, targetDir, { recursive: true });
170
+ }
60
171
  renameGitignoreFilesBack(targetDir);
172
+ // 2. Patch package.json files
61
173
  const rootPkgPath = path.join(targetDir, "package.json");
62
174
  const rootPkg = JSON.parse(readFileSync(rootPkgPath, "utf-8"));
63
175
  rootPkg.name = projectName;
64
- writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + "\n");
65
- console.log("Installing dependencies (pnpm install) ...");
66
- execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
67
- // packages/shared ships no dist/ (gitignored) — backend/frontend resolve
68
- // @restack/shared to compiled output, so it must be built once up front
69
- // or the very first `pnpm dev` fails to resolve the import.
70
- console.log("Building @restack/shared ...");
71
- execSync("pnpm --filter @restack/shared build", { cwd: targetDir, stdio: "inherit" });
176
+ if (packageManager !== "pnpm") {
177
+ rootPkg.workspaces = ["frontend", "backend", "packages/*"];
178
+ }
179
+ writeFileSync(rootPkgPath, `${JSON.stringify(rootPkg, null, 2)}\n`);
180
+ const backendPkgPath = path.join(targetDir, "backend/package.json");
181
+ if (existsSync(backendPkgPath)) {
182
+ const backendPkg = JSON.parse(readFileSync(backendPkgPath, "utf-8"));
183
+ if (database === "sqlite") {
184
+ if (backendPkg.dependencies) {
185
+ delete backendPkg.dependencies.postgres;
186
+ backendPkg.dependencies["better-sqlite3"] = "^13.0.3";
187
+ }
188
+ if (backendPkg.devDependencies) {
189
+ backendPkg.devDependencies["@types/better-sqlite3"] = "^7.6.13";
190
+ }
191
+ }
192
+ else {
193
+ if (backendPkg.dependencies) {
194
+ delete backendPkg.dependencies["better-sqlite3"];
195
+ }
196
+ if (backendPkg.devDependencies) {
197
+ delete backendPkg.devDependencies["@types/better-sqlite3"];
198
+ }
199
+ }
200
+ if (packageManager !== "pnpm") {
201
+ if (backendPkg.dependencies?.["@restack/shared"] === "workspace:*") {
202
+ backendPkg.dependencies["@restack/shared"] = "*";
203
+ }
204
+ }
205
+ writeFileSync(backendPkgPath, `${JSON.stringify(backendPkg, null, 2)}\n`);
206
+ }
207
+ const frontendPkgPath = path.join(targetDir, "frontend/package.json");
208
+ if (existsSync(frontendPkgPath) && packageManager !== "pnpm") {
209
+ const frontendPkg = JSON.parse(readFileSync(frontendPkgPath, "utf-8"));
210
+ if (frontendPkg.dependencies?.["@restack/shared"] === "workspace:*") {
211
+ frontendPkg.dependencies["@restack/shared"] = "*";
212
+ writeFileSync(frontendPkgPath, `${JSON.stringify(frontendPkg, null, 2)}\n`);
213
+ }
214
+ }
215
+ s.stop(color.green(`Scaffolded ${projectName} files successfully!`));
216
+ // 3. Install dependencies
217
+ if (isCommandAvailable(packageManager)) {
218
+ s.start(`Installing dependencies using ${packageManager} ...`);
219
+ try {
220
+ execSync(`${packageManager} install`, { cwd: targetDir, stdio: "ignore" });
221
+ s.stop(color.green(`Dependencies installed using ${packageManager}!`));
222
+ }
223
+ catch {
224
+ s.stop(color.yellow(`Could not automatically run ${packageManager} install. You can run it manually.`));
225
+ }
226
+ // 4. Build @restack/shared
227
+ s.start("Building @restack/shared package ...");
228
+ try {
229
+ if (packageManager === "pnpm") {
230
+ execSync("pnpm --filter @restack/shared build", { cwd: targetDir, stdio: "ignore" });
231
+ }
232
+ else if (packageManager === "bun") {
233
+ execSync("bun --filter @restack/shared build", { cwd: targetDir, stdio: "ignore" });
234
+ }
235
+ else {
236
+ execSync("npm run build --workspace=@restack/shared", { cwd: targetDir, stdio: "ignore" });
237
+ }
238
+ s.stop(color.green("@restack/shared package built!"));
239
+ }
240
+ catch {
241
+ s.stop(color.yellow("Could not automatically build @restack/shared. Run the build script manually."));
242
+ }
243
+ }
244
+ else {
245
+ p.log.warn(color.yellow(`Note: ${packageManager} was not found on your system. Run '${packageManager} install' manually.`));
246
+ }
247
+ // 5. Initialize git
72
248
  try {
73
249
  execSync("git init", { cwd: targetDir, stdio: "ignore" });
74
250
  }
75
251
  catch {
76
- // git not available — non-fatal, scaffolding still succeeded
252
+ // Non-fatal
77
253
  }
78
- console.log(`
79
- \x1b[32m✔ Scaffolded project successfully!\x1b[0m
80
-
81
- Next steps to get started:
254
+ const dbConfigStep = database === "postgres"
255
+ ? `• For Backend:
256
+ ${color.cyan("cp backend/.env.example backend/.env")}
257
+ (Set ${color.yellow("DATABASE_URL")} and a min-32-character ${color.yellow("JWT_SECRET")})`
258
+ : `• For Backend:
259
+ ${color.cyan("cp backend/.env.example backend/.env")}
260
+ (SQLite database file will be created automatically at ${color.yellow("sqlite.db")})`;
261
+ p.note(`1. Go to your project directory:
262
+ ${color.cyan(`cd ${projectName}`)}
82
263
 
83
- 1. Go to your project directory:
84
- \x1b[36mcd ${projectName}\x1b[0m
264
+ 2. Configure environment variables:
265
+ ${dbConfigStep}
85
266
 
86
- 2. Configure environment variables:
87
- • For Backend:
88
- \x1b[36mcp backend/.env.example backend/.env\x1b[0m
89
- (Open backend/.env and set \x1b[33mDATABASE_URL\x1b[0m and a min-32-chars \x1b[33mJWT_SECRET\x1b[0m)
267
+ • For Frontend:
268
+ ${color.cyan("cp frontend/.env.example frontend/.env")}
90
269
 
91
- • For Frontend:
92
- \x1b[36mcp frontend/.env.example frontend/.env\x1b[0m
270
+ 3. Push database tables:
271
+ ${color.cyan(`${packageManager} --filter backend db:push`)}
93
272
 
94
- 3. Push database tables (creates users & auth_sessions tables):
95
- \x1b[36mpnpm --filter backend db:push\x1b[0m
96
-
97
- 4. Start development servers:
98
- • Backend (running at http://localhost:3001):
99
- \x1b[36mpnpm --filter backend dev\x1b[0m
100
-
101
- • Frontend (running at http://localhost:3000):
102
- \x1b[36mpnpm --filter frontend dev\x1b[0m
103
-
104
- Enjoy building your app with the Restack Pattern!
105
- `);
273
+ 4. Start development servers:
274
+ • Backend (API running at http://localhost:3001):
275
+ ${color.cyan(`${packageManager} --filter backend dev`)}
276
+ • Frontend (Next.js running at http://localhost:3000):
277
+ ${color.cyan(`${packageManager} --filter frontend dev`)}`, "Next steps to get started:");
278
+ p.outro(color.green(`All done! Enjoy building with the Restack Pattern!`));
106
279
  }
107
280
  main().catch((err) => {
108
- console.error(err);
281
+ p.cancel(color.red(String(err)));
109
282
  process.exit(1);
110
283
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-restack-pattern",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Scaffold a new Next.js + Hono + Drizzle monorepo using the Restack Pattern (Semi-DDD) template.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -12,18 +12,24 @@
12
12
  "dist",
13
13
  "template"
14
14
  ],
15
- "scripts": {
16
- "build": "tsc",
17
- "dev": "tsx src/index.ts",
18
- "sync-template": "node scripts/sync-template.mjs",
19
- "prepublishOnly": "pnpm sync-template && pnpm build"
15
+ "publishConfig": {
16
+ "access": "public"
20
17
  },
21
18
  "devDependencies": {
22
19
  "@types/node": "^20.11.17",
23
- "tsx": "^4.7.1",
20
+ "tsx": "^4.23.15",
24
21
  "typescript": "^5.8.3"
25
22
  },
26
23
  "engines": {
27
24
  "node": ">=20"
25
+ },
26
+ "dependencies": {
27
+ "@clack/prompts": "^1.8.1",
28
+ "picocolors": "^1.1.1"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "dev": "tsx src/index.ts",
33
+ "sync-template": "node scripts/sync-template.mjs"
28
34
  }
29
- }
35
+ }
@@ -1,62 +1,73 @@
1
- # DEVELOPMENT.md
2
-
3
- This document outlines the architecture, build guidelines, and code conventions for the Restack Pattern monorepo. All developers and automated tools contributing to this project must follow these rules.
4
-
5
- ## Project Stack
6
- - **Frontend**: Next.js 16 (App Router)
7
- - **Backend**: Hono
8
- - **ORM**: Drizzle ORM
9
- - **Database**: PostgreSQL (Postgres-js driver)
10
- - **Validation**: Zod (shared schemas)
11
- - **Package Manager**: pnpm workspaces
12
-
13
- ## Monorepo Layout
14
- - `frontend/` — Next.js 16 App Router
15
- - `backend/` — Hono API
16
- - `packages/shared/` — Shared Zod schemas (the single source of truth)
17
-
18
- ## Build and Dev Commands
19
- All commands should be run from the repository root:
20
- - `pnpm install` — Install all dependencies
21
- - `pnpm --filter @restack/shared build` — Build shared Zod schemas (run first!)
22
- - `pnpm --filter backend dev` — Start Hono dev server on port 3001
23
- - `pnpm --filter frontend dev` — Start Next.js dev server on port 3000
24
- - `pnpm --filter backend db:push` — Push Drizzle schema to PostgreSQL database
25
- - `pnpm --filter backend db:studio` — Open Drizzle Studio database viewer
26
- - `pnpm --filter backend test` — Run Vitest tests
27
-
28
- ## Architecture Rules: Semi-DDD (Feature-Driven)
29
- Code is organized by business domain/feature, not by file type.
30
-
31
- ### Backend Layout (`backend/src/domains/[feature]/`)
32
- Each domain has 4 primary files:
33
- - `[feature].routes.ts` — HTTP endpoints & input validation (imports Zod schema from `@restack/shared`)
34
- - `[feature].service.ts` — Pure business logic only (no direct DB access)
35
- - `[feature].repo.ts` — Drizzle queries only (no business logic)
36
- - `[feature].model.ts` — Drizzle table definitions
37
-
38
- **Cross-Domain Dependency Rule**: The `auth` domain may import from `user` (e.g. `usersTable` from `user.model.ts`). The reverse (user importing from auth) is STRICTLY FORBIDDEN.
39
-
40
- ### Frontend Layout (`frontend/domains/[feature]/`)
41
- - `components/` — UI components (no direct data fetching)
42
- - `hooks/` — Reactive state & data fetching logic
43
- - `services/` — Thin HTTP wrappers around `apiClient`
44
- - `types.ts` — Local TS type declarations
45
- - `frontend/app/` is strictly for layout, routing config, metadata, or entry gates. Do not implement custom JSX layouts, forms, state, or complex components here. Always delegate rendering to domain feature components.
46
-
47
- ## Critical Gotcha: Shared Package Build Requirement
48
- `@restack/shared`'s `package.json` resolves to `dist/`, which is gitignored. When editing Zod schemas in `packages/shared/src/schemas/`, you MUST compile them by running:
49
- `pnpm --filter @restack/shared build`
50
- (or keep `pnpm --filter @restack/shared dev` watch mode running in a terminal) or the frontend and backend will fail to resolve the updated types or throw compilation errors.
51
-
52
- ## Auth & Cookie Strategy
53
- - Short-lived JWT `access_token` (15m) and opaque `refresh_token` (30d) are both set as `httpOnly` cookies at `Path=/`.
54
- - Access tokens are automatically refreshed by the client-side `apiClient` on a 401 response.
55
- - Cookie configuration: `SameSite=None` in production (requires `Secure=true`), and `SameSite=Lax` in development (`Secure=false` since local dev runs on HTTP).
56
- - There is no server-side auth gate (`middleware.ts`/`proxy.ts`) because cookies cannot be read server-side when frontend and backend deploy to unrelated domains (Topology B). Authenticated routes are protected via backend `requireAuth` middleware, and frontend pages are protected via `<RequireAuth>` or client-side checks in hooks.
57
-
58
- ## Structured & Correlation Logging
59
- - Every incoming HTTP request is tagged with a unique `requestId` via Hono's `requestId` middleware.
60
- - A request-scoped child logger (`logger.child({ requestId })`) is bound to Hono's context.
61
- - In route handlers, always extract the request-specific child logger from Hono's context (`c.get("logger") || logger`) to log events. This ensures all logs generated during a request carry the correct `requestId` and `userId` context (which is automatically enriched by the `requireAuth` middleware).
62
-
1
+ # DEVELOPMENT.md
2
+
3
+ This document outlines the architecture, build guidelines, and code conventions for the Restack Pattern monorepo. All developers and automated tools contributing to this project must follow these rules.
4
+
5
+ ## Project Stack
6
+ - **Frontend**: Next.js 16 (App Router with Turbopack & Typed Routes)
7
+ - **Backend**: Hono
8
+ - **ORM**: Drizzle ORM
9
+ - **Databases**: PostgreSQL (`postgres-js`) & SQLite (`better-sqlite3`)
10
+ - **Primary Keys**: RFC 9562 UUIDv7 or Serial Auto-Increment
11
+ - **Validation**: Zod (shared schemas)
12
+ - **Quality**: Biome v2 (sub-50ms linter & formatter)
13
+ - **Package Manager**: pnpm workspaces (also supports npm & bun)
14
+
15
+ ## Monorepo Layout
16
+ - `frontend/` — Next.js 16 App Router
17
+ - `backend/` — Hono API
18
+ - `packages/shared/` — Shared Zod schemas and utilities (single source of truth)
19
+ - `packages/create-restack-pattern/` — Interactive CLI scaffolder published on npm
20
+
21
+ ## Build and Dev Commands
22
+ All commands should be run from the repository root:
23
+ - `pnpm install` — Install all dependencies
24
+ - `pnpm --filter @restack/shared build` — Build shared Zod schemas (run first!)
25
+ - `pnpm --filter backend dev` — Start Hono dev server on port 3001
26
+ - `pnpm --filter frontend dev` — Start Next.js dev server on port 3000
27
+ - `pnpm --filter backend db:push` — Push Drizzle schema to database
28
+ - `pnpm --filter backend db:studio` — Open Drizzle Studio database viewer
29
+ - `pnpm --filter backend test` — Run Vitest tests
30
+ - `pnpm lint` — Run Biome linter across all packages (<50ms)
31
+ - `pnpm format` — Auto-format all code with Biome
32
+
33
+ ## Architecture Rules: Semi-DDD (Feature-Driven)
34
+ Code is organized by business domain/feature, not by file type.
35
+
36
+ ### Backend Layout (`backend/src/domains/[feature]/`)
37
+ Each domain has 4 primary files:
38
+ - `[feature].routes.ts` — HTTP endpoints & input validation (imports Zod schema from `@restack/shared`)
39
+ - `[feature].service.ts` — Pure business logic only (no direct DB access)
40
+ - `[feature].repo.ts` — Drizzle queries only (no business logic)
41
+ - `[feature].model.ts` — Drizzle table definitions
42
+
43
+ **Cross-Domain Dependency Rule**: The `auth` domain may import from `user` (e.g. `usersTable` from `user.model.ts`). The reverse (user importing from auth) is STRICTLY FORBIDDEN.
44
+
45
+ ### Frontend Layout (`frontend/domains/[feature]/`)
46
+ - `components/` — UI components (no direct data fetching)
47
+ - `hooks/` — Reactive state & data fetching logic
48
+ - `services/` — Thin HTTP wrappers around `apiClient`
49
+ - `types.ts` — Local TS type declarations
50
+ - `frontend/app/` is strictly for layout, routing config, metadata, or entry gates. Do not implement custom JSX layouts, forms, state, or complex components here. Always delegate rendering to domain feature components.
51
+
52
+ ## Critical Gotcha: Shared Package Build Requirement
53
+ `@restack/shared`'s `package.json` resolves to `dist/`, which is gitignored. When editing Zod schemas in `packages/shared/src/schemas/`, you MUST compile them by running:
54
+ `pnpm --filter @restack/shared build`
55
+ (or keep `pnpm --filter @restack/shared dev` watch mode running in a terminal) or the frontend and backend will fail to resolve the updated types or throw compilation errors.
56
+
57
+ ## Auth & Cookie Strategy (Hybrid Auth & RBAC)
58
+ - **Hybrid Auth**: Accepts tokens via `Authorization: Bearer <token>` header first (for mobile, scripts, and external clients) or `access_token` HttpOnly cookie (for web browsers).
59
+ - **Dual Token Issuance**: Endpoints `/register`, `/login`, and `/refresh` return `{ user, accessToken, refreshToken }` in JSON body while simultaneously issuing HttpOnly cookies.
60
+ - **RBAC Guard**: Use `requireRole(...roles)` after `requireAuth` to restrict routes by role:
61
+ ```ts
62
+ userRoutes.get("/admin", requireAuth, requireRole("admin"), (c) => ...)
63
+ ```
64
+ - Short-lived JWT `access_token` (15m) and opaque `refresh_token` (30d) are both set as `httpOnly` cookies at `Path=/`.
65
+ - Access tokens are automatically refreshed by the client-side `apiClient` on a 401 response.
66
+ - Cookie configuration: `SameSite=None` in production (requires `Secure=true`), and `SameSite=Lax` in development (`Secure=false` since local dev runs on HTTP).
67
+
68
+ ## Structured & Correlation Logging
69
+ - Every incoming HTTP request is tagged with a unique `requestId` via Hono's `requestId` middleware.
70
+ - A request-scoped child logger (`logger.child({ requestId })`) is bound to Hono's context.
71
+ - In route handlers, always extract the request-specific child logger from Hono's context (`c.get("logger") || logger`) to log events. This ensures all logs generated during a request carry the correct `requestId`, `userId`, and `authMethod`.
72
+
73
+
@@ -11,23 +11,26 @@
11
11
  "test": "vitest run"
12
12
  },
13
13
  "dependencies": {
14
- "@hono/node-server": "^2.0.6",
15
- "@hono/zod-validator": "^0.8.0",
14
+ "@hono/node-server": "^2.1.5",
15
+ "@hono/zod-validator": "^0.9.1",
16
16
  "@restack/shared": "workspace:*",
17
17
  "bcryptjs": "^3.0.3",
18
+ "better-sqlite3": "^13.0.3",
18
19
  "dotenv": "^17.4.2",
19
- "drizzle-orm": "^0.45.2",
20
- "hono": "^4.12.27",
21
- "jose": "^6.2.3",
22
- "pino": "^10.3.1",
20
+ "drizzle-orm": "^0.45.4",
21
+ "hono": "^4.13.13",
22
+ "jose": "^6.2.12",
23
+ "pino": "^10.4.0",
23
24
  "postgres": "^3.4.9",
24
- "zod": "^4.4.3"
25
+ "zod": "^4.6.7"
25
26
  },
26
27
  "devDependencies": {
28
+ "@types/better-sqlite3": "^9.6.0",
27
29
  "@types/node": "^20.11.17",
28
- "drizzle-kit": "^0.31.10",
29
- "pino-pretty": "^13.1.3",
30
- "tsx": "^4.7.1",
30
+ "drizzle-kit": "^0.31.11",
31
+ "fast-check": "^4.10.2",
32
+ "pino-pretty": "^13.2.0",
33
+ "tsx": "^4.23.15",
31
34
  "typescript": "^5.8.3",
32
35
  "vitest": "^3.0.0"
33
36
  },
@@ -1,5 +1,5 @@
1
1
  import "dotenv/config"
2
- import { z } from "zod";
2
+ import { z } from "zod"
3
3
 
4
4
  const envSchema = z.object({
5
5
  // Memastikan DATABASE_URL ada dan berformat URL valid
@@ -20,4 +20,4 @@ const envSchema = z.object({
20
20
  LOG_FILE_PATH: z.string().optional(),
21
21
  })
22
22
 
23
- export const env = envSchema.parse(process.env)
23
+ export const env = envSchema.parse(process.env)
@@ -1,10 +1,10 @@
1
- import { pgTable, serial, integer, text, timestamp } from "drizzle-orm/pg-core"
1
+ import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"
2
2
  import { usersTable } from "../user/user.model.js"
3
3
 
4
4
  /**
5
5
  * Auth Sessions Table Schema.
6
6
  * Tracks active, refreshable user login sessions.
7
- *
7
+ *
8
8
  * Security rules:
9
9
  * - We only store the SHA-256 hash of the refresh token (`refreshTokenHash`),
10
10
  * protecting the user session from being compromised if the database leaks.
@@ -12,7 +12,9 @@ import { usersTable } from "../user/user.model.js"
12
12
  */
13
13
  export const authSessionsTable = pgTable("auth_sessions", {
14
14
  id: serial("id").primaryKey(),
15
- userId: integer("user_id").notNull().references(() => usersTable.id, { onDelete: "cascade" }),
15
+ userId: integer("user_id")
16
+ .notNull()
17
+ .references(() => usersTable.id, { onDelete: "cascade" }),
16
18
  // sha256 of the opaque refresh token — never store the raw token.
17
19
  refreshTokenHash: text("refresh_token_hash").notNull().unique(),
18
20
  expiresAt: timestamp("expires_at").notNull(),