@nubase/create 0.1.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 (50) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +148 -0
  3. package/package.json +55 -0
  4. package/templates/backend/.env.development.template +4 -0
  5. package/templates/backend/.env.test.template +4 -0
  6. package/templates/backend/db/schema.sql +95 -0
  7. package/templates/backend/docker/dev/docker-compose.yml +21 -0
  8. package/templates/backend/docker/dev/postgresql-init/dump.sql +95 -0
  9. package/templates/backend/docker/test/docker-compose.yml +21 -0
  10. package/templates/backend/docker/test/postgresql-init/dump.sql +95 -0
  11. package/templates/backend/drizzle.config.ts +10 -0
  12. package/templates/backend/package.json +47 -0
  13. package/templates/backend/src/api/routes/auth.ts +209 -0
  14. package/templates/backend/src/api/routes/index.ts +2 -0
  15. package/templates/backend/src/api/routes/ticket.ts +62 -0
  16. package/templates/backend/src/auth/index.ts +37 -0
  17. package/templates/backend/src/db/helpers/drizzle.ts +34 -0
  18. package/templates/backend/src/db/schema/index.ts +4 -0
  19. package/templates/backend/src/db/schema/ticket.ts +13 -0
  20. package/templates/backend/src/db/schema/user-workspace.ts +17 -0
  21. package/templates/backend/src/db/schema/user.ts +10 -0
  22. package/templates/backend/src/db/schema/workspace.ts +9 -0
  23. package/templates/backend/src/db/seed.ts +71 -0
  24. package/templates/backend/src/helpers/env.ts +10 -0
  25. package/templates/backend/src/index.ts +55 -0
  26. package/templates/backend/src/middleware/workspace-middleware.ts +11 -0
  27. package/templates/backend/tsconfig.json +15 -0
  28. package/templates/backend/vitest.config.ts +8 -0
  29. package/templates/frontend/.env.development.template +1 -0
  30. package/templates/frontend/index.html +13 -0
  31. package/templates/frontend/package.json +30 -0
  32. package/templates/frontend/postcss.config.js +5 -0
  33. package/templates/frontend/src/auth/__PROJECT_NAME_PASCAL__AuthController.ts +59 -0
  34. package/templates/frontend/src/config.tsx +76 -0
  35. package/templates/frontend/src/main.tsx +6 -0
  36. package/templates/frontend/src/resources/ticket.ts +32 -0
  37. package/templates/frontend/src/styles/theme.css +3 -0
  38. package/templates/frontend/src/vite-env.d.ts +9 -0
  39. package/templates/frontend/tsconfig.json +17 -0
  40. package/templates/frontend/vite.config.ts +14 -0
  41. package/templates/root/README.md +94 -0
  42. package/templates/root/biome.json.template +40 -0
  43. package/templates/root/package.json +30 -0
  44. package/templates/root/turbo.json +18 -0
  45. package/templates/schema/package.json +28 -0
  46. package/templates/schema/src/api-endpoints.ts +34 -0
  47. package/templates/schema/src/index.ts +3 -0
  48. package/templates/schema/src/schema/auth.ts +89 -0
  49. package/templates/schema/src/schema/ticket.ts +68 -0
  50. package/templates/schema/tsconfig.json +14 -0
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { execSync } from "child_process";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import chalk from "chalk";
9
+ import { program } from "commander";
10
+ import fse from "fs-extra";
11
+ import prompts from "prompts";
12
+ var __filename = fileURLToPath(import.meta.url);
13
+ var __dirname = path.dirname(__filename);
14
+ var TEMPLATE_DIR = path.join(__dirname, "..", "templates");
15
+ function toPascalCase(str) {
16
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
17
+ }
18
+ function toCamelCase(str) {
19
+ const pascal = toPascalCase(str);
20
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
21
+ }
22
+ function toKebabCase(str) {
23
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase();
24
+ }
25
+ function replaceInContent(content, options) {
26
+ const kebabName = toKebabCase(options.name);
27
+ const pascalName = toPascalCase(options.name);
28
+ const camelName = toCamelCase(options.name);
29
+ return content.replace(/__PROJECT_NAME__/g, kebabName).replace(/__PROJECT_NAME_PASCAL__/g, pascalName).replace(/__PROJECT_NAME_CAMEL__/g, camelName).replace(/__DB_NAME__/g, options.dbName).replace(/__DB_USER__/g, options.dbUser).replace(/__DB_PASSWORD__/g, options.dbPassword).replace(/__DEV_PORT__/g, String(options.devPort)).replace(/__TEST_PORT__/g, String(options.testPort)).replace(/__BACKEND_PORT__/g, String(options.backendPort)).replace(/__FRONTEND_PORT__/g, String(options.frontendPort));
30
+ }
31
+ function copyTemplateDir(src, dest, options) {
32
+ fse.ensureDirSync(dest);
33
+ const entries = fs.readdirSync(src, { withFileTypes: true });
34
+ for (const entry of entries) {
35
+ const srcPath = path.join(src, entry.name);
36
+ let destName = replaceInContent(entry.name, options);
37
+ if (destName.endsWith(".template")) {
38
+ destName = destName.slice(0, -9);
39
+ }
40
+ const destPath = path.join(dest, destName);
41
+ if (entry.isDirectory()) {
42
+ copyTemplateDir(srcPath, destPath, options);
43
+ } else {
44
+ const content = fs.readFileSync(srcPath, "utf-8");
45
+ const processedContent = replaceInContent(content, options);
46
+ fs.writeFileSync(destPath, processedContent, "utf-8");
47
+ }
48
+ }
49
+ }
50
+ async function main() {
51
+ program.name("@nubase/create").description("Create a new Nubase application").argument("[project-name]", "Name of the project").option("--db-name <name>", "Database name").option("--db-user <user>", "Database user").option("--db-password <password>", "Database password").option("--dev-port <port>", "Development database port", "5434").option("--test-port <port>", "Test database port", "5435").option("--backend-port <port>", "Backend server port", "3001").option("--frontend-port <port>", "Frontend dev server port", "3002").option("--skip-install", "Skip npm install").parse();
52
+ const args = program.args;
53
+ const opts = program.opts();
54
+ console.log(chalk.bold.cyan("\n Welcome to Nubase!\n"));
55
+ let projectName = args[0];
56
+ if (!projectName) {
57
+ const response = await prompts({
58
+ type: "text",
59
+ name: "projectName",
60
+ message: "What is your project name?",
61
+ initial: "my-app",
62
+ validate: (value) => /^[a-z0-9-]+$/.test(value) ? true : "Project name must be lowercase with hyphens only"
63
+ });
64
+ if (!response.projectName) {
65
+ console.log(chalk.red("\nProject name is required."));
66
+ process.exit(1);
67
+ }
68
+ projectName = response.projectName;
69
+ }
70
+ const dbName = opts.dbName || toKebabCase(projectName).replace(/-/g, "_");
71
+ const dbUser = opts.dbUser || dbName;
72
+ const dbPassword = opts.dbPassword || dbName;
73
+ const options = {
74
+ name: projectName,
75
+ dbName,
76
+ dbUser,
77
+ dbPassword,
78
+ devPort: Number.parseInt(opts.devPort, 10),
79
+ testPort: Number.parseInt(opts.testPort, 10),
80
+ backendPort: Number.parseInt(opts.backendPort, 10),
81
+ frontendPort: Number.parseInt(opts.frontendPort, 10)
82
+ };
83
+ const targetDir = path.join(process.cwd(), projectName);
84
+ if (fs.existsSync(targetDir)) {
85
+ const response = await prompts({
86
+ type: "confirm",
87
+ name: "overwrite",
88
+ message: `Directory ${projectName} already exists. Overwrite?`,
89
+ initial: false
90
+ });
91
+ if (!response.overwrite) {
92
+ console.log(chalk.yellow("\nOperation cancelled."));
93
+ process.exit(0);
94
+ }
95
+ fse.removeSync(targetDir);
96
+ }
97
+ console.log(chalk.blue(`
98
+ Creating project in ${chalk.bold(targetDir)}...
99
+ `));
100
+ const templates = ["root", "schema", "backend", "frontend"];
101
+ for (const template of templates) {
102
+ const templatePath = path.join(TEMPLATE_DIR, template);
103
+ if (!fs.existsSync(templatePath)) {
104
+ console.log(chalk.yellow(`Template ${template} not found, skipping...`));
105
+ continue;
106
+ }
107
+ if (template === "root") {
108
+ copyTemplateDir(templatePath, targetDir, options);
109
+ } else {
110
+ const destPath = path.join(
111
+ targetDir,
112
+ `${toKebabCase(projectName)}-${template}`
113
+ );
114
+ copyTemplateDir(templatePath, destPath, options);
115
+ }
116
+ console.log(chalk.green(` \u2713 Created ${template}`));
117
+ }
118
+ if (!opts.skipInstall) {
119
+ console.log(chalk.blue("\nInstalling dependencies...\n"));
120
+ try {
121
+ execSync("npm install", {
122
+ cwd: targetDir,
123
+ stdio: "inherit"
124
+ });
125
+ console.log(chalk.green("\n \u2713 Dependencies installed"));
126
+ } catch {
127
+ console.log(
128
+ chalk.yellow("\n \u26A0 Failed to install dependencies. Run npm install manually.")
129
+ );
130
+ }
131
+ }
132
+ console.log(chalk.bold.green("\n Success! Your Nubase project is ready.\n"));
133
+ console.log(chalk.bold(" Next steps:\n"));
134
+ console.log(chalk.cyan(` cd ${projectName}`));
135
+ console.log(chalk.cyan(" npm run db:up # Start PostgreSQL"));
136
+ console.log(chalk.cyan(" npm run db:seed # Seed the database"));
137
+ console.log(chalk.cyan(" npm run dev # Start development\n"));
138
+ console.log(chalk.dim(" Database connection:"));
139
+ console.log(chalk.dim(` Host: localhost:${options.devPort}`));
140
+ console.log(chalk.dim(` Database: ${options.dbName}`));
141
+ console.log(chalk.dim(` User: ${options.dbUser}`));
142
+ console.log(chalk.dim(` Password: ${options.dbPassword}
143
+ `));
144
+ }
145
+ main().catch((error) => {
146
+ console.error(chalk.red("Error:"), error.message);
147
+ process.exit(1);
148
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@nubase/create",
3
+ "version": "0.1.1",
4
+ "description": "Create a new Nubase application",
5
+ "type": "module",
6
+ "bin": {
7
+ "nubase-create": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/index.ts --format esm --dts --clean",
15
+ "dev": "tsup src/index.ts --format esm --watch",
16
+ "typecheck": "tsc --noEmit",
17
+ "lint": "biome lint .",
18
+ "lint:fix": "biome lint --write .",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "chalk": "^5.3.0",
23
+ "commander": "^12.1.0",
24
+ "fs-extra": "^11.2.0",
25
+ "prompts": "^2.4.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/fs-extra": "^11.0.4",
29
+ "@types/node": "^22.10.2",
30
+ "@types/prompts": "^2.4.9",
31
+ "tsup": "^8.3.5",
32
+ "typescript": "^5.7.2"
33
+ },
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "keywords": [
38
+ "nubase",
39
+ "create",
40
+ "scaffold",
41
+ "fullstack",
42
+ "react",
43
+ "typescript"
44
+ ],
45
+ "author": "",
46
+ "license": "MIT",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "https://github.com/nubase/nubase.git",
53
+ "directory": "packages/create"
54
+ }
55
+ }
@@ -0,0 +1,4 @@
1
+ DATABASE_URL="postgres://__DB_NAME___app:__DB_PASSWORD__@localhost:__DEV_PORT__/__DB_NAME__"
2
+ DATABASE_URL_ADMIN="postgres://__DB_USER__:__DB_PASSWORD__@localhost:__DEV_PORT__/__DB_NAME__"
3
+ DEBUG_AUTH_TOKEN=dev-secret-123
4
+ JWT_SECRET=your-jwt-secret-change-in-production
@@ -0,0 +1,4 @@
1
+ DATABASE_URL="postgres://__DB_NAME___app:__DB_PASSWORD__@localhost:__TEST_PORT__/__DB_NAME__"
2
+ DATABASE_URL_ADMIN="postgres://__DB_USER__:__DB_PASSWORD__@localhost:__TEST_PORT__/__DB_NAME__"
3
+ DEBUG_AUTH_TOKEN=test-secret-123
4
+ JWT_SECRET=your-jwt-secret-change-in-production
@@ -0,0 +1,95 @@
1
+ -- __PROJECT_NAME_PASCAL__ Database Schema
2
+ -- This file is the source of truth for database structure
3
+
4
+ -- Create app user with restricted permissions (for RLS)
5
+ DO $$
6
+ BEGIN
7
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
8
+ CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
9
+ END IF;
10
+ END
11
+ $$;
12
+
13
+ -- Enable RLS on the database
14
+ ALTER DATABASE __DB_NAME__ SET row_security = on;
15
+
16
+ -- ============================================================
17
+ -- WORKSPACES TABLE
18
+ -- ============================================================
19
+ CREATE TABLE IF NOT EXISTS workspaces (
20
+ id SERIAL PRIMARY KEY,
21
+ slug VARCHAR(100) UNIQUE NOT NULL,
22
+ name VARCHAR(255) NOT NULL,
23
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
24
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
25
+ );
26
+
27
+ -- Grant permissions to app user
28
+ GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
29
+ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
30
+
31
+ -- ============================================================
32
+ -- USERS TABLE
33
+ -- ============================================================
34
+ CREATE TABLE IF NOT EXISTS users (
35
+ id SERIAL PRIMARY KEY,
36
+ email VARCHAR(255) UNIQUE NOT NULL,
37
+ username VARCHAR(100) UNIQUE NOT NULL,
38
+ password_hash VARCHAR(255) NOT NULL,
39
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
41
+ );
42
+
43
+ -- Grant permissions to app user
44
+ GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
45
+ GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
46
+
47
+ -- ============================================================
48
+ -- USER_WORKSPACES TABLE (Association with RLS)
49
+ -- ============================================================
50
+ CREATE TABLE IF NOT EXISTS user_workspaces (
51
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
52
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
53
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
54
+ PRIMARY KEY (user_id, workspace_id)
55
+ );
56
+
57
+ -- Enable RLS on user_workspaces
58
+ ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
59
+
60
+ -- Policy: Users can only see their own workspace associations
61
+ CREATE POLICY user_workspaces_isolation ON user_workspaces
62
+ USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
63
+
64
+ -- Grant permissions to app user
65
+ GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
66
+
67
+ -- ============================================================
68
+ -- TICKETS TABLE (with RLS)
69
+ -- ============================================================
70
+ CREATE TABLE IF NOT EXISTS tickets (
71
+ id SERIAL PRIMARY KEY,
72
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
73
+ title VARCHAR(255) NOT NULL,
74
+ description TEXT,
75
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
76
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
77
+ );
78
+
79
+ -- Enable RLS on tickets
80
+ ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
81
+
82
+ -- Policy: Users can only see tickets in their current workspace
83
+ CREATE POLICY tickets_workspace_isolation ON tickets
84
+ USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
85
+
86
+ -- Grant permissions to app user
87
+ GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
88
+ GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
89
+
90
+ -- ============================================================
91
+ -- DEFAULT DATA
92
+ -- ============================================================
93
+ -- Create default workspace
94
+ INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
95
+ ON CONFLICT (slug) DO NOTHING;
@@ -0,0 +1,21 @@
1
+ services:
2
+ postgresql:
3
+ image: postgres:17.5
4
+ container_name: __PROJECT_NAME__-db-dev
5
+ ports:
6
+ - "__DEV_PORT__:5432"
7
+ environment:
8
+ POSTGRES_DB: __DB_NAME__
9
+ POSTGRES_USER: __DB_USER__
10
+ POSTGRES_PASSWORD: __DB_PASSWORD__
11
+ volumes:
12
+ - postgresql-data:/var/lib/postgresql/data
13
+ - ./postgresql-init:/docker-entrypoint-initdb.d
14
+ healthcheck:
15
+ test: ["CMD-SHELL", "pg_isready -U __DB_USER__ -d __DB_NAME__"]
16
+ interval: 5s
17
+ timeout: 5s
18
+ retries: 5
19
+
20
+ volumes:
21
+ postgresql-data:
@@ -0,0 +1,95 @@
1
+ -- __PROJECT_NAME_PASCAL__ Database Schema
2
+ -- This file is the source of truth for database structure
3
+
4
+ -- Create app user with restricted permissions (for RLS)
5
+ DO $$
6
+ BEGIN
7
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
8
+ CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
9
+ END IF;
10
+ END
11
+ $$;
12
+
13
+ -- Enable RLS on the database
14
+ ALTER DATABASE __DB_NAME__ SET row_security = on;
15
+
16
+ -- ============================================================
17
+ -- WORKSPACES TABLE
18
+ -- ============================================================
19
+ CREATE TABLE IF NOT EXISTS workspaces (
20
+ id SERIAL PRIMARY KEY,
21
+ slug VARCHAR(100) UNIQUE NOT NULL,
22
+ name VARCHAR(255) NOT NULL,
23
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
24
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
25
+ );
26
+
27
+ -- Grant permissions to app user
28
+ GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
29
+ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
30
+
31
+ -- ============================================================
32
+ -- USERS TABLE
33
+ -- ============================================================
34
+ CREATE TABLE IF NOT EXISTS users (
35
+ id SERIAL PRIMARY KEY,
36
+ email VARCHAR(255) UNIQUE NOT NULL,
37
+ username VARCHAR(100) UNIQUE NOT NULL,
38
+ password_hash VARCHAR(255) NOT NULL,
39
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
41
+ );
42
+
43
+ -- Grant permissions to app user
44
+ GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
45
+ GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
46
+
47
+ -- ============================================================
48
+ -- USER_WORKSPACES TABLE (Association with RLS)
49
+ -- ============================================================
50
+ CREATE TABLE IF NOT EXISTS user_workspaces (
51
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
52
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
53
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
54
+ PRIMARY KEY (user_id, workspace_id)
55
+ );
56
+
57
+ -- Enable RLS on user_workspaces
58
+ ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
59
+
60
+ -- Policy: Users can only see their own workspace associations
61
+ CREATE POLICY user_workspaces_isolation ON user_workspaces
62
+ USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
63
+
64
+ -- Grant permissions to app user
65
+ GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
66
+
67
+ -- ============================================================
68
+ -- TICKETS TABLE (with RLS)
69
+ -- ============================================================
70
+ CREATE TABLE IF NOT EXISTS tickets (
71
+ id SERIAL PRIMARY KEY,
72
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
73
+ title VARCHAR(255) NOT NULL,
74
+ description TEXT,
75
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
76
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
77
+ );
78
+
79
+ -- Enable RLS on tickets
80
+ ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
81
+
82
+ -- Policy: Users can only see tickets in their current workspace
83
+ CREATE POLICY tickets_workspace_isolation ON tickets
84
+ USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
85
+
86
+ -- Grant permissions to app user
87
+ GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
88
+ GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
89
+
90
+ -- ============================================================
91
+ -- DEFAULT DATA
92
+ -- ============================================================
93
+ -- Create default workspace
94
+ INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
95
+ ON CONFLICT (slug) DO NOTHING;
@@ -0,0 +1,21 @@
1
+ services:
2
+ postgresql:
3
+ image: postgres:17.5
4
+ container_name: __PROJECT_NAME__-db-test
5
+ ports:
6
+ - "__TEST_PORT__:5432"
7
+ environment:
8
+ POSTGRES_DB: __DB_NAME__
9
+ POSTGRES_USER: __DB_USER__
10
+ POSTGRES_PASSWORD: __DB_PASSWORD__
11
+ volumes:
12
+ - postgresql-data:/var/lib/postgresql/data
13
+ - ./postgresql-init:/docker-entrypoint-initdb.d
14
+ healthcheck:
15
+ test: ["CMD-SHELL", "pg_isready -U __DB_USER__ -d __DB_NAME__"]
16
+ interval: 5s
17
+ timeout: 5s
18
+ retries: 5
19
+
20
+ volumes:
21
+ postgresql-data:
@@ -0,0 +1,95 @@
1
+ -- __PROJECT_NAME_PASCAL__ Database Schema
2
+ -- This file is the source of truth for database structure
3
+
4
+ -- Create app user with restricted permissions (for RLS)
5
+ DO $$
6
+ BEGIN
7
+ IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '__DB_NAME___app') THEN
8
+ CREATE ROLE __DB_NAME___app WITH LOGIN PASSWORD '__DB_PASSWORD__';
9
+ END IF;
10
+ END
11
+ $$;
12
+
13
+ -- Enable RLS on the database
14
+ ALTER DATABASE __DB_NAME__ SET row_security = on;
15
+
16
+ -- ============================================================
17
+ -- WORKSPACES TABLE
18
+ -- ============================================================
19
+ CREATE TABLE IF NOT EXISTS workspaces (
20
+ id SERIAL PRIMARY KEY,
21
+ slug VARCHAR(100) UNIQUE NOT NULL,
22
+ name VARCHAR(255) NOT NULL,
23
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
24
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
25
+ );
26
+
27
+ -- Grant permissions to app user
28
+ GRANT SELECT, INSERT, UPDATE, DELETE ON workspaces TO __DB_NAME___app;
29
+ GRANT USAGE, SELECT ON SEQUENCE workspaces_id_seq TO __DB_NAME___app;
30
+
31
+ -- ============================================================
32
+ -- USERS TABLE
33
+ -- ============================================================
34
+ CREATE TABLE IF NOT EXISTS users (
35
+ id SERIAL PRIMARY KEY,
36
+ email VARCHAR(255) UNIQUE NOT NULL,
37
+ username VARCHAR(100) UNIQUE NOT NULL,
38
+ password_hash VARCHAR(255) NOT NULL,
39
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
40
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
41
+ );
42
+
43
+ -- Grant permissions to app user
44
+ GRANT SELECT, INSERT, UPDATE, DELETE ON users TO __DB_NAME___app;
45
+ GRANT USAGE, SELECT ON SEQUENCE users_id_seq TO __DB_NAME___app;
46
+
47
+ -- ============================================================
48
+ -- USER_WORKSPACES TABLE (Association with RLS)
49
+ -- ============================================================
50
+ CREATE TABLE IF NOT EXISTS user_workspaces (
51
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
52
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
53
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
54
+ PRIMARY KEY (user_id, workspace_id)
55
+ );
56
+
57
+ -- Enable RLS on user_workspaces
58
+ ALTER TABLE user_workspaces ENABLE ROW LEVEL SECURITY;
59
+
60
+ -- Policy: Users can only see their own workspace associations
61
+ CREATE POLICY user_workspaces_isolation ON user_workspaces
62
+ USING (workspace_id = COALESCE(current_setting('app.current_workspace_id', true)::INTEGER, workspace_id));
63
+
64
+ -- Grant permissions to app user
65
+ GRANT SELECT, INSERT, UPDATE, DELETE ON user_workspaces TO __DB_NAME___app;
66
+
67
+ -- ============================================================
68
+ -- TICKETS TABLE (with RLS)
69
+ -- ============================================================
70
+ CREATE TABLE IF NOT EXISTS tickets (
71
+ id SERIAL PRIMARY KEY,
72
+ workspace_id INTEGER NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
73
+ title VARCHAR(255) NOT NULL,
74
+ description TEXT,
75
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
76
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
77
+ );
78
+
79
+ -- Enable RLS on tickets
80
+ ALTER TABLE tickets ENABLE ROW LEVEL SECURITY;
81
+
82
+ -- Policy: Users can only see tickets in their current workspace
83
+ CREATE POLICY tickets_workspace_isolation ON tickets
84
+ USING (workspace_id = current_setting('app.current_workspace_id', true)::INTEGER);
85
+
86
+ -- Grant permissions to app user
87
+ GRANT SELECT, INSERT, UPDATE, DELETE ON tickets TO __DB_NAME___app;
88
+ GRANT USAGE, SELECT ON SEQUENCE tickets_id_seq TO __DB_NAME___app;
89
+
90
+ -- ============================================================
91
+ -- DEFAULT DATA
92
+ -- ============================================================
93
+ -- Create default workspace
94
+ INSERT INTO workspaces (slug, name) VALUES ('default', 'Default Workspace')
95
+ ON CONFLICT (slug) DO NOTHING;
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from "drizzle-kit";
2
+
3
+ export default defineConfig({
4
+ out: "./drizzle",
5
+ schema: "./src/db/schema",
6
+ dialect: "postgresql",
7
+ dbCredentials: {
8
+ url: process.env.DATABASE_URL_ADMIN!,
9
+ },
10
+ });
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "__PROJECT_NAME__-backend",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "NODE_ENV=development tsx watch src/index.ts",
7
+ "dev:test": "PORT=__BACKEND_PORT__ DB_PORT=__TEST_PORT__ NODE_ENV=test tsx watch src/index.ts",
8
+ "build": "tsc",
9
+ "start": "node dist/index.js",
10
+ "db:dev:up": "docker compose -f docker/dev/docker-compose.yml up -d",
11
+ "db:dev:down": "docker compose -f docker/dev/docker-compose.yml down",
12
+ "db:dev:kill": "docker compose -f docker/dev/docker-compose.yml down -v",
13
+ "db:dev:seed": "NODE_ENV=development tsx src/db/seed.ts",
14
+ "db:test:up": "docker compose -f docker/test/docker-compose.yml up -d",
15
+ "db:test:down": "docker compose -f docker/test/docker-compose.yml down",
16
+ "db:test:kill": "docker compose -f docker/test/docker-compose.yml down -v",
17
+ "db:seed": "tsx src/db/seed.ts",
18
+ "db:schema-sync": "cp db/schema.sql docker/dev/postgresql-init/dump.sql && cp db/schema.sql docker/test/postgresql-init/dump.sql",
19
+ "typecheck": "tsc --noEmit",
20
+ "lint": "biome check .",
21
+ "lint:fix": "biome check . --write --unsafe",
22
+ "test": "vitest run",
23
+ "test:watch": "vitest"
24
+ },
25
+ "dependencies": {
26
+ "@hono/node-server": "^1.13.7",
27
+ "@nubase/backend": "*",
28
+ "bcrypt": "^5.1.1",
29
+ "dotenv": "^16.4.7",
30
+ "drizzle-orm": "^0.38.3",
31
+ "hono": "^4.6.14",
32
+ "jsonwebtoken": "^9.0.2",
33
+ "pg": "^8.13.1",
34
+ "__PROJECT_NAME__-schema": "*"
35
+ },
36
+ "devDependencies": {
37
+ "@faker-js/faker": "^9.3.0",
38
+ "@types/bcrypt": "^5.0.2",
39
+ "@types/jsonwebtoken": "^9.0.7",
40
+ "@types/node": "^22.10.2",
41
+ "@types/pg": "^8.11.10",
42
+ "drizzle-kit": "^0.30.1",
43
+ "tsx": "^4.19.2",
44
+ "typescript": "^5.7.2",
45
+ "vitest": "^2.1.8"
46
+ }
47
+ }