Documentation Index
Fetch the complete documentation index at: https://docs.wellapp.ai/llms.txt
Use this file to discover all available pages before exploring further.
A card represents a physical or virtual payment card (credit, debit, prepaid, corporate, or virtual) associated with a workspace. Each card record identifies its cardholder — either a Company or a People (mutually exclusive) — and carries card-identification attributes such as the last four digits, anonymized PAN, brand, type, and lifecycle dates. The entity is used to enrich payment-means data and surfaces in the financial graph as a composite via composite_cards_list on related roots (companies, people) and as a first-class cards records root.
| Naming | Value |
|---|
| Object | Card |
Resource type (JSON:API type) | card |
| Collection / records root | cards |
| REST base | /v1/cards |
| Entity class | Card |
API operations
| Operation | Method & path | Status |
|---|
| List | GET /v1/cards | ✅ Implemented |
| Retrieve | GET /v1/cards/{id} | ✅ Implemented |
| Create | POST /v1/cards | 🟡 Planned |
| Update | PATCH /v1/cards/{id} | 🟡 Planned |
| Delete | DELETE /v1/cards/{id} | 🟡 Planned |
Data model
Attributes
| Field | Type | Required | Constraints | Allowed values | Description |
|---|
| card_id | string, UUID, 🔒 system | ✅ Yes | unique; generated via gen_random_uuid() on insert | — | Public stable identifier for the card. Use this UUID in all API references; never expose the internal pk. |
| last_four_digits | string | ✅ Yes | length = 4; CHECK last_four_digits ~ ’^[0-9]{4}$‘ | Exactly 4 numeric digits | The last four digits of the card number. Required for all cards; used for display and matching against payment-means records. |
| anonymized_pan | string | ⚪ No | max length 30; nullable | — | Partially masked PAN string, e.g. ‘4539xXXXXXXXXXX4291’. Populated by connectors that provide it; absent when only the last four digits are known. |
| brand | enum (CardBrandEnum) | ⚪ No | nullable; native PostgreSQL enum ‘card_brand_enum’ | visa, mastercard, amex, discover, diners, jcb, unionpay | Card network / brand. Sourced from the connector’s card metadata. Null when the brand cannot be determined. |
| type | enum (CardTypeEnum) | ⚪ No | nullable; native PostgreSQL enum ‘card_type_enum’ | credit, debit, prepaid, corporate, virtual | Functional category of the card. Corporate and virtual cards commonly surface from expense-management connectors. |
| expiration_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date after which the card is no longer valid. Typically the last day of the expiry month. |
| start_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date from which the card becomes valid. Common on UK-issued cards that carry an explicit start date on the face. |
| issue_date | date | ⚪ No | nullable; stored as PostgreSQL DATE | — | The date the card was issued by the issuing institution. Distinct from start_date — a card may be issued before it becomes valid. |
| cardholder_name | string | ⚪ No | max length 100; nullable | — | The name embossed or printed on the card. May differ from the linked People.full_name when the card was issued in a trade name or role name. |
| created_at | datetime, 🔒 system | ✅ Yes | set once via onCreate lifecycle hook; never updated | — | Timestamp when the card record was created in the Well database. Auto-populated; not accepted from the API client. |
| updated_at | datetime, 🔒 system | ⚪ No | set via onCreate and onUpdate lifecycle hooks; nullable in schema | — | Timestamp of the last modification to this record. Auto-managed by MikroORM lifecycle hooks. |
| deleted_at | datetime | ⚪ No | nullable; soft-delete sentinel; all active-record queries filter deleted_at IS NULL | null (active) or a past ISO timestamp (soft-deleted) | Soft-delete timestamp. When set, the card is logically deleted. Indexes on (company_pk, deleted_at) and (workspace_pk, deleted_at) are defined to avoid scanning deleted tuples in Hasura traversal paths. |
Relationships
| Name | Type | Required | Description |
|---|
| company | to-one (company) | ⚪ No — mutually exclusive with people; one cardholder FK must be non-null for a meaningful record | The Company that owns or is the named cardholder of this card. FK: cards.company_pk → companies.pk. Indexed as (company_pk, deleted_at) to support Hasura traversal companies.cards without sequential scans. Set to NULL on company deletion (ON DELETE SET NULL). |
| people | to-one (people) | ⚪ No — mutually exclusive with company; at most one cardholder FK is set | The People (person) who is the named cardholder on this card. FK: cards.people_pk → peoples.pk. Indexed as (people_pk) to support Hasura traversal peoples.cards. Set to NULL on people deletion (ON DELETE SET NULL). |
| workspace | to-one (workspace) | ⚪ No (nullable FK, but all well-formed records carry a workspace) | The tenant workspace this card belongs to. FK: cards.workspace_pk → workspaces.pk. Indexed as (workspace_pk, deleted_at) for workspace-scoped queries and Hasura RLS filtering. Set to NULL on workspace deletion (ON DELETE SET NULL). |
System-computed
card_id is generated by PostgreSQL gen_random_uuid() on INSERT; the value is immutable after creation.
created_at is set once by the MikroORM @Property({ onCreate }) lifecycle hook at record creation and is never modified thereafter.
updated_at is set by the MikroORM @Property({ onCreate, onUpdate }) lifecycle hooks — populated on both create and every subsequent update.
deleted_at is null for all active records. Setting it to a timestamp soft-deletes the card; the row is retained in the database. Every active-record query must filter deleted_at IS NULL.
- Cardholder is polymorphic: exactly one of
company or people should be non-null for a meaningful card record. Neither FK carries a database-level mutual-exclusion constraint — the invariant is enforced at the service layer.
- Three composite indexes are maintained for hot-path Hasura traversals:
idx_cards_company_deleted (company_pk, deleted_at), idx_cards_people (people_pk), and idx_cards_workspace_deleted (workspace_pk, deleted_at). These were added in Migration20260416200000 after Query Insights revealed sequential scans on the companies.cards, peoples.cards, and workspace-scoped traversal paths.
- The
cards root is surfaced as a composite_cards_list array composite on the companies and people records roots (source_fields: card_id, brand, last_four_digits, type). The composite is defined in composites.yml under companies and people with display_type: relation_list and a sort_proxy of cards_aggregate.min.brand.
- Cards are also linked from payment_means via the
payment_means.card relationship (FK: payment_means.card_pk → cards.pk, ON DELETE SET NULL), added in Migration20260102111942. This underpins the composite_payment_means_summary composite renderer which reads payment_means.card.brand, payment_means.card.last_four_digits, and payment_means.card.type.
Example
{
"data": {
"type": "card",
"id": "c3a7e2f1-84bb-4f91-b9c3-1d2e5f6a7b89",
"attributes": {
"card_id": "c3a7e2f1-84bb-4f91-b9c3-1d2e5f6a7b89",
"last_four_digits": "4291",
"anonymized_pan": "4539xXXXXXXXXXX4291",
"brand": "visa",
"type": "corporate",
"expiration_date": "2027-09-30",
"start_date": "2023-10-01",
"issue_date": "2023-09-15",
"cardholder_name": "Alice Moreau",
"created_at": "2023-09-15T10:22:00.000Z",
"updated_at": "2024-03-01T08:14:35.000Z",
"deleted_at": null
},
"relationships": {
"company": {
"data": { "type": "company", "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
},
"people": {
"data": null
},
"workspace": {
"data": { "type": "workspace", "id": "f9e8d7c6-b5a4-3210-fedc-ba9876543210" }
}
}
}
}
Source: apps/api/src/database/entities/Card.ts · domain: financial-graph · tier: Supporting