Skip to content

feat: Phase 9.1 — Environment, DevOps & Staging Deployment#64

Merged
FSM1 merged 33 commits into
mainfrom
feat/phase-9.1
Feb 9, 2026
Merged

feat: Phase 9.1 — Environment, DevOps & Staging Deployment#64
FSM1 merged 33 commits into
mainfrom
feat/phase-9.1

Conversation

@FSM1

@FSM1 FSM1 commented Feb 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • Environment-aware configuration (configurable ports, TEE simulator guard, HashRouter for IPFS, Web3Auth network switching, structured logging)
  • Docker infrastructure: API Dockerfile with pnpm deploy --prod --legacy, staging Docker Compose (7 services), Caddyfile with hostname-based routing
  • Full schema database migration for fresh staging databases
  • Tag-triggered CI/CD workflow (v*-staging* → build API/TEE images → build web app → deploy to VPS)
  • Grafana Alloy log shipping to Grafana Cloud Loki + Better Stack uptime monitoring
  • Infrastructure provisioned and first deployment verified — staging is live

Staging Environment

Service URL
API https://api-staging.cipherbox.cc
Web https://app-staging.cipherbox.cc
Health https://api-staging.cipherbox.cc/health

7 containers running: API, PostgreSQL, Redis, IPFS (Kubo), TEE worker, Caddy, Alloy

Key Files

  • .github/workflows/deploy-staging.yml — CI/CD pipeline
  • apps/api/Dockerfile — Multi-stage API build
  • docker/docker-compose.staging.yml — All staging services
  • docker/Caddyfile — Reverse proxy + static file serving
  • docker/alloy-config.river — Log aggregation config
  • apps/api/src/run-migrations.ts — Programmatic migration runner

Test plan

  • API health endpoint returns 200 with database up
  • Web app returns 200 and serves SPA
  • All 7 Docker containers running on VPS
  • CI/CD pipeline completes successfully on tag push
  • Grafana Alloy shipping logs to Loki
  • Better Stack monitoring API health

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Hosted staging environment with tag-triggered deployment and a desktop system tray with background sync.
  • Bug Fixes

    • Removed a dead env var; reduced verbose DB logs; environment-aware Web3Auth network selection; hash-based routing for IPFS-hosted web; safer, idempotent migrations; more permissive e2e logout check.
  • Chores

    • Improved containerization (multi-stage builds, .dockerignore), migration runner, centralized log shipping, CI release/commit lint setup.
  • Documentation

    • Deployment runbook, monitoring guide, lessons-learned, roadmap and state updates.

