Skip to content

Developer Workflow Guide ​

This document describes the branching strategy, CI/CD pipeline, local development setup, and staging/production promotion flow for Compath.


Environments ​

EnvironmentBackend URLFrontend URLBranchAuto-deploy
Localhttp://localhost:3000http://localhost:5173any—
Staginghttps://brain-staging.compath.appCloudflare Pages branch previewstaging✅ on push
Productionhttps://brain.compath.apphttps://my.compath.appmain✅ on push
Internal adminhttps://brain.compath.apphttps://my.compath.app/admin/*main✅ on push

Branch Strategy ​

main ─────────────────────────────────────────────── production
  │
  └── staging ──────────────────────────────────────── staging env
        │
        ├── feat/calendar-sync
        ├── fix/booking-notification
        └── chore/update-deps

Branch naming ​

TypePatternExample
New featurefeat/<short-description>feat/reschedule-booking
Bug fixfix/<short-description>fix/pending-booking-calendar
Chore / maintenancechore/<short-description>chore/update-go-deps
Hotfix (prod only)hotfix/<short-description>hotfix/payment-500-error

Protected branches ​

BranchRules
mainRequires PR, CI must pass, 1 approval
stagingRequires PR, CI must pass

Standard Developer Flow ​

1. branch off staging
       git checkout staging && git pull
       git checkout -b feat/my-feature

2. develop locally
       make dev           # backend with hot reload
       bun dev            # frontend

3. push and open PR → staging
       git push -u origin feat/my-feature
       # Open PR targeting staging (not main)

4. CI runs automatically
       ✓ Go tests + race detector
       ✓ golangci-lint
       ✓ TypeScript type-check
       ✓ Swagger generation

5. PR reviewed and merged → staging
       → deploy-staging workflow fires
       → backend image tagged :staging pushed to GHCR
       → staging containers restarted on VPS
       → https://brain-staging.compath.app live within ~3 min

6. QA on staging
       → verify feature works end-to-end
       → run any manual checks

7. open PR: staging → main
       → requires 1 approval
       → CI reruns
       → run pending prod migrations first (see "Promoting staging → production" below)

8. merge → production
       → deploy-backend workflow fires
       → image tagged :latest pushed to GHCR
       → production containers restarted on VPS

Hotfix Flow (production issue, no time for staging) ​

1. branch off main
       git checkout main && git pull
       git checkout -b hotfix/critical-bug

2. fix, test locally

3. open PR → main
       → CI runs
       → requires 1 approval (expedited)

4. run pending prod migrations first (see "Promoting staging → production" below — hotfixes must not carry migrations of their own) → merge → production auto-deploys

5. backport to staging
       git checkout staging && git pull
       git cherry-pick <commit-sha>
       git push

Prototype UI Promotion Flow (prototype -> main) ​

Use this for validated UI in prototype routes/components when /proto must not ship to main.

bash
# 1) Create a clean promotion branch from main
git fetch origin
git switch main
git pull --ff-only origin main
git switch -c promote/<scope>-<yyyy-mm-dd>

# 2) Cherry-pick production-ready commits from prototype
git cherry-pick -x <sha1> <sha2> ...

# 3) For mixed commits (prod + proto), apply without commit and exclude proto files
git cherry-pick -n <sha>
git restore --source=HEAD --staged --worktree frontend/src/app/proto
git commit -m "promote(ui): <summary> (proto excluded)"

# 4) Verify no proto leakage
git diff --name-only origin/main...HEAD | rg "^frontend/src/app/proto/"
rg -n "@/app/proto" frontend/src --glob "!frontend/src/app/proto/**"

# 5) Validate and publish
cd frontend
bun run type-check
bun run build
git push -u origin promote/<scope>-<yyyy-mm-dd>

