Skip to content

POC Application Flow — End to End

Overview

Complete application flow for the REC Verifiable Credentialing Platform POC, from organization setup to credential claiming. This documents how every component connects — frontend, backend, Velocity CIH API, and the employee wallet experience.

Current Status (2026-04-15): All blockers resolved. CIH API integration verified working. Tenant already created on staging (ID: 69df3e9a1788f7af31e8edb1). Ready for backend implementation.


Actors

Actor Role in Platform What They Do
SuperAdmin (Platform Admin) PLATFORM_ADMIN Creates orgs, approves KYB, enables issuer
Staffing Company Admin (Org Admin) ORG_ADMIN Manages employees, issues credentials, monitors dashboard
Staffing Company User (Org User) ORG_USER / ISSUER Creates credentials, sends emails
Employee (Worker/Candidate) No login required Receives email, claims credential into wallet

Phase 1: Organization Setup (One-Time)

Step 1 — SuperAdmin Login

User: SuperAdmin
Frontend: /login → Auth0 OAuth 2.0 + MFA
Backend: POST /api/v1/auth/callback → JWT session
Role check: UserRole.PLATFORM_ADMIN
Redirect: /superadmin/dashboard

Step 2 — Create Staffing Company (Organization)

User: SuperAdmin
Frontend: MultiStepRegistrationForm.tsx
  Step 1: Org name, domain, website, country
  Step 2: Admin user (email, name, password)
  Step 3: Accept Velocity Network terms, review & submit

Backend: POST /api/v1/admin/organizations
  → Creates Organization (status: DRAFT)
  → Creates Admin User (role: ORG_ADMIN)
  → Creates Auth0 organization
  → Generates organization DID (or uses pre-registered DID)

Database:
  organizations: { status: DRAFT, kybStatus: PENDING }
  users: { role: ORG_ADMIN, organizationId: <org-id> }

Step 3 — KYB Approval

User: SuperAdmin
Frontend: /superadmin/organizations/[id] → KYB management

Flow:
  SuperAdmin reviews org documents
  → Approves KYB

Backend: PUT /api/v1/superadmin/organizations/{id}/kyb-status
  Body: { kybStatus: "approved" }

Database:
  organizations: { kybStatus: APPROVED, kybApprovedAt: now }

Step 4 — Create Tenant on Velocity Network ✅ VERIFIED

User: System (triggered by SuperAdmin)
Frontend: "Create Tenant" button on org detail page

Backend → Velocity CIH API:
  POST https://stagingcih.velocitycareerlabs.io/operator/tenants/create
  Auth: Authorization: Bearer 01692F50514EDA42AFE52C0F526B1B2BF383B8631AF77B6340A3631EAAD712BC
  Body: {
    "tenant": {
      "did": "did:web:stagingregistrar.velocitynetwork.foundation:d:curo-rec.test",
      "name": "Curo REC Test Organization",
      "logo": "https://stagingmedia.velocitynetwork.foundation/400x400-lwZVXpKa_jLkS2ClnS7Sm.jpeg",
      "caoDid": "did:web:stagingregistrar.velocitynetwork.foundation:d:ilssi.org"
    },
    "keys": [
      { "purposes": ["ISSUING_METADATA"], "algorithm": "SECP256K1", "encoding": "hex",
        "kidFragment": "#vc-signing-key-1", "key": "<from keys.json>" },
      { "purposes": ["DLT_TRANSACTIONS"], "algorithm": "SECP256K1", "encoding": "hex",
        "kidFragment": "#eth-account-key-1", "key": "<from keys.json>" },
      { "purposes": ["EXCHANGES"], "algorithm": "SECP256K1", "encoding": "hex",
        "kidFragment": "#exchange-key-1", "key": "<from keys.json>" }
    ]
  }

Response: {
  "tenant": {
    "id": "69df3e9a1788f7af31e8edb1",
    "createdAt": "2026-04-15T07:30:34.947Z",
    "did": "...",
    "primaryAccount": "0xC13c62b227c9Fde4Cd0f1B278a276fA411Fc2Add",
    ...
  },
  "keyMetadatas": [...],
  "requestId": "bfVTCLzAS_"
}