FSM1 and others added 28 commits February 9, 2026 00:52
Phase 09.1: Environment Changes, DevOps & Staging Deployment
- Implementation decisions documented
- Phase boundary established

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Mark Phase 9 (Desktop Client) complete in ROADMAP and STATE
- Add Phase 9.1 (Env/DevOps/Staging) to roadmap
- Update progress to 91% (63/69 plans)
- Add 09-07-SUMMARY.md for system tray/sync plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 9.1: Environment Changes, DevOps & Staging Deployment
- Standard stack identified (Docker, GHCR, Caddy, Pinata, Cloudflare)
- Architecture patterns documented (VPS staging, IPFS web hosting)
- Critical finding: IPNS keys are random, not derived — environment salt not needed
- Pitfalls catalogued (migrations, TEE simulator guard, SPA routing on IPFS)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 9.1: Environment Changes, DevOps & Staging Deployment
- 5 plans in 3 waves
- 3 parallel (wave 1), 1 sequential (wave 2), 1 verification (wave 3)
- Ready for execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address 2 blockers and 4 warnings:
- Plan 06: depends_on 09.1-02, wave 2 (was wave 1 with file conflict)
- Plan 04: remove alloy-config.river from SCP, specify Pinata/Cloudflare commands
- Plan 04: document migration command fallback for container path issues
- Plan 05: add Better Stack uptime truth (moved from Plan 06)
- Plan 06: remove non-executor-verifiable Better Stack truth

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix Plan 06 dependency: wave 2, depends_on 09.1-02 (was wave 1)
- Remove alloy-config.river from Plan 04 initial deploy SCP
- Specify exact Pinata CLI and Cloudflare API commands in Plan 04
- Move Better Stack truth from Plan 06 to Plan 05
- Expand Plan 01 with logging cleanup and dead env var removal
- Update domain references from cipherbox.io to cipherbox.cc
- Add Plan 06 to ROADMAP plan list (was missing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Multi-stage Docker build: deps, build, production stages
- node:22-alpine base with corepack pnpm@10
- Non-root user (appuser:1001) for security
- .dockerignore excludes web, desktop, tee-worker, tests, docs
- Build from monorepo root with workspace lockfile resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create baseline migration (1700000000000-FullSchema) with all 9 tables
- Tables: users, refresh_tokens, auth_methods, vaults, pinned_cids,
  folder_ipns, tee_key_state, tee_key_rotation_log, ipns_republish_schedule
- Includes all columns, types, constraints, indexes, and foreign keys
- Timestamp precedes existing incremental migrations (1737520000000, 1738972800000)
- Fresh staging database can be initialized with migration:run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 6 services: api, postgres, redis, ipfs (kubo), tee-worker, caddy
- GHCR image references for api and tee-worker containers
- All non-public ports bound to 127.0.0.1 for security
- Caddy reverse-proxies HTTPS to API with Cloudflare Origin CA certs
- Health checks on postgres, redis, and ipfs with depends_on
- Kubo resource limits: 2G memory, 1.0 CPU
- Named volumes for persistent data across restarts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eanup

- Make API port configurable via PORT env var (default 3000) for containers
- Replace console.log with NestJS Logger in bootstrap
- Set NestJS log levels: all in dev, log/error/warn in staging/production
- Change TypeORM logging from boolean to array: errors+warnings+migrations in dev, errors+migrations in staging/production
- Relax TEE simulator guard to check CIPHERBOX_ENVIRONMENT instead of NODE_ENV
- Switch web app from BrowserRouter to HashRouter for IPFS gateway compatibility
- Add environment-aware Web3Auth network selection via VITE_ENVIRONMENT
- Remove dead VITE_PINATA_GATEWAY_URL from .env.example and e2e workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 1/1
- Generate full schema migration for fresh staging DB initialization

SUMMARY: .planning/phases/09.1-env-devops-staging/09.1-03-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2
- Create API Dockerfile and .dockerignore
- Create staging Docker Compose and Caddyfile

SUMMARY: .planning/phases/09.1-env-devops-staging/09.1-02-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 1/1
- Fix API port, TEE guard, hash routing, and logging

SUMMARY: .planning/phases/09.1-env-devops-staging/09.1-01-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Stage 1 (build): install deps, copy source, compile TypeScript
- Stage 2 (production): copy dist + node_modules, run as non-root node user
- CI can now build from source without a separate build step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create alloy-config.river for Docker container log discovery and Loki shipping
- Add Alloy service to staging Docker Compose with Docker socket mount (read-only)
- Add json-file log rotation (10m max-size, 3 files) to all 7 services
- Grafana Cloud credentials injected via environment variables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Grafana Cloud Free setup instructions (account, Loki credentials, GitHub Secrets)
- Better Stack Uptime setup for health check monitoring
- LogQL query examples for per-service and cross-service log search
- Dashboard suggestions for staging observability
- Troubleshooting guide for Alloy and Loki connectivity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 4-job GitHub Actions workflow: build-api, build-tee, deploy-web, deploy-vps
- Triggers on v*-staging* tags (e.g., v1.0.0-staging.1)
- Builds and pushes API + TEE worker Docker images to GHCR
- Builds web app with staging env vars, uploads to Pinata, updates DNSLink
- SSHs into VPS to pull images, run migrations, restart Docker Compose
- Programmatic run-migrations.ts for Docker containers (no ts-node needed)
- All secrets from GitHub Secrets, .env generated at deploy time

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2
- Add Grafana Alloy config and Docker Compose service
- Document monitoring setup instructions

SUMMARY: .planning/phases/09.1-env-devops-staging/09.1-06-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tasks completed: 2/2
- Update TEE worker Dockerfile to multi-stage
- Create tag-triggered deployment workflow

SUMMARY: .planning/phases/09.1-env-devops-staging/09.1-04-SUMMARY.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add environment: staging to deploy-web and deploy-vps jobs. Split
secrets into vars (non-sensitive) and secrets (sensitive) for cleaner
environment management. Add Grafana Loki env vars to .env.staging.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- GHCR requires lowercase repository owner in image tags
- Replace shell-based multipart body construction with curl -F flags
  to correctly handle binary file uploads to Pinata
- Lowercase GITHUB_REPOSITORY_OWNER in deploy-vps for docker compose pull

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pinata pinFileToIPFS requires all files to share a common directory
prefix in the filename field for directory uploads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- API Dockerfile: use pnpm deploy for flat node_modules without symlinks
  (pnpm workspace symlinks break when copied across Docker stages)
- Workflow: generate .env from .env.staging for docker compose variable
  substitution, add GITHUB_REPOSITORY_OWNER and TAG to env file
- Workflow: ensure certs symlink exists in arrange step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
pnpm v10 requires --legacy for deploy without injected workspace packages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ThrottlerModule is imported in app.module.ts at runtime, so it must
be in dependencies (not devDependencies) for pnpm deploy --prod.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cloudflare Universal SSL only covers *.cipherbox.cc, not
*.staging.cipherbox.cc. Use api-staging and app-staging instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Pinata gateway + Cloudflare DNSLink with direct file serving
from Caddy on the VPS. Eliminates dependency on paid IPFS gateway.

- Caddyfile: split into API and web app server blocks by hostname
- Docker Compose: mount /opt/cipherbox/web into Caddy container
- Workflow: build-web uploads artifact, deploy-vps SCPs to VPS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Mark all 6 plans complete, add 09.1-05 summary documenting infrastructure
provisioning and deployment fixes, update roadmap and state tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 9, 2026 04:18
@coderabbitai

coderabbitai Bot commented Feb 9, 2026

Copy link
Copy Markdown

Walkthrough

Adds a staging deployment pipeline and infra: multi-stage Dockerfiles, .dockerignore, Caddy + docker-compose staging stack with monitoring (Grafana Alloy → Loki), tag-triggered GitHub Actions for build/deploy (images, web artifact, VPS deploy), baseline DB migration and migration runner, environment-aware web/API updates, routing change, and extensive planning/learnings docs.

Changes

Cohort / File(s) Summary
Docker & Staging Compose
docker/Caddyfile, docker/docker-compose.staging.yml, docker/alloy-config.river, docker/MONITORING.md, .dockerignore
Adds Caddy origin-TLS config, a 6–7 service staging compose (api, postgres, redis, ipfs, tee-worker, caddy, alloy), Grafana Alloy → Loki log shipping, monitoring docs, and .dockerignore to reduce build context.
Multi-stage Dockerfiles
apps/api/Dockerfile, tee-worker/Dockerfile
Introduce multi-stage builds, workspace-aware copy, non-root runtime users, and optimized production images for API and TEE worker.
CI/CD & Release Automation
.github/workflows/deploy-staging.yml, .github/workflows/release-please.yml, .github/workflows/e2e.yml, .release-please-manifest.json, release-please-config.json
Adds tag-triggered staging deploy workflow (build/push API & TEE, build web artifact, Pinata/CID flow, VPS deploy via SSH/docker-compose), Release Please workflow, and removes VITE_PINATA_GATEWAY_URL from e2e env creation.
API: runtime, logging, migrations
apps/api/src/main.ts, apps/api/src/app.module.ts, apps/api/src/run-migrations.ts, apps/api/src/migrations/1700000000000-FullSchema.ts, apps/api/src/migrations/*
Make port configurable, use Nest Logger with env-based verbosity, refine TypeORM logging and sync behavior, add programmatic migration runner, add baseline FullSchema migration and idempotency hardening for incremental migrations.
Web: routing & Web3Auth
apps/web/src/routes/index.tsx, apps/web/src/lib/web3auth/config.ts, apps/web/.env.example
Switch BrowserRouter→HashRouter for IPFS compatibility; make Web3Auth network selection environment-aware via VITE_ENVIRONMENT; remove dead VITE_PINATA_GATEWAY_URL from example env.
TEE & runtime guards
tee-worker/src/services/tee-keys.ts
Change TEE simulator production guard to consult CIPHERBOX_ENVIRONMENT (preferred) rather than NODE_ENV; update related messaging.
Migrations: baseline & fixes
apps/api/src/migrations/*, apps/api/package.json
Add FullSchema baseline migration, idempotent ALTER/INDEX in incremental migrations; bump API package version and move @nestjs/throttler into dependencies.
Planning, Docs & Learnings
.planning/..., .learnings/2026-02-09-staging-deployment-first-deploy.md, .claude/claude.md
Add extensive Phase 09.1 planning, plans (01–06), summaries, research, state/roadmap updates, and a lessons-learned deployment writeup.
Tooling & repo config
commitlint.config.js, .husky/commit-msg, package.json, .release-please-manifest.json, apps/web/package.json, packages/crypto/package.json, tee-worker/package.json, tests/e2e/package.json
Add commitlint + Husky commit-msg hook, release-please manifest/config, bump several package versions, and add commit tooling devDependencies.
E2E tests
tests/e2e/tests/full-workflow.spec.ts
Relax post-logout URL assertion to allow optional hash fragment (compatible with HashRouter).

Sequence Diagram

sequenceDiagram
    actor Developer
    participant GitHub as rgba(59,130,246,0.5) GitHub Actions
    participant GHCR as rgba(99,102,241,0.5) GHCR
    participant Pinata as rgba(16,185,129,0.5) Pinata
    participant Cloudflare as rgba(234,88,12,0.5) Cloudflare
    participant VPS as rgba(20,83,45,0.5) VPS (Docker Compose)
    participant Loki as rgba(11,122,255,0.5) Grafana Loki

    Developer->>GitHub: Push tag v*-staging*
    GitHub->>GitHub: build-api job (build image)
    GitHub->>GHCR: Push API image
    GitHub->>GitHub: build-tee job (build image)
    GitHub->>GHCR: Push TEE image
    GitHub->>GitHub: build-web job (build dist artifact)
    GitHub->>Pinata: Upload web dist (CID)
    Pinata-->>GitHub: Return CID
    GitHub->>Cloudflare: Update DNSLink with CID
    GitHub->>GitHub: deploy-vps job (artifact + env)
    GitHub->>VPS: SCP .env.staging and configs
    VPS->>VPS: Pull images, run migrations (run-migrations.ts)
    VPS->>VPS: docker-compose up (api, db, redis, ipfs, tee, caddy, alloy)
    VPS->>Loki: Alloy ships container logs to Loki
    VPS-->>GitHub: Deployment complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: introducing Phase 9.1 for environment configuration, DevOps infrastructure, and staging deployment—the core focus of all file changes in this comprehensive PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/phase-9.1

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
.husky/commit-msg (1)

1-1: Quote $1 for robustness.

While the commit message file path rarely contains spaces, quoting the argument is a shell best practice.

Suggested fix
-pnpm commitlint --edit $1
+pnpm commitlint --edit "$1"
apps/api/src/migrations/1700000000000-FullSchema.ts (2)

29-39: Inconsistent column naming conventions across tables.

The users and refresh_tokens tables use camelCase columns ("publicKey", "userId", "tokenHash"), while vaults, pinned_cids, folder_ipns, etc., use snake_case ("owner_id", "user_id"). This likely mirrors the entity definitions, but the inconsistency can be confusing. Consider unifying in a future migration.


22-24: CREATE EXTENSION requires elevated privileges — document the prerequisite.

CREATE EXTENSION IF NOT EXISTS "uuid-ossp" needs the CREATE privilege on the database (typically superuser or a role with CREATE on the schema). In containerized Postgres this is usually fine (default user is superuser), but if staging/production uses a managed database (e.g., RDS) with a restricted role, this may fail. Worth a note in the migration or deployment docs.

.github/workflows/deploy-staging.yml (1)

30-37: Tagging images with :latest on every staging push may cause confusion.

Both API and TEE images are tagged with :latest alongside the git-ref tag. Since this is a staging workflow, the :latest tag will point to the most recent staging build rather than a stable release. If any compose file or script references :latest expecting a production-stable image, it will unexpectedly get a staging build. Consider using :staging-latest instead, or removing the :latest tag entirely.

Also applies to: 57-64


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In @.github/workflows/deploy-staging.yml:
- Around line 103-107: The deploy-vps job lacks explicit permissions; update the
deploy-vps job block to add a permissions mapping that grants only the needed
scopes: set packages: read (for GHCR docker login used later) and contents: read
(for checkout/artifact download), ensuring the job name deploy-vps is updated
with this minimal permissions section to avoid relying on repo defaults.

In @.planning/phases/09.1-env-devops-staging/09.1-05-SUMMARY.md:
- Around line 99-106: The VPS row in the markdown table currently exposes the
origin IP "76.13.151.200" under the "VPS" component; replace that IP with a
non-public placeholder (e.g., "<redacted-ip>" or "private") or remove the IP
entirely and add a note pointing to a private/secure source (Secrets Manager or
internal docs) for the real address so the server IP is not stored in version
control; update the "VPS" table cell where "Hostinger, 76.13.151.200, Docker +
Compose" appears (look for the "VPS" table row) accordingly.

In @.planning/phases/09.1-env-devops-staging/09.1-06-PLAN.md:
- Around line 199-200: The monitor URL uses the wrong subdomain format; replace
the dotted multi-level hostname `https://api.staging.cipherbox.cc/health` with
the actual staging host `https://api-staging.cipherbox.cc/health` used
throughout the PR (search for occurrences of `api.staging.cipherbox.cc` and
update them to `api-staging.cipherbox.cc`) so the Better Stack monitor matches
the real staging hostname and Cloudflare wildcard workaround.

In @.planning/phases/09.1-env-devops-staging/09.1-CONTEXT.md:
- Around line 17-18: Update the context doc to reflect that the web app is
served via Caddy rather than IPFS/Pinata: replace the phrase "Web app hosted on
IPFS as static build, pinned to Pinata" (and the similar mentions at the spots
referencing CI pinning, Pinata CID automation, and Pinata free-tier hosting)
with text stating the CI builds a static site and deploys it to the Caddy server
using the Caddyfile for hostname-based routing; also keep Cloudflare handling
SSL but note Caddy is the origin server and remove references to Pinata
pinning/CID automation and Pinata free-tier strategy so the document matches the
implemented Caddy-based hosting approach.

In `@apps/api/src/run-migrations.ts`:
- Around line 43-46: The catch block currently prints the full error with
console.error('Migration failed:', error) which may expose DB connection
details; update the catch (error) { ... } block to avoid leaking secrets by
logging a sanitized message instead—e.g., log a generic "Migration failed" plus
limited info such as error.name and a truncated/sanitized error.message (or use
a sanitizeError helper that strips host/port/user/db fields), then call
process.exit(1) as before; modify the console.error call in the catch to use the
sanitized output rather than the raw error object.

In `@docker/MONITORING.md`:
- Around line 96-98: The LogQL expression `{service="api"} |= "error" or
{service="api"} |= "ERROR"` is invalid because you cannot use `or` between two
stream selectors; replace the two-pipeline approach with a single selector using
a case-insensitive regex pipeline, e.g. use the selector `{service="api"}`
combined with a regex operator and the `(?i)` flag (e.g. `{service="api"} |~
"(?i)error"`) so the query matches "error" in any case without causing a parse
error.
🧹 Nitpick comments (12)
docker/Caddyfile (1)

9-12: Consider adding request timeouts and header hardening.

The reverse proxy to the API has no explicit timeout or header configuration. For staging this is fine, but before promoting to production you'll want header_up / header_down directives and possibly transport timeouts.

tee-worker/Dockerfile (1)

13-18: Production image includes devDependencies — consider a prune step.

COPY --from=build /app/node_modules carries over all dependencies including devDependencies (TypeScript, types, etc.), bloating the production image. Consider adding a prune step or a separate install for production deps only.

♻️ Proposed fix to exclude devDependencies
 # Stage 2: Production runtime
 FROM node:20-alpine
 WORKDIR /app
 COPY --from=build /app/dist ./dist
-COPY --from=build /app/node_modules ./node_modules
-COPY package.json ./
+COPY --from=build /app/package.json ./
+COPY --from=build /app/package-lock.json ./
+RUN npm ci --omit=dev
apps/api/src/migrations/1700000000000-FullSchema.ts (2)

28-38: Inconsistent column naming convention across tables.

Tables 1–3 (users, refresh_tokens, auth_methods) use camelCase columns ("userId", "tokenHash", "publicKey"), while tables 4–9 use snake_case ("user_id", "owner_id", "size_bytes"). This likely mirrors existing entity definitions, but it's worth noting for consistency going forward.

Also applies to: 43-61, 66-78


157-169: tee_key_state comment says "singleton row" but schema has no enforcement.

Nothing prevents inserting multiple rows. If this table is truly intended to hold a single row, consider adding a check constraint or a fixed sentinel value with a unique constraint to enforce it at the database level.

Example: enforce singleton via a check constraint
     CONSTRAINT "PK_tee_key_state" PRIMARY KEY ("id")
+    -- Enforce singleton: only one row allowed
   )
 `);
+await queryRunner.query(`
+  CREATE UNIQUE INDEX "IDX_tee_key_state_singleton" ON "tee_key_state" ((true))
+`);

Alternatively, use a column with a fixed default value and a unique constraint on it.

apps/api/src/run-migrations.ts (1)

15-25: The suggested refactor won't work — these DataSources must remain separate due to different path requirements.

data-source.ts targets source files (src/**/*.entity.ts, src/migrations/*.ts) for TypeORM CLI usage with ts-node, while run-migrations.ts requires compiled paths (dist/**/*.entity.js, dist/migrations/*.js) for production Docker containers where TypeScript is unavailable. The different logging configurations are also intentional (development vs. production).

The duplication of connection parameters (host, port, credentials, database name) across both files is a valid concern for maintenance, but importing the shared DataSource directly would break the migration runner. If avoiding duplication is desired, consider extracting environment-based database connection parameters into a shared utility function that both files can use, while keeping the DataSource instantiation separate.

docker/docker-compose.staging.yml (3)

41-53: Redis has no authentication configured.

While Redis is bound to 127.0.0.1 and only accessible within the Docker network and from the host, consider adding a password via --requirepass for defense-in-depth on the staging VPS. Any local process or compromised container on the host can reach it.

For staging this is acceptable, but worth hardening before production.


80-91: TEE worker lacks a healthcheck.

The API depends on postgres and redis health, but the tee-worker has no healthcheck. If the TEE worker crashes or fails to start, there's no automated detection. Consider adding a basic HTTP health check:

Suggested addition
   tee-worker:
     image: ghcr.io/${GITHUB_REPOSITORY_OWNER:-OWNER}/cipherbox-tee-worker:${TAG:-latest}
     restart: unless-stopped
     environment:
       NODE_ENV: production
       PORT: 3001
       TEE_MODE: simulator
       CIPHERBOX_ENVIRONMENT: staging
       TEE_WORKER_SECRET: ${TEE_WORKER_SECRET}
     ports:
       - '127.0.0.1:3001:3001'
     logging: *default-logging
+    healthcheck:
+      test: ['CMD-SHELL', 'wget -qO- http://localhost:3001/health || exit 1']
+      interval: 10s
+      timeout: 5s
+      retries: 5

108-108: Pin the Alloy image tag for reproducible deployments.

All other images in this compose file use pinned versions (postgres:16-alpine, redis:7-alpine, ipfs/kubo:v0.34.0, caddy:2-alpine), but grafana/alloy:latest could break unexpectedly on a future pull.

Suggested fix
-    image: grafana/alloy:latest
+    image: grafana/alloy:v1.13.0

The current stable version (as of Feb 2026) is v1.13.0. Pin it for consistency with other services.

.github/workflows/deploy-staging.yml (4)

191-195: GITHUB_TOKEN in the SSH script is expanded into the remote command.

${{ secrets.GITHUB_TOKEN }} is interpolated by GitHub Actions before the script is sent to the VPS via SSH. The literal token value appears in the remote shell command. This is a common pattern with appleboy/ssh-action and the token is ephemeral (~job duration), so risk is low. However, be aware that:

  1. The token may briefly appear in /proc/<pid>/cmdline on the VPS.
  2. If SSH action logging is verbose, it could leak into logs (though appleboy masks secrets).

An alternative is to pass it as an envs parameter of the SSH action, which is slightly cleaner. Low priority for staging.


35-37: Tagging images with :latest alongside the version tag may cause confusion.

Both build-api and build-tee push a :latest tag on every staging deployment. If you later add a production workflow, its images will also overwrite :latest. Consider using :staging-latest or :latest-staging to namespace it, or remove the :latest tag entirely since the compose file uses ${TAG} for versioned references.

Also applies to: 62-64


148-155: Inconsistent version pinning between appleboy/scp-action@v0.1.7 and appleboy/ssh-action@v1.

scp-action is pinned to a patch version (v0.1.7) while ssh-action uses a major version tag (v1). For reproducibility and security (preventing supply-chain attacks via tag mutation), consider pinning both to either the same granularity or using commit SHAs.


117-145: Review the .env.staging generation for correctness and secret handling.

A few observations on this heredoc block:

  1. The quoted heredoc (<< 'ENVEOF') correctly prevents shell expansion while GHA expressions (${{ }}) are still interpolated — good practice to avoid accidental expansion of values containing $ or backticks.
  2. Lines 142–145 correctly append GITHUB_REPOSITORY_OWNER (lowercased) and TAG outside the heredoc since they require shell operations.
  3. The generated .env.staging is SCPed to the VPS and then copied to .env (Line 169). This means secrets (DB password, JWT secret, TEE secret, Loki API key) exist as plaintext files on the VPS. Ensure the VPS has appropriate file permissions (e.g., chmod 600 .env*) — consider adding that to the "Arrange files" step.
Suggested hardening
             # Move files to correct locations
             mv /opt/cipherbox/.env.staging /opt/cipherbox/docker/.env.staging 2>/dev/null || true
             cd /opt/cipherbox/docker

             # Docker Compose reads .env automatically for variable substitution
             cp .env.staging .env
+
+            # Restrict env file permissions (contains secrets)
+            chmod 600 .env .env.staging

             # Ensure certs symlink exists for Caddy
             ln -sf /opt/cipherbox/certs /opt/cipherbox/docker/certs 2>/dev/null || true

Comment thread .github/workflows/deploy-staging.yml
Comment thread .planning/phases/09.1-env-devops-staging/09.1-05-SUMMARY.md
Comment thread .planning/phases/09.1-env-devops-staging/09.1-06-PLAN.md Outdated
Comment thread .planning/phases/09.1-env-devops-staging/09.1-CONTEXT.md Outdated
Comment thread apps/api/src/run-migrations.ts Outdated
Comment thread docker/MONITORING.md
FullSchema baseline + incremental migrations ran in a single transaction.
AddTokenPrefix failed because tokenPrefix already existed from baseline,
rolling back the entire schema creation.

Fix: use IF NOT EXISTS / IF EXISTS in incremental migrations so they're
no-ops when baseline already applied the changes.

Also remove --env-file from deploy workflow migration command since the
api service inherits env vars from env_file in docker-compose.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds staging-ready environment configuration and deployment infrastructure across the API, web app, and TEE worker, including Docker/Compose, CI/CD, migrations, and monitoring/log shipping.

Changes:

  • Introduces staging Docker Compose stack (API/Postgres/Redis/IPFS/TEE worker/Caddy/Alloy) plus Caddy reverse-proxy and Grafana Alloy Loki shipping.
  • Adds API production Docker build + programmatic migration runner and a full schema baseline migration for fresh staging DBs.
  • Updates web routing/auth config for staging compatibility (HashRouter + env-aware Web3Auth network) and adds tag-triggered staging deployment workflow.

Reviewed changes

Copilot reviewed 39 out of 40 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
tee-worker/src/services/tee-keys.ts Uses CIPHERBOX_ENVIRONMENT to block simulator mode only in production.
tee-worker/Dockerfile Switches TEE worker image to a multi-stage build and runs as non-root.
pnpm-lock.yaml Moves @nestjs/throttler into production dependencies resolution.
docker/docker-compose.staging.yml Defines 7-service staging stack with log rotation and Alloy.
docker/alloy-config.river Adds Alloy River config to discover Docker logs and ship to Loki.
docker/MONITORING.md Documents Grafana Cloud + Better Stack monitoring setup and queries.
docker/Caddyfile Adds hostname-based routing for API + SPA static serving and HTTP→HTTPS redirect.
apps/web/src/routes/index.tsx Switches React Router from BrowserRouter to HashRouter for static hosting.
apps/web/src/lib/web3auth/config.ts Selects Web3Auth network based on VITE_ENVIRONMENT.
apps/web/.env.example Removes unused VITE_PINATA_GATEWAY_URL from example env.
apps/api/src/run-migrations.ts Adds programmatic TypeORM migration runner for production images.
apps/api/src/migrations/1700000000000-FullSchema.ts Adds “baseline full schema” migration for fresh staging DBs.
apps/api/src/main.ts Makes API port configurable and reduces log verbosity outside dev.
apps/api/src/app.module.ts Narrows TypeORM logging to migrations/errors(/warn) instead of query spam.
apps/api/package.json Moves @nestjs/throttler into dependencies.
apps/api/Dockerfile Adds multi-stage pnpm build + pnpm deploy --prod --legacy runtime layout.
.planning/phases/09.1-env-devops-staging/* Adds planning/research/plan summaries documenting the staging work.
.planning/phases/09-desktop-client/09-07-SUMMARY.md Updates planning artifacts for completed desktop phase.
.planning/STATE.md Updates overall project state/progress and recent decisions.
.planning/ROADMAP.md Marks Phase 9 complete and inserts/marks Phase 9.1 complete.
.planning/ENVIRONMENTS.md Updates environment documentation (URLs, formatting tweaks).
.learnings/2026-02-09-staging-deployment-first-deploy.md Captures deployment lessons learned (GHCR casing, pnpm deploy, CF SSL, etc.).
.github/workflows/e2e.yml Removes unused VITE_PINATA_GATEWAY_URL from E2E env setup.
.github/workflows/deploy-staging.yml Adds tag-triggered staging deploy pipeline building images, shipping web artifact, migrating, and deploying to VPS.
.dockerignore Adds Docker build context exclusions for monorepo builds.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

Comment thread apps/api/src/migrations/1700000000000-FullSchema.ts Outdated
Comment thread .github/workflows/deploy-staging.yml Outdated
Comment on lines +58 to +61
// Index for token prefix lookups (added by incremental migration 1738972800000)
await queryRunner.query(
`CREATE INDEX "IDX_refresh_token_prefix" ON "refresh_tokens" ("tokenPrefix")`
);

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The baseline migration creates refresh_tokens.tokenPrefix and IDX_refresh_token_prefix, but the later 1738972800000-AddTokenPrefix migration also adds the same column/index. On a fresh DB, TypeORM will run both migrations and the second one will fail (duplicate column/index). Either remove tokenPrefix + index from this baseline migration, or make AddTokenPrefix idempotent (use ADD COLUMN IF NOT EXISTS / CREATE INDEX IF NOT EXISTS via raw SQL, or otherwise guard on column/index existence).

Copilot uses AI. Check for mistakes.
Comment thread tee-worker/src/services/tee-keys.ts Outdated
Comment thread tee-worker/Dockerfile Outdated
Comment thread docker/MONITORING.md
Comment thread .planning/ENVIRONMENTS.md Outdated
Comment thread .github/workflows/deploy-staging.yml Outdated
Comment on lines +107 to +110
alloy:
image: grafana/alloy:latest
restart: unless-stopped
volumes:

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Alloy image is pinned to latest, which makes deployments non-reproducible and can introduce breaking changes unexpectedly. Prefer pinning to a specific Alloy version tag (and upgrading intentionally) to keep staging stable.

Copilot uses AI. Check for mistakes.
- Add explicit permissions (contents:read, packages:read) to deploy-vps job
- Redact VPS origin IP from version-controlled docs
- Fix api.staging -> api-staging subdomain in MONITORING.md, ENVIRONMENTS.md, plans
- Update CONTEXT.md to reflect Caddy hosting (not IPFS/Pinata)
- Sanitize migration runner error output to avoid leaking DB credentials
- Fix invalid LogQL syntax (use case-insensitive regex instead of `or`)
- Prune devDependencies from tee-worker production Docker image
- Add TEE worker healthcheck to docker-compose
- Pin Alloy image to v1.6.1 (was :latest)
- Add Redis password auth (--requirepass) for defense-in-depth
- Pin ssh-action to v1.2.0 for reproducible CI
- chmod 600 .env files on VPS to protect secrets

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @.github/workflows/deploy-staging.yml:
- Around line 198-221: The deployment script currently continues even if the
migrations command `docker compose run --rm api node dist/run-migrations.js`
fails; add a fail-fast behavior by either inserting `set -e` (or `set -o
errexit`) at the top of the script block so any command error aborts the job, or
immediately check the migration exit status (e.g., capture `$?` right after the
`docker compose run ...` command and call `exit 1` with a clear error log via
`echo`/`echo >&2` if non-zero) to ensure `docker compose up -d` never runs when
migrations fail.

In `@apps/api/src/app.module.ts`:
- Around line 74-78: The synchronize flag is currently enabled for any NODE_ENV
that's not 'production', which allows staging to auto-sync and risks schema
drift; update the synchronize assignment in the TypeOrm/Nest module config (the
property named synchronize) to only be true for explicit development (e.g.,
configService.get<string>('NODE_ENV') === 'development') so staging uses
migrations (as run-migrations.ts / FullSchema migration intend); locate the
synchronize line in app.module.ts and change its condition accordingly.
🧹 Nitpick comments (4)
docker/docker-compose.staging.yml (2)

10-21: API service lacks a healthcheck.

The postgres and redis services have healthchecks, and api depends on them via condition: service_healthy. However, api itself has no healthcheck. This means docker compose run --rm api node dist/run-migrations.js (in the deploy workflow) and any future depends_on from other services can't gate on API readiness. Consider adding a healthcheck so the deployment can verify the API is up after docker compose up -d.

Suggested healthcheck
   api:
     image: ghcr.io/${GITHUB_REPOSITORY_OWNER:-OWNER}/cipherbox-api:${TAG:-latest}
     restart: unless-stopped
     env_file: .env.staging
     ports:
       - '127.0.0.1:3000:3000'
     logging: *default-logging
+    healthcheck:
+      test: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1']
+      interval: 10s
+      timeout: 5s
+      retries: 5
+      start_period: 15s
     depends_on:
       postgres:
         condition: service_healthy
       redis:
         condition: service_healthy

50-51: Redis password leaked in docker inspect output via healthcheck args.

The redis-cli -a ${REDIS_PASSWORD} in the healthcheck test will be interpolated and stored in the container's config, visible via docker inspect. Since this is localhost-bound staging, the risk is low, but you can avoid it by using REDISCLI_AUTH environment variable instead.

Suggested fix
   redis:
     image: redis:7-alpine
     restart: unless-stopped
     command: ['redis-server', '--requirepass', '${REDIS_PASSWORD}']
+    environment:
+      REDISCLI_AUTH: ${REDIS_PASSWORD}
     ports:
       - '127.0.0.1:6379:6379'
     volumes:
       - redis_staging:/data
     logging: *default-logging
     healthcheck:
-      test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping']
+      test: ['CMD', 'redis-cli', 'ping']
       interval: 5s
       timeout: 5s
       retries: 5
.github/workflows/deploy-staging.yml (2)

66-101: build-web job lacks explicit permissions.

Unlike build-api, build-tee, and deploy-vps, this job doesn't declare permissions. While it doesn't need GHCR write access, explicitly setting contents: read follows the same least-privilege pattern used by the other jobs.

Suggested fix
   build-web:
     name: Build Web App
     runs-on: ubuntu-latest
     environment: staging
+    permissions:
+      contents: read
     steps:

30-37: Consider using Docker build cache for faster CI builds.

Both build-api and build-tee use docker/build-push-action@v5 without cache configuration. Adding GitHub Actions cache can significantly speed up rebuilds.

Comment thread .github/workflows/deploy-staging.yml
Comment thread apps/api/src/app.module.ts Outdated
FSM1 and others added 3 commits February 9, 2026 06:11
- Add commitlint with config-conventional and husky commit-msg hook
- Add Release Please config for automated changelogs and version bumps
- Add release-please.yml GitHub Actions workflow
- Normalize all package versions to 0.1.0 across the monorepo
- Document commit message rules and release process in CLAUDE.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add set -euo pipefail to deploy script so migration failure halts deploy
- Change synchronize to only enable in development (not all non-production)
- Remove stale VITE_PINATA_GATEWAY_URL from build env and ENVIRONMENTS.md
- Fix certs symlink: rm existing before ln -sf to avoid nested link
- Use REDISCLI_AUTH env var instead of -a flag in Redis healthcheck
- Harden TEE simulator guard when CIPHERBOX_ENVIRONMENT is unset
- Fix FullSchema migration header comment about baseline procedure
- Fix E2E logout test regex to accept HashRouter URLs (#/)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The previous change to only synchronize in 'development' broke CI where
NODE_ENV=test. Allow both development and test to auto-sync schema.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants