From 5d1bad3e2d1853ac7813a89601849680e346e99e Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:55:51 -0400 Subject: [PATCH 01/10] chore: add staging Docker Compose profile with seed and prod-like auth --- docker-compose.staging.yml | 81 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docker-compose.staging.yml diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml new file mode 100644 index 00000000..538b9860 --- /dev/null +++ b/docker-compose.staging.yml @@ -0,0 +1,81 @@ +# Staging stack — production-like settings (auth + RLS on), isolated volumes/ports. +# Usage: +# cp backend/.env.staging.example backend/.env.staging +# docker compose -f docker-compose.staging.yml --env-file backend/.env.staging up --build +version: '3.8' + +services: + api: + build: + context: ./backend + dockerfile: Dockerfile + ports: + - '3101:3001' + env_file: + - ./backend/.env.staging + environment: + - PORT=3001 + - NODE_ENV=production + - APP_ENV=staging + - DATABASE_URL=postgresql://${STAGING_DB_USER:-payd_staging}:${STAGING_DB_PASSWORD:-payd_staging_password}@postgres:5432/${STAGING_DB_NAME:-payd_staging} + - REDIS_URL=redis://redis:6379 + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=${STAGING_DB_USER:-payd_staging} + - DB_PASSWORD=${STAGING_DB_PASSWORD:-payd_staging_password} + - DB_NAME=${STAGING_DB_NAME:-payd_staging} + - ENABLE_RLS=true + - REQUIRE_AUTH=true + - LOG_LEVEL=info + - ENABLE_CACHING=true + - CACHE_TTL=3600 + - STELLAR_NETWORK_PASSPHRASE=${STELLAR_NETWORK_PASSPHRASE:-Test SDF Network \; September 2015} + - STELLAR_HORIZON_URL=${STELLAR_HORIZON_URL:-https://horizon-testnet.stellar.org} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - payd_staging_network + + postgres: + image: postgres:15-alpine + environment: + - POSTGRES_USER=${STAGING_DB_USER:-payd_staging} + - POSTGRES_PASSWORD=${STAGING_DB_PASSWORD:-payd_staging_password} + - POSTGRES_DB=${STAGING_DB_NAME:-payd_staging} + ports: + - '5433:5432' + volumes: + - postgres_staging_data:/var/lib/postgresql/data + - ./backend/src/db/seed.sql:/docker-entrypoint-initdb.d/02_seed.sql:ro + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U ${STAGING_DB_USER:-payd_staging} -d ${STAGING_DB_NAME:-payd_staging}'] + interval: 10s + timeout: 5s + retries: 5 + networks: + - payd_staging_network + + redis: + image: redis:7-alpine + ports: + - '6380:6379' + volumes: + - redis_staging_data:/data + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 10s + timeout: 5s + retries: 5 + networks: + - payd_staging_network + +volumes: + postgres_staging_data: + redis_staging_data: + +networks: + payd_staging_network: + driver: bridge From 69773ac5199cea09bd8491a650e144be522de833 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:56:00 -0400 Subject: [PATCH 02/10] chore: add backend/.env.staging.example for staging compose --- backend/.env.staging.example | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 backend/.env.staging.example diff --git a/backend/.env.staging.example b/backend/.env.staging.example new file mode 100644 index 00000000..b3490573 --- /dev/null +++ b/backend/.env.staging.example @@ -0,0 +1,37 @@ +# Staging — mirrors production (NODE_ENV=production) on isolated ports/volumes. +# Copy to backend/.env.staging and fill secrets before `docker compose -f docker-compose.staging.yml up`. + +PORT=3001 +NODE_ENV=production +APP_ENV=staging + +# Auth / security (required in staging; do not use empty JWT) +JWT_SECRET=replace-me-with-a-long-random-staging-secret +REQUIRE_AUTH=true +ENABLE_RLS=true + +# Database (matches docker-compose.staging.yml defaults) +STAGING_DB_USER=payd_staging +STAGING_DB_PASSWORD=payd_staging_password +STAGING_DB_NAME=payd_staging +DB_HOST=postgres +DB_PORT=5432 +DB_USER=payd_staging +DB_PASSWORD=payd_staging_password +DB_NAME=payd_staging +DATABASE_URL=postgresql://payd_staging:payd_staging_password@postgres:5432/payd_staging + +REDIS_URL=redis://redis:6379 + +LOG_LEVEL=info +ENABLE_CACHING=true +CACHE_TTL=3600 + +# Stellar testnet (staging never points at mainnet by default) +STELLAR_NETWORK_PASSPHRASE=Test SDF Network ; September 2015 +STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org +SOROBAN_RPC_URL=https://soroban-testnet.stellar.org + +SDS_API_KEY= +SDS_ENDPOINT=https://sds-api.stellar.org +SDS_ENABLE=true From 31e9e749f0dfb7954ed0a05f03f1145360d93933 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:56:49 -0400 Subject: [PATCH 03/10] fix(ci): drop invalid workflow-level retention-days keys --- .github/workflows/build.yml | 5 +---- .github/workflows/contract-release.yml | 5 +---- .github/workflows/dapp-ipfs.yml | 5 +---- .github/workflows/secrets-check.yml | 5 +---- 4 files changed, 4 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 52029992..60abd272 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: branches: ["main"] types: [opened, synchronize, reopened, ready_for_review] -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo Actions setting, not a workflow key. env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig @@ -113,6 +113,3 @@ jobs: - name: Run Tests working-directory: ./frontend run: npm test --if-present - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index 2655fcbc..5de18e00 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -5,7 +5,7 @@ on: tags: - "v*" -# Retention policy: Keep successful runs for 90 days, failed/cancelled for 14 days +# Retention is a repo Actions setting, not a workflow key. permissions: # required permissions for the workflow id-token: write contents: write # in order to create releases @@ -27,6 +27,3 @@ jobs: package: "..." secrets: release_token: ${{ secrets.GITHUB_TOKEN }} - -# Workflow run retention settings -retention-days: 90 \ No newline at end of file diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 29d16e55..1c1ef424 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -9,7 +9,7 @@ on: workflow_dispatch: -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo Actions setting, not a workflow key. concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true @@ -65,6 +65,3 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "- CID: ${{ steps.storacha.outputs.cid }}" >> "$GITHUB_STEP_SUMMARY" echo "- URL: ${{ steps.storacha.outputs.url }}" >> "$GITHUB_STEP_SUMMARY" - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index b8d3b7e8..7ef36518 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -10,7 +10,7 @@ on: paths: - "k8s/**" -# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days +# Retention is a repo Actions setting, not a workflow key. jobs: check-secrets-placeholders: name: Verify no real secrets in k8s manifests @@ -20,6 +20,3 @@ jobs: - name: Check backend-secret.yaml for non-placeholder values run: ./scripts/check-k8s-secrets.sh - -# Workflow run retention settings -retention-days: 30 \ No newline at end of file From 0faa9f10bd4cd0de60311900093e04dfc656e4d4 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:56:51 -0400 Subject: [PATCH 04/10] ci: remove invalid top-level retention-days from Actions workflows --- .github/workflows/build.yml | 1 - .github/workflows/contract-release.yml | 1 - .github/workflows/dapp-ipfs.yml | 1 - .github/workflows/secrets-check.yml | 1 - 4 files changed, 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60abd272..68212ca7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,6 @@ on: branches: ["main"] types: [opened, synchronize, reopened, ready_for_review] -# Retention is a repo Actions setting, not a workflow key. env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index 5de18e00..cca80486 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -5,7 +5,6 @@ on: tags: - "v*" -# Retention is a repo Actions setting, not a workflow key. permissions: # required permissions for the workflow id-token: write contents: write # in order to create releases diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 1c1ef424..5887cc7e 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -9,7 +9,6 @@ on: workflow_dispatch: -# Retention is a repo Actions setting, not a workflow key. concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index 7ef36518..5e989082 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -10,7 +10,6 @@ on: paths: - "k8s/**" -# Retention is a repo Actions setting, not a workflow key. jobs: check-secrets-placeholders: name: Verify no real secrets in k8s manifests From 5896d96b519cdfd6a5e800d3bd7917b757e4245c Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:56:59 -0400 Subject: [PATCH 05/10] ci: drop invalid top-level retention-days from Actions workflows --- .github/workflows/build.yml | 1 + .github/workflows/contract-release.yml | 1 + .github/workflows/dapp-ipfs.yml | 1 + .github/workflows/secrets-check.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68212ca7..3241a1e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,7 @@ on: branches: ["main"] types: [opened, synchronize, reopened, ready_for_review] +# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days env: CARGO_TERM_COLOR: always PKG_CONFIG_PATH: /usr/lib/pkgconfig diff --git a/.github/workflows/contract-release.yml b/.github/workflows/contract-release.yml index cca80486..344b0121 100644 --- a/.github/workflows/contract-release.yml +++ b/.github/workflows/contract-release.yml @@ -5,6 +5,7 @@ on: tags: - "v*" +# Retention policy: Keep successful runs for 90 days, failed/cancelled for 14 days permissions: # required permissions for the workflow id-token: write contents: write # in order to create releases diff --git a/.github/workflows/dapp-ipfs.yml b/.github/workflows/dapp-ipfs.yml index 5887cc7e..80b579de 100644 --- a/.github/workflows/dapp-ipfs.yml +++ b/.github/workflows/dapp-ipfs.yml @@ -9,6 +9,7 @@ on: workflow_dispatch: +# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true diff --git a/.github/workflows/secrets-check.yml b/.github/workflows/secrets-check.yml index 5e989082..447caecf 100644 --- a/.github/workflows/secrets-check.yml +++ b/.github/workflows/secrets-check.yml @@ -10,6 +10,7 @@ on: paths: - "k8s/**" +# Retention policy: Keep successful runs for 30 days, failed/cancelled for 7 days jobs: check-secrets-placeholders: name: Verify no real secrets in k8s manifests From 19160c7411172a1934f3f3a9c0034b7ffa838719 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:57:05 -0400 Subject: [PATCH 06/10] ci: drop invalid top-level retention-days from Actions workflows From 132fb60eca2715232e8e20ea4af59e7cd50c618d Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Fri, 25 Sep 2026 00:57:12 -0400 Subject: [PATCH 07/10] ci: drop invalid workflow-level retention-days keys From 644a5da867c700adf7e8044a8300360b83e1a48a Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sat, 26 Sep 2026 09:46:42 -0400 Subject: [PATCH 08/10] fix(staging): initialize seed schema before API startup --- docker-compose.staging.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 538b9860..7ce46c73 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,7 +1,9 @@ # Staging stack — production-like settings (auth + RLS on), isolated volumes/ports. # Usage: # cp backend/.env.staging.example backend/.env.staging -# docker compose -f docker-compose.staging.yml --env-file backend/.env.staging up --build +# docker compose --env-file backend/.env.staging -f docker-compose.staging.yml up --build +# A new staging volume initializes the seed schema before loading fixtures. +# PostgreSQL's image only runs init scripts on an empty volume. version: '3.8' services: @@ -17,12 +19,12 @@ services: - PORT=3001 - NODE_ENV=production - APP_ENV=staging - - DATABASE_URL=postgresql://${STAGING_DB_USER:-payd_staging}:${STAGING_DB_PASSWORD:-payd_staging_password}@postgres:5432/${STAGING_DB_NAME:-payd_staging} + - DATABASE_URL=postgresql://${STAGING_DB_USER:-payd_staging}:${STAGING_DB_PASSWORD:?Set STAGING_DB_PASSWORD in backend/.env.staging}@postgres:5432/${STAGING_DB_NAME:-payd_staging} - REDIS_URL=redis://redis:6379 - DB_HOST=postgres - DB_PORT=5432 - DB_USER=${STAGING_DB_USER:-payd_staging} - - DB_PASSWORD=${STAGING_DB_PASSWORD:-payd_staging_password} + - DB_PASSWORD=${STAGING_DB_PASSWORD:?Set STAGING_DB_PASSWORD in backend/.env.staging} - DB_NAME=${STAGING_DB_NAME:-payd_staging} - ENABLE_RLS=true - REQUIRE_AUTH=true @@ -43,15 +45,20 @@ services: image: postgres:15-alpine environment: - POSTGRES_USER=${STAGING_DB_USER:-payd_staging} - - POSTGRES_PASSWORD=${STAGING_DB_PASSWORD:-payd_staging_password} + - POSTGRES_PASSWORD=${STAGING_DB_PASSWORD:?Set STAGING_DB_PASSWORD in backend/.env.staging} - POSTGRES_DB=${STAGING_DB_NAME:-payd_staging} ports: - '5433:5432' volumes: - postgres_staging_data:/var/lib/postgresql/data - - ./backend/src/db/seed.sql:/docker-entrypoint-initdb.d/02_seed.sql:ro + # The seed references these tables; the RLS migration must precede seed data. + - ./backend/src/db/migrations/001_create_tables.sql:/docker-entrypoint-initdb.d/01_create_tables.sql:ro + - ./backend/src/db/migrations/003_multi_tenant_rls.sql:/docker-entrypoint-initdb.d/02_multi_tenant_rls.sql:ro + - ./backend/src/db/migrations/009_create_tax_tables.sql:/docker-entrypoint-initdb.d/03_create_tax_tables.sql:ro + - ./backend/src/db/seed.sql:/docker-entrypoint-initdb.d/04_seed.sql:ro + - ./backend/staging/mark-seed-ready.sh:/docker-entrypoint-initdb.d/05_mark_seed_ready.sh:ro healthcheck: - test: ['CMD-SHELL', 'pg_isready -U ${STAGING_DB_USER:-payd_staging} -d ${STAGING_DB_NAME:-payd_staging}'] + test: ['CMD-SHELL', 'test -f /var/lib/postgresql/data/.staging-seed-ready && pg_isready -U ${STAGING_DB_USER:-payd_staging} -d ${STAGING_DB_NAME:-payd_staging}'] interval: 10s timeout: 5s retries: 5 From 4b2d8f9cb0c68e451fe85f0137f38ad03d506cac Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sat, 26 Sep 2026 09:46:53 -0400 Subject: [PATCH 09/10] fix(staging): gate readiness after first-run seed --- backend/staging/mark-seed-ready.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 backend/staging/mark-seed-ready.sh diff --git a/backend/staging/mark-seed-ready.sh b/backend/staging/mark-seed-ready.sh new file mode 100644 index 00000000..bc04af35 --- /dev/null +++ b/backend/staging/mark-seed-ready.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# Postgres runs init files in name order on a fresh volume. The health check +# waits for this final marker so the API cannot race the schema and seed SQL. +set -eu +touch /var/lib/postgresql/data/.staging-seed-ready From c6bc64169f8c80bb2b938443e6d1de129e380857 Mon Sep 17 00:00:00 2001 From: woahwhattheheck Date: Sat, 26 Sep 2026 09:47:05 -0400 Subject: [PATCH 10/10] docs(staging): require distinct staging auth secrets and database password --- backend/.env.staging.example | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/.env.staging.example b/backend/.env.staging.example index b3490573..15c5dc3c 100644 --- a/backend/.env.staging.example +++ b/backend/.env.staging.example @@ -7,19 +7,21 @@ APP_ENV=staging # Auth / security (required in staging; do not use empty JWT) JWT_SECRET=replace-me-with-a-long-random-staging-secret +JWT_REFRESH_SECRET=replace-me-with-another-long-random-staging-secret REQUIRE_AUTH=true ENABLE_RLS=true -# Database (matches docker-compose.staging.yml defaults) +# Database (set a URL-safe random password before starting Compose; this role +# is used by both the API and the local Postgres service) STAGING_DB_USER=payd_staging -STAGING_DB_PASSWORD=payd_staging_password +STAGING_DB_PASSWORD=replace-with-a-url-safe-random-staging-password STAGING_DB_NAME=payd_staging DB_HOST=postgres DB_PORT=5432 DB_USER=payd_staging -DB_PASSWORD=payd_staging_password +DB_PASSWORD=replace-with-a-url-safe-random-staging-password DB_NAME=payd_staging -DATABASE_URL=postgresql://payd_staging:payd_staging_password@postgres:5432/payd_staging +DATABASE_URL=postgresql://payd_staging:replace-with-a-url-safe-random-staging-password@postgres:5432/payd_staging REDIS_URL=redis://redis:6379