Database:
  organizations: {
    velocityRegistrarId: "69df3e9a1788f7af31e8edb1",
    tenantStatus: ACTIVE,
    tenantCreatedAt: "2026-04-15T07:30:34.947Z"
  }

⚠️ NOTE: Our Curo REC tenant is already created. Backend should:
  - Only call this endpoint for NEW staffing companies (not for Curo REC itself)
  - For Curo REC, load existing tenant ID from config/env

Step 5 — Enable Issuer Capabilities

User: SuperAdmin
Frontend: "Enable Issuer" toggle on org detail page

Backend: PUT /api/v1/superadmin/organizations/{id}/enable-issuer

Database:
  organizations: { status: ISSUER_ENABLED, issuerEnabledAt: now }

Result: Organization can now issue credentials.
All credential endpoints check: org.status === ISSUER_ENABLED

Phase 2: Credential Issuance (Core POC — Repeatable)

Step 6 — Staffing Company Admin Login

User: Org Admin
Frontend: /login → Auth0 with org context
Backend: JWT with organizationId claim
Guard: IssuerCapabilityGuard checks org.status === ISSUER_ENABLED
Redirect: /dashboard (org-scoped)

Step 7a — Manual Entry (Single Employee)

User: Org Admin / Org User
Frontend: /credentials/manual → ManualCredentialForm.tsx

Form fields:
  - Select existing employee OR create new
  - Job title, department
  - Start date, end date
  - Work location
  - Credential type: EmploymentPastV1.1
  - Preview before submit

Backend: POST /api/v1/issuer/credentials/manual
  → Creates/finds Employee record
  → Creates CredentialOffer (status: CREATED)

Database:
  employees: { firstName, lastName, email, jobTitle, startDate, endDate, ... }
  credential_offers: { credentialType: "EmploymentPastV1.1", status: CREATED, payload: {...} }

Step 7b — CSV Batch Upload (Multiple Employees)

User: Org Admin / Org User
Frontend: CSV upload component

CSV columns:
  employeeId, firstName, lastName, email, jobTitle, department,
  workLocation, startDate, endDate, supervisorName

Flow:
  1. Upload CSV file
  2. Preview & validate (show errors if any)
  3. Confirm import

Backend: POST /api/v1/issuer/employees/import
  → Validates each row
  → Creates Employee records
  → Returns: { imported: 150, errors: 2, skipped: 0 }

Database:
  employees: 150 new records created

Step 8 — Generate Credential Offers ✅ READY (CIH Batch API)

User: Org Admin (triggers batch or single)
Frontend: "Generate Offers" button on employee list or credential page

CIH makes this MUCH simpler than the old Agent Operator API.

ONE-TIME SETUP (per tenant):
  ┌─────────────────────────────────────────────────────────┐
  │ POST /operator/issuer-services/create                   │
  │   Body: {                                                │
  │     tenantId: "69df3e9a1788f7af31e8edb1",                │
  │     service: {                                           │
  │       velocityNetworkServiceId:                          │
  │         "#vlc-credential-agent-operator-v-1-1",          │
  │       authMethods: ["verifiable_presentation"],          │
  │       authMode: "internal",                              │
  │       termsUrl: "https://curo-rec.test/terms",           │
  │       disclosureRequest: {                               │
  │         types: [{ type: "EmailV1.1" }],                  │
  │         purpose: "Issuing employment credential",        │
  │         retentionPeriod: "P30D"                          │
  │       }                                                  │
  │     }                                                    │
  │   }                                                      │
  └─────────────────────────────────────────────────────────┘

PER-EMPLOYEE OR BATCH ISSUANCE:

  SINGLE:
  ┌─────────────────────────────────────────────────────────┐
  │ POST /operator/credentials/create                       │
  │   Body: {                                                │
  │     tenantId: "69df3e9a1788f7af31e8edb1",                │
  │     credential: {                                        │
  │       type: ["EmploymentPastV1.1"],                      │
  │       credentialSubject: {                               │
  │         vendorUserId: "employee@email.com",              │
  │         company: "<org-did>",                            │
  │         companyName: {                                   │
  │           localized: { en: "Acme Staffing Ltd" }         │
  │         },                                               │
  │         title: {                                         │
  │           localized: { en: "Software Engineer" }         │
  │         },                                               │
  │         startMonthYear: { month: 1, year: 2020 },        │
  │         endMonthYear: { month: 12, year: 2023 },         │
  │         location: {                                      │
  │           countryCode: "GB",                             │
  │           regionCode: "GB-LND"                           │
  │         }                                                │
  │       }                                                  │
  │     }                                                    │
  │   }                                                      │
  │   → Returns: { credential: { id, ... }, requestId }      │
  └─────────────────────────────────────────────────────────┘

  BATCH (preferred for CSV upload):
  ┌─────────────────────────────────────────────────────────┐
  │ POST /operator/credentials/create-many                  │
  │   Body: {                                                │
  │     tenantId: "69df3e9a1788f7af31e8edb1",                │
  │     credentials: [ {...}, {...}, {...} ]                 │
  │   }                                                      │
  │   → Returns: { credentials: [...], requestId }           │
  └─────────────────────────────────────────────────────────┘

GET CLAIM LINKS + QR CODES:
  ┌─────────────────────────────────────────────────────────┐
  │ POST /operator/issue-links/refresh                      │
  │   Body: { tenantId, credentialIds: [...] }               │
  │   → Returns: {                                           │
  │       issueLinks: [                                      │
  │         {                                                │
  │           credentialId: "...",                           │
  │           url: "https://...",  (HTTPS claim URL)         │
  │           qrCode: "..."        (deep link / base64 PNG)  │
  │         }                                                │
  │       ]                                                  │
  │     }                                                    │
  └─────────────────────────────────────────────────────────┘

Database:
  credential_offers: {
    velocityOfferId: <credential.id>,
    deepLinkUrl: <issueLink.url>,
    credentialManifestId: <tenantId>,
    payload: <credentialSubject>,
    status: CREATED → SENT (after email)
  }

Step 9 — Send Claim Emails

User: Org Admin clicks "Send Emails" (or auto-triggered after offers)
Frontend: Review email template → confirm send

Backend: EmailService processes each credential offer
  For each employee:
    1. Load org's email template (Handlebars)
    2. Generate secure claim token (JWT, 30-day expiry)
    3. Build email with:
       - Credential details (job title, company, dates)
       - Claim link: https://platform.com/claim/{token}
       - QR code image (from Step 8e)
       - Instructions for downloading wallet
    4. Send via SMTP
    5. Track delivery status

Database:
  email_logs: { status: QUEUED → SENT → DELIVERED, messageId, ... }
  credential_offers: { status: SENT, sentAt: now }

Phase 3: Claim & Tracking (Employee Side)

Step 10 — Employee Receives Email

User: Employee (no platform login needed)

Email contains:
  ┌─────────────────────────────────────────┐
  │  Hi John,                               │
  │                                         │
  │  Acme Staffing has issued a verified    │
  │  employment credential for your role    │
  │  as Software Engineer.                  │
  │                                         │
  │  [Claim Your Credential]  ← link       │
  │                                         │
  │  Or scan this QR code:                  │
  │  ┌───────┐                              │
  │  │ █▀▀█▀ │  ← QR code                  │
  │  │ █▄▄█▄ │                              │
  │  └───────┘                              │
  │                                         │
  │  This link expires in 30 days.          │
  └─────────────────────────────────────────┘

Step 11 — Claim Landing Page

User: Employee clicks link in email
Frontend: /claim/[token].tsx (PUBLIC page, no login required)

Flow:
  1. Verify claim token (JWT)
  2. If valid → show credential details:
     - Issuer: Acme Staffing Ltd
     - Role: Software Engineer
     - Period: Jan 2020 — Dec 2023
     - Status: Ready to claim
  3. Show claim options:
     - QR code (scan with Velocity wallet)
     - Deep link button (open wallet app directly)
     - Download wallet instructions

Backend: GET /api/v1/issuer/claim/{token}/status
  → Validates token
  → Returns credential details + claim status

Step 12 — Employee Claims Credential

User: Employee uses mobile wallet

Option A — QR Code:
  1. Employee opens Velocity wallet app on phone
  2. Scans QR code from claim page
  3. Wallet connects to Velocity Agent
  4. Employee reviews credential details
  5. Employee taps "Accept"
  6. Credential stored in wallet

Option B — Deep Link:
  1. Employee clicks deep link on mobile
  2. Opens: velocity-network-testnet://issue?request_uri=<url>
  3. Velocity wallet opens automatically
  4. Same accept flow as above

