Skip to content

Task 31: Fresh AWS Environment — Go-Live Checklist

Status: ⬜ PENDING Context: Task 30 (Velocity per-org tenant isolation) is code-complete and verified end-to-end against two real, isolated tenants on Velocity staging (issue + revoke, confirmed directly against CIH's own API, not just the local DB). Before moving to a fresh AWS environment, everything below needs to be true there too — several of these are things that only broke once a second real organization existed, so a fresh environment is exactly where they'd resurface if skipped.


0. Code & secrets — before you touch AWS at all

# Item Why it matters
0.1 Commit and push every file changed today (see list below) All of Task 30's fixes are still uncommitted on docs-restructured locally. A fresh AWS deploy pulls from git — if this isn't pushed, the new environment hits the exact same bugs we just spent this session fixing.
0.2 Decide: rotate-only vs. rotate + scrub git history for the real staging Bearer token + 3 private keys committed in 13-verified-integration-log.md (Task 30 §4, still open) Moving environments is a natural moment to rotate rather than carry old exposed credentials into the new setup.
0.3 Generate fresh TENANT_KEYS_ENCRYPTION_KEY and COLUMN_ENCRYPTION_KEY for AWS (openssl rand -hex 32 each) Don't reuse the local dev values — these encrypt every org's Velocity keys and other sensitive columns at rest.
0.4 Decide: fresh Postgres data vs. migrate local dev data Local DB currently has test-data debris (a duplicate stuck-PENDING "Curo REC Test Organization" row, POC test orgs). Recommend a clean DB on AWS rather than carrying that over.

Files changed today (git status):

app/backend/.env.example
app/backend/src/admin/config/velocity-staging.config.ts
app/backend/src/admin/dto/velocity-tenant.dto.ts
app/backend/src/admin/services/velocity-registrar.service.ts
app/backend/src/admin/services/velocity-tenant.service.ts
app/backend/src/issuer/services/claim-sync.service.ts
app/backend/src/issuer/services/claim.service.ts
app/backend/src/issuer/services/credential.service.ts
app/backend/src/issuer/services/manual-credential.service.ts
app/backend/src/issuer/services/notification.service.ts
app/backend/src/shared/services/cih/cih-api.types.ts
app/backend/src/shared/services/vnf-sdk.service.ts
docs/requirements/tasks/POC/30-velocity-tenant-isolation-action-plan.md   (new)
docs/requirements/tasks/POC/31-aws-migration-go-live-checklist.md          (new, this file)


1. AWS infrastructure prerequisites

# Item Notes
1.1 RDS Postgres instance provisioned Match the local schema — run migrations (§3), don't hand-copy the local DB.
1.2 Backend compute target decided Per M4-01, this is still an open decision in the repo's own docs (ECS/EB/other) — resolve before deploying.
1.3 Frontend hosting decided Same open-decision status as 1.2.
1.4 S3 bucket for documents provisioned (AWS_S3_BUCKET)
1.5 Secrets stored in Secrets Manager / Parameter Store, not baked into task definitions or images Especially the Velocity bearer token, keys.json, DB credentials, JWT/encryption keys.
1.6 Outbound HTTPS allowed to: Velocity CIH (stagingcih.velocitycareerlabs.io), Velocity Registrar (stagingregistrar.velocitynetwork.foundation), Auth0 tenant, SMTP host No inbound public webhook endpoint is needed anymore — the Task 26 webhook approach was removed today (CIH has no webhook support at all); claim status syncs via the existing 5-minute poll instead. One less networking requirement than the old design assumed.

2. Environment variables

Legend: Fresh = must be a new value for AWS, don't reuse local dev's. Reuse = same value is fine to carry over (shared account/service). Dead = present in .env.example for back-compat/history only, not read by any code path anymore — don't waste time sourcing these.

Variable Fresh/Reuse/Dead Notes
NODE_ENV Fresh (production)
DB_HOST / DB_PORT / DB_USERNAME / DB_PASSWORD / DB_DATABASE Fresh Point at the new RDS instance.
DB_SYNCHRONIZE Fresh (false) .env.example ships true, which is dev-only — auto schema sync must be off in production; migrations (§3) own schema changes instead.
DB_SSL Fresh (true if RDS enforces it)
AUTH0_DOMAIN / AUTH0_AUDIENCE / AUTH0_CLIENT_ID / AUTH0_CLIENT_SECRET Decide (see §4.1) Reuse the existing dev Auth0 tenant, or a fresh prod tenant — pick deliberately, don't default.
AUTH0_CALLBACK_URL Fresh Must point at the new frontend's real URL.
AUTH0_MGMT_CLIENT_ID / AUTH0_MGMT_CLIENT_SECRET Decide with the above Machine-to-machine app used to auto-provision an Auth0 Organization per company at onboarding time — confirm this M2M app exists and has the right grants in whichever Auth0 tenant you land on.
JWT_SECRET Fresh
SMTP_* Decide Reuse the existing relay, or provision one for AWS (e.g. SES).
VNF_ENVIRONMENT Decide explicitly Currently staging. Confirm whether AWS is still targeting Velocity staging or promoting to Velocity production — don't assume.
VNF_CIH_API_URL / VNF_CIH_API_PATH_PREFIX / VNF_CIH_BEARER_TOKEN Fresh if rotating (§0.2), else Reuse This is the single CAO bearer token — legitimately shared across all tenants by design, not per-org.
VNF_ORGANIZATION_DID / VNF_TENANT_ID Reuse (bootstrap org only) Not used for issuance/revocation/claim-sync as of today's fix — only seeds the keys.json fallback for the platform's own org and the raw SDK test controller. Don't confuse with per-org routing.
VNF_CAO_DID Reuse Genuinely global — one CAO serves every tenant.
VNF_SERVICE_ID / VNF_ORG_NAME Reuse
VNF_ORG_LOGO Reuse, but understand its real scope As of today, only used as a last-resort fallback if a per-org verified-profile lookup fails — every real organization's tenant now sources its own logo from its Velocity Registrar profile instead (VelocityRegistrarService.getVerifiedProfile).
VNF_KEYS_FILE_PATH Fresh path, Fresh keys.json if rotating Points at the platform's own bootstrap keys.json (gitignored) — not any client org's keys, those live encrypted in the DB per-org.
VNF_REGISTRAR_BASE_URL / VNF_REGISTRAR_API_PATH Reuse
VNF_REGISTRAR_JWT Fresh — and expect to refresh it periodically Per its own .env.example comment, this is obtained from a Velocity Registrar dashboard session — it is not a long-lived service credential and will need manual renewal. Worth flagging to whoever owns Velocity ops for the new environment.
VELOCITY_REGISTRAR_TIMEOUT / _RETRY_ATTEMPTS / _RETRY_DELAY Reuse
COLUMN_ENCRYPTION_KEY / TENANT_KEYS_ENCRYPTION_KEY Fresh See §0.3.
SESSION_RETENTION_DAYS / AUDIT_LOG_RETENTION_DAYS / EMAIL_LOG_RETENTION_DAYS Reuse or set per compliance requirements
PUBLIC_API_URL Dead Removed today — was only ever used to register a claim webhook CIH doesn't support. Do not re-add.
VNF_WEBHOOK_BEARER_TOKEN Dead Same removal.
VNF_ISSUER_DID Dead as a primary source Still read as a last-resort fallback in two legacy code paths (candidate claim emails / wallet manifests) if an offer somehow has no organization link at all — should never actually trigger in practice. Not documented in .env.example; don't add it back as a "real" config item.
REMINDER_ENABLED / REMINDER_AFTER_DAYS / REMINDER_INTERVAL_DAYS / REMINDER_MAX_COUNT Reuse
LOG_LEVEL Fresh (info or warn for prod, not debug) Local dev logs are extremely verbose (full JWT payloads on every request) — don't ship that verbosity to production logs.
AWS_REGION / AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_S3_BUCKET Fresh Prefer an IAM role over long-lived access keys if the compute target supports it (ECS task role, etc.) rather than putting keys in env at all.

3. Database

# Item
3.1 Run all 6 migrations against the fresh RDS instance: UpdateAdminRoles, AddOrganizationStatusFields, AddSoftDeleteToOrganizations, AddSoftDeleteToKybRecords, AddAuth0OrganizationFields, AddVelocityTenantFields (npm run migration:run).
3.2 Confirm DB_SYNCHRONIZE=false before first boot — TypeORM sync against a fresh empty DB can create a schema that silently diverges from what the migrations would have produced.
3.3 Confirm the new DB is genuinely empty of the local dev test-data debris noted in §0.4 before treating it as "clean."

4. Auth0 setup + first admin user

Superseded (2026-08-14): the admin@curo.com hardcoded bypass, and reliance on manually editing initialize_admin.ts, are gone — see the auth security pass below. First-admin bootstrap is now automatic. This section reflects the current design.

# Item Notes
4.1 Decide: same Auth0 tenant as local dev, or a fresh one for AWS The local dev tenant (dev-hw8ecuci62ss2qp2.us.auth0.com) looks like a shared dev tenant — deliberately choose whether AWS reuses it or gets its own.
4.2 Update the Auth0 application's Allowed Callback URLs (and logout/web origins) to the new frontend URL Matches AUTH0_CALLBACK_URL above.
4.3 Confirm the Auth0 Application's Organizations → Login Flow is set to "Prompt for organization" /signin (org login) relies on Auth0's own hosted org-prompt screen; /signin/admin relies on the same setting being bypassable by pre-supplying an organization param — both routes share one Auth0 Application by design (confirmed working 2026-08-14), not two separate ones.
4.4 Set INITIAL_ADMIN_EMAIL (+ optionally _FIRST_NAME/_LAST_NAME) in the backend env AdminBootstrapService runs automatically on first boot against an empty DB: creates the user in Auth0 via the Management API, emails them a password-setup link, and seeds the local PLATFORM_ADMIN row. No manual script-editing needed anymore. No-ops safely if a Platform Admin already exists.
4.5 Confirm the Auth0 Management API M2M app (AUTH0_MANAGEMENT_CLIENT_ID/SECRETnote the full "MANAGEMENT" spelling, a shorter AUTH0_MGMT_CLIENT_ID name was a separate, inconsistent credential and has been retired) can create both users and Auth0 Organizations Needs create:users (for 4.4) and create:organizations (for per-company onboarding) scopes in whichever tenant was chosen in 4.1 — don't assume scopes carry over to a fresh tenant.
4.6 Set NEXT_PUBLIC_PLATFORM_ORG_NAME (frontend env, defaults to "Curo") to the platform operator's own Auth0 Organization name And make sure a matching row actually exists in the local organizations table with auth0_org_id set to that real Auth0 Organization id — this was found missing entirely in local dev on 2026-08-14 (the real Auth0 org curo / org_NY1G4Wk6bt7Bdseg existed in Auth0 but had no corresponding local row, so /signin/admin's lookup silently failed and admins would have seen Auth0's org-prompt screen too). Confirm this row exists on any new environment before relying on the admin sign-in flow.
4.7 Confirm INTERNAL_SYNC_SECRET is set to the same real random value in both the frontend and backend env POST /auth/sync now rejects every request without it (this used to be silently unchecked — see the auth security pass below). Generate a fresh value per environment; don't reuse the local dev value (dev-internal-sync-secret-key) anywhere real.

5. Velocity Network setup — informed directly by what broke today

# Item Why
5.1 Re-fetch the live CIH OpenAPI spec (GET {VNF_CIH_API_URL}/documentation/json) and diff its tenants/create schema against what the code sends, before relying on it in the new environment We proved today that this schema drifts (caoDid was removed, webhook fields never existed) between the cached spec the codebase shipped with and what's actually live, months apart. Don't assume today's fix stays valid indefinitely — make this a standing pre-flight check, not a one-time fix.
5.2 Confirm the bootstrap org's keys.json is available at VNF_KEYS_FILE_PATH in the new environment (freshly rotated if §0.2 happened)
5.3 Confirm VNF_ENVIRONMENT/CIH URL actually point at the intended Velocity environment (staging vs. production) for AWS See §2 — don't let this default silently.
5.4 Confirm VNF_REGISTRAR_JWT is current, not an expired dashboard session token carried over from local dev See §2 — this is a session credential, not a service credential.

6. End-to-end org + credential smoke test (repeat exactly what we just verified locally)

# Step What "pass" looks like
6.1 Register a test organization on the Velocity Registrar dashboard, get its did:web:... + keys.json
6.2 Create the org in the platform (Step 1: DID + keys.json upload), approve KYB, click Enable Issuer Org row gets a velocity_registrar_id distinct from the platform's own bootstrap tenant, tenant_status = ACTIVE.
6.3 Issue one manual credential from that org GET /operator/credentials/get?tenantId=<this org's tenant> on CIH shows the credential; the same query against any other tenant does not.
6.4 Revoke it Confirm the revoke hit the same tenant, not the bootstrap org's.
6.5 Repeat 6.1–6.4 with a second test organization Proves isolation holds with more than one real org in the new environment too — don't just trust that the code carried over correctly; re-verify, since 5.1 shows the ground truth can shift under you.

7. Final go/no-go

# Check
7.1 npx tsc --noEmit clean
7.2 npm run lint — no new errors vs. the pre-existing baseline
7.3 No secrets committed to git in the new state of the repo (re-check 13-verified-integration-log.md per §0.2)
7.4 All §2 env vars sourced from Secrets Manager/Parameter Store in the deployed environment, none hardcoded in images or task definitions
7.5 §6 smoke test passed with two distinct real organizations