Monorepo Overview
What This Is
Section titled “What This Is”A pnpm monorepo managed with Turborepo containing all web apps, shared packages, VPS service definitions, and infrastructure-as-code for the NetSaaS organization.
- Package manager: pnpm 10.28.2
- Runtime: Node 24
- Task runner: Turborepo (
turbo.json)
Repository Structure
Section titled “Repository Structure”apps/ — Web applications (SvelteKit or Astro)packages/ — Shared internal packages (@repo/ui, @repo/eslint-config, @repo/typescript-config)services/ — Docker Compose stacks deployed to Hetzner VPS instances (services/vps0/, vps1/, …)infra/ — VPS init scripts, security audit scriptsutils/scripts/ — CLI tools (create-app, generate-env-files, infra-summary, etc.)utils/templates/— App scaffolding templates (astro, sveltekit)main.tf — Terraform root: provisions all infra (Cloudflare, Hetzner, GitHub).github/ — CI/CD workflowsApps are web apps that get auto-deployed to Cloudflare Pages in CI. Each lives in apps/<app-name>/ and has an app.config.json defining:
| Field | Description |
|---|---|
domain | The production domain (e.g. pm.netsaas.cloud) |
env_vars | Key/value env vars, can reference secrets via ${SECRET_NAME} |
protected | "true" enables Cloudflare Zero Trust Access |
Apps are either SvelteKit (adapter-cloudflare) or Astro — both output static/edge builds.
Creating a New App
Section titled “Creating a New App”pnpm create-app [sveltekit|astro] --name <name> --url <url> [--protected] [--pocketbase <url>] [--no-pocketbase]All flags are optional — omitting any will fall back to interactive prompts.
Convention: Use Astro for websites/landing pages. Use SvelteKit for interactive web apps.
Shared Packages
Section titled “Shared Packages”| Package | Description |
|---|---|
@repo/eslint-config | Shared ESLint flat configs. Entry points: ./ (base), ./svelte, ./astro |
@repo/typescript-config | Shared TS configs: svelte.json and vite.json. Both strict mode |
Services (VPS)
Section titled “Services (VPS)”Backend services (PocketBase instances, monitoring, utilities) run as Docker Compose stacks on Hetzner VPS instances.
- Each VPS has its own directory:
services/vps<N>/compose.yaml - VPS IDs must be consecutive integers starting from 0 (validated by Terraform)
- Services use Caddy labels in compose for automatic reverse proxy + TLS
- Volumes with
backup: 'true'label are auto-backed-up to Cloudflare R2 - Protected services get Cloudflare Zero Trust Access via
protected: 'true'label
Environment Variables & Secrets
Section titled “Environment Variables & Secrets”- Secrets are stored in
.env.enc(SOPS-encrypted with age keys) at repo root .env.devat repo root provides local development overridesgenerate-env-files.mjsdecrypts secrets and generates per-app.envfiles at build time- Generated
.envfiles are ephemeral — auto-cleaned after the wrapped command exits - Never commit
.envfiles — they are gitignored
Infrastructure (Terraform)
Section titled “Infrastructure (Terraform)”Single main.tf at repo root manages all infrastructure declaratively.
- Providers: Cloudflare, Hetzner Cloud, GitHub, random
- State backend: Cloudflare R2 (S3-compatible)
- Terraform auto-discovers apps from
apps/*/package.jsonand VPS services fromservices/vps*/compose.yaml
Single GitHub Actions workflow (.github/workflows/ci.yml) triggered on push to main.
- Change detection — diffs
HEAD^1..HEADto determine what changed (supports[[FORCE-DEPLOY]]in commit messages) - Terraform — plan/apply infra changes
- Build — incremental app builds via Turbo
- Deploy — Cloudflare Pages via Wrangler, rsync + docker compose up for VPS services
Development Commands
Section titled “Development Commands”| Command | Description |
|---|---|
d <app> | Start dev server for an app (with auto env generation) |
b <app> | Build an app (with env generation) |
t <test> | Run a service test (with auto env generation) |
pnpm fix | Auto-format + lint fix + svelte-check + PocketBase URL validation |
pnpm create-app | Scaffold a new app |
Key Conventions
Section titled “Key Conventions”- All apps use Tailwind CSS v4 with daisyUI v5
- Svelte apps use Svelte 5 (runes mode)
- PocketBase is the standard backend — each app that needs data gets its own instance on a VPS
- Validation:
valibot. Formatting: Prettier. Linting: ESLint flat config - TypeScript strict mode everywhere. Target ESNext
- If creating a new PocketBase collection, always add
created(autodate, onCreate) andupdated(autodate) fields manually — they are not created automatically