create-restack-pattern 0.1.8 → 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 (119) hide show
  1. package/dist/index.js +284 -74
  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
@@ -1,17 +1,24 @@
1
1
  "use client"
2
2
 
3
- import { useState } from "react"
4
3
  import Link from "next/link"
4
+ import { useState } from "react"
5
5
  import { Button } from "@/components/ui/button"
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@/components/ui/card"
6
14
  import { Input } from "@/components/ui/input"
7
15
  import { Label } from "@/components/ui/label"
8
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
9
16
  import { useLogin } from "../hooks/use-login"
10
17
 
11
18
  /**
12
19
  * LoginForm UI Component (Auth Domain).
13
20
  * Handles rendering the login form and calling the `useLogin` hook.
14
- *
21
+ *
15
22
  * Semi-DDD rules:
16
23
  * - UI components should stay isolated from direct backend API fetching logic.
17
24
  * - Local react state controls input values, and mutation runs through the domain hook.
@@ -1,14 +1,14 @@
1
1
  "use client"
2
2
 
3
- import { useState } from "react"
4
3
  import { useRouter } from "next/navigation"
4
+ import { useState } from "react"
5
5
  import { Button } from "@/components/ui/button"
6
6
  import { authApi } from "../services/auth.api"
7
7
 
8
8
  /**
9
9
  * LogoutButton Component (Auth Domain).
10
10
  * Simple action component to trigger sessions termination and redirect.
11
- *
11
+ *
12
12
  * Semi-DDD rules:
13
13
  * - Simple action triggers can invoke services directly if no reactive local states/business orchestration is required.
14
14
  */
@@ -1,17 +1,24 @@
1
1
  "use client"
2
2
 
3
- import { useState } from "react"
4
3
  import Link from "next/link"
4
+ import { useState } from "react"
5
5
  import { Button } from "@/components/ui/button"
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardFooter,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@/components/ui/card"
6
14
  import { Input } from "@/components/ui/input"
7
15
  import { Label } from "@/components/ui/label"
8
- import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
9
16
  import { useRegister } from "../hooks/use-register"
10
17
 
11
18
  /**
12
19
  * RegisterForm UI Component (Auth Domain).
13
20
  * Handles rendering the register form and calling the `useRegister` hook.
14
- *
21
+ *
15
22
  * Semi-DDD rules:
16
23
  * - UI components should stay isolated from direct backend API fetching logic.
17
24
  * - Local react state controls input values, and mutation runs through the domain hook.
@@ -1,13 +1,13 @@
1
1
  "use client"
2
2
 
3
- import { LoginSchema, type LoginInput } from "@restack/shared"
3
+ import { type LoginInput, LoginSchema } from "@restack/shared"
4
4
  import { useAuthMutation } from "@/hooks/use-auth-mutation"
5
5
  import { authApi } from "../services/auth.api"
6
6
 
7
7
  /**
8
8
  * Domain-specific hook for Login (Auth Domain).
9
9
  * Orchestrates login form mutation by wrapping the reusable useAuthMutation hook.
10
- *
10
+ *
11
11
  * Semi-DDD rules:
12
12
  * - Domain hooks hide specific Zod validation schemas, API calls, and loading states from components.
13
13
  */
@@ -1,17 +1,20 @@
1
1
  "use client"
2
2
 
3
- import { RegisterSchema, type RegisterInput } from "@restack/shared"
3
+ import { type RegisterInput, RegisterSchema } from "@restack/shared"
4
4
  import { useAuthMutation } from "@/hooks/use-auth-mutation"
5
5
  import { authApi } from "../services/auth.api"
6
6
 
7
7
  /**
8
8
  * Domain-specific hook for Registration (Auth Domain).
9
9
  * Orchestrates registration form mutation by wrapping the reusable useAuthMutation hook.
10
- *
10
+ *
11
11
  * Semi-DDD rules:
12
12
  * - Domain hooks hide specific Zod validation schemas, API calls, and loading states from components.
13
13
  */
14
14
  export function useRegister() {
15
- const { submit, error, isPending } = useAuthMutation<RegisterInput>(RegisterSchema, authApi.register)
15
+ const { submit, error, isPending } = useAuthMutation<RegisterInput>(
16
+ RegisterSchema,
17
+ authApi.register,
18
+ )
16
19
  return { register: submit, error, isPending }
17
20
  }
@@ -1,21 +1,19 @@
1
- import type { AuthUser, LoginInput, RegisterInput } from "@restack/shared"
1
+ import type { AuthResponse, LoginInput, RegisterInput } from "@restack/shared"
2
2
  import { client, unwrap } from "@/lib/api-client"
3
3
 
4
4
  /**
5
5
  * Service API layer for the Auth domain.
6
6
  * Thin wrappers that perform API calls to the Hono backend endpoints.
7
- *
7
+ *
8
8
  * Semi-DDD rules:
9
9
  * - Domain services (`domains/[feature]/services/*.api.ts`) handle only raw HTTP calls.
10
10
  * - Request and response structures are mapped using schemas shared from `@restack/shared`.
11
11
  */
12
12
  export const authApi = {
13
13
  register: (data: RegisterInput) =>
14
- unwrap<{ user: AuthUser }>(client.api.auth.register.$post({ json: data })),
14
+ unwrap<AuthResponse>(client.api.auth.register.$post({ json: data })),
15
15
 
16
- login: (data: LoginInput) =>
17
- unwrap<{ user: AuthUser }>(client.api.auth.login.$post({ json: data })),
16
+ login: (data: LoginInput) => unwrap<AuthResponse>(client.api.auth.login.$post({ json: data })),
18
17
 
19
- logout: () =>
20
- unwrap<{ success: boolean }>(client.api.auth.logout.$post()),
18
+ logout: () => unwrap<{ success: boolean }>(client.api.auth.logout.$post()),
21
19
  }
@@ -6,7 +6,7 @@ import { useProfile } from "../hooks/use-profile"
6
6
  /**
7
7
  * ProfileCard UI component.
8
8
  * Displays the loaded user profile information in a card layout.
9
- *
9
+ *
10
10
  * Semi-DDD rules:
11
11
  * - Domain components (`domains/[feature]/components/*`) are responsible for UI layout and rendering.
12
12
  * - They should never perform direct HTTP calls or manage global state; instead, they consume data
@@ -25,9 +25,7 @@ export function ProfileCard() {
25
25
  <CardTitle>{profile.name}</CardTitle>
26
26
  <CardDescription>{profile.email}</CardDescription>
27
27
  </CardHeader>
28
- <CardContent className="text-xs text-muted-foreground">
29
- User ID: {profile.id}
30
- </CardContent>
28
+ <CardContent className="text-xs text-muted-foreground">User ID: {profile.id}</CardContent>
31
29
  </Card>
32
30
  )
33
31
  }
@@ -4,15 +4,14 @@ import { client, unwrap } from "@/lib/api-client"
4
4
  /**
5
5
  * Service API layer for the User domain.
6
6
  * Thin wrappers that perform API calls to the Hono backend endpoints.
7
- *
7
+ *
8
8
  * Semi-DDD rules:
9
9
  * - Domain services (`domains/[feature]/services/*.api.ts`) handle only raw HTTP calls.
10
10
  * - They must not contain UI state or react state; delegate that strictly to domain hooks.
11
11
  * - Request and response structures are mapped using schemas shared from `@restack/shared`.
12
12
  */
