create-enterprise-backend 0.1.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 (49) hide show
  1. package/bin/cli.js +8 -0
  2. package/core/install.js +5 -0
  3. package/core/run.js +95 -0
  4. package/core/scaffold.js +37 -0
  5. package/package.json +28 -0
  6. package/templates/express/mongo/nodemon.json +6 -0
  7. package/templates/express/mongo/package.json +21 -0
  8. package/templates/express/mongo/src/app.js +41 -0
  9. package/templates/express/mongo/src/config/db.js +16 -0
  10. package/templates/express/mongo/src/config/env.js +9 -0
  11. package/templates/express/mongo/src/middlewares/db-guard.js +11 -0
  12. package/templates/express/mongo/src/middlewares/jwt.guard.js +18 -0
  13. package/templates/express/mongo/src/modules/auth/auth.controller.js +32 -0
  14. package/templates/express/mongo/src/modules/auth/auth.routes.js +12 -0
  15. package/templates/express/mongo/src/modules/auth/auth.service.js +68 -0
  16. package/templates/express/mongo/src/modules/auth/user.model.js +14 -0
  17. package/templates/express/mongo/src/routes.js +5 -0
  18. package/templates/express/mongo/src/server.js +11 -0
  19. package/templates/express/mongo/src/utils/error-handler.js +10 -0
  20. package/templates/fastify/mongo/nodemon.json +6 -0
  21. package/templates/fastify/mongo/package.json +23 -0
  22. package/templates/fastify/mongo/src/app.js +40 -0
  23. package/templates/fastify/mongo/src/config/db.js +16 -0
  24. package/templates/fastify/mongo/src/config/env.js +9 -0
  25. package/templates/fastify/mongo/src/middlewares/README.md +2 -0
  26. package/templates/fastify/mongo/src/modules/auth/auth.controller.js +32 -0
  27. package/templates/fastify/mongo/src/modules/auth/auth.routes.js +24 -0
  28. package/templates/fastify/mongo/src/modules/auth/auth.schema.js +22 -0
  29. package/templates/fastify/mongo/src/modules/auth/auth.service.js +68 -0
  30. package/templates/fastify/mongo/src/modules/auth/user.model.js +14 -0
  31. package/templates/fastify/mongo/src/plugins/jwt.js +8 -0
  32. package/templates/fastify/mongo/src/routes.js +11 -0
  33. package/templates/fastify/mongo/src/server.js +15 -0
  34. package/templates/fastify/mongo/src/services/README.md +2 -0
  35. package/templates/fastify/mongo/src/utils/db-guard.js +11 -0
  36. package/templates/fastify/mongo/src/utils/error-handler.js +18 -0
  37. package/templates/fastify/mongo/src/utils/jwt-guard.js +10 -0
  38. package/templates/fastify/supabase/package.json +18 -0
  39. package/templates/fastify/supabase/src/app.js +23 -0
  40. package/templates/fastify/supabase/src/config/env.js +18 -0
  41. package/templates/fastify/supabase/src/config/supabase.js +23 -0
  42. package/templates/fastify/supabase/src/modules/auth/auth.controller.js +7 -0
  43. package/templates/fastify/supabase/src/modules/auth/auth.routes.js +9 -0
  44. package/templates/fastify/supabase/src/modules/auth/auth.service.js +7 -0
  45. package/templates/fastify/supabase/src/plugins/request-content.js +16 -0
  46. package/templates/fastify/supabase/src/routes.js +5 -0
  47. package/templates/fastify/supabase/src/server.js +27 -0
  48. package/templates/fastify/supabase/src/utils/auth-guard.js +27 -0
  49. package/templates/fastify/supabase/src/utils/error-handler.js +10 -0
