create-stackrjs 1.4.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 (113) hide show
  1. package/README.md +154 -0
  2. package/dist/index.js +1135 -0
  3. package/package.json +62 -0
  4. package/template/architectures/microservices/_gitignore +26 -0
  5. package/template/architectures/microservices/apps/web/Dockerfile +11 -0
  6. package/template/architectures/microservices/apps/web/next-env.d.ts +5 -0
  7. package/template/architectures/microservices/apps/web/next.config.mjs +4 -0
  8. package/template/architectures/microservices/apps/web/package.json +25 -0
  9. package/template/architectures/microservices/apps/web/src/app/account/page.tsx +56 -0
  10. package/template/architectures/microservices/apps/web/src/app/globals.css +3 -0
  11. package/template/architectures/microservices/apps/web/src/app/layout.tsx +16 -0
  12. package/template/architectures/microservices/apps/web/src/app/login/page.tsx +70 -0
  13. package/template/architectures/microservices/apps/web/src/app/page.tsx +33 -0
  14. package/template/architectures/microservices/apps/web/src/app/register/page.tsx +71 -0
  15. package/template/architectures/microservices/apps/web/src/lib/api.ts +15 -0
  16. package/template/architectures/microservices/apps/web/tsconfig.json +23 -0
  17. package/template/architectures/microservices/docker-compose.yml +45 -0
  18. package/template/architectures/microservices/gateway/nginx.conf +20 -0
  19. package/template/architectures/microservices/package.json +21 -0
  20. package/template/architectures/microservices/services/api/Dockerfile +11 -0
  21. package/template/architectures/microservices/services/api/package.json +34 -0
  22. package/template/architectures/microservices/services/api/prisma/schema.prisma +17 -0
  23. package/template/architectures/microservices/services/api/prisma.config.ts +17 -0
  24. package/template/architectures/microservices/services/api/src/auth.ts +22 -0
  25. package/template/architectures/microservices/services/api/src/db.ts +12 -0
  26. package/template/architectures/microservices/services/api/src/index.ts +22 -0
  27. package/template/architectures/microservices/services/api/src/routes/auth.ts +73 -0
  28. package/template/architectures/microservices/services/api/tsconfig.json +16 -0
  29. package/template/base/_eslintrc.json +3 -0
  30. package/template/base/_gitignore +24 -0
  31. package/template/base/_prettierrc.json +6 -0
  32. package/template/base/next-env.d.ts +5 -0
  33. package/template/base/next.config.mjs +4 -0
  34. package/template/base/package.json +27 -0
  35. package/template/base/src/app/globals.css +14 -0
  36. package/template/base/src/app/layout.tsx +16 -0
  37. package/template/base/src/app/page.tsx +10 -0
  38. package/template/base/tsconfig.json +23 -0
  39. package/template/extras/account-better-auth/src/server/account.ts +52 -0
  40. package/template/extras/account-clerk/src/server/account.ts +64 -0
  41. package/template/extras/account-nextauth/src/server/account.ts +59 -0
  42. package/template/extras/account-supabase/src/lib/supabase/admin.ts +14 -0
  43. package/template/extras/account-supabase/src/server/account.ts +65 -0
  44. package/template/extras/admin/src/app/admin/page.tsx +55 -0
  45. package/template/extras/auth-pages/src/app/account/page.tsx +37 -0
  46. package/template/extras/auth-pages/src/app/forgot-password/page.tsx +33 -0
  47. package/template/extras/auth-pages/src/app/login/page.tsx +32 -0
  48. package/template/extras/auth-pages/src/app/page.tsx +46 -0
  49. package/template/extras/auth-pages/src/app/register/page.tsx +36 -0
  50. package/template/extras/auth-pages/src/app/reset-password/page.tsx +49 -0
  51. package/template/extras/auth-pages/src/components/auth/ui.tsx +57 -0
  52. package/template/extras/auth-pages-drizzle/src/server/actions.ts +122 -0
  53. package/template/extras/auth-pages-mongoose/src/server/actions.ts +121 -0
  54. package/template/extras/auth-pages-prisma/src/server/actions.ts +117 -0
  55. package/template/extras/better-auth/prisma/schema.prisma +70 -0
  56. package/template/extras/better-auth/src/app/account/page.tsx +42 -0
  57. package/template/extras/better-auth/src/app/api/auth/[...all]/route.ts +5 -0
  58. package/template/extras/better-auth/src/app/login/page.tsx +42 -0
  59. package/template/extras/better-auth/src/app/register/page.tsx +44 -0
  60. package/template/extras/better-auth/src/components/auth/ui.tsx +58 -0
  61. package/template/extras/better-auth/src/lib/auth-client.ts +7 -0
  62. package/template/extras/better-auth/src/server/auth.ts +13 -0
  63. package/template/extras/clerk/middleware.ts +14 -0
  64. package/template/extras/clerk/src/app/account/page.tsx +30 -0
  65. package/template/extras/clerk/src/app/layout.tsx +19 -0
  66. package/template/extras/clerk/src/app/page.tsx +40 -0
  67. package/template/extras/clerk/src/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  68. package/template/extras/clerk/src/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  69. package/template/extras/dashboard/src/app/dashboard/page.tsx +48 -0
  70. package/template/extras/databases/mongodb/docker-compose.yml +13 -0
  71. package/template/extras/databases/mysql/docker-compose.yml +14 -0
  72. package/template/extras/databases/postgres/docker-compose.yml +15 -0
  73. package/template/extras/drizzle/mysql/drizzle.config.ts +9 -0
  74. package/template/extras/drizzle/mysql/src/server/db.ts +11 -0
  75. package/template/extras/drizzle/mysql/src/server/schema.ts +17 -0
  76. package/template/extras/drizzle/postgres/drizzle.config.ts +9 -0
  77. package/template/extras/drizzle/postgres/src/server/db.ts +11 -0
  78. package/template/extras/drizzle/postgres/src/server/schema.ts +20 -0
  79. package/template/extras/landing/src/app/page.tsx +53 -0
  80. package/template/extras/mongoose/src/server/db.ts +12 -0
  81. package/template/extras/mongoose/src/server/models.ts +25 -0
  82. package/template/extras/nextauth/middleware.ts +11 -0
  83. package/template/extras/nextauth/src/app/api/auth/[...nextauth]/route.ts +3 -0
  84. package/template/extras/nextauth/src/server/auth.config.ts +39 -0
  85. package/template/extras/nextauth/src/server/roles.ts +5 -0
  86. package/template/extras/nextauth/src/types/next-auth.d.ts +22 -0
  87. package/template/extras/nextauth-drizzle/src/server/auth.ts +44 -0
  88. package/template/extras/nextauth-mongoose/src/server/auth.ts +42 -0
  89. package/template/extras/nextauth-prisma/src/server/auth.ts +43 -0
  90. package/template/extras/prisma/base/prisma/schema.prisma +67 -0
  91. package/template/extras/prisma/base/prisma.config.ts +10 -0
  92. package/template/extras/prisma/mysql/src/server/db.ts +25 -0
  93. package/template/extras/prisma/postgres/src/server/db.ts +18 -0
  94. package/template/extras/shadcn/components.json +20 -0
  95. package/template/extras/shadcn/postcss.config.mjs +7 -0
  96. package/template/extras/shadcn/src/app/globals.css +59 -0
  97. package/template/extras/shadcn/src/components/ui/button.tsx +42 -0
  98. package/template/extras/shadcn/src/components/ui/card.tsx +54 -0
  99. package/template/extras/shadcn/src/components/ui/input.tsx +19 -0
  100. package/template/extras/shadcn/src/lib/utils.ts +7 -0
  101. package/template/extras/shadcn/tailwind.config.ts +71 -0
  102. package/template/extras/supabase/middleware.ts +13 -0
  103. package/template/extras/supabase/src/app/account/page.tsx +32 -0
  104. package/template/extras/supabase/src/app/login/page.tsx +47 -0
  105. package/template/extras/supabase/src/app/register/page.tsx +52 -0
  106. package/template/extras/supabase/src/components/auth/sign-out-button.tsx +25 -0
  107. package/template/extras/supabase/src/components/auth/ui.tsx +58 -0
  108. package/template/extras/supabase/src/lib/supabase/client.ts +9 -0
  109. package/template/extras/supabase/src/lib/supabase/middleware.ts +34 -0
  110. package/template/extras/supabase/src/lib/supabase/server.ts +29 -0
  111. package/template/extras/tailwind/postcss.config.mjs +7 -0
  112. package/template/extras/tailwind/src/app/globals.css +3 -0
  113. package/template/extras/tailwind/tailwind.config.ts +9 -0
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "create-stackrjs",
3
+ "version": "1.4.0",
4
+ "description": "Scaffold a runnable full-stack Next.js project in one command",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-stackrjs": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "template"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18.18.0"
15
+ },
16
+ "scripts": {
17
+ "build": "tsup",
18
+ "dev": "tsup --watch",
19
+ "start": "node dist/index.js",
20
+ "typecheck": "tsc --noEmit",
21
+ "test": "vitest run",
22
+ "test:watch": "vitest",
23
+ "smoke": "tsx scripts/smoke.ts",
24
+ "release": "semantic-release",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "cli",
29
+ "scaffold",
30
+ "nextjs",
31
+ "starter",
32
+ "fullstack",
33
+ "create",
34
+ "generator"
35
+ ],
36
+ "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/Tristan-stack/stackr.git"
40
+ },
41
+ "bugs": {
42
+ "url": "https://github.com/Tristan-stack/stackr/issues"
43
+ },
44
+ "homepage": "https://github.com/Tristan-stack/stackr#readme",
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@clack/prompts": "^0.7.0",
50
+ "picocolors": "^1.0.1"
51
+ },
52
+ "devDependencies": {
53
+ "@semantic-release/changelog": "^6.0.3",
54
+ "@semantic-release/git": "^10.0.1",
55
+ "@types/node": "^20.14.0",
56
+ "semantic-release": "^24.2.0",
57
+ "tsup": "^8.3.5",
58
+ "tsx": "^4.19.2",
59
+ "typescript": "^5.7.2",
60
+ "vitest": "^2.1.8"
61
+ }
62
+ }
@@ -0,0 +1,26 @@
1
+ node_modules
2
+ .pnp
3
+ .pnp.js
4
+
5
+ # next.js
6
+ .next/
7
+ out/
8
+ build/
9
+ dist/
10
+
11
+ # misc
12
+ .DS_Store
13
+ *.pem
14
+ *.log
15
+
16
+ # env files
17
+ .env
18
+ .env*.local
19
+
20
+ # typescript
21
+ next-env.d.ts
22
+ *.tsbuildinfo
23
+
24
+ # prisma
25
+ services/api/prisma/migrations/dev.db*
26
+ services/api/src/generated
@@ -0,0 +1,11 @@
1
+ FROM node:20-alpine
2
+ WORKDIR /app
3
+
4
+ COPY package.json ./
5
+ RUN npm install
6
+
7
+ COPY . .
8
+ RUN npm run build
9
+
10
+ EXPOSE 3000
11
+ CMD ["npm", "start"]
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,4 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {};
3
+
4
+ export default nextConfig;
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@app/web",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "next": "^15.1.3",
14
+ "react": "^18.3.1",
15
+ "react-dom": "^18.3.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.14.0",
19
+ "@types/react": "^18.3.12",
20
+ "@types/react-dom": "^18.3.1",
21
+ "eslint": "^8.57.1",
22
+ "eslint-config-next": "^15.1.3",
23
+ "typescript": "^5.7.2"
24
+ }
25
+ }
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { api } from "@/lib/api";
6
+
7
+ interface Me {
8
+ id: string;
9
+ email: string;
10
+ }
11
+
12
+ export default function AccountPage() {
13
+ const router = useRouter();
14
+ const [me, setMe] = useState<Me | null>(null);
15
+ const [loading, setLoading] = useState(true);
16
+
17
+ useEffect(() => {
18
+ let active = true;
19
+ api("/auth/me").then(async (res) => {
20
+ if (!active) return;
21
+ if (res.ok) setMe(await res.json());
22
+ else router.replace("/login");
23
+ setLoading(false);
24
+ });
25
+ return () => {
26
+ active = false;
27
+ };
28
+ }, [router]);
29
+
30
+ async function logout() {
31
+ await api("/auth/logout", { method: "POST" });
32
+ router.replace("/login");
33
+ router.refresh();
34
+ }
35
+
36
+ if (loading) {
37
+ return <main className="flex min-h-screen items-center justify-center p-8">Loading…</main>;
38
+ }
39
+
40
+ return (
41
+ <main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
42
+ <h1 className="text-2xl font-bold">Your account</h1>
43
+ <p className="text-gray-600">
44
+ Signed in as <strong>{me?.email}</strong>. This page is protected — it reads your session
45
+ from the API service.
46
+ </p>
47
+ <button
48
+ type="button"
49
+ onClick={logout}
50
+ className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
51
+ >
52
+ Sign out
53
+ </button>
54
+ </main>
55
+ );
56
+ }
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,16 @@
1
+ import "./globals.css";
2
+ import type { Metadata } from "next";
3
+ import type { ReactNode } from "react";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Stackr app",
7
+ description: "Generated by Stackr (microservices)",
8
+ };
9
+
10
+ export default function RootLayout({ children }: { children: ReactNode }) {
11
+ return (
12
+ <html lang="en">
13
+ <body className="min-h-screen bg-gray-50 text-gray-900 antialiased">{children}</body>
14
+ </html>
15
+ );
16
+ }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { api } from "@/lib/api";
7
+
8
+ export default function LoginPage() {
9
+ const router = useRouter();
10
+ const [email, setEmail] = useState("");
11
+ const [password, setPassword] = useState("");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [pending, setPending] = useState(false);
14
+
15
+ async function onSubmit(e: React.FormEvent) {
16
+ e.preventDefault();
17
+ setPending(true);
18
+ setError(null);
19
+ const res = await api("/auth/login", {
20
+ method: "POST",
21
+ body: JSON.stringify({ email, password }),
22
+ });
23
+ setPending(false);
24
+ if (res.ok) {
25
+ router.push("/account");
26
+ router.refresh();
27
+ } else {
28
+ const data = await res.json().catch(() => ({}));
29
+ setError(data.error ?? "Login failed.");
30
+ }
31
+ }
32
+
33
+ return (
34
+ <main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
35
+ <h1 className="text-2xl font-bold">Sign in</h1>
36
+ <form onSubmit={onSubmit} className="flex flex-col gap-3">
37
+ <input
38
+ type="email"
39
+ required
40
+ placeholder="you@example.com"
41
+ value={email}
42
+ onChange={(e) => setEmail(e.target.value)}
43
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm"
44
+ />
45
+ <input
46
+ type="password"
47
+ required
48
+ placeholder="Password"
49
+ value={password}
50
+ onChange={(e) => setPassword(e.target.value)}
51
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm"
52
+ />
53
+ {error && <p className="text-sm text-red-600">{error}</p>}
54
+ <button
55
+ type="submit"
56
+ disabled={pending}
57
+ className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-50"
58
+ >
59
+ {pending ? "Signing in…" : "Sign in"}
60
+ </button>
61
+ </form>
62
+ <p className="text-sm text-gray-600">
63
+ No account?{" "}
64
+ <Link href="/register" className="underline">
65
+ Register
66
+ </Link>
67
+ </p>
68
+ </main>
69
+ );
70
+ }
@@ -0,0 +1,33 @@
1
+ import Link from "next/link";
2
+
3
+ export default function Home() {
4
+ return (
5
+ <main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center gap-6 p-8 text-center">
6
+ <h1 className="text-4xl font-bold tracking-tight">It works 🎉</h1>
7
+ <p className="text-gray-600">
8
+ This is the <strong>web</strong> service. It talks to the <strong>api</strong> service
9
+ through the gateway. Register an account, then sign in.
10
+ </p>
11
+ <div className="flex gap-3">
12
+ <Link
13
+ href="/register"
14
+ className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700"
15
+ >
16
+ Register
17
+ </Link>
18
+ <Link
19
+ href="/login"
20
+ className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
21
+ >
22
+ Sign in
23
+ </Link>
24
+ <Link
25
+ href="/account"
26
+ className="rounded-md border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100"
27
+ >
28
+ Account
29
+ </Link>
30
+ </div>
31
+ </main>
32
+ );
33
+ }
@@ -0,0 +1,71 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import Link from "next/link";
6
+ import { api } from "@/lib/api";
7
+
8
+ export default function RegisterPage() {
9
+ const router = useRouter();
10
+ const [email, setEmail] = useState("");
11
+ const [password, setPassword] = useState("");
12
+ const [error, setError] = useState<string | null>(null);
13
+ const [pending, setPending] = useState(false);
14
+
15
+ async function onSubmit(e: React.FormEvent) {
16
+ e.preventDefault();
17
+ setPending(true);
18
+ setError(null);
19
+ const res = await api("/auth/register", {
20
+ method: "POST",
21
+ body: JSON.stringify({ email, password }),
22
+ });
23
+ setPending(false);
24
+ if (res.ok) {
25
+ router.push("/account");
26
+ router.refresh();
27
+ } else {
28
+ const data = await res.json().catch(() => ({}));
29
+ setError(data.error ?? "Registration failed.");
30
+ }
31
+ }
32
+
33
+ return (
34
+ <main className="mx-auto flex min-h-screen max-w-sm flex-col justify-center gap-6 p-8">
35
+ <h1 className="text-2xl font-bold">Create your account</h1>
36
+ <form onSubmit={onSubmit} className="flex flex-col gap-3">
37
+ <input
38
+ type="email"
39
+ required
40
+ placeholder="you@example.com"
41
+ value={email}
42
+ onChange={(e) => setEmail(e.target.value)}
43
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm"
44
+ />
45
+ <input
46
+ type="password"
47
+ required
48
+ minLength={8}
49
+ placeholder="Password (min 8 characters)"
50
+ value={password}
51
+ onChange={(e) => setPassword(e.target.value)}
52
+ className="rounded-md border border-gray-300 px-3 py-2 text-sm"
53
+ />
54
+ {error && <p className="text-sm text-red-600">{error}</p>}
55
+ <button
56
+ type="submit"
57
+ disabled={pending}
58
+ className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-700 disabled:opacity-50"
59
+ >
60
+ {pending ? "Creating…" : "Create account"}
61
+ </button>
62
+ </form>
63
+ <p className="text-sm text-gray-600">
64
+ Already registered?{" "}
65
+ <Link href="/login" className="underline">
66
+ Sign in
67
+ </Link>
68
+ </p>
69
+ </main>
70
+ );
71
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tiny fetch wrapper for talking to the API service. In the browser, requests go
3
+ * through the gateway (NEXT_PUBLIC_API_URL, e.g. http://localhost:8080/api),
4
+ * which proxies /api/* to the API service. `credentials: "include"` carries the
5
+ * httpOnly session cookie.
6
+ */
7
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
8
+
9
+ export async function api(path: string, init?: RequestInit): Promise<Response> {
10
+ return fetch(`${API_URL}${path}`, {
11
+ ...init,
12
+ credentials: "include",
13
+ headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
14
+ });
15
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": {
18
+ "@/*": ["./src/*"]
19
+ }
20
+ },
21
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
22
+ "exclude": ["node_modules"]
23
+ }
@@ -0,0 +1,45 @@
1
+ services:
2
+ db:
3
+ image: postgres:16
4
+ restart: unless-stopped
5
+ environment:
6
+ POSTGRES_USER: postgres
7
+ POSTGRES_PASSWORD: postgres
8
+ POSTGRES_DB: app
9
+ ports:
10
+ - "5432:5432"
11
+ volumes:
12
+ - db_data:/var/lib/postgresql/data
13
+
14
+ api:
15
+ build: ./services/api
16
+ restart: unless-stopped
17
+ environment:
18
+ DATABASE_URL: postgresql://postgres:postgres@db:5432/app?schema=public
19
+ AUTH_SECRET: ${AUTH_SECRET:-dev-insecure-change-me}
20
+ PORT: "3001"
21
+ depends_on:
22
+ - db
23
+
24
+ web:
25
+ build: ./apps/web
26
+ restart: unless-stopped
27
+ environment:
28
+ # The browser reaches the API through the gateway (see gateway/nginx.conf).
29
+ NEXT_PUBLIC_API_URL: http://localhost:8080/api
30
+ depends_on:
31
+ - api
32
+
33
+ gateway:
34
+ image: nginx:1.27-alpine
35
+ restart: unless-stopped
36
+ ports:
37
+ - "8080:80"
38
+ volumes:
39
+ - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro
40
+ depends_on:
41
+ - web
42
+ - api
43
+
44
+ volumes:
45
+ db_data:
@@ -0,0 +1,20 @@
1
+ # Reverse proxy: a single public origin in front of the web app and the API.
2
+ # Requests to /api/* are forwarded to the API service (prefix stripped); every
3
+ # other path goes to the Next.js web app.
4
+ server {
5
+ listen 80;
6
+
7
+ location /api/ {
8
+ proxy_pass http://api:3001/;
9
+ proxy_set_header Host $host;
10
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
11
+ proxy_set_header X-Forwarded-Proto $scheme;
12
+ }
13
+
14
+ location / {
15
+ proxy_pass http://web:3000;
16
+ proxy_set_header Host $host;
17
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
18
+ proxy_set_header X-Forwarded-Proto $scheme;
19
+ }
20
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "workspaces": [
6
+ "apps/*",
7
+ "services/*"
8
+ ],
9
+ "scripts": {
10
+ "dev": "npm run dev --workspaces --if-present",
11
+ "build": "npm run build --workspaces --if-present",
12
+ "dev:web": "npm run dev --workspace apps/web",
13
+ "dev:api": "npm run dev --workspace services/api",
14
+ "db:migrate": "npm run db:migrate --workspace services/api",
15
+ "db:studio": "npm run db:studio --workspace services/api",
16
+ "compose:up": "docker compose up -d",
17
+ "compose:down": "docker compose down"
18
+ },
19
+ "dependencies": {},
20
+ "devDependencies": {}
21
+ }
@@ -0,0 +1,11 @@
1
+ FROM node:20-alpine
2
+ WORKDIR /app
3
+
4
+ COPY package.json ./
5
+ RUN npm install
6
+
7
+ COPY . .
8
+ RUN npx prisma generate && npm run build
9
+
10
+ EXPOSE 3001
11
+ CMD ["npm", "start"]
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@app/api",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "typecheck": "tsc --noEmit",
11
+ "db:migrate": "prisma migrate dev",
12
+ "db:studio": "prisma studio",
13
+ "postinstall": "prisma generate"
14
+ },
15
+ "dependencies": {
16
+ "@hono/node-server": "^1.13.7",
17
+ "@prisma/adapter-pg": "^7.8.0",
18
+ "@prisma/client": "^7.8.0",
19
+ "bcryptjs": "^2.4.3",
20
+ "hono": "^4.6.14",
21
+ "jose": "^5.9.6",
22
+ "pg": "^8.13.1",
23
+ "zod": "^3.24.1"
24
+ },
25
+ "devDependencies": {
26
+ "@types/bcryptjs": "^2.4.6",
27
+ "@types/node": "^20.14.0",
28
+ "@types/pg": "^8.11.10",
29
+ "dotenv": "^16.4.7",
30
+ "prisma": "^7.8.0",
31
+ "tsx": "^4.19.2",
32
+ "typescript": "^5.7.2"
33
+ }
34
+ }
@@ -0,0 +1,17 @@
1
+ generator client {
2
+ provider = "prisma-client"
3
+ output = "../src/generated/prisma"
4
+ moduleFormat = "esm"
5
+ importFileExtension = "js"
6
+ }
7
+
8
+ datasource db {
9
+ provider = "postgresql"
10
+ }
11
+
12
+ model User {
13
+ id String @id @default(cuid())
14
+ email String @unique
15
+ passwordHash String
16
+ createdAt DateTime @default(now())
17
+ }
@@ -0,0 +1,17 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { config } from "dotenv";
4
+ import { defineConfig, env } from "prisma/config";
5
+
6
+ // The workspace `.env` lives at the monorepo root (two levels up), so load it
7
+ // explicitly rather than relying on the current working directory.
8
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
+ config({ path: path.join(root, ".env") });
10
+
11
+ // Prisma 7 reads connection config from here (not the schema). The API service
12
+ // owns its database; the URL comes from the workspace `.env`.
13
+ export default defineConfig({
14
+ schema: "prisma/schema.prisma",
15
+ migrations: { path: "prisma/migrations" },
16
+ datasource: { url: env("DATABASE_URL") },
17
+ });
@@ -0,0 +1,22 @@
1
+ import { SignJWT, jwtVerify } from "jose";
2
+
3
+ const secret = new TextEncoder().encode(process.env.AUTH_SECRET ?? "dev-insecure-change-me");
4
+
5
+ /** Sign a 7-day session token for a user id. */
6
+ export async function signSession(userId: string): Promise<string> {
7
+ return new SignJWT({ sub: userId })
8
+ .setProtectedHeader({ alg: "HS256" })
9
+ .setIssuedAt()
10
+ .setExpirationTime("7d")
11
+ .sign(secret);
12
+ }
13
+
14
+ /** Verify a session token, returning the user id or null when invalid/expired. */
15
+ export async function verifySession(token: string): Promise<string | null> {
16
+ try {
17
+ const { payload } = await jwtVerify(token, secret);
18
+ return typeof payload.sub === "string" ? payload.sub : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
@@ -0,0 +1,12 @@
1
+ import { PrismaClient } from "./generated/prisma/client.js";
2
+ import { PrismaPg } from "@prisma/adapter-pg";
3
+
4
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
5
+
6
+ // Reuse a single PrismaClient across hot reloads in development. Prisma 7 connects
7
+ // through a driver adapter (connection string from `.env`).
8
+ const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
9
+
10
+ export const prisma = globalForPrisma.prisma ?? new PrismaClient({ adapter });
11
+
12
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
@@ -0,0 +1,22 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
4
+ import { authRoutes } from "./routes/auth.js";
5
+
6
+ const app = new Hono();
7
+
8
+ // Allow the web app's origin to send/receive the session cookie.
9
+ app.use(
10
+ "*",
11
+ cors({
12
+ origin: (origin) => origin ?? "*",
13
+ credentials: true,
14
+ }),
15
+ );
16
+
17
+ app.get("/health", (c) => c.json({ status: "ok" }));
18
+ app.route("/auth", authRoutes);
19
+
20
+ const port = Number(process.env.PORT ?? 3001);
21
+ serve({ fetch: app.fetch, port });
22
+ console.log(`API service listening on :${port}`);