BloodConnect Database
BloodConnect stores its operational data in a single Amazon DynamoDB table. Every entity — users, donor locations, blood requests, donor-search orchestration state, donation records and notifications — lives in this one table and is distinguished by prefixed partition and sort keys (single-table design).
This document describes the as-built schema: the key shapes, access patterns and indexes that the application code actually reads and writes.
Physical table configuration
Source of truth: iac/terraform/aws/dynamodb/dynamodb.tf.
Setting |
Value |
|---|---|
Billing mode |
|
Primary key |
|
Streams |
Enabled, |
Local secondary index |
|
Global secondary index |
|
The five indexed attributes (PK, SK, LSI1SK, GSI1PK, GSI1SK)
are all strings. LSI1 and GSI1 are overloaded: their meaning depends
on which entity wrote the item, so a single index serves multiple access
patterns.
Design conventions
Single-table design. All entities share one table; the key prefix (
USER#,BLOOD_REQ#,DONOR_SEARCH#,DONATION#,NOTIFICATION#) identifies the entity type.Key overloading.
GSI1PK/GSI1SKandLSI1SKcarry different composite values per entity. Items only join an index when they populate the relevant key attributes; otherwise they are invisible to that index.``#`` delimiter. Composite keys join segments with
#(which sorts before alphanumerics, keeping prefixed items grouped).ISO 8601 timestamps.
createdAt/updatedAtare stored as sortable UTC strings (YYYY-MM-DDTHH:MM:SSZ) so they work as sort-key segments.DTO-backed attributes. Item attributes are the fields of the matching DTO; the model’s
fromDto/toDtoonly rewrites the key attributes. Attribute lists below reference the DTO rather than duplicating every field, so the doc does not drift as DTOs evolve.
Entity catalogue
User profile
Source: core/services/aws/commons/ddbModels/UserModel.ts · attributes:
UserDetailsDTO (commons/dto/UserDTO.ts).
Key |
Value |
|---|---|
|
|
|
|
Access patterns
Get / update a user by id — primary key lookup on
PK = USER#<userId>,SK = PROFILE.
The user profile item is not projected onto any secondary index.
Donor location (donor-search index)
Source: core/services/aws/commons/ddbModels/LocationModel.ts · attributes:
LocationDTO (commons/dto/UserDTO.ts).
A user may have multiple preferred locations; each is a separate item under the same user partition. This item carries the geospatial donor-search index.
Key |
Value |
|---|---|
|
|
|
|
|
|
|
|
Access patterns
List a user’s locations — query
PK = USER#<userId>,SK begins_with LOCATION#.Find available donors near a point, by blood group — query
GSI1withGSI1PK = LOC#<countryCode>#<bloodGroup>#AVAIL#<h3Res8 cell>(H3SearchDynamoDbOperations.queryDonorsInHex). The donor-search workflow resolves the request location to its H3 resolution-8 cell, queries that cell, then walks outward rings of neighbouring res-8 cells for wider waves.
h3Res8 is the partition cell; h3Res10 and latitude/longitude are
stored on the row for fine distance refinement. The search read path also
projects displayName and phoneE164.
Note
displayName and phoneE164 are read by the search path but are not
currently written by LocationService.updateUserLocation — a known gap;
donor name/phone are sourced elsewhere until this is resolved.
Blood request post
Source: core/services/aws/commons/ddbModels/BloodDonationModel.ts ·
attributes: DonationDTO (commons/dto/DonationDTO.ts).
Key |
Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
Access patterns
Get one request — exact primary-key lookup on
PK = BLOOD_REQ#<seekerId>,SK = BLOOD_REQ#<createdAt>#<requestPostId>(getDonationRequest).List a seeker’s requests by creation date — query
PK = BLOOD_REQ#<seekerId>,SK begins_with BLOOD_REQ#<createdAt-prefix>(getDonationRequestsByDate).Find nearby pending requests (public feed), by blood group — query
GSI1withGSI1PK = REQ#<countryCode>#<bloodGroup>#PENDING#<h3Res5 cell>, sorted byGSI1SK(createdAt) (NearbyBloodRequestsDynamoDbOperations.queryBloodRequestsInHex).
GSI1PK/GSI1SK and LSI1SK are only written when the corresponding
fields (status, countryCode, requestedBloodGroup, h3Res5) are
present. The GSI is written for all statuses, but the only read above pins
PENDING. LSI1SK is written but not read by any current query (see the
LSI1 note under Overloaded index summary).
Accepted donation
Source: core/services/aws/commons/ddbModels/AcceptDonationModel.ts ·
attributes: AcceptDonationDTO (commons/dto/DonationDTO.ts).
Records a donor accepting a specific request. It shares the request’s partition
(BLOOD_REQ#<seekerId>) so acceptances co-locate with the request post.
Key |
Value |
|---|---|
|
|
|
|
Access patterns
Get one donor’s acceptance — exact lookup
PK = BLOOD_REQ#<seekerId>,SK = ACCEPTED#<requestPostId>#<donorId>(getAcceptedRequest).List donors who accepted a request — query
PK = BLOOD_REQ#<seekerId>,SK begins_with ACCEPTED#<requestPostId>(queryAcceptedRequests).
Donor-search orchestration state
Source: core/services/aws/commons/ddbModels/DonorSearchModel.ts ·
attributes: DonorSearchDTO (commons/dto/DonationDTO.ts).
Tracks the state of the wave-based donor-search process for a request (current level, retry count, notified donors, wave history).
Key |
Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
<bucket> is PENDING while the search is in progress, otherwise the
completionReason (or terminal status) — e.g. RADIUS_EXHAUSTED,
FOUND_ENOUGH.
Access patterns
Get search state for a seeker’s request — exact primary-key lookup on
PK = DONOR_SEARCH#<seekerId>,SK = DONOR_SEARCH#<createdAt>#<requestPostId>(getDonorSearchItem).List searches by outcome bucket — query
GSI1withGSI1PK = DONOR_SEARCH#PENDING(in-flight / “stuck” searches) orGSI1PK = DONOR_SEARCH#RADIUS_EXHAUSTED(underserved), sorted bycreatedAt. This read is performed by the monitoring client (clients/monitoring) querying DynamoDB directly; no backend handler uses it.
Donation record
Source: core/services/aws/commons/ddbModels/DonationRecordModel.ts ·
attributes: DonationRecordDTO (commons/dto/DonationDTO.ts).
Key |
Value |
|---|---|
|
|
|
|
Access patterns
Get a donor’s donation history — supported by the key design (
PK = DONATION#<donorId>,SK begins_with DONATION#), butDonationRecordDynamoDbOperationsadds no entity-specific query yet — only base item CRUD (create/getItem/update/delete) is wired.
Notification
Source: core/services/aws/commons/ddbModels/DonationNotificationModel.ts ·
operations: DonationNotificationDynamoDbOperations · attributes:
DonationNotificationDTO / NotificationDTO (commons/dto/NotificationDTO.ts).
All notifications — blood-request, acceptance, ignore, and generic
COMMON — are written through DonationNotificationModel as one item shape.
The secondary-index keys are populated only for the blood-related types.
Key |
Value |
|---|---|
|
|
|
|
|
|
|
|
|
|
<type> is a NotificationType (BLOOD_REQ_POST, REQ_ACCEPTED,
REQ_IGNORED, COMMON); <id> is the requestPostId for blood-related
types. GSI1 keys are written only for BLOOD_REQ_POST, REQ_ACCEPTED
and REQ_IGNORED; LSI1SK whenever status is set. Writes use a
conditional put (attribute_not_exists(PK)), so re-sends are idempotent.
Access patterns
Get a specific notification — exact lookup
PK = NOTIFICATION#<userId>,SK = <type>#<requestPostId>(getBloodDonationNotification).Find everyone notified for a request (optionally by status) — query
GSI1withGSI1PK = <requestPostId>, optionallyGSI1SK begins_with NOTIFICATION#<status>(queryBloodDonationNotifications).
Note
A second adapter, NotificationModel (same PK/SK shape, no index
keys), is defined but not currently wired to any operations class — only
its NOTIFICATION# prefix constant is reused. Listed for completeness.
Overloaded index summary
Because GSI1 and LSI1 are shared, the same physical index serves several
entities. The table below summarises what each writes.
Entity |
|
|
|---|---|---|
Donor location |
|
|
Blood request post |
|
|
Donor-search state |
|
|
Notification |
|
|
Note
LSI1 is written but not currently read. LSI1SK is populated by the
three entities below, but no code path queries LSI1 (nothing calls
getIndex('LSI', ...)). Status-scoped reads are served by GSI1 instead.
The LSI is effectively provisioned for future status-filtered lookups.
Entity |
|
|---|---|
Blood request post |
|
Donor-search state |
|
Notification |
|
Geospatial design (H3)
Donor and request matching is geospatial, built on Uber `H3 <https://h3geo.org/>`_ hexagonal cells, which give uniform-area neighbours and a clean ring-walk for expanding the search radius.
Three H3 resolutions are used (commons/libs/constants/NoMagicNumbers.ts):
Constant |
Resolution |
Role |
|---|---|---|
|
5 (~8.54 km edge) |
|
|
8 (~0.461 km edge) |
|
|
10 |
Stored on rows for fine distance refinement |
Cells are derived with h3-js latLngToCell (see
core/application/utils/h3.ts). Search queries the GSI1 partition for the
centre cell, then expands by querying neighbouring cells obtained from
gridRing (with a gridDiskDistances fallback near H3 pentagons), refining
final ordering by haversine distance using the stored fine-resolution
coordinates.
Note
A row is only matched by the H3 search once it carries the H3 keys
(LOC#…#h3Res8, REQ#…#h3Res5) and the h3Res8 / h3Res10
attributes; rows are backfilled per environment.
Status enumerations
Reference for the <status> segments above (commons/dto/DonationDTO.ts,
commons/dto/NotificationDTO.ts):
DonationStatus—PENDING,COMPLETED,CANCELLED,MANAGED,EXPIRED.DonorSearchStatus—PENDING,COMPLETED(withDonorSearchCompletionReason:FOUND_ENOUGH,DONORS_ACCEPTED,REQUEST_CLOSED,RADIUS_EXHAUSTED).AcceptDonationStatus/NotificationStatus—PENDING,ACCEPTED,COMPLETED,IGNORED.