Express API for the TalentTrust decentralized freelancer escrow protocol. Handles contract metadata, reputation, and integration with Stellar/Soroban.
- Queue-Based Background Jobs: Durable job processing with BullMQ and Redis
- Contract Processing: Asynchronous blockchain contract operations
- Email Notifications: Non-blocking email delivery
- Reputation System: Background reputation score calculations
- Blockchain Sync: Efficient blockchain data synchronization
- Idempotent Event Processing: Guaranteed safe event replay with deduplication
- Strict Schema Validation: Contract-specific payload validation
- Audit Trail: Complete processing history and statistics
- Stale-While-Revalidate Caching: SWR caching for upstream resources with degraded signals
The backend includes dependency-level chaos testing to simulate upstream outages and verify graceful degradation.
GET /api/v1/contractsreturns upstream data during normal operation.- On upstream failures with graceful degradation enabled, it returns a safe fallback payload with
degraded: true. - If graceful degradation is disabled, it returns
503withcontracts_unavailable.
GRACEFUL_DEGRADATION_ENABLED=true|false(defaulttrue)UPSTREAM_CONTRACTS_URL(defaulthttps://example.invalid/contracts)UPSTREAM_TIMEOUT_MS(default1200, bounded to100..10000)CHAOS_MODE=off|error|timeout|random(defaultoff)CHAOS_TARGETS(comma-separated dependencies likecontracts)CHAOS_PROBABILITY(float0..1, used byrandommode)
Detailed architecture and security notes are in docs/backend/chaos-testing.md.
Developer onboarding and blue-green local setup are documented in docs/backend/developer-onboarding-blue-green.md. Detailed authentication design, lifecycle, and refresh-token rotation semantics are documented in AUTH.md.
Outbound RPC queries to the Stellar/Soroban network are wrapped in an automatic retry-with-backoff mechanism to protect against transient network failures.
- Idempotent Reads: Queries like
getLedgerEntries,getLatestLedger,getEvents,simulateTransaction, and the polling checkgetTransactionare wrapped with a jittered exponential back-off helper. - Mutating Transactions: Submitting signed transactions via
sendTransactionis never retried automatically to prevent accidental double-submission or transaction collisions.
You can configure retry behavior using the following environment variables:
| Variable | Default | Description |
|---|---|---|
SOROBAN_RPC_RETRY_ATTEMPTS |
5 |
Maximum number of retry attempts for read calls. |
SOROBAN_RPC_RETRY_BASE_DELAY_MS |
200 |
The initial base delay in milliseconds, scaled exponentially with jitter. |
Detailed authentication design, lifecycle, and refresh-token rotation semantics are documented in AUTH.md.
The backend enforces a consistent API error envelope and status-code policy across request validation, routing, dependency failures, and unexpected runtime errors.
All handled errors return:
{
"error": {
"code": "machine_readable_code",
"message": "safe message",
"requestId": "request-correlation-id"
}
}400for malformed JSON (invalid_json) and request validation errors (validation_error)404for unknown routes (not_found)503for expected dependency outages (dependency_unavailable)500for unexpected failures (internal_error)
Error code values are stable machine-readable API contract strings. Clients may branch on them, and new codes should be appended without renaming or removing existing values.
| Code | Meaning |
|---|---|
bad_request |
The request could not be processed. |
conflict |
The request conflicts with the current resource state. |
contract_metadata_mismatch |
Contract metadata failed the pinned-value check. |
dependency_unavailable |
A required upstream service is temporarily unavailable. |
ERR_CONFLICT |
Optimistic concurrency version conflict. |
ERR_INVALID_VERSION |
Update version is not a non-negative integer. |
ERR_MISSING_VERSION |
Update version field is missing. |
forbidden |
The authenticated user is not permitted to perform the action. |
internal_error |
An unexpected error occurred. |
invalid_json |
Request body JSON is malformed. |
invalid_webhook_signature |
Webhook signature verification failed. |
not_found |
The requested resource was not found. |
payload_too_large |
Request payload exceeds the configured limit. |
rate_limited |
Too many requests were sent in the allowed window. |
unauthorized |
Authentication is required or invalid. |
unsupported_media_type |
Request content type is unsupported. |
validation_error |
Request or business-rule validation failed. |
Detailed notes are in docs/backend/error-handling.md.
The backend now includes a deterministic contract event processing pipeline focused on three semantics:
- Ingestion: validate inbound event payloads before business processing.
- Deduplication: compute a stable event identity key (
contractId:eventId:sequence) and treat replays as idempotent duplicates. - Persistence: store accepted events through a repository abstraction (current implementation: in-memory).
| Method | Path | Description |
|---|---|---|
POST |
/api/v1/events |
Process events with idempotency guarantees |
POST |
/api/v1/events/validate |
Validate events without processing |
GET |
/api/v1/events/stats |
Processing statistics |
GET |
/api/v1/contracts/{contractId}/history |
Contract event history |
accepted(202): new, valid event persisted.duplicate(200): replayed event already processed.invalid(400): payload failed validation.error(500): unexpected internal persistence/processing failure.
- Node.js 18+
- npm
git clone <your-repo-url>
cd talenttrust-backend
npm install| Script | Description |
|---|---|
npm run build |
Compile TypeScript to dist/ |
npm start |
Run production server |
npm run dev |
Run with ts-node-dev (hot reload) |
npm test |
Run Jest tests |
npm run test:ci |
Run tests with coverage enforcement (≥95%) |
npm run lint |
Run ESLint |
npm run lint:fix |
Auto-fix lint issues |
npm run audit:ci |
Fail on HIGH/CRITICAL npm vulnerabilities |
Copy environment template:
cp .env.example .envKey event ingestion configuration:
# Event Ingestion Configuration
ENABLE_STRICT_VALIDATION=true
ENABLE_PAYLOAD_INTEGRITY_CHECK=true
MAX_EVENT_AGE_MS=86400000
EVENT_BATCH_SIZE=100
EVENT_TIMEOUT_MS=5000
IDEMPOTENCY_TTL_MS=3600000Queue workers enforce a wall-clock timeout for every job attempt. When a job exceeds its timeout, the queue manager aborts the processor AbortSignal, fails the attempt, and lets the existing retry/DLQ policy decide whether to retry or retain the job as failed.
| Variable | Default | Description |
|---|---|---|
QUEUE_JOB_TIMEOUT_MS |
30000 |
Default per-job attempt timeout in milliseconds. |
QUEUE_JOB_TIMEOUT_EMAIL_NOTIFICATION_MS |
QUEUE_JOB_TIMEOUT_MS |
Timeout override for email notification jobs. |
QUEUE_JOB_TIMEOUT_CONTRACT_PROCESSING_MS |
QUEUE_JOB_TIMEOUT_MS |
Timeout override for contract processing jobs. |
QUEUE_JOB_TIMEOUT_REPUTATION_UPDATE_MS |
QUEUE_JOB_TIMEOUT_MS |
Timeout override for reputation update jobs. |
QUEUE_JOB_TIMEOUT_REPUTATION_RECOMPUTE_MS |
QUEUE_JOB_TIMEOUT_MS |
Timeout override for reputation recompute jobs. |
QUEUE_JOB_TIMEOUT_BLOCKCHAIN_SYNC_MS |
QUEUE_JOB_TIMEOUT_MS |
Timeout override for blockchain sync jobs. |
Processors receive an AbortSignal as optional context and should stop outbound work when it is aborted. The manager still fails the attempt on timeout when a processor ignores the signal, and it prevents the same job from being executed again while the timed-out processor is still active.
For full configuration details, see docs/backend/config.md.
The audit export endpoint streams compliance exports as NDJSON or CSV without loading the full table into memory.
GET /api/v1/audit/export
Requires admin or auditor role. Returns a streamed file attachment.
| Parameter | Type | Description |
|---|---|---|
from |
ISO-8601 | Start of time range (inclusive). e.g. 2024-01-01T00:00:00.000Z |
to |
ISO-8601 | End of time range (inclusive). |
action |
string | Filter by event type (e.g. CONTRACT_CREATED). |
severity |
string | Filter by severity: INFO, WARNING, or CRITICAL. |
actor |
string | Filter by actor ID. |
resource |
string | Filter by resource type (e.g. contract, user). |
resourceId |
string | Filter by resource instance ID. |
All parameters are optional. Omitting them exports all records.
- NDJSON (default) — one JSON object per line,
Content-Type: application/x-ndjson - CSV — header row + one data row per entry, columns:
id,timestamp,action,severity,actor,resource,resourceId,ipAddress,correlationId,metadata
Rows are fetched via a SQLite cursor and piped to a temp file in configurable batch sizes (default 500). The response is then streamed from the temp file. Peak heap usage is proportional to one batch, not the total result set.
All sensitive metadata fields (password, token, secret, credential, apikey, api_key, private) are replaced with [REDACTED] and email addresses are partially masked before the data reaches the export file.
- Backend Notification Services
- Event Ingestion Idempotency
- SLA/SLO Definitions and Alert Thresholds
- SLO Runtime Evaluation
- Redis Testing Guide
- Escrow Contract Lifecycle & Bounds
- Contract Event Indexer Cursor Model & Replay Protection
- Data Retention, Archival, and Purge Lifecycle
GitHub Actions runs four gates on every push and pull request to main:
- Lint — ESLint with TypeScript-aware rules
- Test — Jest with ≥95% line/function/statement coverage
- Build — TypeScript strict compilation (runs after lint + test pass)
- Security Audit —
npm audit --audit-level=high
All four checks must pass before a PR can be merged. See docs/backend/branch-protection.md for the recommended GitHub branch protection settings.
MIT License - see LICENSE file for details.
src/
├── index.ts # Server entry point
├── app.ts # Express app factory
└── routes/
├── health.ts # GET /health
└── contracts.ts # GET /api/v1/contracts
See docs/backend/architecture.md for design decisions and planned integrations.
The TalentTrust Backend implements hardened HTTP response policies and origin controls.
- Security Headers: Managed via Helmet (CSP, HSTS, etc.).
- CORS Policy: Strict allowlist controlled by environment configuration.
The CORS policy is driven by the CORS_ALLOWED_ORIGINS environment variable, a comma-separated list of allowed origins.
# Allow specific origins
CORS_ALLOWED_ORIGINS=https://app.example.com,https://admin.example.com| Environment | Default | Behavior |
|---|---|---|
production |
Deny-by-default (empty allowlist) | Only origins in CORS_ALLOWED_ORIGINS are accepted. Wildcard (*) and localhost origins are rejected at startup. |
development / staging / test |
http://localhost:3000,http://localhost:3001 |
Localhost origins are pre-configured for convenience. Explicitly setting the variable overrides the default. |
- The request
Originheader is reflected inAccess-Control-Allow-Originonly when it exists in the allowlist. - Arbitrary origins are never echoed back.
- Wildcard (
*) is never used asAccess-Control-Allow-Originbecause credentials are always enabled. Access-Control-Allow-Credentials: trueis set for all allowlisted requests.- Requests without an
Originheader (server-to-server, curl, health checks) are allowed.
GET, POST, PUT, PATCH, DELETE, OPTIONS
Content-Type, Authorization
For detailed information, see Security Documentation.
The test suite includes both unit and integration coverage:
- Unit tests for validation, dedupe key construction, repository behavior, and processor semantics.
- Integration-style tests for HTTP ingestion and persistence behavior through Express routes.
- Failure-path tests for malformed payloads, duplicate replays, and unexpected processing errors.
Coverage thresholds are enforced in Jest at 95% for statements, branches, functions, and lines (for included modules).
All queue processors (src/queue/processors/) use the structured logger from src/logger.ts — never console.log / console.warn / console.error.
| Concern | Rule |
|---|---|
| Logger instantiation | Each processor calls createLogger({ processor: '<name>', ...correlationCtx }) at the top of its handler, binding correlationId and requestId from the job payload. |
| Log record shape | Every record carries timestamp, level, message, and service: "talenttrust-backend". |
| PII at info/warn level | Recipient email addresses, userId, and contractId must not appear in message strings at info or warn level. They may be logged at debug level as structured fields. |
| Error path | Validation errors emit a warn record (via log.warn(...)) before throwing, so observers can correlate the rejection with the job's correlation context. |
| Job IDs | Email tracking IDs are generated with generateEmailId() (uses crypto.randomUUID()). Never use Date.now() + Math.random() for IDs. |
import { createLogger } from '../../logger';
export async function processMyJob(payload: MyPayload): Promise<JobResult> {
const log = createLogger({
processor: 'my-processor',
...(payload.correlationId && { correlationId: payload.correlationId }),
...(payload.requestId && { requestId: payload.requestId }),
});
if (!isValid(payload)) {
log.warn('Validation failed: reason'); // structured, no PII
throw new Error('...');
}
log.info('Job started');
// ...
log.info('Job completed', { someMetric: 42 });
return { success: true };
}The service now uses a single shutdown registry for SIGTERM/SIGINT handling. On shutdown it performs the following steps in order:
- Close the HTTP listener so new requests stop arriving.
- Stop accepting new webhook deliveries and wait for in-flight deliveries, with a bounded grace period before fallback checkpointing.
- Stop accepting new transaction polls and queue jobs, wait for in-flight work to finish, and checkpoint any remaining pending state if the grace period expires.
- Close BullMQ workers and downstream connections before exiting.
This prevents polls and queue jobs from being interrupted mid-flight while preserving a checkpointable state for pending work.
- Input validation is strict at ingestion boundaries to reject malformed payloads early.
- Replay and duplicate delivery are handled as idempotent outcomes using a deterministic dedupe key.
- JSON body limit is constrained to reduce accidental oversized request risk.
- Current persistence is in-memory and intended for testability and local development; production hardening should add durable storage and capacity limits.
- Trust boundary remains the ingestion endpoint; event authenticity and signature verification are future integration concerns.
All configuration is managed through src/config/ and validated at startup using Zod. This ensures a fail-fast behavior with clear error messages. Copy .env.example to .env to get started. See docs/backend/config.md for full details.
| Variable | Default | Description |
|---|---|---|---|
| PORT | 3001 | HTTP port for the Express server |
| NODE_ENV | development | Runtime environment (development, staging, production, test) |
| API_BASE_URL | http://localhost:${PORT} | Base URL for the API |
| DEBUG | false | Enable/disable debug logging |
| CORS_ALLOWED_ORIGINS | (see CORS Configuration) | Comma-separated list of allowed CORS origins. In production defaults to deny-by-default; in development defaults to http://localhost:3000. |
| DATABASE_URL | (optional) | Database connection string |
| JWT_SECRET | (optional) | Secret used for JWT signing (min 8 chars) |
| IDEMPOTENCY_TTL_MS | 3600000 | Idempotency key TTL in ms (default 1 hour); after expiry keys are eligible for eviction and re-submission is processed fresh |
| STELLAR_HORIZON_URL | https://horizon-testnet.stellar.org | Stellar Horizon API endpoint |
| STELLAR_NETWORK_PASSPHRASE | Test SDF Network ; September 2015 | Network passphrase for signing |
| SOROBAN_RPC_URL | https://soroban-testnet.stellar.org | Soroban JSON-RPC endpoint |
| SOROBAN_CONTRACT_ID | (empty) | Deployed escrow contract ID |
| ACTIVE_COLOR | blue | Active backend color for blue-green routing |
| BLUE_PORT | 3001 | Port for the 'blue' backend |
| GREEN_PORT | 3002 | Port for the 'green' backend |
GET /health- Health checkGET /api/v1/contracts- Get contractsGET /api/v1/reputation/:id- Get freelancer reputation profilePUT /api/v1/reputation/:id- Update freelancer reputation profile
See docs/backend/reputation-api.md for detailed Reputation API info.
The service exposes a comprehensive health check endpoint at GET /health that aggregates dependency probes.
Request:
curl http://localhost:3000/healthResponse (200 OK - healthy):
{
"status": "ok",
"service": "talenttrust-backend",
"timestamp": "2024-01-15T10:30:00.000Z",
"uptimeSeconds": 1234,
"probes": [
{
"name": "db",
"ok": true,
"status": "up",
"latencyMs": 45
},
{
"name": "redis",
"ok": true,
"status": "up",
"latencyMs": 12
}
]
}Response (503 Service Unavailable - degraded):
{
"status": "degraded",
"service": "talenttrust-backend",
"timestamp": "2024-01-15T10:30:00.000Z",
"uptimeSeconds": 1234,
"probes": [
{
"name": "db",
"ok": false,
"status": "down",
"detail": "disk I/O error",
"latencyMs": 5000
}
]
}Probe Status Mapping:
Each probe reports one of three statuses:
| Status | Meaning | HTTP |
|---|---|---|
up |
Dependency is healthy and responsive | 200 |
degraded |
Dependency is slow or warning | 503 |
down |
Dependency is unreachable or failed | 503 |
Database Probe (db):
- Verifies SQLite connectivity with lightweight
SELECT 1query - Thresholds: up (<1000ms), degraded (1000-3000ms), down (≥3000ms or error)
- Security: Query is hardcoded with no user input
- Configuration:
DB_BUSY_TIMEOUT(default 5000ms)
Redis Probe (redis):
- Tests Redis with PING command
- Timeout: 3000ms
- Configuration:
REDIS_HOST,REDIS_PORT,REDIS_PASSWORD
Other Probes:
stellar-rpc: Stellar/Soroban RPC reachability (5s timeout)queue: BullMQ job queue health (degraded if failed jobs exceed threshold)circuit-breaker: Reports open circuit breakersenv: Verifies required environment variables
Production Security:
In production (NODE_ENV=production), probe details are stripped to prevent topology leakage to unauthenticated callers.
GET /api/v1/contracts- List contracts (placeholder)
POST /api/v1/contracts/:contractId/metadata- Create metadataGET /api/v1/contracts/:contractId/metadata- List metadata with paginationGET /api/v1/contracts/:contractId/metadata/:id- Get single metadataPATCH /api/v1/contracts/:contractId/metadata/:id- Update metadataDELETE /api/v1/contracts/:contractId/metadata/:id- Delete metadata
See docs/backend/contract-metadata-api.md for detailed API documentation.
The API uses Bearer token authentication. Include the token in the Authorization header:
Authorization: Bearer <your-auth-token>
Demo tokens for testing:
demo-admin-token- Admin user with full accessdemo-user-token- Regular user with limited access
GET /health- Service health status
GET /api/v1/contracts- List contracts (placeholder)
POST /api/v1/contracts/:contractId/metadata- Create metadataGET /api/v1/contracts/:contractId/metadata- List metadata with paginationGET /api/v1/contracts/:contractId/metadata/:id- Get single metadataPATCH /api/v1/contracts/:contractId/metadata/:id- Update metadataDELETE /api/v1/contracts/:contractId/metadata/:id- Delete metadata
See docs/backend/contract-metadata-api.md for detailed API documentation.
The API uses Bearer token authentication. Include the token in the Authorization header:
Authorization: Bearer <your-auth-token>
Demo tokens for testing:
demo-admin-token- Admin user with full accessdemo-user-token- Regular user with limited access
The API uses Role-Based Access Control (RBAC) with four roles: admin,
freelancer, client, and guest. Protected endpoints require a
Bearer <token> header.
See docs/backend/authentication-authorization.md for the full access control matrix, architecture, and security notes.
For API key authentication (used by internal/external service integrations), see docs/api-keys.md for the complete lifecycle, scope reference, and rotation guidance.
The API now includes a schema-based request validation framework for:
- Route
params - URL
query - JSON request
body
Validation behaviour:
- Unknown fields are stripped.
- Required fields are enforced.
- Type and range/length constraints are validated.
Validation middleware returns HTTP 400 with the shape:
{
"error": "Validation failed",
"details": ["query.admin is not allowed"]
}See docs/backend/request-validation-framework.md for implementation details and security notes.
- Fork the repo and create a branch:
git checkout -b feature/<ticket>-description - Make changes, run
npm run lint && npm run test:ci && npm run build - Open a pull request — CI runs all four gates automatically
The backend uses an embedded SQLite database (via better-sqlite3) — no external service required.
| Environment variable | Default | Description |
|---|---|---|
DB_PATH |
talenttrust.db |
Path to the SQLite file. Use :memory: for ephemeral mode. |
Schema migrations run automatically on startup and record applied versions in schema_version. See docs/backend/database.md for full documentation: schema, versioning, rollback guidance, repository API, configuration, and security notes.
Upstream RPC calls (Stellar/Soroban) are protected by a built-in circuit breaker.
| State | Behaviour |
|---|---|
CLOSED |
Normal operation |
OPEN |
Fast-fail — returns 503 without calling upstream |
HALF_OPEN |
Single probe; success → CLOSED, failure → OPEN |
| Environment variable | Default | Description |
|---|---|---|
STELLAR_RPC_URL |
https://soroban-testnet.stellar.org |
Stellar JSON-RPC endpoint |
Live state is available at GET /api/v1/circuit-breaker/status. See docs/backend/circuit-breaker.md for full reference.
The blockchain-sync background job ingests on-chain Soroban contract events
into the local indexer. It scans a ledger range, fetches events from the
Soroban RPC layer, and persists each event so downstream consumers (reputation,
escrow flows) see the latest chain state.
| Behaviour | Detail |
|---|---|
| Real RPC ingestion | Events are fetched via SorobanRpcService.getEvents (no more stubbed batches). |
| Idempotent persistence | Each event is keyed by contractId:eventId:ledger; replayed or retried batches never double-write. |
| Circuit-breaker guarded | Every RPC call runs through the shared breaker; an open circuit fast-fails the job. |
| Resumable | Progress is checkpointed per batch via a cursor, so a restarted job resumes from the last synced ledger instead of re-scanning from zero. |
| Fail-and-retry | RPC/timeout errors throw so the queue retries the job rather than silently reporting success. |
| SSRF-guarded | SOROBAN_RPC_URL is validated against the SSRF allow-list before any egress. |
Job payload (BlockchainSyncPayload):
| Environment variable | Default | Description |
|---|---|---|
SOROBAN_RPC_URL |
https://soroban-testnet.stellar.org |
Soroban JSON-RPC endpoint (must be a public, SSRF-safe URL). |
SOROBAN_CONTRACT_ID |
(empty) | When set, events are filtered to this contract. |
When neither startBlock nor a stored cursor exists, the job starts from ledger
0; when endBlock is omitted, the current chain head is discovered via
getLatestLedger. If there is nothing new to sync, the job returns early
without making event calls.
All routes under /api/v1/admin/* are protected by JWT authentication.
- Header:
Authorization: Bearer <token> - Validation: Ensures token is valid and not expired.
The /api/v1/events endpoint requires an Idempotency-Key header to prevent duplicate processing of the same smart contract event.
- Header:
Idempotency-Key: <unique-uuid-or-hash> - Behavior: If a key is seen again within 1 hour, the cached response is returned instead of re-processing.
A pipeline for indexing escrow and dispute lifecycle updates from smart contracts.
- Endpoint:
POST /api/v1/events - Supported Events:
escrow:created,escrow:completed,dispute:initiated,dispute:resolved.
A request-scoped storage utility backed by Node.js AsyncLocalStorage to automatically propagate requestId and correlationId context fields down the async execution tree.
- Middleware: The
requestContextmiddleware seeds the store withrequestIdand optionalcorrelationIdparsed from HTTP headers (X-Request-IdandX-Correlation-Id). - Observability: The global
Loggerautomatically reads from this store when logging, ensuring logs emitted by downstream services, repository queries, and operations carry the correct correlation context without manual parameter passing. - Safety: Safe defaults (no IDs) are provided when running outside of a request context (e.g. background tasks or scheduled jobs), and request contexts are concurrently isolated to prevent cross-request context leakage.
Run unit and integration tests to verify these features:
npm testTalentTrust Backend follows a strict policy for handling secrets. All sensitive information must be managed through the SecretsManager.
For more information, see the Secrets Handling Documentation.
MIT
The TransactionPoller service manages blockchain transaction confirmations using an exponential backoff strategy.
- Configurable Retries: Set
maxRetriesto limit the number of backoff polling attempts. - Duration Ceiling: An absolute wall-clock duration limit (
maxTotalDurationMs) can be set as a circuit breaker. If the transaction takes longer than this ceiling, polling is halted and the transaction transitions toTIMEOUT. This acts as an absolute guard and takes precedence overmaxRetriesif reached first. - Idempotent Polling: Safely restarts after an app crash without duplicating tracking logic.
Transaction state is persisted in SQLite through the transactions table, keyed by transaction hash. The store is exposed via the TransactionsDbInterface with two implementations:
| Implementation | Backing store | Feature flag |
|---|---|---|
SqliteTransactionStore |
SQLite transactions table via better-sqlite3 |
USE_SQLITE_TRANSACTION_STORE=true (default) |
InMemoryTransactionStore |
In-memory Map |
USE_SQLITE_TRANSACTION_STORE=false |
The TransactionPoller accepts an optional store parameter in its constructor. When omitted, it uses the global transactionsDb instance, which is created by the factory function createTransactionsDb() based on the USE_SQLITE_TRANSACTION_STORE environment variable.
| Column | Type | Description |
|---|---|---|
hash |
TEXT PRIMARY KEY |
Blockchain transaction hash |
status |
TEXT |
One of PENDING, SUCCESS, FAILED, TIMEOUT |
receipt |
TEXT (JSON) |
Full blockchain receipt, serialised as JSON |
last_checked_at |
TEXT (ISO-8601) |
Timestamp of the last poll attempt |
retry_count |
INTEGER |
Number of retries performed |
started_at |
TEXT (ISO-8601) |
When polling for this transaction began |
- All queries use parameterised prepared statements — receipt JSON and other values are never interpolated into SQL strings.
- Receipt data is serialised with
JSON.stringifyon write and parsed with a safe wrapper (try/catcharoundJSON.parse) on read. Malformed receipts are returned asundefinedrather than crashing the caller. - The
INSERT ... ON CONFLICT(hash) DO UPDATEpattern ensures that repeated calls toset()update the existing row without creating duplicates. - Schema migrations are handled by
src/db/migrations.ts(versions 5 and 9 create thetransactionstable and add thestarted_atcolumn).
On application startup, call recoverPendingTransactions() to rehydrate in-flight polling:
- Load all rows from the
transactionstable wherestatus = 'PENDING'. - For each pending transaction, restore
retry_countandlast_checked_at. - Resume the polling loop from the current retry index using
calculateDelayfromsrc/utils/retry.ts. - Terminal transactions (
SUCCESS,FAILED,TIMEOUT) are not reloaded — they remain in the database for audit but are excluded from recovery.
- Parameterised SQL: All queries use
?placeholders. Receipt JSON is passed as a bound parameter, never concatenated. - Corrupted receipts:
safeParseReceiptwrapsJSON.parsein a try-catch. If a stored receipt is malformed, the field returnsundefinedand the transaction is handled safely. - Fail closed: A
get()that throws (e.g., database I/O error) returnsundefinedrather than propagating the exception to the poller.
Reusable retry policies for handling transient failures, located in src/utils/retry.ts.
import { withRetry } from './utils/retry';
const data = await withRetry(() => fetchFromApi(), {
maxAttempts: 5,
baseDelayMs: 200,
maxDelayMs: 5000,
jitter: true,
});| Option | Type | Default | Description |
|---|---|---|---|
maxAttempts |
number | 3 | Maximum retry attempts |
baseDelayMs |
number | 200 | Base delay in ms |
maxDelayMs |
number | 5000 | Max delay cap in ms |
jitter |
boolean | true | Adds randomness to delay |
isRetryable |
function | () => true |
Controls which errors retry |
{ "network": "soroban", // or "stellar" "startBlock": 1000, // optional — resumes from the last cursor when omitted "endBlock": 1100 // optional — defaults to the current chain head }