Reference documentation for the TalentTrust persistence layer.
TalentTrust uses SQLite (via better-sqlite3) as its embedded relational database. The database is opened as a singleton on application startup and all writes go through typed repository classes.
SQLite was chosen because:
- No external service is required — the database is a single file on disk.
better-sqlite3provides a synchronous, zero-latency API that integrates naturally with Express.- Tests use an in-memory database (
:memory:) for full isolation and speed. - The repository abstraction makes swapping to PostgreSQL straightforward in future.
| Environment variable | Default | Description |
|---|---|---|
DB_PATH |
talenttrust.db |
Absolute or relative path to the SQLite file. |
Set DB_PATH=:memory: to use an in-memory database (data is lost on process exit).
| Column | Type | Constraints |
|---|---|---|
id |
TEXT | PRIMARY KEY (UUID v4) |
username |
TEXT | NOT NULL, UNIQUE |
email |
TEXT | NOT NULL, UNIQUE |
role |
TEXT | NOT NULL, CHECK IN ('client', 'freelancer', 'both') |
created_at |
TEXT | NOT NULL (ISO-8601) |
| Column | Type | Constraints |
|---|---|---|
id |
TEXT | PRIMARY KEY (UUID v4) |
title |
TEXT | NOT NULL |
client_id |
TEXT | NOT NULL, REFERENCES users(id) |
freelancer_id |
TEXT | NOT NULL, REFERENCES users(id) |
amount |
INTEGER | NOT NULL, CHECK >= 0 (stored in stroops; 1 XLM = 10,000,000 stroops) |
status |
TEXT | NOT NULL, CHECK IN ('draft','active','completed','disputed','cancelled') |
version |
INTEGER | NOT NULL, default 0, CHECK >= 0 (optimistic concurrency control) |
created_at |
TEXT | NOT NULL (ISO-8601) |
Indexes: idx_contracts_client_id, idx_contracts_freelancer_id, idx_contracts_status.
import { ContractRepository } from "./repositories/contractRepository";
const repo = new ContractRepository(getDb());| Method | Signature | Description |
|---|---|---|
findAll |
() → Contract[] |
All contracts, newest first |
findById |
(id: string) → Contract | undefined |
Single contract by UUID |
findByClientId |
(clientId: string) → Contract[] |
Contracts for a client |
create |
(data) → Contract |
Insert a new contract |
updateStatus |
(id, status) → Contract | undefined |
Change status field |
delete |
(id: string) → boolean |
Remove contract; returns false if not found |
import { UserRepository } from "./repositories/userRepository";
const repo = new UserRepository(getDb());| Method | Signature | Description |
|---|---|---|
findAll |
() → User[] |
All users, newest first |
findById |
(id: string) → User | undefined |
Single user by UUID |
findByEmail |
(email: string) → User | undefined |
Lookup by email |
create |
(data) → User |
Insert a new user |
delete |
(id: string) → boolean |
Remove user; throws if FK contracts exist |
Schema migrations run automatically on startup via runMigrations() in src/db/migrations.ts.
schema_versiontable tracks each applied migration version (INTEGER PRIMARY KEY) and timestamp.- Migrations are ordered, immutable, and sequential (
1..N). - Startup applies only missing versions, making reruns idempotent and safe for repeated deploys.
- Each migration runs inside a SQLite transaction.
- If a migration throws, SQLite rolls back that migration's partial changes.
- Failed migrations are not recorded in
schema_version, so recovery is deterministic.
- For urgent rollback to previous app code, restore a DB snapshot made before deployment.
- Because migrations are additive and idempotent, redeploying the fixed build safely resumes from the last applied version.
- Keep migration files immutable; create a new migration to correct prior schema changes instead of editing old ones.
SQLite is configured with WAL (Write-Ahead Logging) journal mode and synchronous=NORMAL for optimal concurrency and performance.
| Pragma | Setting | Description |
|---|---|---|
journal_mode |
WAL |
Enables write-ahead logging for better concurrent read/write |
synchronous |
NORMAL |
Reduces fsync calls; safe with WAL mode |
busy_timeout |
5000 (configurable via DB_BUSY_TIMEOUT) |
Wait time in ms before throwing SQLITE_BUSY |
WAL mode benefits:
- Readers don't block writers and writers don't block readers
- Better concurrency for read-heavy workloads typical in API servers
- Reduced disk I/O due to batching of commits
Tradeoffs:
- Durability: With
synchronous=NORMAL, SQLite may lose the last few transactions if the OS crashes (but not if only the application crashes). The WAL file is synced at checkpoints. - Extra files: Creates
-waland-shmfiles alongside the database file - Network filesystems: WAL mode may not work reliably on NFS or network shares
For mission-critical deployments where every transaction must survive OS crashes, set synchronous=FULL at the cost of:
- Higher latency per write (additional fsync calls)
- Lower write throughput under heavy load
To change:
// In database.ts, after opening connection:
instance.pragma("synchronous = FULL");When multiple processes access the database, SQLite may return SQLITE_BUSY. The busy_timeout pragma makes SQLite wait and retry before failing:
- Default:
5000ms (5 seconds) - Override via environment:
DB_BUSY_TIMEOUT=10000 - Set to
0to fail immediately on contention
| Concern | Mitigation |
|---|---|
| SQL injection | All queries use better-sqlite3 prepared statements with parameter binding — no string interpolation. |
| CHECK constraints | status and role columns are validated at the DB level as a second line of defence. |
| FK enforcement | PRAGMA foreign_keys = ON is set on every connection to prevent orphaned records. |
| File permissions | In production, restrict the DB file: chmod 600 talenttrust.db. |
| Credentials | No passwords are stored. Authentication is delegated to Stellar key-based or third-party auth. |
Tests use an in-memory SQLite database injected at construction time:
npm test -- --coverageExpected coverage ≥ 95 % on all src/db/* and src/repositories/* modules.