feat: Phase 9.1 — Environment, DevOps & Staging Deployment#64
Conversation
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>
WalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
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. Comment |
There was a problem hiding this comment.
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_downdirectives and possibly transport timeouts.tee-worker/Dockerfile (1)
13-18: Production image includes devDependencies — consider a prune step.
COPY --from=build /app/node_modulescarries 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=devapps/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_statecomment 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.tstargets source files (src/**/*.entity.ts,src/migrations/*.ts) for TypeORM CLI usage with ts-node, whilerun-migrations.tsrequires 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.1and only accessible within the Docker network and from the host, consider adding a password via--requirepassfor 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-workerhas 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), butgrafana/alloy:latestcould break unexpectedly on a future pull.Suggested fix
- image: grafana/alloy:latest + image: grafana/alloy:v1.13.0The 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_TOKENin 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 withappleboy/ssh-actionand the token is ephemeral (~job duration), so risk is low. However, be aware that:
- The token may briefly appear in
/proc/<pid>/cmdlineon the VPS.- If SSH action logging is verbose, it could leak into logs (though appleboy masks secrets).
An alternative is to pass it as an
envsparameter of the SSH action, which is slightly cleaner. Low priority for staging.
35-37: Tagging images with:latestalongside the version tag may cause confusion.Both
build-apiandbuild-teepush a:latesttag on every staging deployment. If you later add a production workflow, its images will also overwrite:latest. Consider using:staging-latestor:latest-stagingto namespace it, or remove the:latesttag entirely since the compose file uses${TAG}for versioned references.Also applies to: 62-64
148-155: Inconsistent version pinning betweenappleboy/scp-action@v0.1.7andappleboy/ssh-action@v1.
scp-actionis pinned to a patch version (v0.1.7) whilessh-actionuses 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.staginggeneration for correctness and secret handling.A few observations on this heredoc block:
- 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.- Lines 142–145 correctly append
GITHUB_REPOSITORY_OWNER(lowercased) andTAGoutside the heredoc since they require shell operations.- The generated
.env.stagingis 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
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>
There was a problem hiding this comment.
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
| // Index for token prefix lookups (added by incremental migration 1738972800000) | ||
| await queryRunner.query( | ||
| `CREATE INDEX "IDX_refresh_token_prefix" ON "refresh_tokens" ("tokenPrefix")` | ||
| ); |
There was a problem hiding this comment.
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).
| alloy: | ||
| image: grafana/alloy:latest | ||
| restart: unless-stopped | ||
| volumes: |
There was a problem hiding this comment.
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.
- 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>
There was a problem hiding this comment.
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
postgresandredisservices have healthchecks, andapidepends on them viacondition: service_healthy. However,apiitself has no healthcheck. This meansdocker compose run --rm api node dist/run-migrations.js(in the deploy workflow) and any futuredepends_onfrom other services can't gate on API readiness. Consider adding a healthcheck so the deployment can verify the API is up afterdocker 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 indocker inspectoutput via healthcheck args.The
redis-cli -a ${REDIS_PASSWORD}in the healthcheck test will be interpolated and stored in the container's config, visible viadocker inspect. Since this is localhost-bound staging, the risk is low, but you can avoid it by usingREDISCLI_AUTHenvironment 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-webjob lacks explicitpermissions.Unlike
build-api,build-tee, anddeploy-vps, this job doesn't declarepermissions. While it doesn't need GHCR write access, explicitly settingcontents: readfollows 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-apiandbuild-teeusedocker/build-push-action@v5without cache configuration. Adding GitHub Actions cache can significantly speed up rebuilds.
- 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>
Summary
pnpm deploy --prod --legacy, staging Docker Compose (7 services), Caddyfile with hostname-based routingv*-staging*→ build API/TEE images → build web app → deploy to VPS)Staging Environment
7 containers running: API, PostgreSQL, Redis, IPFS (Kubo), TEE worker, Caddy, Alloy
Key Files
.github/workflows/deploy-staging.yml— CI/CD pipelineapps/api/Dockerfile— Multi-stage API builddocker/docker-compose.staging.yml— All staging servicesdocker/Caddyfile— Reverse proxy + static file servingdocker/alloy-config.river— Log aggregation configapps/api/src/run-migrations.ts— Programmatic migration runnerTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Documentation