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.