Rules:

  • Never open PRs from prototype directly to main.
  • Open PRs from promote/* to main only.
  • If a commit introduces dependencies not yet in main, promote the minimal required file(s) as a separate commit.

Local Development Setup ​

Prerequisites ​

  • Go 1.25+
  • Bun 1.x
  • Docker + Docker Compose

1. Start local services ​

bash
# From repo root — starts PostgreSQL + Adminer
npm run docker:up

2. Backend ​

bash
cd backend
cp .env.example .env      # fill in required values
make migrate-up           # run migrations
make dev                  # hot reload at http://localhost:3000

Key env vars for local development:

DATABASE_URL=postgresql://postgres:postgres@localhost:5432/compath?sslmode=disable
SUPABASE_URL=https://<your-project>.supabase.co
SERVER_PORT=3000
ENVIRONMENT=development

3. Frontend ​

bash
cd frontend
cp .env.example .env.local    # fill in required values
bun install
bun dev                        # dev server at http://localhost:5173

Key env vars:

VITE_API_URL=http://localhost:3000
VITE_SUPABASE_URL=https://<your-project>.supabase.co
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEY=<your-publishable-key>

4. Verify setup ​

bash
curl http://localhost:3000/api/v1/health   # → {"status":"ok"}

Adminer (DB browser): http://localhost:8080

  • Server: postgres, User: postgres, Password: postgres, DB: compath

CI Pipeline ​

CI runs automatically on every PR. All jobs must pass before merge is allowed.

On PRs targeting staging or main ​

JobWhat it checksBlocks merge
testgo test -race on every package, then ./migrations/ on its own, against a real Postgres✅ Yes
lintgolangci-lint⚠️ Warning only (continue-on-error)
swaggerswag init succeeds✅ Yes
type-checktsc --noEmit✅ Yes

Workflow files ​

FileTriggerPurpose
ci-backend.ymlPR → staging or mainTest, lint, swagger
ci-frontend.ymlPR → staging or mainType-check
deploy-staging.ymlPush to stagingBuild :staging image, deploy, migrate
deploy-backend.ymlPush to mainBuild :latest image, deploy production

Staging Environment ​

Architecture ​

VPS (same server as production)
├── compath-backend         :3000  ← production
├── compath-backend-staging :3001  ← staging (joins production nginx network)
├── compath-postgres        :5432  ← production DB
├── compath-postgres-staging:5433  ← staging DB (separate data)
├── compath-redis                  ← production cache
├── compath-redis-staging          ← staging cache (separate)
└── compath-nginx           :80/:443 ← routes both domains
    ├── brain.compath.app     → compath-backend
    └── brain-staging.compath.app → compath-backend-staging

Cloudflare SSL Setup (one-time, covers all subdomains) ​

SSL is handled by Cloudflare — no certbot required. The nginx config uses a Cloudflare Origin Certificate (wildcard *.compath.app) which is already installed for production. If the Origin Certificate doesn't exist yet, follow these steps:

1. Generate the Cloudflare Origin Certificate ​

  1. Go to Cloudflare Dashboard → compath.app → SSL/TLS → Origin Server
  2. Click Create Certificate
  3. Choose:
    • Key type: RSA (2048)
    • Hostnames: compath.app, *.compath.app
    • Validity: 15 years
  4. Click Create
  5. Copy the Origin Certificate (PEM) and Private Key

2. Install on the VPS ​

bash
ssh compath-vps
sudo mkdir -p /etc/ssl/cloudflare

# Paste the Origin Certificate
sudo nano /etc/ssl/cloudflare/compath.app.pem

# Paste the Private Key
sudo nano /etc/ssl/cloudflare/compath.app.key

sudo chmod 644 /etc/ssl/cloudflare/compath.app.pem
sudo chmod 600 /etc/ssl/cloudflare/compath.app.key

If production is already running, the cert files are already on the VPS — staging reuses them.

3. Set Cloudflare SSL mode ​

In Cloudflare Dashboard → compath.app → SSL/TLS → Overview:

  • Set encryption mode to Full (strict)

This ensures Cloudflare validates the Origin Certificate when connecting to your VPS.


First-time staging setup on the VPS ​

SSH into the VPS and run these steps once:

bash
ssh compath-vps
cd /home/compath

# 1. Create the staging env file
cp infrastructure/deployment/.env infrastructure/deployment/.env.staging
nano infrastructure/deployment/.env.staging
# Edit: STAGING_POSTGRES_*, STAGING_REDIS_PASSWORD, SUPABASE_URL (staging project), etc.

# 2. Add DNS record in Cloudflare
#    Type: A  Name: brain-staging  Content: <VPS IP>  Proxy: ON (orange cloud)
#    (The wildcard Origin Certificate already covers *.compath.app — no cert work needed)

# 3. Reload nginx to pick up staging.conf
docker exec compath-nginx nginx -s reload

# Verify nginx config is valid before reloading
docker exec compath-nginx nginx -t

# 4. Start staging stack for the first time
docker compose -f infrastructure/deployment/docker-compose.staging.yml up -d

# 5. Run staging migrations
docker exec compath-backend-staging ./migrate -command up

# 6. Health check
curl -sf https://brain-staging.compath.app/api/v1/health

Required GitHub Secrets ​

The staging deploy reuses the existing production VPS secrets. No new secrets needed unless you want to separate them.

SecretUsed byValue
VPS_HOSTdeploy-staging, deploy-backendVPS IP address
VPS_USERNAMEdeploy-staging, deploy-backendSSH user
VPS_SSH_KEYdeploy-staging, deploy-backendPrivate key
VPS_PORTdeploy-staging, deploy-backendSSH port (usually 22)
GHCR_TOKENdeploy-staging, deploy-backendGitHub PAT with read:packages
GHCR_USERNAMEdeploy-staging, deploy-backendGitHub username

Staging .env.staging reference ​

env
# Server
SERVER_PORT=3000
SERVER_HOST=0.0.0.0
ENVIRONMENT=staging

# Database (staging-specific)
STAGING_POSTGRES_USER=postgres
STAGING_POSTGRES_PASSWORD=<strong-password>
STAGING_POSTGRES_DB=compath_staging
DATABASE_URL=postgresql://postgres:<password>@compath-postgres-staging:5432/compath_staging?sslmode=disable

# Redis (staging-specific)
STAGING_REDIS_PASSWORD=<strong-password>
REDIS_URL=redis://:${STAGING_REDIS_PASSWORD}@compath-redis-staging:6379

# Supabase — use your STAGING Supabase project, not production
SUPABASE_URL=https://<staging-project>.supabase.co
SUPABASE_ANON_KEY=<staging-anon-key>

# Email — use test mode or a staging Resend domain
RESEND_API_KEY=<resend-key>
RESEND_FROM_EMAIL=no-reply@staging.compath.app

# Google Calendar — use a separate GCP project or the same with staging redirect URI
GOOGLE_CLIENT_ID=<google-client-id>
GOOGLE_CLIENT_SECRET=<google-client-secret>
GOOGLE_REDIRECT_URI=https://brain-staging.compath.app/api/v1/calendar/callback/google
GOOGLE_WEBHOOK_URL=https://brain-staging.compath.app/api/v1/webhooks/google/calendar

# Axiom — same API token as production, separate dataset
AXIOM_TOKEN=<axiom-api-token>
VECTOR_AXIOM_DATASET=compath-staging

Running migrations on staging ​

bash
ssh compath-vps
docker exec compath-backend-staging ./migrate -command version
docker exec compath-backend-staging ./migrate -command up

Frontend Staging (Cloudflare Pages) ​

Cloudflare Pages automatically creates a branch preview for every branch. When the staging branch is pushed:

  • Branch preview URL: https://staging.compath.pages.dev
  • To use a custom domain (app-staging.compath.app):
    1. Go to Cloudflare Pages → your project → Settings → Environment Variables
    2. Add a staging environment with VITE_API_URL=https://brain-staging.compath.app
    3. Go to Custom Domains → add app-staging.compath.app mapped to the staging branch

Environment variables per branch (Cloudflare Pages dashboard) ​

VariableProduction (main)Staging (staging)
VITE_API_URLhttps://brain.compath.apphttps://brain-staging.compath.app
VITE_SUPABASE_URLprod Supabase URLstaging Supabase URL
VITE_SUPABASE_PUBLISHABLE_DEFAULT_KEYprod publishable keystaging publishable key

Admin Console (single-origin, optional CF Access) ​

The admin system-management UI lives at my.compath.app/admin/*. There is no separate admin host — the routes are mounted directly on the main app and gated by RequireAdmin (RBAC: account_type === "admin").

If you want an SSO layer on top of the RBAC gate, configure a path-based Cloudflare Access Self-Hosted Application:

  • Application domain: my.compath.app
  • Path: /admin*
  • Identity providers: same SSO / email OTP used by the rest of the team's CF Access apps
  • Policy: allow the internal team group or explicit admin email list

This keeps the SSO gate without the cross-origin Supabase session problems an admin.compath.app host would create. Local dev "just works": admin pages load on localhost:5173/admin/* against the same Supabase session as the rest of the app.

If admin.compath.app was previously configured as a CF Pages custom domain, either remove it or add a CF Redirect Rule admin.compath.app/* → my.compath.app/admin/$1 so old bookmarks don't 404.


Deployment Checklist ​

Deploying to staging ​

Staging deploys automatically on every merge to staging, and deploy-staging.yml runs pending migrations itself after the backend starts — no manual step, even for a PR with a schema change. To confirm what staging is at:

bash
ssh compath-vps
docker exec compath-backend-staging ./migrate -command version

Promoting staging → production ​

Migrate first, then merge. The new binary usually reads something the pending migrations add (a column in a shared SELECT list, an enum value it writes), so deploying before migrating 500s those paths until the migration lands. The canonical, step-by-step version of this lives in CLAUDE.md → "Deployment Checklist"; the short form:

  1. Open PR from staging → main on GitHub; CI reruns — wait for green; get approval. Do not merge yet.
  2. Check production's migration version and run the pending migrations from the image of the exact commit being released, without deploying it. Images are tagged staging-<short-sha> only for pushes that touch backend/** or the deployment infrastructure (the workflow has a paths: filter), so pin the last staging commit that changed the backend — that is the binary being released — rather than the moving :staging tag, so a staging push landing mid-release cannot apply migrations main's binary has never seen. Read the DSN out of the running container — --env-file keeps the quotes and migrate dies on the driver name. --first-parent matters: PRs land on staging as merge commits and the merge commit is what got built; without it git log returns a feature commit inside the merged branch, for which no image exists:
    bash
    ssh compath-vps 'docker exec compath-backend ./migrate -command version'
    SHA=$(git log --first-parent -1 --format=%H origin/staging -- backend infrastructure/deployment .github/workflows/deploy-staging.yml | cut -c1-7)
    ssh compath-vps "DBURL=\$(docker exec compath-backend printenv DATABASE_URL); \
      docker run --rm --network deployment_compath-network -e DATABASE_URL=\"\$DBURL\" \
      ghcr.io/bms-harmony/compath/backend:staging-$SHA ./migrate -command up"
  3. Merge — with a merge commit, never squash (squashing drops the second parent and the next release conflicts everywhere). deploy-backend.yml fires automatically and Cloudflare Pages deploys the frontend in parallel.
  4. Verify:
    bash
    ssh compath-vps 'docker exec compath-backend ./migrate -command version'
    curl -sf https://brain.compath.app/api/v1/health

Common Commands Reference ​

bash
# Check staging is healthy
curl -sf https://brain-staging.compath.app/api/v1/health

# View staging logs
ssh compath-vps "docker logs compath-backend-staging --tail 100 -f"

# View staging compose status
ssh compath-vps "docker compose -f /home/compath/infrastructure/deployment/docker-compose.staging.yml ps"

# Restart staging backend manually (service is `staging-backend`, not `backend`)
ssh compath-vps "docker compose -f /home/compath/infrastructure/deployment/docker-compose.staging.yml restart staging-backend"

# Connect to staging database
ssh compath-vps -t "docker exec -it compath-postgres-staging psql -U postgres -d compath_staging"

# Roll back last staging migration
ssh compath-vps "docker exec compath-backend-staging ./migrate -command down"

Last updated: