Skip to content

Monorepo Overview

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)
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 scripts
utils/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 workflows

Apps 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:

FieldDescription
domainThe production domain (e.g. pm.netsaas.cloud)
env_varsKey/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.

Terminal window
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.

PackageDescription
@repo/eslint-configShared ESLint flat configs. Entry points: ./ (base), ./svelte, ./astro
@repo/typescript-configShared TS configs: svelte.json and vite.json. Both strict mode

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
  • Secrets are stored in .env.enc (SOPS-encrypted with age keys) at repo root
  • .env.dev at repo root provides local development overrides
  • generate-env-files.mjs decrypts secrets and generates per-app .env files at build time
  • Generated .env files are ephemeral — auto-cleaned after the wrapped command exits
  • Never commit .env files — they are gitignored

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.json and VPS services from services/vps*/compose.yaml

Single GitHub Actions workflow (.github/workflows/ci.yml) triggered on push to main.

  1. Change detection — diffs HEAD^1..HEAD to determine what changed (supports [[FORCE-DEPLOY]] in commit messages)
  2. Terraform — plan/apply infra changes
  3. Build — incremental app builds via Turbo
  4. Deploy — Cloudflare Pages via Wrangler, rsync + docker compose up for VPS services
CommandDescription
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 fixAuto-format + lint fix + svelte-check + PocketBase URL validation
pnpm create-appScaffold a new app
  • 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) and updated (autodate) fields manually — they are not created automatically