DynamoDB tables

Four tables. Names come from settings and are environment-suffixed outside release (see Settings & secrets reference); the clients that wrap them are described in AWS clients.

Overview

Table (setting) Client PK Sort key Role

SUPPLIER_DATA_VALIDATION_TASK_TABLE_NAME

components/task_execution_table.py

task_id

— (GSI supplier_key-process_name-index)

Async task lifecycle and audit log

FINAL_STATUS_TABLE_NAME

components/aws/dynamodb/final_status_table.py

correlation_id

created_at

Underwriting verdicts per execution

TOOL_TABLE_NAME

components/aws/dynamodb/tool/tool_table.py

supplier_key

type

Per-supplier/person tool state

CHANGELOG_TABLE_NAME

components/aws/dynamodb/changelog_table.py

supplier_key

log_ts

Append-only audit of verdict and tool changes

task_id and correlation_id are the same value for a given execution.

One correlation_id is created per process execution and is used as:

  • the value returned to the caller by the request step,

  • the task_id primary key in TaskExecutionTable,

  • the partition key of FinalStatusTable and the correlation_id attribute on ToolTable,

  • the task_correlation_id embedded in the SWF workflow input, so the handle step can reload the request.

For Entrust runs the correlation_id is the Onfido workflow_run_id, which is what makes a redelivered webhook idempotent. Never mint a second correlation id mid-flow.

Task Execution Table

Tracks each execution from creation to completion. Written pending by request tasks (or the Entrust session endpoint) and updated by handle tasks and the purge flow.

Key: PK task_id. GSI: supplier_key-process_name-index — every list query in the codebase goes through it and auto-paginates on LastEvaluatedKey.

item = {
    'task_id': task_id,
    'supplier_key': supplier_key,
    'input': input_,
    'output': output,
    'status': self.PENDING_STATUS,
    'process_name': process_name,
    'create_ts': datetime.utcnow().isoformat(),
    'additional_data': additional_data,
    'requested_by': requested_by,
}
if source:
    item.update(source=source.value)

update(task_id, item_data) always stamps update_ts and rejects any status outside TASK_STATUSES with RuntimeError('Status not allowed'). Typical updates add output, additional_data, default_result_statuses and the S3 result path.

Statuses: pending → processed | failed | purged.

Queries:

Method Behaviour

get(task_id)

Direct read; raises KeyError('Item') when absent.

get_by_supplier_key_and_process_name

GSI query, auto-paginated.

get_latest_processed_task(supplier_key, process_name, person_id)

Newest processed or failed row for that person (by create_ts) — powers GET /tasks/result.

get_latest_task(…​)

Newest non-purged row for that person.

Au10tix rows

input = {supplier_key, person_id, document_id}, output = {raw_result_s3_path}, additional_data = {full_result_s3_path}.

Entrust rows

Entrust reuses the validate_identity_document process name (keeping the GSI, dedup and admin reporting intact) but stores an Entrust-shaped row:

{
  "task_id": "b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d",
  "supplier_key": "9b1c4d3e-6f2a-4b8c-9d0e-1f2a3b4c5d6e",
  "process_name": "validate_identity_document",
  "status": "processed",
  "source": "onboarding",
  "requested_by": "owner@example.com",
  "create_ts": "2026-08-12T08:52:44.918002",
  "update_ts": "2026-08-12T09:15:12.114900",
  "input": {
    "supplier_key": "9b1c4d3e-6f2a-4b8c-9d0e-1f2a3b4c5d6e",
    "person_id": "44f2a1b0-7c3d-4e5f-8a9b-0c1d2e3f4a5b",
    "applicant_id": "c3d4e5f6-a7b8-49c0-8d1e-2f3a4b5c6d7e",
    "workflow_run_id": "b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d"
  },
  "output": {
    "raw_result_s3_path": "entrust/identity_document_validation/supplier_key=9b1c.../person_id=44f2.../2026-08-12T09:15:11.204812.json",
    "workflow_run_id": "b1f7d0c2-3f4e-4a1b-9c8d-2e5f6a7b8c9d",
    "applicant_id": "c3d4e5f6-a7b8-49c0-8d1e-2f3a4b5c6d7e",
    "workflow_id": "41aa2ce5-7c0e-4dbe-91c3-12e058a2d1cf",
    "status": "approved",
    "dashboard_url": "https://dashboard.onfido.com/results/...",
    "created_at": "2026-08-12T08:52:45Z",
    "updated_at": "2026-08-12T09:14:03Z",
    "document_actions": {
      "existing_documents_archived": 2,
      "id_files_uploaded": 3,
      "liveness_files_uploaded": 1,
      "uploaded_total": 4,
      "approved": 4
    }
  },
  "additional_data": {
    "hosted_link": "https://alt.onfido.app/l/eyJhbGciOi...",
    "link_expires_at": "2026-08-19T09:14:03Z",
    "sdk_token": "eyJhbGciOiJFUzUxMiJ9...",
    "documents_uploaded": true,
    "uploaded_document_ids": ["7a1b", "7a1c", "7a1d", "7a1e"],
    "documents_approved": true,
    "document_actions": {
      "existing_documents_archived": 2,
      "id_files_uploaded": 3,
      "liveness_files_uploaded": 1,
      "uploaded_total": 4,
      "approved": 4
    },
    "full_result_s3_path": "entrust/identity_document_validation/supplier_key=9b1c.../person_id=44f2.../2026-08-12T09:15:11.204812.json"
  }
}
Attribute Notes