Result: Credential is now in employee's wallet
Velocity Agent records the claim event

Step 13 — Status Update

Backend: Polls credential status via CIH API

  GET /operator/credentials/get?tenantId=69df3e9a1788f7af31e8edb1
  Response: {
    "credentials": [
      {
        "id": "...",
        "tags": ["Feb2024Batch"],
        "status": "ISSUED" | "CLAIMED" | "REVOKED",
        "createdAt": "...",
        ...
      }
    ]
  }

Alternative: Webhook delivery if configured on tenant creation
  (tenant.webhookUrl + tenant.webHookAuth.bearerToken)

Database:
  credential_offers: { status: CLAIMED, claimedAt: now }

Frontend: Dashboard updates via polling

Ongoing: Monitoring & Management

Step 14 — Dashboard

User: Org Admin
Frontend: /credentials (org-scoped dashboard)

Shows:
  ┌─────────────────────────────────────────────────┐
  │  Credentials Overview                           │
  │                                                 │
  │  Total: 150  │  Sent: 120  │  Claimed: 85      │
  │  Pending: 30 │  Expired: 5 │  Revoked: 0       │
  │                                                 │
  │  ┌──────────────────────────────────────────┐   │
  │  │ Name          │ Status  │ Sent    │ Claim│   │
  │  │ John Smith    │ Claimed │ Apr 10  │ Apr 11│  │
  │  │ Jane Doe      │ Sent    │ Apr 10  │ —    │   │
  │  │ Bob Wilson    │ Pending │ —       │ —    │   │
  │  └──────────────────────────────────────────┘   │
  │                                                 │
  │  [Export CSV]  [Filter ▼]  [Search 🔍]          │
  └─────────────────────────────────────────────────┘

Backend: GET /api/v1/issuer/credentials?orgId=<id>&page=1&limit=20

Step 15 — Revoke (If Needed)

User: Org Admin
Frontend: Credential detail page → "Revoke" button

Backend:
  1. POST /operator/credentials/revoke
     Body: {
       tenantId: "69df3e9a1788f7af31e8edb1",
       credentialIds: ["..."],
       reason: "Employment record corrected"
     }
  2. Update DB: credential_offers.status = REVOKED

Result: Credential marked as revoked on Velocity Network
Employee's wallet shows credential as invalid

Data Flow Diagram

┌──────────┐     ┌──────────┐     ┌──────────────┐     ┌──────────┐
│ Frontend │────→│ Backend  │────→│ Velocity     │────→│ Employee │
│ (Next.js)│←────│ (NestJS) │←────│ Agent API    │     │ Wallet   │
└──────────┘     └─────┬────┘     └──────────────┘     └──────────┘
                       │
                 ┌─────┴────┐
                 │PostgreSQL │
                 │           │
                 │ organizations  │
                 │ employees      │
                 │ credential_offers │
                 │ email_logs     │
                 │ audit_logs     │
                 └───────────┘

Employee → Credential Subject Mapping

Employee Entity Field CredentialSubject Field Required
organization.organizationDid company Yes
organization.name companyName.localized.en Yes
jobTitle title.localized.en Yes
startDate (month, year) startMonthYear Yes
endDate (month, year) endMonthYear Yes (past employment)
workLocation → country location.countryCode Yes
workLocation → region location.regionCode No
department description.localized.en No

Current Implementation Status

Step Description Status
1 SuperAdmin login DONE
2 Create organization DONE
3 KYB approval DONE
4 Create tenant on Velocity ✅ API VERIFIED — needs backend code wiring
5 Enable issuer DONE
6 Org Admin login DONE
7a Manual credential entry DONE
7b CSV batch upload DONE
8 Generate credential offers ✅ API READY — needs backend code wiring
9 Send claim emails DONE (needs real credential data integration)
10 Employee receives email DONE
11 Claim landing page DONE (needs QR code — GAP-01)
12 Employee claims via wallet Ready once Step 8 wired up
13 Status update from CIH Ready once Step 8 wired up
14 Dashboard monitoring DONE (will show real data after Step 8)
15 Revoke credential Ready once Step 4 wired up

All 15 steps ready. Remaining work: wire up CIH API calls in backend services.


References