30 KiB
LetsBe Biz — Repository Structure
Date: February 27, 2026 Team: Claude Opus 4.6 Architecture Team Document: 09 of 09 Status: Proposal — Competing with independent team
Table of Contents
- Decision: Monorepo
- Turborepo Configuration
- Directory Tree
- Package Architecture
- Dependency Graph
- Migration Plan
- Development Workflow
- Monorepo Trade-offs
1. Decision: Monorepo
Why Monorepo?
| Factor | Monorepo | Multi-Repo | Winner |
|---|---|---|---|
| Shared types | Single source of truth; import directly | npm publish on every change; version drift | Monorepo |
| Atomic changes | Change type + all consumers in one PR | Coordinate releases across repos | Monorepo |
| CI/CD | One pipeline, matrix builds | Per-repo pipelines, dependency triggering | Monorepo |
| Code discovery | grep across everything |
Search multiple repos separately | Monorepo |
| Prisma schema | One schema, shared by Hub and types | Duplicate or publish as package | Monorepo |
| Developer onboarding | Clone one repo, npm install, done |
Clone 3-4 repos, configure each | Monorepo |
| Build caching | Turborepo caches across packages | Each repo builds independently | Monorepo |
| Independence | Packages are more coupled | Fully independent deploy | Multi-Repo |
| Repo size | Grows over time | Each repo stays lean | Multi-Repo |
| CI isolation | Bad test in one package blocks others | Fully isolated | Multi-Repo |
Decision: Monorepo with Turborepo. The shared types, Prisma schema, and tight coupling between Safety Wrapper ↔ Hub ↔ Secrets Proxy make a monorepo the clear winner. The provisioner (Bash) stays as a separate package within the monorepo but could also remain as a standalone repo if the team prefers — it has no TypeScript dependencies.
What Stays Outside the Monorepo
| Component | Reason |
|---|---|
| OpenClaw | Upstream dependency. Pulled as Docker image. Not forked. |
| Tool Docker stacks | Compose files and nginx configs live in the provisioner package. |
| Mobile app | React Native/Expo has different build tooling. Lives in packages/mobile but uses its own metro.config.js. |
2. Turborepo Configuration
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"lint": {},
"test": {
"dependsOn": ["^build"],
"env": ["DATABASE_URL", "NODE_ENV"]
},
"test:p0": {
"dependsOn": ["^build"],
"cache": false
},
"test:p1": {
"dependsOn": ["^build"],
"cache": false
},
"test:integration": {
"dependsOn": ["build"],
"cache": false
},
"test:benchmark": {
"dependsOn": ["build"],
"cache": false
},
"dev": {
"cache": false,
"persistent": true
},
"db:push": {
"cache": false
},
"db:generate": {
"outputs": ["node_modules/.prisma/**"]
}
}
}
Root package.json
{
"name": "letsbe-biz",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"test": "turbo run test",
"test:p0": "turbo run test:p0",
"test:integration": "turbo run test:integration",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"format": "prettier --write \"packages/*/src/**/*.{ts,tsx}\"",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.3.0",
"prettier": "^3.4.0",
"typescript": "^5.7.0"
},
"engines": {
"node": ">=22.0.0"
}
}
3. Directory Tree
letsbe-biz/
├── .gitea/
│ └── workflows/
│ ├── ci.yml # Monorepo CI (lint, typecheck, test)
│ ├── safety-wrapper.yml # SW-specific pipeline
│ ├── secrets-proxy.yml # SP-specific pipeline
│ ├── hub.yml # Hub pipeline
│ ├── integration.yml # Integration test pipeline
│ ├── deploy-staging.yml # Auto-deploy to staging
│ ├── deploy-hub.yml # Production Hub deploy
│ └── tenant-update.yml # Tenant server rollout
│
├── packages/
│ ├── safety-wrapper/ # Safety Wrapper (localhost:8200)
│ │ ├── src/
│ │ │ ├── index.ts # Entry point: HTTP server startup
│ │ │ ├── server.ts # Express/Fastify HTTP server
│ │ │ ├── config.ts # Configuration loading
│ │ │ ├── classification/
│ │ │ │ ├── engine.ts # Command classification engine
│ │ │ │ ├── shell-classifier.ts # Shell command allowlist + classification
│ │ │ │ ├── docker-classifier.ts # Docker subcommand classification
│ │ │ │ └── rules.ts # Classification rule definitions
│ │ │ ├── autonomy/
│ │ │ │ ├── resolver.ts # Autonomy level resolution
│ │ │ │ ├── external-comms.ts # External Communications Gate
│ │ │ │ └── approval-queue.ts # Local approval queue (SQLite)
│ │ │ ├── executors/
│ │ │ │ ├── shell.ts # Shell command executor (execFile)
│ │ │ │ ├── docker.ts # Docker command executor
│ │ │ │ ├── file.ts # File read/write executor
│ │ │ │ └── env.ts # Env read/update executor
│ │ │ ├── secrets/
│ │ │ │ ├── registry.ts # Encrypted SQLite secrets vault
│ │ │ │ ├── injection.ts # SECRET_REF resolution
│ │ │ │ └── api.ts # Secrets side-channel API
│ │ │ ├── hub/
│ │ │ │ ├── client.ts # Hub communication (register, heartbeat, config)
│ │ │ │ └── config-sync.ts # Config versioning and delta sync
│ │ │ ├── metering/
│ │ │ │ ├── token-tracker.ts # Per-agent, per-model token tracking
│ │ │ │ └── bucket.ts # Hourly bucket aggregation
│ │ │ ├── audit/
│ │ │ │ └── logger.ts # Append-only audit log
│ │ │ └── db/
│ │ │ ├── schema.sql # SQLite schema (secrets, approvals, audit, usage, state)
│ │ │ └── migrations/ # SQLite migration files
│ │ ├── test/
│ │ │ ├── p0/
│ │ │ │ ├── classification.test.ts # 100+ classification tests
│ │ │ │ └── autonomy.test.ts # Level × tier matrix tests
│ │ │ ├── p1/
│ │ │ │ ├── shell-executor.test.ts
│ │ │ │ ├── docker-executor.test.ts
│ │ │ │ └── hub-client.test.ts
│ │ │ └── integration/
│ │ │ └── openclaw-routing.test.ts
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ │
│ ├── secrets-proxy/ # Secrets Proxy (localhost:8100)
│ │ ├── src/
│ │ │ ├── index.ts # Entry point
│ │ │ ├── proxy.ts # HTTP proxy server (transparent)
│ │ │ ├── redaction/
│ │ │ │ ├── pipeline.ts # 4-layer pipeline orchestrator
│ │ │ │ ├── layer1-aho-corasick.ts # Registry-based exact match
│ │ │ │ ├── layer2-regex.ts # Pattern safety net
│ │ │ │ ├── layer3-entropy.ts # Shannon entropy filter
│ │ │ │ └── layer4-json-keys.ts # Sensitive key name detection
│ │ │ └── config.ts
│ │ ├── test/
│ │ │ ├── p0/
│ │ │ │ ├── redaction.test.ts # 50+ redaction tests (TDD)
│ │ │ │ ├── false-positives.test.ts # False positive prevention
│ │ │ │ └── performance.test.ts # <10ms latency benchmark
│ │ │ └── adversarial/
│ │ │ └── bypass-attempts.test.ts # Adversarial attack tests
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ │
│ ├── hub/ # Hub (Next.js — existing codebase, migrated)
│ │ ├── src/
│ │ │ ├── app/ # Next.js App Router (existing structure)
│ │ │ │ ├── admin/ # Staff admin dashboard (existing)
│ │ │ │ ├── api/
│ │ │ │ │ ├── auth/ # Authentication (existing)
│ │ │ │ │ ├── v1/
│ │ │ │ │ │ ├── admin/ # Admin API (existing)
│ │ │ │ │ │ ├── tenant/ # NEW: Safety Wrapper protocol
│ │ │ │ │ │ │ ├── register/
│ │ │ │ │ │ │ ├── heartbeat/
│ │ │ │ │ │ │ ├── config/
│ │ │ │ │ │ │ ├── usage/
│ │ │ │ │ │ │ ├── approval-request/
│ │ │ │ │ │ │ └── approval-response/
│ │ │ │ │ │ ├── customer/ # NEW: Customer-facing API
│ │ │ │ │ │ │ ├── dashboard/
│ │ │ │ │ │ │ ├── agents/
│ │ │ │ │ │ │ ├── usage/
│ │ │ │ │ │ │ ├── approvals/
│ │ │ │ │ │ │ ├── billing/
│ │ │ │ │ │ │ └── tools/
│ │ │ │ │ │ ├── orchestrator/ # DEPRECATED: keep for backward compat, redirect
│ │ │ │ │ │ ├── public/ # Public API (existing)
│ │ │ │ │ │ └── webhooks/ # Stripe webhooks (existing)
│ │ │ │ │ └── cron/ # Cron endpoints (existing)
│ │ │ │ └── login/ # Login page (existing)
│ │ │ ├── lib/
│ │ │ │ ├── services/ # Business logic (existing + new)
│ │ │ │ │ ├── automation-worker.ts # Existing
│ │ │ │ │ ├── billing-service.ts # NEW: Token billing, Stripe Meters
│ │ │ │ │ ├── chat-relay-service.ts # NEW: App→Hub→SW→OpenClaw
│ │ │ │ │ ├── config-generator.ts # Existing (updated)
│ │ │ │ │ ├── push-notification.ts # NEW: Expo Push service
│ │ │ │ │ ├── tenant-protocol.ts # NEW: SW registration/heartbeat
│ │ │ │ │ └── ... # Other existing services
│ │ │ │ └── ...
│ │ │ ├── hooks/ # React Query hooks (existing)
│ │ │ └── components/ # UI components (existing)
│ │ ├── prisma/
│ │ │ ├── schema.prisma # Shared Prisma schema (existing + new models)
│ │ │ ├── migrations/ # Prisma migrations
│ │ │ └── seed.ts # Database seeding
│ │ ├── test/
│ │ │ ├── unit/ # Existing unit tests (10 files)
│ │ │ ├── api/ # NEW: API endpoint tests
│ │ │ └── integration/ # NEW: Hub↔SW protocol tests
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ ├── next.config.ts
│ │ └── tsconfig.json
│ │
│ ├── website/ # Website (letsbe.biz — separate Next.js app)
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── page.tsx # Landing page
│ │ │ │ ├── onboarding/ # AI-powered onboarding flow
│ │ │ │ │ ├── business/ # Step 1: Business description
│ │ │ │ │ ├── tools/ # Step 2: Tool recommendation
│ │ │ │ │ ├── customize/ # Step 3: Customization
│ │ │ │ │ ├── server/ # Step 4: Server selection
│ │ │ │ │ ├── domain/ # Step 5: Domain setup
│ │ │ │ │ ├── agents/ # Step 6: Agent config (optional)
│ │ │ │ │ ├── payment/ # Step 7: Stripe checkout
│ │ │ │ │ └── status/ # Step 8: Provisioning status
│ │ │ │ ├── demo/ # Interactive demo page
│ │ │ │ └── pricing/ # Pricing page
│ │ │ └── lib/
│ │ │ ├── ai-classifier.ts # Gemini Flash business classifier
│ │ │ └── resource-calc.ts # Resource requirement calculator
│ │ ├── Dockerfile
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── mobile/ # Mobile App (Expo Bare Workflow)
│ │ ├── src/
│ │ │ ├── screens/
│ │ │ │ ├── LoginScreen.tsx
│ │ │ │ ├── ChatScreen.tsx
│ │ │ │ ├── DashboardScreen.tsx
│ │ │ │ ├── ApprovalsScreen.tsx
│ │ │ │ ├── UsageScreen.tsx
│ │ │ │ ├── SettingsScreen.tsx
│ │ │ │ └── SecretsScreen.tsx
│ │ │ ├── components/
│ │ │ ├── hooks/
│ │ │ ├── stores/ # Zustand stores
│ │ │ ├── services/ # API client, push notifications
│ │ │ └── navigation/ # React Navigation
│ │ ├── app.json
│ │ ├── eas.json # EAS Build + Update config
│ │ ├── metro.config.js
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── shared-types/ # Shared TypeScript types
│ │ ├── src/
│ │ │ ├── classification.ts # Command classification types
│ │ │ ├── autonomy.ts # Autonomy level types
│ │ │ ├── secrets.ts # Secrets registry types
│ │ │ ├── protocol.ts # Hub ↔ SW protocol types
│ │ │ ├── billing.ts # Token metering types
│ │ │ ├── agents.ts # Agent configuration types
│ │ │ └── index.ts # Barrel export
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ ├── shared-prisma/ # Shared Prisma client (generated)
│ │ ├── prisma/
│ │ │ └── schema.prisma # → symlink to packages/hub/prisma/schema.prisma
│ │ ├── package.json
│ │ └── tsconfig.json
│ │
│ └── provisioner/ # Provisioner (Bash — migrated from letsbe-ansible-runner)
│ ├── provision.sh # Main entry point
│ ├── steps/
│ │ ├── step-01-system-update.sh
│ │ ├── step-02-docker-install.sh
│ │ ├── step-03-create-user.sh
│ │ ├── step-04-generate-secrets.sh
│ │ ├── step-05-deploy-stacks.sh
│ │ ├── step-06-nginx-configs.sh
│ │ ├── step-07-ssl-certs.sh
│ │ ├── step-08-backup-setup.sh
│ │ ├── step-09-firewall.sh
│ │ └── step-10-deploy-ai.sh # REWRITTEN: OpenClaw + Safety Wrapper
│ ├── stacks/ # Docker Compose files for 28+ tools
│ │ ├── chatwoot/
│ │ │ └── docker-compose.yml
│ │ ├── nextcloud/
│ │ │ └── docker-compose.yml
│ │ ├── letsbe/ # NEW: LetsBe AI stack
│ │ │ └── docker-compose.yml # OpenClaw + Safety Wrapper + Secrets Proxy
│ │ └── ...
│ ├── nginx/ # nginx configs for 33+ tools
│ ├── templates/ # Config templates
│ │ ├── openclaw-config.json5.tmpl
│ │ ├── safety-wrapper.json.tmpl
│ │ ├── tool-registry.json.tmpl
│ │ └── agent-templates/ # Per-business-type agent configs
│ ├── references/ # Tool cheat sheets (deployed to tenant)
│ │ ├── portainer.md
│ │ ├── nextcloud.md
│ │ ├── chatwoot.md
│ │ ├── ghost.md
│ │ ├── calcom.md
│ │ ├── stalwart.md
│ │ └── ...
│ ├── skills/ # OpenClaw skills (deployed to tenant)
│ │ └── letsbe-tools/
│ │ └── SKILL.md # Master tool skill
│ ├── agents/ # Default agent configs (deployed to tenant)
│ │ ├── dispatcher/
│ │ │ └── SOUL.md
│ │ ├── it-admin/
│ │ │ └── SOUL.md
│ │ ├── marketing/
│ │ │ └── SOUL.md
│ │ ├── secretary/
│ │ │ └── SOUL.md
│ │ └── sales/
│ │ └── SOUL.md
│ ├── test/
│ │ ├── step-10.bats # bats-core tests for step 10
│ │ ├── cleanup.bats # n8n cleanup verification
│ │ └── full-run.bats # Full provisioner integration test
│ ├── Dockerfile
│ └── package.json # Minimal — just for monorepo workspace inclusion
│
├── test/ # Cross-package integration tests
│ ├── docker-compose.integration.yml # Full stack for integration tests
│ ├── fixtures/
│ │ ├── openclaw-config.json5
│ │ ├── safety-wrapper-config.json
│ │ ├── tool-registry.json
│ │ └── test-secrets.json
│ └── e2e/
│ ├── signup-to-chat.test.ts
│ ├── approval-flow.test.ts
│ └── secrets-never-leak.test.ts
│
├── docs/ # Documentation (existing)
│ ├── technical/
│ ├── strategy/
│ ├── legal/
│ └── architecture-proposal/
│ └── claude/ # This proposal
│
├── turbo.json
├── package.json # Root workspace config
├── tsconfig.base.json # Shared TypeScript config
├── .gitignore
├── .eslintrc.js # Shared ESLint config
├── .prettierrc
└── README.md
4. Package Architecture
Package Responsibilities
| Package | Language | Purpose | Depends On | Deployed As |
|---|---|---|---|---|
safety-wrapper |
TypeScript | Command gating, tool execution, Hub comm, audit | shared-types |
Docker container on tenant VPS |
secrets-proxy |
TypeScript | LLM traffic redaction (4-layer pipeline) | shared-types |
Docker container on tenant VPS |
hub |
TypeScript (Next.js) | Admin dashboard, customer portal, billing, tenant protocol | shared-types, shared-prisma |
Docker container on central server |
website |
TypeScript (Next.js) | Marketing site, onboarding flow | — | Docker container on central server |
mobile |
TypeScript (Expo) | Customer mobile app | shared-types |
iOS/Android app (EAS Build) |
shared-types |
TypeScript | Type definitions shared across packages | — | npm workspace dependency |
shared-prisma |
TypeScript | Generated Prisma client | — | npm workspace dependency |
provisioner |
Bash | VPS provisioning scripts, tool stacks | — | Docker container (on-demand) |
Package Size Estimates
| Package | Estimated LOC | Files | Build Output |
|---|---|---|---|
safety-wrapper |
~3,000-4,000 | ~30 | ~200KB JS |
secrets-proxy |
~1,500-2,000 | ~15 | ~100KB JS |
hub |
~15,000+ (existing) + ~3,000 new | ~250+ | Next.js standalone |
website |
~2,000-3,000 | ~20 | Next.js standalone |
mobile |
~4,000-5,000 | ~40 | Expo bundle |
shared-types |
~500-800 | ~10 | ~50KB JS |
provisioner |
~5,000 (existing + new) | ~50+ | Bash scripts |
5. Dependency Graph
┌──────────────┐
│ shared-types │
└──────┬───────┘
┌────────────┼────────────┬────────────┐
│ │ │ │
┌────────▼──────┐ ┌──▼────────┐ ┌─▼──────┐ ┌──▼──────┐
│safety-wrapper │ │secrets- │ │ hub │ │ mobile │
│ │ │proxy │ │ │ │ │
└───────────────┘ └───────────┘ └────┬───┘ └─────────┘
│
┌──────▼──────┐
│shared-prisma│
└─────────────┘
┌───────────┐ ┌───────────┐
│ website │ │provisioner│
│(no deps) │ │(Bash, no │
│ │ │ TS deps) │
└───────────┘ └───────────┘
Key constraints:
shared-typeshas ZERO dependencies. It's pure TypeScript type definitions.shared-prismadepends only on Prisma and the schema file.safety-wrapperandsecrets-proxynever import fromhub(no circular deps).hubnever imports fromsafety-wrapperorsecrets-proxy(communication via HTTP protocol).websiteis fully independent — no shared package dependencies.provisioneris Bash — no TypeScript dependencies at all.
6. Migration Plan
Current State (5 Separate Repos)
letsbe-hub → packages/hub (TypeScript, Next.js)
letsbe-ansible-runner → packages/provisioner (Bash)
letsbe-orchestrator → DEPRECATED (capabilities → safety-wrapper)
letsbe-sysadmin-agent → DEPRECATED (capabilities → safety-wrapper)
letsbe-mcp-browser → DEPRECATED (replaced by OpenClaw native browser)
Migration Steps
Step 1: Create Monorepo (Week 1, Day 1-2)
# Create new repo
mkdir letsbe-biz && cd letsbe-biz
git init
npm init -y
# Install Turborepo
npm install turbo --save-dev
# Create workspace structure
mkdir -p packages/{safety-wrapper,secrets-proxy,hub,website,mobile,shared-types,shared-prisma,provisioner}
# Create turbo.json (from Section 2)
# Create root package.json (from Section 2)
# Create tsconfig.base.json
Step 2: Migrate Hub (Week 1, Day 1)
# Copy Hub source (preserve git history via subtree or fresh copy)
cp -r ../letsbe-hub/src packages/hub/src
cp -r ../letsbe-hub/prisma packages/hub/prisma
cp ../letsbe-hub/package.json packages/hub/
cp ../letsbe-hub/next.config.ts packages/hub/
cp ../letsbe-hub/tsconfig.json packages/hub/
cp ../letsbe-hub/Dockerfile packages/hub/
# Update Hub package.json:
# - name: "@letsbe/hub"
# - Add workspace dependency on shared-types, shared-prisma
# Verify Hub builds
cd packages/hub && npm install && npm run build
Step 3: Migrate Provisioner (Week 1, Day 1)
# Copy provisioner scripts
cp -r ../letsbe-ansible-runner/* packages/provisioner/
# Add minimal package.json for workspace inclusion
echo '{"name":"@letsbe/provisioner","private":true}' > packages/provisioner/package.json
Step 4: Create New Packages (Week 1, Day 2)
# shared-types — create from scratch
cd packages/shared-types
npm init -y --scope=@letsbe
# Add type definitions
# safety-wrapper — create from scratch
cd packages/safety-wrapper
npm init -y --scope=@letsbe
# Scaffold Express/Fastify server
# secrets-proxy — create from scratch
cd packages/secrets-proxy
npm init -y --scope=@letsbe
# Scaffold HTTP proxy
Step 5: Verify Everything Works (Week 1, Day 2)
# From repo root:
npm install # Install all workspace dependencies
turbo run build # Build all packages
turbo run typecheck # Type check all packages
turbo run test # Run all tests (Hub's existing 10 tests)
turbo run lint # Lint all packages
Step 6: Archive Old Repos (Week 2)
Once the monorepo is confirmed working and the team has switched:
- Mark
letsbe-orchestratoras archived (deprecated) - Mark
letsbe-sysadmin-agentas archived (deprecated) - Mark
letsbe-mcp-browseras archived (deprecated) - Keep
letsbe-hubandletsbe-ansible-runnerread-only for reference - Update Gitea CI to point to new monorepo
Git History Preservation
Option A (Recommended): Fresh start with reference.
- New monorepo gets a clean git history.
- Old repos remain accessible (read-only archive) for historical reference.
- This is cleaner and avoids complex git subtree merges.
Option B: Preserve history via git subtree.
- Use
git subtree addto bring Hub and provisioner history into the monorepo. - More complex but preserves
git blamelineage.
Recommendation: Option A. The codebase is being substantially restructured. Historical blame on the old code is less valuable than a clean starting point. The old repos stay available for reference.
7. Development Workflow
Daily Development
# Start all dev servers (Hub + Safety Wrapper + Secrets Proxy)
turbo run dev --parallel
# Run tests for a specific package
turbo run test --filter=safety-wrapper
# Run P0 tests only
turbo run test:p0
# Build a specific package
turbo run build --filter=secrets-proxy
# Type check everything
turbo run typecheck
# Lint everything
turbo run lint
Adding a Shared Type
# 1. Add type to packages/shared-types/src/classification.ts
# 2. Export from index.ts
# 3. Import in consuming package:
# import { CommandTier } from '@letsbe/shared-types';
# 4. Turbo automatically rebuilds shared-types before dependent packages
Adding a New Package
# 1. Create directory
mkdir packages/new-package
# 2. Initialize
cd packages/new-package
npm init -y --scope=@letsbe
# 3. Add to root workspaces (already covered by packages/* glob)
# 4. Add to turbo.json pipeline if needed
# 5. Add Dockerfile if it's a deployed service
Docker Development
# docker-compose.dev.yml (root level, for local development)
services:
postgres:
image: postgres:16-alpine
ports: ['5432:5432']
environment:
POSTGRES_DB: hub_dev
POSTGRES_USER: hub
POSTGRES_PASSWORD: devpass
hub:
build:
context: .
dockerfile: packages/hub/Dockerfile
ports: ['3000:3000']
environment:
DATABASE_URL: postgresql://hub:devpass@postgres:5432/hub_dev
depends_on: [postgres]
safety-wrapper:
build:
context: .
dockerfile: packages/safety-wrapper/Dockerfile
ports: ['8200:8200']
secrets-proxy:
build:
context: .
dockerfile: packages/secrets-proxy/Dockerfile
ports: ['8100:8100']
8. Monorepo Trade-offs
Advantages Realized
| Advantage | Concrete Benefit |
|---|---|
| Atomic type changes | Change CommandTier enum in shared-types → all consumers updated in same PR |
| Turborepo caching | Rebuild only changed packages; CI runs ~60% faster after first run |
| Shared tooling | One ESLint config, one Prettier config, one TypeScript base config |
| Cross-package refactoring | Rename a protocol field → update Safety Wrapper + Hub in one commit |
| Single dependency tree | No version conflicts between packages; hoisted node_modules |
| Simplified onboarding | Clone one repo → npm install → turbo run dev → everything running |
Disadvantages Accepted
| Disadvantage | Mitigation |
|---|---|
| Larger repo size | Turborepo's --filter flag runs only affected packages |
| Bash in TypeScript monorepo | Provisioner is loosely coupled — workspace inclusion is just for organization |
| Mobile build complexity | Expo has its own build system (EAS); it coexists but doesn't use Turbo for builds |
| CI runs all checks | Path-based triggers (see pipeline YAML) skip unrelated packages |
| Single repo = single SPOF | Gitea backup strategy; consider GitHub mirror for disaster recovery |
When to Reconsider
The monorepo should be split if:
- The team grows beyond 8-10 engineers and package ownership boundaries become clear
- Mobile app development cadence diverges significantly from backend
- A package needs a fundamentally different build system or language (e.g., Rust Safety Wrapper rewrite)
- CI times exceed 20 minutes even with caching
None of these are likely before reaching 100 customers.
End of Document — 09 Repository Structure