task_id

The Onfido workflow_run_id.

input

{supplier_key, person_id, applicant_id, workflow_run_id}. The presence of applicant_id / workflow_run_id is what distinguishes Entrust rows from Au10tix rows (which carry document_id) during coexistence.

additional_data.hosted_link, .link_expires_at, .sdk_token

Written at session creation; drive the dedup/reuse check.

additional_data.documents_uploaded

true once stale captures were archived and the fresh artifacts uploaded — guards against duplicate uploads on webhook redelivery.

additional_data.uploaded_document_ids

The ids the uploads returned, so the approved path can approve exactly that set.

additional_data.documents_approved

true once those ids were promoted to approved (approved verdict only).

additional_data.document_actions

{existing_documents_archived, id_files_uploaded, liveness_files_uploaded, uploaded_total, approved}; also copied onto output.

additional_data.full_result_s3_path

Set to the archived run JSON on the terminal update.

output

On the terminal update: {raw_result_s3_path, workflow_run_id, applicant_id, workflow_id, workflow_version_id, status, dashboard_url, output, reasons, error, created_at, updated_at, document_actions} — None values are omitted.

A review verdict leaves the row pending (only guards and document_actions are persisted) until a later completion; a task-level failure sets failed.

Final Status Table

The underwriting verdict for one execution. Every write also appends a changelog row. Multiple versions per correlation_id are kept, sorted by created_at.

Key: PK correlation_id, sort created_at.

item = {
    'correlation_id': correlation_id,
    'data': data,
    'updated_by': created_by,
    'updated_at': current_timestamp,
    'created_by': created_by,
    'created_at': current_timestamp
}
Process data contents

validate_identity_document

DocumentAuthenticity, IsDocumentExpired, IDNameMatch, OverallQuality — each VERIFIED / NOT_VERIFIED

validate_personal_information

one key per PII field — MATCH / NO_MATCH

download_personal_background_report

{access: {<Section>: PASS|FAIL|ERROR}, advance: {…}}

download_business_background_report

not written

Method Behaviour

save

put_item a new version; writes an INIT changelog row.

update

Reload the first version, replace data, stamp updated_by/updated_at, put_item, write an UPDATE changelog row.

get_by_correlation_id

The first (oldest) version; raises NotFoundFinalStatus.

get_latest_by_correlation_id

The last (newest) version.

created_by is automatic-uw-tool for automated verdicts, override for the first manually-created row, and the underwriter’s login_id for updated_by on overrides.

Tool Table

Per-supplier, per-person status of each verification tool. Powers GET /tools.

Key: PK supplier_key, sort type (the ToolType name).

item = {
    'supplier_key': supplier_key,
    'person_id': person_id,
    'type': tool_type.name,
    'status': ToolStatus.NOT_RUN.value,
    'correlation_id': correlation_id,
    'updated_by': created_by,
    'updated_at': current_timestamp,
    'created_by': created_by,
    'created_at': current_timestamp
}

update() sets status, error_message, correlation_id, updated_by/updated_at and falls back to save() when the row does not exist (upsert). SUPPORTED_TOOLS — AU10TIX, ENTRUST, THOMSON_REUTERS_CLEAR_ID, THOMSON_REUTERS_PERSONAL_BACKGROUND_REPORT — is the set get_tools_and_create_missing / create_missing_tools auto-create for a supplier/person.

Because the sort key is type alone, a supplier with several owners has one row per (supplier_key, type) — person_id is a plain attribute. Lookups therefore use scan with filters on supplier_key + person_id + type rather than a key query. This is a known hot spot; see Python standards.

Status vocabulary and display names: api/tools.adoc#tool-vocabulary.

Changelog Table

Append-only audit trail of every mutation to final-status and tool rows, written automatically by FinalStatusTable and ToolTable.

Key: PK supplier_key, sort log_ts.

self.__table.put_item(Item={
    'supplier_key': supplier_key,
    'type': changelog_type.value,      # FINAL_STATUS | TOOL
    'action': changelog_action.value,   # INIT | UPDATE
    'data': data,                       # full snapshot of the row that changed
    'log_ts': datetime.utcnow().isoformat(),
    'app_version': version.__version__
})

app_version comes from version.py, tying every change to a code version.

Relationships

erDiagram
    TASK_EXECUTION ||--o| FINAL_STATUS : "correlation_id (= task_id)"
    TASK_EXECUTION ||--o| S3_OBJECT : "result path"
    TOOL }o--|| SUPPLIER : "supplier_key + person_id"
    FINAL_STATUS ||--o{ CHANGELOG : "audited"
    TOOL ||--o{ CHANGELOG : "audited"