13
13
  export const userApi = {
14
- me: () =>
15
- unwrap<{ user: AuthUser }>(client.api.user.me.$get()),
14
+ me: () => unwrap<{ user: AuthUser }>(client.api.user.me.$get()),
16
15
 
17
16
  updateProfile: (data: UpdateProfileInput) =>
18
17
  unwrap<{ user: AuthUser }>(client.api.user.profile.$put({ json: data })),
@@ -0,0 +1,12 @@
1
+ import { createEnv } from "@t3-oss/env-nextjs"
2
+ import { z } from "zod"
3
+
4
+ export const env = createEnv({
5
+ server: {},
6
+ client: {
7
+ NEXT_PUBLIC_API_URL: z.string().url().default("http://localhost:3001"),
8
+ },
9
+ experimental__runtimeEnv: {
10
+ NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
11
+ },
12
+ })
@@ -1,7 +1,7 @@
1
1
  "use client"
2
2
 
3
- import { useState } from "react"
4
3
  import { useRouter } from "next/navigation"
4
+ import { useState } from "react"
5
5
  import type { ZodType } from "zod"
6
6
 
7
7
  /**
@@ -9,7 +9,10 @@ import type { ZodType } from "zod"
9
9
  * Standardizes Zod input validation, pending state tracking, error handling,
10
10
  * and automatic client-side redirection on successful mutation.
11
11
  */
12
- export function useAuthMutation<TInput>(schema: ZodType<TInput>, mutate: (data: TInput) => Promise<unknown>) {
12
+ export function useAuthMutation<TInput>(
13
+ schema: ZodType<TInput>,
14
+ mutate: (data: TInput) => Promise<unknown>,
15
+ ) {
13
16
  const router = useRouter()
14
17
  const [error, setError] = useState<string | null>(null)
15
18
  const [isPending, setIsPending] = useState(false)
@@ -17,7 +20,7 @@ export function useAuthMutation<TInput>(schema: ZodType<TInput>, mutate: (data:
17
20
  const submit = async (data: TInput) => {
18
21
  const parsed = schema.safeParse(data)
19
22
  if (!parsed.success) {
20
- setError(parsed.error.issues[0].message)
23
+ setError(parsed.error.issues[0]?.message ?? "Validation failed")
21
24
  return
22
25
  }
23
26
 
@@ -1,22 +1,40 @@
1
1
  import { hc } from "hono/client"
2
+ import { env } from "@/env"
2
3
  import type { AppType } from "../../backend/src/hono-app"
3
4
 
4
- const BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001"
5
+ const BASE_URL = env.NEXT_PUBLIC_API_URL
5
6
 
6
7
  export class ApiError extends Error {
7
- constructor(message: string, public status: number) {
8
+ constructor(
9
+ message: string,
10
+ public status: number,
11
+ ) {
8
12
  super(message)
9
13
  }
10
14
  }
11
15
 
16
+ // In-memory Bearer token storage (for mobile clients, native apps, or Bearer auth mode)
17
+ let bearerToken: string | null = null
18
+
19
+ export const setAuthToken = (token: string | null) => {
20
+ bearerToken = token
21
+ }
22
+
23
+ export const getAuthToken = () => bearerToken
24
+
12
25
  // Share a single refresh request promise to prevent TOCTOU race conditions
13
26
  let refreshPromise: Promise<Response> | null = null
14
27
 
15
28
  function refreshSession() {
16
29
  if (!refreshPromise) {
30
+ const headers: Record<string, string> = {}
31
+ if (bearerToken) {
32
+ headers.Authorization = `Bearer ${bearerToken}`
33
+ }
17
34
  refreshPromise = fetch(`${BASE_URL}/api/auth/refresh`, {
18
35
  method: "POST",
19
36
  credentials: "include",
37
+ headers,
20
38
  }).finally(() => {
21
39
  refreshPromise = null
22
40
  })
@@ -25,22 +43,39 @@ function refreshSession() {
25
43
  }
26
44
 
27
45
  const customFetch: typeof fetch = async (input, init) => {
28
- const newInit = {
46
+ const headers = new Headers(init?.headers)
47
+
48
+ if (bearerToken && !headers.has("Authorization")) {
49
+ headers.set("Authorization", `Bearer ${bearerToken}`)
50
+ }
51
+
52
+ const newInit: RequestInit = {
29
53
  ...init,
30
54
  credentials: "include" as const,
55
+ headers,
31
56
  }
32
57
 
33
58
  const res = await fetch(input, newInit)
34
59
 
35
60
  // Silently rotate access token on 401, unless it's the auth endpoints themselves
36
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
61
+ const url =
62
+ typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url
37
63
  const isAuthRoute = url.includes("/api/auth/")
38
64
 
39
65
  if (res.status === 401 && !isAuthRoute) {
40
66
  const refreshRes = await refreshSession()
41
67
  if (refreshRes.ok) {
68
+ try {
69
+ const data = (await refreshRes.clone().json()) as { accessToken?: string }
70
+ if (data.accessToken && bearerToken) {
71
+ bearerToken = data.accessToken
72
+ headers.set("Authorization", `Bearer ${bearerToken}`)
73
+ }
74
+ } catch {
75
+ // ignore JSON parse error
76
+ }
42
77
  // Retry the original request
43
- return fetch(input, newInit)
78
+ return fetch(input, { ...newInit, headers })
44
79
  }
45
80
  }
46
81
 
@@ -1,4 +1,4 @@
1
- import { clsx, type ClassValue } from "clsx"
1
+ import { type ClassValue, clsx } from "clsx"
2
2
  import { twMerge } from "tailwind-merge"
3
3
 
4
4
  /**
@@ -0,0 +1,11 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Tell Next.js to compile and bundle the @restack/shared package as if it were part of the frontend's own source code
5
+ transpilePackages: ["@restack/shared"],
6
+ // Enables static route typing for Next JS App Router navigation
7
+ // when use <Link> or router.push(), the editor will check or autocomplete valid routes and catch typos at compile time.
8
+ typedRoutes: true,
9
+ };
10
+
11
+ export default nextConfig;
@@ -10,27 +10,28 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@restack/shared": "workspace:*",
13
+ "@t3-oss/env-nextjs": "^0.13.11",
13
14
  "class-variance-authority": "^0.7.1",
14
15
  "clsx": "^2.1.1",
15
- "hono": "^4.12.27",
16
- "next": "16.2.9",
17
- "radix-ui": "^1.6.1",
18
- "react": "19.2.4",
19
- "react-dom": "19.2.4",
20
- "shadcn": "^4.12.0",
21
- "sharp": "^0.35.3",
22
- "tailwind-merge": "^3.6.0",
16
+ "hono": "^4.13.13",
17
+ "next": "16.4.2",
18
+ "radix-ui": "^1.8.1",
19
+ "react": "19.3.1",
20
+ "react-dom": "19.3.1",
21
+ "shadcn": "^4.21.5",
22
+ "sharp": "^0.35.5",
23
+ "tailwind-merge": "^3.7.0",
23
24
  "tw-animate-css": "^1.4.0",
24
- "zod": "^4.4.3"
25
+ "zod": "^4.6.7"
25
26
  },
26
27
  "devDependencies": {
27
- "@tailwindcss/postcss": "^4",
28
+ "@tailwindcss/postcss": "^4.3.3",
28
29
  "@types/node": "^20",
29
- "@types/react": "^19",
30
- "@types/react-dom": "^19",
30
+ "@types/react": "^19.3.0",
31
+ "@types/react-dom": "^19.3.0",
31
32
  "eslint": "^9",
32
- "eslint-config-next": "16.2.9",
33
- "tailwindcss": "^4",
33
+ "eslint-config-next": "16.4.2",
34
+ "tailwindcss": "^4.3.3",
34
35
  "typescript": "^5"
35
36
  },
36
37
  "engines": {
@@ -5,6 +5,10 @@
5
5
  "allowJs": true,
6
6
  "skipLibCheck": true,
7
7
  "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "exactOptionalPropertyTypes": true,
10
+ "noImplicitReturns": true,
11
+ "noFallthroughCasesInSwitch": true,
8
12
  "noEmit": true,
9
13
  "esModuleInterop": true,
10
14
  "module": "esnext",
@@ -40,3 +40,18 @@ test-app/
40
40
  docs/concerns.md
41
41
 
42
42
  docs/tutorial.md
43
+
44
+ # Graphify knowledge graph outputs
45
+ graphify-out/
46
+ .graphify*
47
+
48
+ node_modules/
49
+ .e2e/results/
50
+ .e2e/cache/
51
+ .e2e/sessions/
52
+ .e2e/report.json
53
+ .e2e/ai-trace.json
54
+ .e2e/junit.xml
55
+ .e2e/summary.md
56
+ .e2e/logs/
57
+ .e2e/videos/
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "restack-pattern",
3
+ "private": true,
4
+ "license": "MIT",
5
+ "scripts": {
6
+ "dev": "pnpm --parallel dev",
7
+ "build": "pnpm --parallel build",
8
+ "lint": "biome check .",
9
+ "format": "biome format --write .",
10
+ "test:e2e": "e2e run"
11
+ },
12
+ "workspaces": [
13
+ "frontend",
14
+ "backend",
15
+ "packages/*"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "devDependencies": {
21
+ "@biomejs/biome": "^2.5.15",
22
+ "@e2e-dev/web": "^0.15.2",
23
+ "e2e": "^0.21.2"
24
+ }
25
+ }
@@ -16,7 +16,7 @@
16
16
  "dev": "tsc --watch"
17
17
  },
18
18
  "dependencies": {
19
- "zod": "^4.4.3"
19
+ "zod": "^4.6.7"
20
20
  },
21
21
  "devDependencies": {
22
22
  "typescript": "^5.8.3"
@@ -1,2 +1,3 @@
1
1
  export * from "./schemas/auth.schema.js"
2
2
  export * from "./schemas/user.schema.js"
3
+ export * from "./utils/uuid.js"
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod"
2
+ import type { AuthUser } from "./user.schema.js"
2
3
 
3
4
  /** Zod validation schema for login form and request payload. */
4
5
  export const LoginSchema = z.object({
@@ -18,3 +19,10 @@ export type LoginInput = z.infer<typeof LoginSchema>
18
19
 
19
20
  /** TypeScript input type inferred from RegisterSchema. */
20
21
  export type RegisterInput = z.infer<typeof RegisterSchema>
22
+
23
+ /** Auth response payload returned on register, login, and token rotation. */
24
+ export interface AuthResponse {
25
+ user: AuthUser
26
+ accessToken: string
27
+ refreshToken: string
28
+ }
@@ -5,6 +5,7 @@ export interface AuthUser {
5
5
  id: number
6
6
  name: string
7
7
  email: string
8
+ role: string
8
9
  }
9
10
 
10
11
  /** Zod validation schema for updating user profile (partial updates). */
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Zero-dependency RFC 9562 compliant UUIDv7 generator.
3
+ *
4
+ * UUIDv7 combines a 48-bit UNIX timestamp (milliseconds) with 74 bits of
5
+ * cryptographically secure randomness. Unlike UUIDv4, UUIDv7 is strictly
6
+ * time-ordered, making it ideal for primary keys in B-Tree database indexes
7
+ * while avoiding URL enumeration vulnerabilities of auto-incrementing serial IDs.
8
+ *
9
+ * Compatible with Node.js, browsers, and Edge runtimes via globalThis.crypto.
10
+ */
11
+ export function uuidv7(): string {
12
+ const bytes = new Uint8Array(16)
13
+ globalThis.crypto.getRandomValues(bytes)
14
+ const now = Date.now()
15
+
16
+ // 48-bit timestamp (milliseconds since epoch) in big-endian
17
+ bytes[0] = (now / 0x1_00_00_00_00_00) & 0xff
18
+ bytes[1] = (now / 0x1_00_00_00_00) & 0xff
19
+ bytes[2] = (now / 0x1_00_00_00) & 0xff
20
+ bytes[3] = (now / 0x1_00_00) & 0xff
21
+ bytes[4] = (now / 0x1_00) & 0xff
22
+ bytes[5] = now & 0xff
23
+
24
+ // Version 7: set 4 bits (0111) in byte 6
25
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x70
26
+
27
+ // Variant 1 (RFC 9562): set 2 bits (10) in byte 8
28
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80
29
+
30
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")
31
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`
32
+ }
33
+
34
+ /**
35
+ * Validates whether a given string is a valid UUIDv7 format.
36
+ */
37
+ export function isUuidv7(uuid: string): boolean {
38
+ const uuidv7Regex = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
39
+ return uuidv7Regex.test(uuid)
40
+ }
@@ -28,6 +28,32 @@
28
28
  * @description Enables all strict type-checking options to prevent common runtime bugs.
29
29
  */
30
30
  "strict": true,
31
+ /**
32
+ * @noUncheckedIndexedAccess true
33
+ * @description Adds 'undefined' to any un-declared index access (e.g. arr[i] or record[key]),
34
+ * forcing explicit null/undefined checks and preventing runtime crashes.
35
+ */
36
+ "noUncheckedIndexedAccess": true,
37
+
38
+ /**
39
+ * @exactOptionalPropertyTypes true
40
+ * @description Differentiates between omitted optional properties and properties explicitly set to 'undefined',
41
+ * preventing unintended database column mutations and serialization bugs.
42
+ */
43
+ "exactOptionalPropertyTypes": true,
44
+
45
+ /**
46
+ * @noImplicitReturns true
47
+ * @description Ensures all execution branches in a function explicitly return a value,
48
+ * preventing accidental implicit 'undefined' returns.
49
+ */
50
+ "noImplicitReturns": true,
51
+
52
+ /**
53
+ * @noFallthroughCasesInSwitch true
54
+ * @description Reports an error for fallthrough cases in switch statements that are missing a 'break' or 'return'.
55
+ */
56
+ "noFallthroughCasesInSwitch": true,
31
57
 
32
58
  /**
33
59
  * @skipLibCheck true
@@ -0,0 +1,12 @@
1
+ packages:
2
+ - "frontend"
3
+ - "backend"
4
+ - "packages/*"
5
+ allowBuilds:
6
+ better-sqlite3: true
7
+ esbuild: true
8
+ sharp: true
9
+ unrs-resolver: true
10
+ minimumReleaseAgeExclude:
11
+ - '@e2e-dev/web@0.15.2'
12
+ - e2e@0.21.2
@@ -0,0 +1,6 @@
1
+ DATABASE_URL=postgresql://postgres:postgres@localhost:5432/restack_pattern
2
+ JWT_SECRET=your-secret-key-min-32-characters
3
+ NODE_ENV=development
4
+ PORT=3001
5
+ FRONTEND_URL=http://localhost:3000
6
+ COOKIE_SAME_SITE=Lax
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "drizzle-kit"
2
+ import "dotenv/config"
3
+
4
+ export default defineConfig({
5
+ schema: "./src/domains/*/*.model.ts",
6
+ out: "./drizzle",
7
+ dialect: "postgresql",
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL!,
10
+ },
11
+ })
@@ -0,0 +1,27 @@
1
+ import postgres from "postgres"
2
+ import dotenv from "dotenv"
3
+ import path from "path"
4
+ import { fileURLToPath } from "url"
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ dotenv.config({ path: path.resolve(__dirname, "../.env") })
8
+
9
+ const dbUrl = process.env.DATABASE_URL
10
+ if (!dbUrl) {
11
+ console.error("\x1b[31m[ERROR] DATABASE_URL is not set in backend/.env\x1b[0m")
12
+ process.exit(1)
13
+ }
14
+
15
+ console.log("\x1b[36mChecking PostgreSQL connection...\x1b[0m")
16
+ const sql = postgres(dbUrl, { max: 1, connect_timeout: 5 })
17
+
18
+ try {
19
+ await sql`SELECT 1`
20
+ console.log("\x1b[32m✔ PostgreSQL connection successful!\x1b[0m")
21
+ await sql.end()
22
+ process.exit(0)
23
+ } catch (err) {
24
+ console.error("\x1b[31m[ERROR] Database connection failed!\x1b[0m", err.message)
25
+ await sql.end()
26
+ process.exit(1)
27
+ }
@@ -0,0 +1,6 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js"
2
+ import postgres from "postgres"
3
+ import { env } from "../../config/env.js"
4
+
5
+ const client = postgres(env.DATABASE_URL, { max: 1 })
6
+ export const db = drizzle(client)
@@ -0,0 +1,6 @@
1
+ DATABASE_URL=sqlite.db
2
+ JWT_SECRET=your-secret-key-min-32-characters
3
+ NODE_ENV=development
4
+ PORT=3001
5
+ FRONTEND_URL=http://localhost:3000
6
+ COOKIE_SAME_SITE=Lax
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "drizzle-kit"
2
+ import "dotenv/config"
3
+
4
+ export default defineConfig({
5
+ schema: "./src/domains/*/*.model.ts",
6
+ out: "./drizzle",
7
+ dialect: "sqlite",
8
+ dbCredentials: {
9
+ url: process.env.DATABASE_URL || "sqlite.db",
10
+ },
11
+ })