DynamoDB Tables
This page documents the five primary DynamoDB tables used by the Payments Service: payments, debit cards, bank accounts, account history, and refunds.
Storage is split across two regions:
-
Primary region (
us-east-2in prod,us-west-2in test) — theprod-paymentssingle table holds payment records, v2 debit card records (asDEBITCARD#items), and v2 bank account records (asBANKINFO#items). The API Lambda’s bank account store (seecmd/api/main.gowiringbankaccountstore.NewDDBRepository(ddb, config.DynamoTable, …)—ddbis the primary-region client,DYNAMO_TABLEis the single table) reads and writes from this region only. -
Legacy region (
us-east-1) — the named legacy tablespinless-default-card,bank-accounts,bank-account-history, andusio-debit-refundslive here. They predate the single-table model; the debit-card and bank-account tables act as migration fallbacks, while the refunds table is still the canonical refund store.
The service also uses several additional tables not documented on this page — the fraud log (prod-float-service-log), Usio transaction staging (usio-debit-transactions, usio-credit-transactions, tokenization), jpm-transaction-details, and pinless-changes. See the Terraform data "aws_dynamodb_table" blocks in deploy/main.tf for the full inventory.
prod-payments
The primary payment record store. Every payment submitted through the service — pinless debit, ACH credit, ACH debit — produces one record here. ACH payments are written in ACHSENT status and updated in place as they clear or return.
Keys and Indexes
| Key / Index | Type | Description |
|---|---|---|
|
PK |
Unique payment identifier. Generated by the payment processor (JPM or Usio) on submission. |
|
GSI (PK: |
Retrieves all payments for a given user. |
|
GSI (PK: |
Retrieves payments with a specific status. Used by |
Attributes
| Attribute | Description |
|---|---|
|
Primary key. Unique payment reference from the processor. |
|
Owning user. |
|
|
|
|
|
|
|
|
|
Payment amount. |
|
Float ID if the payment was for a float; otherwise null. |
|
Subscription ID if the payment was for a subscription; otherwise null. |
|
Installment loan ID if applicable; otherwise null. |
|
Internal card reference. |
|
NACHA return code (e.g. |
|
Human-readable return description for JPM payments; |
|
Last 4 digits of the bank account. Present on ACH payments only. |
|
Last 4 digits of the debit card. Present on pinless payments only. |
|
Payment event classification used by |
|
Record type discriminator. |
|
When the payment was submitted. |
|
When the payment was returned. |
|
When the payment was cleared by the ACH network. |
|
For pinless: date of successful payment. For ACH: set when three business days pass without a return. |
Access Patterns
| Operation | Pattern |
|---|---|
Get payment by confirmation ID |
|
Get all payments for a user |
|
Get payments by status (e.g. for ACH polling) |
|
Notes
-
Pinless payments are written once and never updated.
-
ACH payments follow the lifecycle:
ACHSENT→COMPLETED(cleared) orFAILED(returned). Thereturn_dateandreturn_codefields are only populated on returned payments. -
DynamoDB Streams is enabled on this table. The
kinesis-feederLambda consumes every stream record but only forwards it to theprod-paymentsKinesis stream when the record’sevent_typefield is non-empty. Writes at submission time (with an emptyevent_type) are not published; only subsequent status-update writes that set anevent_typetrigger a Kinesis event. See Event Flows.
Debit Cards: Two-Table Architecture
Debit card storage is split across two tables as part of an ongoing migration from a legacy single-card-per-user model to a multi-card model.
New Table: prod-payments (single table, DEBITCARD records)
The primary debit card store for all new cards. Debit card records share the prod-payments single table with payment records and other entities, distinguished by their key prefix pattern. A user can have multiple debit cards — each card is a separate item. No raw card numbers or CVVs are ever stored.
Keys and Indexes
| Key / Index | Type | Description |
|---|---|---|
PK: |
Primary key (composite) |
One item per card. A user can have multiple |
|
GSI |
Used by |
|
GSI |
Used to look up a specific card by its FloatMe |
Attributes
| Attribute | Description |
|---|---|
|
Owning user. |
|
Unique FloatMe-assigned identifier for this card. |
|
Last 4 digits of the card number. Display only. |
|
Hash of the last 4 digits and expiration date. Used for fraud deduplication. |
|
Usio credit token. Used to submit pinless credit (disbursement) payments. |
|
Usio debit token. Used to submit pinless debit (collection) payments. |
|
Card expiration in |
|
RFC3339 timestamp when the card was added. |
|
Card network: |
|
Whether the card is currently valid for payments. Set to |
|
When the card was marked invalid. |
|
|
|
Which payment types this card is primary for: |
|
Bank account linked to this card, if any. |
Access Patterns
| Operation | Pattern |
|---|---|
Get all cards for a user (by status) |
|
Get specific card by debit_card_id |
|
Find cards expiring this month (for token refresh) |
|
Legacy Table: pinless-default-card
The original debit card store. Stores one record per user (user_id is the PK). This table is read-only for migrated users — it serves as a fallback for users whose card has not yet been written to the new table, and as a backup for older cards.
Migration Fallback Behaviour
When a card lookup on the new table returns no results, the repository automatically falls back to the legacy table:
-
GetByUserIDAndStatusfinds no records in the new table. -
legacyGetByUserIDfetches the single record frompinless-default-card. -
The legacy record is converted to a
DebitCardstruct (issuer = UNKNOWN, a new UUID is assigned asdebit_card_id). -
The converted card is transparently written back to the new table so subsequent lookups no longer need the legacy table.
When a new card is saved and its primary_card_type = ALL, it is also written to the legacy table (overwriting the existing record) so that any callers still reading from the old table remain consistent.
Keys and Indexes
| Key / Index | Type | Description |
|---|---|---|
|
PK |
One record per user. |
|
GSI |
Used to find cards expiring soon for token refresh. Queried alongside the new table’s GSI1 — results are de-duplicated by token reference IDs. |
|
GSI |
Used by fraud checks to detect when multiple users share the same physical card. |
Attributes
| Attribute | Description |
|---|---|
|
Primary key. Owning user. |
|
Last 4 digits of the card number. Display only. |
|
Hash of the last 4 digits and expiration date. Used for fraud deduplication. |
|
Usio credit token. |
|
Usio debit token. |
|
Always |
|
Always |
|
Card expiration date in |
|
When the card was added. |
|
Whether the card is currently valid for payments. |
|
When the card was marked invalid. |
Notes
-
Token refresh (
usio-refresh-scheduler+usio-refresh-worker) queries both tables for expiring cards and de-duplicates results by credit + debit reference ID pair before enqueuing refresh jobs. -
The
is_validflag gates pinless collection attempts. Invalid cards cause the collections engine to fall back to ACH. -
The legacy table will be retired once all users have been migrated to the new table.
bank-accounts and bank-account-history
Stores KMS-encrypted bank account and routing numbers sourced from Plaid. The history table is an append-only log of all changes to a user’s bank accounts.
Keys and Indexes
| Key / Index | Type | Description |
|---|---|---|
|
PK (composite) |
A user can have multiple bank accounts; each is identified by its Plaid |
|
GSI |
Used by fraud checks to detect when multiple users share the same bank account. |
Attributes
| Attribute | Description |
|---|---|
|
Owning user. |
|
Plaid account ID. Used as the sort key to distinguish multiple accounts per user. |
|
KMS-encrypted bank account number. Decrypted at ACH payment submission time. |
|
KMS-encrypted routing number. Decrypted at ACH payment submission time. |
|
Hash of the account and routing number combination. Stored in plain text for fraud deduplication queries. |
|
When the bank account record was created or last updated. |
Access Patterns
| Operation | Pattern |
|---|---|
Get bank account for a user and Plaid account |
|
Fraud check — find users with same account |
|
Notes
-
Account and routing numbers are encrypted with an AWS KMS key before storage and decrypted only within the Lambda execution environment at payment submission time.
-
The
account_routing_hashis computed before encryption and stored separately so fraud checks can operate without decryption. -
When a user updates their bank account, a
blocklist-removeSQS event is published to trigger theblocklist-removerLambda.
usio-debit-refunds
Stores refund records for Usio transactions. Refunds can be tied to a specific payment record or submitted as an arbitrary amount not linked to any payment.
Keys and Indexes
| Key / Index | Type | Description |
|---|---|---|
|
PK |
Usio confirmation ID for the refund transaction. |
|
GSI |
Retrieves all refunds for a given user. |
Attributes
| Attribute | Description |
|---|---|
|
Primary key. Usio confirmation ID of the refund transaction. |
|
User receiving the refund. |
|
Refund amount. |
|
|
|
Payment record ID this refund is for, if applicable. |
|
Float ID, if the original payment was for a float. |
|
Subscription ID, if the original payment was for a subscription. |
|
If no payment record exists but a Usio transaction was found, this is its confirmation ID. |
|
Reason for the refund as entered by the MX team. |
|
Name of the support agent who submitted the refund. |
|
Email of the support agent who submitted the refund. |
|
IP address of the support agent at submission time. |
|
When the refund was submitted. |
Related Pages
-
Architecture — DynamoDB overview in the system context
-
Payment Flow — How payment records are written and updated during the payment lifecycle
-
Payment Syncing — How ACH status updates are written to the payments table; token refresh flow for legacy and new debit card tables
-
Blocklist — How return codes trigger blocklisting via Kinesis events
-
Fraud Detection — How the
hashed_card_number/card_hashandaccount_routing_hashindexes are used for fraud checks