package/bin/cli.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../core/run.js";
4
+
5
+ run().catch((err) => {
6
+ console.error("āŒ Error:", err);
7
+ process.exit(1);
8
+ });
@@ -0,0 +1,5 @@
1
+ import { execSync } from "child_process";
2
+
3
+ export async function installDeps({ targetDir }) {
4
+ execSync("npm install", { cwd: targetDir, stdio: "inherit" });
5
+ }
package/core/run.js ADDED
@@ -0,0 +1,95 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import prompts from "prompts";
4
+ import chalk from "chalk";
5
+ import { generateProject } from "./scaffold.js";
6
+ import { installDeps } from "./install.js";
7
+
8
+ export async function run() {
9
+ console.log(chalk.cyan.bold("\nšŸš€ Create Backend (Enterprise Scaffolder)\n"));
10
+
11
+ const projectNameArg = process.argv[2];
12
+
13
+ const answers = await prompts(
14
+ [
15
+ {
16
+ type: projectNameArg ? null : "text",
17
+ name: "projectName",
18
+ message: "Project name?",
19
+ validate: (v) =>
20
+ v?.trim()?.length ? true : "Project name is required"
21
+ },
22
+ {
23
+ type: "select",
24
+ name: "framework",
25
+ message: "Select framework",
26
+ choices: [
27
+ { title: "Express", value: "express" },
28
+ { title: "Fastify", value: "fastify" }
29
+ ]
30
+ },
31
+ {
32
+ type: "select",
33
+ name: "database",
34
+ message: "Select database",
35
+ choices: [
36
+ { title: "MongoDB", value: "mongo" },
37
+ { title: "Supabase", value: "supabase" },
38
+ ]
39
+ },
40
+ {
41
+ type: "toggle",
42
+ name: "install",
43
+ message: "Install dependencies now?",
44
+ initial: true,
45
+ active: "yes",
46
+ inactive: "no"
47
+ }
48
+ ],
49
+ {
50
+ onCancel: () => {
51
+ console.log(chalk.yellow("\nā›” Cancelled.\n"));
52
+ process.exit(0);
53
+ }
54
+ }
55
+ );
56
+
57
+ const projectName = (projectNameArg || answers.projectName).trim();
58
+ const { framework, database } = answers;
59
+
60
+ const templatePath = path.resolve(
61
+ "templates",
62
+ framework,
63
+ database
64
+ );
65
+
66
+ // āœ… Validate template exists
67
+ if (!fs.existsSync(templatePath)) {
68
+ console.log(
69
+ chalk.red(
70
+ `\nāŒ Template not found for: ${framework} + ${database}\n`
71
+ )
72
+ );
73
+ process.exit(1);
74
+ }
75
+
76
+ const targetDir = path.join(process.cwd(), projectName);
77
+
78
+ console.log(chalk.gray("\nšŸ“¦ Generating project..."));
79
+ await generateProject({
80
+ framework,
81
+ database,
82
+ targetDir,
83
+ projectName
84
+ });
85
+
86
+ if (answers.install) {
87
+ console.log(chalk.gray("\nšŸ“„ Installing dependencies..."));
88
+ await installDeps({ targetDir });
89
+ }
90
+
91
+ console.log(chalk.green.bold("\nāœ… Done!\n"));
92
+ console.log(
93
+ `Next steps:\n cd ${projectName}\n npm run dev\n`
94
+ );
95
+ }
@@ -0,0 +1,37 @@
1
+ import path from "path";
2
+ import fs from "fs-extra";
3
+ import { fileURLToPath } from "url";
4
+
5
+ function getPackageRoot() {
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ return path.resolve(path.dirname(__filename), "..");
8
+ }
9
+
10
+ export async function generateProject({ framework, database, targetDir, projectName }) {
11
+ const root = getPackageRoot();
12
+
13
+ const templateDir = path.join(root, "templates", framework, database);
14
+
15
+ const exists = await fs.pathExists(templateDir);
16
+ if (!exists) {
17
+ throw new Error(`Template not found: ${framework}/${database}`);
18
+ }
19
+
20
+ if (await fs.pathExists(targetDir)) {
21
+ throw new Error(`Target folder already exists: ${projectName}`);
22
+ }
23
+
24
+ await fs.ensureDir(targetDir);
25
+ await fs.copy(templateDir, targetDir, {
26
+ overwrite: false,
27
+ errorOnExist: true
28
+ });
29
+
30
+ // Rename package name inside generated package.json (if template has one)
31
+ const pkgPath = path.join(targetDir, "package.json");
32
+ if (await fs.pathExists(pkgPath)) {
33
+ const pkg = await fs.readJson(pkgPath);
34
+ pkg.name = projectName;
35
+ await fs.writeJson(pkgPath, pkg, { spaces: 2 });
36
+ }
37
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "create-enterprise-backend",
3
+ "version": "0.1.0",
4
+ "description": "Enterprise backend scaffolder (Express/Fastify + Mongo/Supabase)",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-enterprise-backend": "./bin/cli.js"
8
+ },
9
+ "files": ["bin", "core", "templates", "README.md"],
10
+ "keywords": [
11
+ "cli",
12
+ "scaffold",
13
+ "backend",
14
+ "express",
15
+ "fastify",
16
+ "mongodb",
17
+ "supabase"
18
+ ],
19
+ "license": "MIT",
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "fs-extra": "^11.2.0",
26
+ "prompts": "^2.4.2"
27
+ }
28
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "watch": ["src"],
3
+ "ext": "js,json",
4
+ "ignore": ["node_modules"],
5
+ "exec": "clear && node src/server.js"
6
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "backend-app",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "nodemon src/server.js",
8
+ "start": "node src/server.js"
9
+ },
10
+ "dependencies": {
11
+ "express": "^4.19.2",
12
+ "mongoose": "^8.3.0",
13
+ "dotenv": "^16.4.5",
14
+ "bcryptjs": "^2.4.3",
15
+ "jsonwebtoken": "^9.0.2",
16
+ "cors": "^2.8.5"
17
+ },
18
+ "devDependencies": {
19
+ "nodemon": "^3.0.3"
20
+ }
21
+ }
@@ -0,0 +1,41 @@
1
+ import express from "express";
2
+ import cors from "cors";
3
+ import { registerRoutes } from "./routes.js";
4
+ import { globalErrorHandler } from "./utils/error-handler.js";
5
+
6
+ export function buildApp() {
7
+ const app = express();
8
+
9
+ app.use(cors());
10
+ app.use(express.json());
11
+
12
+ // Root
13
+ app.get("/", (req, res) => {
14
+ res.json({
15
+ status: "ok",
16
+ message: "Express API is running"
17
+ });
18
+ });
19
+
20
+ // Health
21
+ app.get("/health", (req, res) => {
22
+ res.json({ status: "ok" });
23
+ });
24
+
25
+ // Register ALL routes
26
+ registerRoutes(app);
27
+
28
+ // āŒ 404 Not Found Handler (Express way)
29
+ app.use((req, res, next) => {
30
+ res.status(404).json({
31
+ statusCode: 404,
32
+ message: "Route not found",
33
+ path: req.originalUrl
34
+ });
35
+ });
36
+
37
+ // āœ… Global Error Handler (MUST be last)
38
+ app.use(globalErrorHandler);
39
+
40
+ return app;
41
+ }
@@ -0,0 +1,16 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export async function connectDB(uri) {
4
+ if (!uri) {
5
+ console.warn("āš ļø MongoDB disabled (MONGO_URI not set)");
6
+ return;
7
+ }
8
+
9
+ try {
10
+ await mongoose.connect(uri);
11
+ console.log("āœ… MongoDB connected");
12
+ } catch (err) {
13
+ console.error("āŒ MongoDB connection failed:", err.message);
14
+ console.warn("āš ļø App running without database");
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ export const env = {
6
+ PORT: process.env.PORT || 5000,
7
+ MONGO_URI: process.env.MONGO_URI || "",
8
+ JWT_SECRET: process.env.JWT_SECRET || "changeme"
9
+ };
@@ -0,0 +1,11 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export function requireDB(req, res, next) {
4
+ if (mongoose.connection.readyState !== 1) {
5
+ return res.status(503).json({
6
+ statusCode: 503,
7
+ message: "Database not connected. Please configure MongoDB."
8
+ });
9
+ }
10
+ next();
11
+ }
@@ -0,0 +1,18 @@
1
+ import jwt from "jsonwebtoken";
2
+ import { env } from "../config/env.js";
3
+
4
+ export function requireAuth(req, res, next) {
5
+ const authHeader = req.headers.authorization;
6
+
7
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
8
+ return res.status(401).json({ message: "Unauthorized" });
9
+ }
10
+
11
+ try {
12
+ const token = authHeader.split(" ")[1];
13
+ req.user = jwt.verify(token, env.JWT_SECRET);
14
+ next();
15
+ } catch {
16
+ res.status(401).json({ message: "Invalid token" });
17
+ }
18
+ }
@@ -0,0 +1,32 @@
1
+ import { registerUser, loginUser } from "./auth.service.js";
2
+ import { env } from "../../config/env.js";
3
+ import { User } from "./user.model.js";
4
+
5
+
6
+ export async function register(req, reply) {
7
+ try {
8
+ const result = await registerUser(req.body, env.JWT_SECRET);
9
+ reply.code(201).send(result);
10
+ } catch (err) {
11
+ reply.code(400).send({ message: err.message });
12
+ }
13
+ }
14
+
15
+ export async function login(req, reply) {
16
+ try {
17
+ const result = await loginUser(req.body, env.JWT_SECRET);
18
+ reply.send(result);
19
+ } catch (err) {
20
+ reply.code(400).send({ message: err.message });
21
+ }
22
+ }
23
+
24
+
25
+ export async function me(req, reply) {
26
+ const userId = req.user.id;
27
+
28
+ const user = await User.findById(userId).select("-password");
29
+
30
+ reply.send(user);
31
+ }
32
+
@@ -0,0 +1,12 @@
1
+ import express from "express";
2
+ import { register, login, me } from "./auth.controller.js";
3
+ import { requireDB } from "../../middlewares/db-guard.js";
4
+ import { requireAuth } from "../../middlewares/jwt.guard.js";
5
+
6
+ const router = express.Router();
7
+
8
+ router.post("/register", requireDB, register);
9
+ router.post("/login", requireDB, login);
10
+ router.get("/me", requireDB, requireAuth, me);
11
+
12
+ export default router;
@@ -0,0 +1,68 @@
1
+ import bcrypt from "bcryptjs";
2
+ import jwt from "jsonwebtoken";
3
+ import { User } from "./user.model.js";
4
+
5
+ export async function registerUser({ name, email, password }, jwtSecret) {
6
+ if (!jwtSecret) {
7
+ throw new Error("JWT_SECRET not configured");
8
+ }
9
+
10
+ const exists = await User.findOne({ email });
11
+ if (exists) {
12
+ throw new Error("Email already exists");
13
+ }
14
+
15
+ const hashedPassword = await bcrypt.hash(password, 10);
16
+
17
+ const user = await User.create({
18
+ name,
19
+ email,
20
+ password: hashedPassword
21
+ });
22
+
23
+ const token = jwt.sign(
24
+ { id: user._id, email: user.email },
25
+ jwtSecret,
26
+ { expiresIn: "7d" }
27
+ );
28
+
29
+ return {
30
+ token,
31
+ user: {
32
+ id: user._id,
33
+ name: user.name,
34
+ email: user.email
35
+ }
36
+ };
37
+ }
38
+
39
+ export async function loginUser({ email, password }, jwtSecret) {
40
+ if (!jwtSecret) {
41
+ throw new Error("JWT_SECRET not configured");
42
+ }
43
+
44
+ const user = await User.findOne({ email });
45
+ if (!user) {
46
+ throw new Error("Invalid credentials");
47
+ }
48
+
49
+ const match = await bcrypt.compare(password, user.password);
50
+ if (!match) {
51
+ throw new Error("Invalid credentials");
52
+ }
53
+
54
+ const token = jwt.sign(
55
+ { id: user._id, email: user.email },
56
+ jwtSecret,
57
+ { expiresIn: "7d" }
58
+ );
59
+
60
+ return {
61
+ token,
62
+ user: {
63
+ id: user._id,
64
+ name: user.name,
65
+ email: user.email
66
+ }
67
+ };
68
+ }
@@ -0,0 +1,14 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const userSchema = new mongoose.Schema(
4
+ {
5
+ name: { type: String, required: true },
6
+ email: { type: String, required: true, unique: true, lowercase: true },
7
+ password: { type: String, required: true }
8
+ },
9
+ { timestamps: true }
10
+ );
11
+
12
+ // āš ļø Prevent model overwrite in dev / watch mode
13
+ export const User =
14
+ mongoose.models.User || mongoose.model("User", userSchema);
@@ -0,0 +1,5 @@
1
+ import authRoutes from "./modules/auth/auth.routes.js";
2
+
3
+ export function registerRoutes(app) {
4
+ app.use("/api/auth", authRoutes);
5
+ }
@@ -0,0 +1,11 @@
1
+ import { buildApp } from "./app.js";
2
+ import { connectDB } from "./config/db.js";
3
+ import { env } from "./config/env.js";
4
+
5
+ const app = buildApp();
6
+
7
+ await connectDB(env.MONGO_URI);
8
+
9
+ app.listen(env.PORT, () => {
10
+ console.log(`šŸš€ Express server running on http://localhost:${env.PORT}`);
11
+ });
@@ -0,0 +1,10 @@
1
+ export function globalErrorHandler(err, req, res, next) {
2
+ console.error(err);
3
+
4
+ res.status(err.statusCode || 500).json({
5
+ statusCode: err.statusCode || 500,
6
+ message: err.message || "Internal Server Error",
7
+ path: req.originalUrl,
8
+ timestamp: new Date().toISOString()
9
+ });
10
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "watch": ["src"],
3
+ "ext": "js,json",
4
+ "ignore": ["node_modules"],
5
+ "exec": "clear && node src/server.js"
6
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "backend-app",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "nodemon src/server.js",
7
+ "start": "node src/server.js"
8
+ },
9
+ "dependencies": {
10
+ "@fastify/jwt": "^7.0.1",
11
+ "bcryptjs": "^2.4.3",
12
+ "dotenv": "^16.4.5",
13
+ "fastify": "^4.27.0",
14
+ "fastify-plugin": "^5.1.0",
15
+ "jsonwebtoken": "^9.0.2",
16
+ "mongoose": "^8.3.0"
17
+ },
18
+ "devDependencies": {
19
+ "nodemon": "^3.0.3"
20
+ }
21
+ }
22
+
23
+
@@ -0,0 +1,40 @@
1
+ import Fastify from "fastify";
2
+ import { globalErrorHandler } from "./utils/error-handler.js";
3
+ import { registerRoutes } from "./routes.js";
4
+ import jwtPlugin from "./plugins/jwt.js";
5
+ import { env } from "./config/env.js";
6
+
7
+ export function buildApp() {
8
+ const app = Fastify({ logger: true });
9
+
10
+
11
+
12
+ // JWT plugin
13
+ app.register(jwtPlugin, { secret: env.JWT_SECRET });
14
+
15
+ // Root
16
+ app.get("/", async () => ({
17
+ status: "ok",
18
+ message: "Fastify API is running"
19
+ }));
20
+
21
+ // Health
22
+ app.get("/health", async () => ({
23
+ status: "ok"
24
+ }));
25
+
26
+ // Register ALL routes here
27
+ registerRoutes(app);
28
+
29
+ // Global error handler (must be last)
30
+ app.setNotFoundHandler((request, reply) => {
31
+ reply.code(404).send({
32
+ statusCode: 404,
33
+ message: "Route not found",
34
+ path: request.url
35
+ });
36
+ });
37
+
38
+
39
+ return app;
40
+ }
@@ -0,0 +1,16 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export async function connectDB(uri, logger = console) {
4
+ if (!uri) {
5
+ logger.warn("āš ļø MongoDB disabled (MONGO_URI not set)");
6
+ return;
7
+ }
8
+
9
+ try {
10
+ await mongoose.connect(uri);
11
+ logger.info("āœ… MongoDB connected");
12
+ } catch (err) {
13
+ logger.error("āŒ MongoDB connection failed:", err.message);
14
+ logger.warn("āš ļø Continuing without database");
15
+ }
16
+ }
@@ -0,0 +1,9 @@
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ export const env = {
6
+ PORT: process.env.PORT || 5000,
7
+ MONGO_URI: process.env.MONGO_URI || "",
8
+ JWT_SECRET: process.env.JWT_SECRET || "changeme"
9
+ };
@@ -0,0 +1,2 @@
1
+ This folder contains reusable Express/Fastify middlewares
2
+ (e.g. auth guards, role guards, rate limiting, etc.)
@@ -0,0 +1,32 @@
1
+ import { registerUser, loginUser } from "./auth.service.js";
2
+ import { env } from "../../config/env.js";
3
+ import { User } from "./user.model.js";
4
+
5
+
6
+ export async function register(req, reply) {
7
+ try {
8
+ const result = await registerUser(req.body, env.JWT_SECRET);
9
+ reply.code(201).send(result);
10
+ } catch (err) {
11
+ reply.code(400).send({ message: err.message });
12
+ }
13
+ }
14
+
15
+ export async function login(req, reply) {
16
+ try {
17
+ const result = await loginUser(req.body, env.JWT_SECRET);
18
+ reply.send(result);
19
+ } catch (err) {
20
+ reply.code(400).send({ message: err.message });
21
+ }
22
+ }
23
+
24
+
25
+ export async function me(req, reply) {
26
+ const userId = req.user.id;
27
+
28
+ const user = await User.findById(userId).select("-password");
29
+
30
+ reply.send(user);
31
+ }
32
+
@@ -0,0 +1,24 @@
1
+ import { register, login, me } from "./auth.controller.js";
2
+ import { registerSchema, loginSchema } from "./auth.schema.js";
3
+ import { requireDB } from "../../utils/db-guard.js";
4
+ import { requireAuth } from "../../utils/jwt-guard.js";
5
+
6
+ export default async function authRoutes(app) {
7
+ app.post("/register", {
8
+ preHandler: requireDB,
9
+ schema: registerSchema,
10
+ handler: register
11
+ });
12
+
13
+ app.post("/login", {
14
+ preHandler: requireDB,
15
+ schema: loginSchema,
16
+ handler: login
17
+ });
18
+
19
+ // šŸ” JWT protected route
20
+ app.get("/me", {
21
+ preHandler: [requireDB, requireAuth],
22
+ handler: me
23
+ });
24
+ }
@@ -0,0 +1,22 @@
1
+ export const registerSchema = {
2
+ body: {
3
+ type: "object",
4
+ required: ["name", "email", "password"],
5
+ properties: {
6
+ name: { type: "string", minLength: 2 },
7
+ email: { type: "string", format: "email" },
8
+ password: { type: "string", minLength: 6 }
9
+ }
10
+ }
11
+ };
12
+
13
+ export const loginSchema = {
14
+ body: {
15
+ type: "object",
16
+ required: ["email", "password"],
17
+ properties: {
18
+ email: { type: "string", format: "email" },
19
+ password: { type: "string", minLength: 6 }
20
+ }
21
+ }
22
+ };
@@ -0,0 +1,68 @@
1
+ import bcrypt from "bcryptjs";
2
+ import jwt from "jsonwebtoken";
3
+ import { User } from "./user.model.js";
4
+
5
+ export async function registerUser({ name, email, password }, jwtSecret) {
6
+ if (!jwtSecret) {
7
+ throw new Error("JWT_SECRET not configured");
8
+ }
9
+
10
+ const exists = await User.findOne({ email });
11
+ if (exists) {
12
+ throw new Error("Email already exists");
13
+ }
14
+
15
+ const hashedPassword = await bcrypt.hash(password, 10);
16
+
17
+ const user = await User.create({
18
+ name,
19
+ email,
20
+ password: hashedPassword
21
+ });
22
+
23
+ const token = jwt.sign(
24
+ { id: user._id, email: user.email },
25
+ jwtSecret,
26
+ { expiresIn: "7d" }
27
+ );
28
+
29
+ return {
30
+ token,
31
+ user: {
32
+ id: user._id,
33
+ name: user.name,
34
+ email: user.email
35
+ }
36
+ };
37
+ }
38
+
39
+ export async function loginUser({ email, password }, jwtSecret) {
40
+ if (!jwtSecret) {
41
+ throw new Error("JWT_SECRET not configured");
42
+ }
43
+
44
+ const user = await User.findOne({ email });
45
+ if (!user) {
46
+ throw new Error("Invalid credentials");
47
+ }
48
+
49
+ const match = await bcrypt.compare(password, user.password);
50
+ if (!match) {
51
+ throw new Error("Invalid credentials");
52
+ }
53
+
54
+ const token = jwt.sign(
55
+ { id: user._id, email: user.email },
56
+ jwtSecret,
57
+ { expiresIn: "7d" }
58
+ );
59
+
60
+ return {
61
+ token,
62
+ user: {
63
+ id: user._id,
64
+ name: user.name,
65
+ email: user.email
66
+ }
67
+ };
68
+ }
@@ -0,0 +1,14 @@
1
+ import mongoose from "mongoose";
2
+
3
+ const userSchema = new mongoose.Schema(
4
+ {
5
+ name: { type: String, required: true },
6
+ email: { type: String, required: true, unique: true, lowercase: true },
7
+ password: { type: String, required: true }
8
+ },
9
+ { timestamps: true }
10
+ );
11
+
12
+ // āš ļø Prevent model overwrite in dev / watch mode
13
+ export const User =
14
+ mongoose.models.User || mongoose.model("User", userSchema);
@@ -0,0 +1,8 @@
1
+ import fp from "fastify-plugin";
2
+ import jwt from "@fastify/jwt";
3
+
4
+ export default fp(async (app, opts) => {
5
+ app.register(jwt, {
6
+ secret: opts.secret
7
+ });
8
+ });
@@ -0,0 +1,11 @@
1
+ import authRoutes from "./modules/auth/auth.routes.js";
2
+ // later you can add more:
3
+ // import userRoutes from "./modules/users/user.routes.js";
4
+
5
+ export async function registerRoutes(app) {
6
+ // Auth routes
7
+ app.register(authRoutes, { prefix: "/api/auth" });
8
+
9
+ // Future modules
10
+ // app.register(userRoutes, { prefix: "/api/users" });
11
+ }
@@ -0,0 +1,15 @@
1
+ import { buildApp } from "./app.js";
2
+ import { connectDB } from "./config/db.js";
3
+ import { env } from "./config/env.js";
4
+
5
+ const app = buildApp();
6
+
7
+ await connectDB(env.MONGO_URI, app.log);
8
+
9
+ app.listen({ port: env.PORT }, (err, address) => {
10
+ if (err) {
11
+ app.log.error(err);
12
+ process.exit(1);
13
+ }
14
+ app.log.info(`šŸš€ Server running at ${address}`);
15
+ });
@@ -0,0 +1,2 @@
1
+ This folder contains shared business logic services
2
+ used across multiple modules.
@@ -0,0 +1,11 @@
1
+ import mongoose from "mongoose";
2
+
3
+ export function requireDB(req, reply, done) {
4
+ if (mongoose.connection.readyState !== 1) {
5
+ return reply.code(503).send({
6
+ statusCode: 503,
7
+ message: "Database not connected. Please configure MongoDB."
8
+ });
9
+ }
10
+ done();
11
+ }
@@ -0,0 +1,18 @@
1
+ export function globalErrorHandler(error, request, reply) {
2
+ // Log full error (for dev / prod logs)
3
+ request.log.error(error);
4
+
5
+ // Default values
6
+ const statusCode = error.statusCode || 500;
7
+ const message =
8
+ statusCode === 500
9
+ ? "Internal Server Error"
10
+ : error.message;
11
+
12
+ reply.code(statusCode).send({
13
+ statusCode,
14
+ message,
15
+ path: request.url,
16
+ timestamp: new Date().toISOString()
17
+ });
18
+ }
@@ -0,0 +1,10 @@
1
+ export async function requireAuth(request, reply) {
2
+ try {
3
+ await request.jwtVerify();
4
+ } catch (err) {
5
+ reply.code(401).send({
6
+ statusCode: 401,
7
+ message: "Unauthorized: Invalid or missing token"
8
+ });
9
+ }
10
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "fastify-supabase-app",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "nodemon src/server.js",
7
+ "start": "node src/server.js"
8
+ },
9
+ "dependencies": {
10
+ "@supabase/supabase-js": "^2.45.0",
11
+ "dotenv": "^16.4.5",
12
+ "fastify": "^4.27.0",
13
+ "fastify-plugin": "^5.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "nodemon": "^3.0.3"
17
+ }
18
+ }
@@ -0,0 +1,23 @@
1
+ import Fastify from "fastify";
2
+ import requestContext from "./plugins/request-content.js";
3
+ import { registerRoutes } from "./routes.js";
4
+ import { globalErrorHandler } from "./utils/error-handler.js";
5
+
6
+ export function buildApp() {
7
+ const app = Fastify({ logger: true });
8
+
9
+ app.register(requestContext);
10
+
11
+ app.get("/", async () => ({
12
+ status: "ok",
13
+ message: "Fastify Supabase Enterprise API"
14
+ }));
15
+
16
+ app.get("/health", async () => ({ status: "ok" }));
17
+
18
+ registerRoutes(app);
19
+
20
+ app.setErrorHandler(globalErrorHandler);
21
+
22
+ return app;
23
+ }
@@ -0,0 +1,18 @@
1
+ import dotenv from "dotenv";
2
+
3
+ dotenv.config();
4
+
5
+ function required(key) {
6
+ if (!process.env[key]) {
7
+ throw new Error(`Missing required environment variable: ${key}`);
8
+ }
9
+ return process.env[key];
10
+ }
11
+
12
+ export const env = {
13
+ PORT: process.env.PORT || 5000,
14
+
15
+ SUPABASE_URL: process.env.SUPABASE_URL,
16
+ SUPABASE_ANON_KEY: process.env.SUPABASE_ANON_KEY,
17
+ SUPABASE_SERVICE_ROLE_KEY: process.env.SUPABASE_SERVICE_ROLE_KEY
18
+ };
@@ -0,0 +1,23 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import { env } from "./env.js";
3
+
4
+ function createSupabaseClient(name, url, key) {
5
+ if (!url || !key) {
6
+ console.warn(`āš ļø Supabase ${name} client disabled (missing env vars)`);
7
+ return null;
8
+ }
9
+
10
+ return createClient(url, key);
11
+ }
12
+
13
+ export const supabasePublic = createSupabaseClient(
14
+ "public",
15
+ env.SUPABASE_URL,
16
+ env.SUPABASE_ANON_KEY
17
+ );
18
+
19
+ export const supabaseAdmin = createSupabaseClient(
20
+ "admin",
21
+ env.SUPABASE_URL,
22
+ env.SUPABASE_SERVICE_ROLE_KEY
23
+ );
@@ -0,0 +1,7 @@
1
+ import { getCurrentUser } from "./auth.service.js";
2
+
3
+ export async function me(req, reply) {
4
+ reply.send({
5
+ user: getCurrentUser(req.user)
6
+ });
7
+ }
@@ -0,0 +1,9 @@
1
+ import { me } from "./auth.controller.js";
2
+ import { requireAuth } from "../../utils/auth-guard.js";
3
+
4
+ export default async function authRoutes(app) {
5
+ app.get("/me", {
6
+ preHandler: requireAuth,
7
+ handler: me
8
+ });
9
+ }
@@ -0,0 +1,7 @@
1
+ export function getCurrentUser(user) {
2
+ return {
3
+ id: user.id,
4
+ email: user.email,
5
+ role: user.role
6
+ };
7
+ }
@@ -0,0 +1,16 @@
1
+ import fp from "fastify-plugin";
2
+ import { supabasePublic } from "../config/supabase.js";
3
+
4
+ export default fp(async (app) => {
5
+ app.decorateRequest("user", null);
6
+
7
+ app.addHook("preHandler", async (req) => {
8
+ const auth = req.headers.authorization;
9
+ if (!auth?.startsWith("Bearer ")) return;
10
+
11
+ const token = auth.split(" ")[1];
12
+ const { data } = await supabasePublic.auth.getUser(token);
13
+
14
+ req.user = data?.user || null;
15
+ });
16
+ });
@@ -0,0 +1,5 @@
1
+ import authRoutes from "./modules/auth/auth.routes.js";
2
+
3
+ export async function registerRoutes(app) {
4
+ app.register(authRoutes, { prefix: "/api/auth" });
5
+ }
@@ -0,0 +1,27 @@
1
+ import { buildApp } from "./app.js";
2
+ import { env } from "./config/env.js";
3
+
4
+ const app = buildApp();
5
+
6
+ const MAX_PORT_TRIES = 20;
7
+
8
+ async function listenSilently(startPort) {
9
+ let port = startPort;
10
+
11
+ for (let i = 0; i < MAX_PORT_TRIES; i++) {
12
+ try {
13
+ await app.listen({ port });
14
+ app.log.info(`šŸš€ Server running at http://localhost:${port}`);
15
+ return;
16
+ } catch (err) {
17
+ if (err.code !== "EADDRINUSE") {
18
+ throw err;
19
+ }
20
+ port++;
21
+ }
22
+ }
23
+
24
+ throw new Error("No available ports found");
25
+ }
26
+
27
+ listenSilently(env.PORT);
@@ -0,0 +1,27 @@
1
+ import { supabasePublic } from "../config/supabase.js";
2
+
3
+ export async function requireAuth(request, reply) {
4
+ if (!supabasePublic) {
5
+ return reply.code(503).send({
6
+ message: "Auth service not configured (Supabase disabled)"
7
+ });
8
+ }
9
+
10
+ const auth = request.headers.authorization;
11
+
12
+ if (!auth || !auth.startsWith("Bearer ")) {
13
+ return reply.code(401).send({ message: "Unauthorized" });
14
+ }
15
+
16
+ const token = auth.split(" ")[1];
17
+
18
+ const { data, error } = await supabasePublic.auth.getUser(token);
19
+
20
+ if (error || !data?.user) {
21
+ return reply.code(401).send({
22
+ message: "Invalid or expired token"
23
+ });
24
+ }
25
+
26
+ request.user = data.user;
27
+ }
@@ -0,0 +1,10 @@
1
+ export function globalErrorHandler(error, request, reply) {
2
+ request.log.error(error);
3
+
4
+ reply.code(error.statusCode || 500).send({
5
+ statusCode: error.statusCode || 500,
6
+ message: error.message || "Internal Server Error",
7
+ path: request.url,
8
+ timestamp: new Date().toISOString()
9
+ });
10
+ }