chore: initial project scaffold

Setup monorepo structure with pnpm workspaces, Turborepo, TypeScript,
Biome, Docker Compose (PostgreSQL, Redis, MinIO), Nginx configs,
PM2 ecosystem, and SSL certificates.
This commit is contained in:
Sase Dev
2026-02-12 00:26:34 +00:00
commit 7fc47ce9cc
12 changed files with 453 additions and 0 deletions

45
.gitignore vendored Normal file
View File

@@ -0,0 +1,45 @@
# Dependencies
node_modules/
.pnpm-store/
# Build outputs
dist/
.next/
.turbo/
out/
# Environment
.env
.env.local
.env.*.local
# Logs
logs/
*.log
npm-debug.log*
# Backups
backups/
# IDE
.vscode/
.idea/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Docker secrets
docker/.env
# PM2
.pm2/
# Coverage
coverage/
# Temp
tmp/
temp/

28
biome.json Normal file
View File

@@ -0,0 +1,28 @@
{
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
"organizeImports": {
"enabled": true
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "all"
}
},
"files": {
"ignore": ["node_modules", "dist", ".next", "*.min.js"]
}
}

54
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
services:
sase-postgres:
image: postgres:17-alpine
container_name: sase-postgres
restart: unless-stopped
env_file: .env
ports:
- "127.0.0.1:5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U sase"]
interval: 10s
timeout: 5s
retries: 5
sase-redis:
image: redis:7.4-alpine
container_name: sase-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
sase-minio:
image: minio/minio:latest
container_name: sase-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9001:9001"
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
redis_data:
minio_data:

36
docker/minio/init-bucket.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
set -e
# Load env vars
source "$(dirname "$0")/../.env"
MINIO_ENDPOINT="http://127.0.0.1:9000"
BUCKET_NAME="sase-schemas"
ALIAS_NAME="sase-minio"
# Install mc (MinIO Client) if not available
if ! command -v mc &> /dev/null; then
echo "Installing MinIO Client..."
curl -sSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /tmp/mc
chmod +x /tmp/mc
MC="/tmp/mc"
else
MC="mc"
fi
# Configure alias
$MC alias set "$ALIAS_NAME" "$MINIO_ENDPOINT" "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"
# Create bucket if it doesn't exist
if $MC ls "$ALIAS_NAME/$BUCKET_NAME" &> /dev/null; then
echo "Bucket '$BUCKET_NAME' already exists."
else
$MC mb "$ALIAS_NAME/$BUCKET_NAME"
echo "Bucket '$BUCKET_NAME' created."
fi
# Set public read (download) policy
$MC anonymous set download "$ALIAS_NAME/$BUCKET_NAME"
echo "Public read access set on '$BUCKET_NAME'."
echo "MinIO bucket initialization complete."

View File

@@ -0,0 +1,59 @@
server {
server_name api.sase.tr;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Block sensitive paths
location ~ /\.(git|env) {
deny all;
return 404;
}
location ~ ^/(node_modules)/ {
deny all;
return 404;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / {
proxy_pass http://127.0.0.1:4000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# 60s timeout for VIN decode operations
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/v2.sase.tr/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/v2.sase.tr/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = api.sase.tr) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name api.sase.tr;
return 404; # managed by Certbot
}

View File

@@ -0,0 +1,59 @@
server {
server_name storage.sase.tr;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Only allow /sase-schemas/ path, 404 everything else
location /sase-schemas/ {
proxy_pass http://127.0.0.1:9000/sase-schemas/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Cache-Control: 1 year, immutable
add_header Cache-Control "public, max-age=31536000, immutable" always;
# CORS
add_header Access-Control-Allow-Origin "https://v2.sase.tr" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "Range, Content-Type" always;
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin "https://v2.sase.tr";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
add_header Access-Control-Allow-Headers "Range, Content-Type";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
add_header Content-Type text/plain;
return 204;
}
}
location / {
return 404;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/v2.sase.tr/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/v2.sase.tr/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = storage.sase.tr) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name storage.sase.tr;
return 404; # managed by Certbot
}

View File

@@ -0,0 +1,57 @@
server {
server_name v2.sase.tr;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Block sensitive paths
location ~ /\.(git|env) {
deny all;
return 404;
}
location ~ ^/(node_modules)/ {
deny all;
return 404;
}
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/v2.sase.tr/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/v2.sase.tr/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = v2.sase.tr) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
server_name v2.sase.tr;
return 404; # managed by Certbot
}

52
ecosystem.config.js Normal file
View File

@@ -0,0 +1,52 @@
module.exports = {
apps: [
{
name: "sase-api",
cwd: "./apps/api",
script: "dist/main.js",
instances: 2,
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: 4000,
},
max_memory_restart: "512M",
error_file: "../../logs/api-error.log",
out_file: "../../logs/api-out.log",
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
},
{
name: "sase-web",
cwd: "./apps/web",
script: "node_modules/.bin/next",
args: "start",
instances: 2,
exec_mode: "cluster",
env: {
NODE_ENV: "production",
PORT: 3000,
},
max_memory_restart: "512M",
error_file: "../../logs/web-error.log",
out_file: "../../logs/web-out.log",
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
},
{
name: "sase-worker",
cwd: "./apps/api",
script: "dist/worker.js",
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
},
max_memory_restart: "256M",
error_file: "../../logs/worker-error.log",
out_file: "../../logs/worker-out.log",
merge_logs: true,
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
},
],
};

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "sase-v2",
"version": "0.1.0",
"private": true,
"packageManager": "pnpm@10.29.3",
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean"
},
"devDependencies": {
"turbo": "^2"
}
}

3
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,3 @@
packages:
- "apps/*"
- "packages/*"

22
tsconfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"paths": {
"@sase/*": ["packages/*/src"]
}
},
"exclude": ["node_modules", "dist", ".next"]
}

22
turbo.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
}
}