Date: 2026-03-20 (revised) Status: Draft Depends on: Plan 1
Run multiple orcha branches simultaneously on Tolaria (RPi 5, 8GB RAM), each accessible at {app,admin,link}-<branch>.tdev.getorcha.com. Shared PostgreSQL (one DB per branch + tolaria DB for branch registry), separate LocalStack container per branch, host JVM per branch from git worktrees. PDFs served via NPM sub_filter + restricted /_s3/ proxy (same pattern as Plan 1 — LocalStack never exposed). Branch lifecycle managed by scripts, processes managed by systemd template services.
Browser
│ HTTPS
▼
DNS (Route53)
*.tdev.getorcha.com → home IP
│ :443
▼
Nginx Proxy Manager
TLS (Let's Encrypt) · Basic auth
3 proxy hosts per branch (auto-created)
sub_filter + /_s3/ on each app proxy host
│ HTTP to per-branch ports
▼
┌──────────────────────────────────────────────────┐
│ Tolaria │
│ │
│ ┌─ Branch: feature-ocr (slot 1) ────────────┐ │
│ │ JVM :18001 app :17001 admin :19001 link│ │
│ │ LocalStack container :14001 │ │
│ │ DB: orcha_feature_ocr │ │
│ │ Worktree: spikes/tolaria/.worktrees/ │ │
│ │ feature-ocr/ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌─ Branch: fix-export (slot 2) ─────────────┐ │
│ │ JVM :18002 app :17002 admin :19002 link│ │
│ │ LocalStack container :14002 │ │
│ │ DB: orcha_fix_export │ │
│ │ Worktree: spikes/tolaria/.worktrees/ │ │
│ │ fix-export/ │ │
│ └────────────────────────────────────────────┘ │
│ │
│ PostgreSQL 18 (shared container) :5432 │
│ DBs: orcha, tolaria, orcha_feature_ocr, ... │
└──────────────────────────────────────────────────┘
Each branch gets a slot number (recycled from lowest available). Ports are deterministic:
| Service | Formula | Slot 1 | Slot 2 | Slot 3 |
|---|---|---|---|---|
| App | 18000 + slot |
18001 | 18002 | 18003 |
| Admin | 17000 + slot |
17001 | 17002 | 17003 |
| Link | 19000 + slot |
19001 | 19002 | 19003 |
| LocalStack | 14000 + slot |
14001 | 14002 | 14003 |
The demo instance (Plan 1) sits outside this allocation scheme — it uses default ports (8888, 7777, 9999, 4566) and is not assigned a slot.
Branches are short-lived and destroyed often. Slots are recycled — when a branch is destroyed, its slot becomes available for the next branch-create. The tolaria database tracks slot allocation (see Branch Registry below).
| Service | Pattern | Example (feature-ocr) |
|---|---|---|
| App | app-{branch}.tdev.getorcha.com |
app-feature-ocr.tdev.getorcha.com |
| Admin | admin-{branch}.tdev.getorcha.com |
admin-feature-ocr.tdev.getorcha.com |
| Link | link-{branch}.tdev.getorcha.com |
link-feature-ocr.tdev.getorcha.com |
Three domains per branch (no S3 subdomain). All covered by the *.tdev.getorcha.com wildcard CNAME.
| Resource | Pattern | Example |
|---|---|---|
| Git worktree | spikes/tolaria/.worktrees/{branch}/ |
.worktrees/feature-ocr/ |
| PostgreSQL DB | orcha_{branch_safe} |
orcha_feature_ocr |
| LocalStack container | localstack-{branch} |
localstack-feature-ocr |
| Systemd service | orcha@{branch}.service |
orcha@feature-ocr.service |
| Env file | spikes/tolaria/.worktrees/{branch}/.env |
Auto-generated |
branch_safe converts hyphens to underscores for valid PostgreSQL database names.
Each branch's app proxy host gets the sub_filter + /_s3/ advanced config, pointing to that branch's LocalStack port:
NPM Advanced Configuration on app-{branch}.tdev.getorcha.com:
# Rewrite LocalStack presigned URLs in HTML responses
sub_filter_once off;
sub_filter_types *;
sub_filter 'http://localhost:{14000+slot}' '/_s3';
# Serve PDFs from branch's LocalStack — restricted to document PDF paths only
location ~ ^/_s3/(v1-orcha-global-storage-[^/]+/documents/[0-9a-f-]+(/(display|email))?\.pdf)$ {
limit_except GET { deny all; }
rewrite ^/_s3/(.*)$ /$1 break;
proxy_pass http://192.168.2.185:{14000+slot};
}
Implementation notes from Plan 1:
sub_filter_types * is required (not text/html) because orcha's Jetty does not send a Content-Type header on some HTML responses.proxy_pass must use Tolaria's LAN IP (192.168.2.185), not 127.0.0.1, because NPM runs on a different machine (192.168.2.117).rewrite directive strips the /_s3 prefix before proxying to LocalStack./_s3/ location bypasses basic auth — by design (iframe same-origin, regex restricts to document UUIDs).A tolaria database in the shared PostgreSQL instance stores branch metadata and slot allocations.
CREATE TABLE branches (
name TEXT PRIMARY KEY,
slot INTEGER NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Find lowest available slot
SELECT s FROM generate_series(1, 999) s
WHERE s NOT IN (SELECT slot FROM branches)
ORDER BY s LIMIT 1;
-- Create
INSERT INTO branches (name, slot) VALUES ('feature-ocr', 1);
-- List
SELECT * FROM branches ORDER BY slot;
-- Destroy
DELETE FROM branches WHERE name = 'feature-ocr';
The branch-create.sh script creates the tolaria DB and branches table if they don't exist (idempotent).
:temp-branch profile)Plan 1 already unified bucket/DB config via #orcha/param and added the :demo profile. Plan 2 adds a :temp-branch profile. Branches run with ENVIRON_NAME=temp-branch.
:temp-branch entriesPer-branch via #env:
;; AWS endpoint — per-branch LocalStack port
:endpoint #profile {#{:local-dev :demo} "http://localhost:4566"
:sandbox "http://localstack:4566"
:temp-branch #env ORCHA_AWS_ENDPOINT
:default nil}
;; Base URLs — per-branch domains
:com.getorcha/app-base-url
#profile {:local-dev "https://orcha.barreto.tech"
:demo "https://app.demo.getorcha.com"
:temp-branch #env ORCHA_APP_BASE_URL
:default "https://app.getorcha.com"}
:com.getorcha/link-base-url
#profile {:local-dev "https://link.orcha.barreto.tech"
:demo "https://link.demo.getorcha.com"
:temp-branch #env ORCHA_LINK_BASE_URL
:default "https://link.getorcha.com"}
;; HTTP server ports
:port #profile {:temp-branch #long #env ORCHA_APP_PORT :default 8888} ;; app
:port #profile {:temp-branch #long #env ORCHA_ADMIN_PORT :default 7777} ;; admin
:port #profile {:temp-branch #long #env ORCHA_LINK_PORT :default 9999} ;; link
Note: #long #env converts the env var string to a long. If Aero doesn't support this nesting, fall back to a custom reader #orcha/env-long.
Via #orcha/param (already unified in Plan 1 — no changes needed):
:db-credentials-json — init_aws.clj seeds with branch-specific DB nameinit_aws.clj seeds /v1-orcha/account-id = "local-stack"init_aws.clj seeds same dev valuesHardcoded :temp-branch values (same as :local-dev):
All entries that currently use #{:local-dev :demo} become #{:local-dev :demo :temp-branch}:
:dev? → true:dev-file-store → {:protocol "file" :path "dump/finances"}:integrations :datev → sandbox endpoints:search :credentials-file → "credentials/google-docai-dev.json":with-seed-data? → true:session :secure? → false:transcription :credentials-file → "credentials/google-docai-dev.json":max-retry-attempts → 1:shutdown-check-ms → 0:wait-time-seconds (matching) → 0# Auto-generated by branch-create.sh
ENVIRON_NAME=temp-branch
# Ports
ORCHA_APP_PORT=18001
ORCHA_ADMIN_PORT=17001
ORCHA_LINK_PORT=19001
# External URLs
ORCHA_APP_BASE_URL=https://app-feature-ocr.tdev.getorcha.com
ORCHA_LINK_BASE_URL=https://link-feature-ocr.tdev.getorcha.com
# Internal endpoint
ORCHA_AWS_ENDPOINT=http://localhost:14001
# DB credentials seeded in LocalStack by init_aws.clj
# via ORCHA_DB_NAME=orcha_feature_ocr
Each branch runs the existing bb dev:init-aws against its own LocalStack. The init script already supports ORCHA_DB_HOST and ORCHA_DB_NAME env vars (added in Plan 1). For temp branches:
ORCHA_DB_NAME=orcha_feature_ocr \
ORCHA_AWS_ENDPOINT=http://localhost:14001 \
bb dev:init-aws
This seeds:
/v1-orcha/account-id = "local-stack" (bucket suffix — same for all branches, each has own LocalStack)/v1-orcha/db-credentials = {"host":"localhost","port":5432,"dbname":"orcha_feature_ocr",...}Note: init_aws.clj reads :endpoint from config.edn via Aero. With ENVIRON_NAME=temp-branch and ORCHA_AWS_ENDPOINT=http://localhost:14001, the config resolves correctly. Must verify during implementation that Babashka's Aero correctly resolves #env in this context.
# /etc/systemd/system/orcha@.service
[Unit]
Description=Orcha (%i)
After=network.target docker.service
Requires=docker.service
[Service]
Type=simple
User=karn
WorkingDirectory=/home/karn/orcha/spikes/tolaria/.worktrees/%i/orcha
EnvironmentFile=/home/karn/orcha/spikes/tolaria/.worktrees/%i/.env
ExecStartPre=/bin/sh -c 'docker start localstack-%i 2>/dev/null || echo "Warning: container localstack-%i not found, run branch-create.sh"'
ExecStart=/usr/bin/java -Xmx1g -jar target/orcha.jar -m com.getorcha.system
ExecStopPost=-/usr/bin/docker stop localstack-%i
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Full provisioning in one command:
tolaria DB exists — createdb tolaria (idempotent), create branches table if not existsSELECT min(s) FROM generate_series(1,999) s WHERE s NOT IN (SELECT slot FROM branches)INSERT INTO branches (name, slot) VALUES (...) (fails on duplicate = branch already exists)git worktree add spikes/tolaria/.worktrees/{branch} origin/{branch}createdb orcha_{branch_safe}docker run -d --name localstack-{branch} -p {14000+slot}:4566 -e PERSISTENCE=1 -e SERVICES=s3,sqs,secretsmanager,ssm -e DEFAULT_REGION=eu-central-1 -v localstack-{branch}:/var/lib/localstack localstack/localstack:latestsource .env && ORCHA_DB_NAME=orcha_{branch_safe} bb dev:init-awscd .worktrees/{branch}/orcha && clojure -X:buildsource .env && bb migrate migratenpm-setup.sh --branch {branch} --slot {slot}sudo systemctl enable --now orcha@{branch}sudo systemctl disable --now orcha@{branch}npm-setup.sh --branch {branch} --teardowndocker rm -f localstack-{branch} + docker volume rm localstack-{branch}dropdb orcha_{branch_safe}git worktree remove spikes/tolaria/.worktrees/{branch}DELETE FROM branches WHERE name = '{branch}' (frees slot)cd spikes/tolaria/.worktrees/{branch} && git pullcd orcha && clojure -X:buildsource ../.env && ORCHA_DB_NAME=orcha_{branch_safe} bb dev:init-awssource ../.env && bb migrate migratesudo systemctl restart orcha@{branch}| Component | RAM | Per-branch? |
|---|---|---|
| OS + system services | ~500MB | No |
| PostgreSQL (shared) | ~300MB | No |
| NPM | ~100MB | No |
Orcha JVM (-Xmx1g) |
~1.0-1.2GB | Yes |
| LocalStack container | ~200-300MB | Yes |
| Fixed overhead | ~900MB | |
| Per branch | ~1.3-1.5GB | |
| Max concurrent branches | ~4-5 |
At 4 branches: ~6.9GB used. Consider -Xmx768m for more headroom.
spikes/tolaria/
├── docs/
│ ├── 2026-03-19-plan1-single-instance-design.md
│ └── 2026-03-19-plan2-multi-branch-design.md
├── plans/
│ ├── 2026-03-19-plan1-single-instance.md
│ └── 2026-03-19-plan2-multi-branch.md
├── scripts/
│ ├── install-prereqs.sh # (Plan 1)
│ ├── npm-setup.sh # (Plan 1, extended for --branch)
│ ├── run-demo.sh # (Plan 1)
│ ├── branch-create.sh # (Plan 2)
│ ├── branch-destroy.sh # (Plan 2)
│ └── branch-update.sh # (Plan 2)
├── config/
│ └── demo.env # (Plan 1)
├── systemd/
│ └── orcha@.service # (Plan 2)
└── .worktrees/ # (Plan 2, gitignored)
├── feature-ocr/
│ ├── .env
│ └── orcha/
└── fix-export/
├── .env
└── orcha/
The npm-setup.sh script from Plan 1 already supports --branch and --slot flags:
*.demo.getorcha.com)--branch feature-ocr --slot 1: creates branch proxy hosts (*-feature-ocr.tdev.getorcha.com)--branch feature-ocr --teardown: removes branch proxy hostsEach branch gets 3 proxy hosts (app, admin, link). The app host includes sub_filter + /_s3/ advanced config. Basic auth applied to all three.
The demo instance (Plan 1) remains separate:
*.demo.getorcha.com domainsENVIRON_NAME=demorun-demo.sh)Plan 2 branches use *.tdev.getorcha.com, allocated ports, and ENVIRON_NAME=temp-branch. They share the same PostgreSQL container and NPM basic auth access list as the demo.
PostgreSQL container management: Started by Plan 1's docker-compose up -d. branch-create.sh verifies PostgreSQL is up (via pg_isready) before proceeding.
bb dev:init-aws with ENVIRON_NAME=temp-branch — does Babashka's Aero resolve #env ORCHA_AWS_ENDPOINT correctly?#long #env nesting in Aero — does #long #env ORCHA_APP_PORT work, or do we need #orcha/env-long?-Xmx1g, monitor, consider -Xmx768m..worktrees/ in .gitignore — add spikes/tolaria/.worktrees/ to .gitignore.