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/SECRET — note 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 |