Status: Accepted (Extended with PostgreSQL Support)
Date: 2026-05-27 (Updated 2026-06-30)
Deciders: Stellar Stream Team
The Stellar Stream backend needs a persistent data store for stream records, events, and metadata. The choice of database technology affects scalability, deployment complexity, operational overhead, and development velocity.
We need to select a database that:
- Persists stream state reliably across restarts
- Supports concurrent reads and writes
- Enables efficient querying of streams by sender, recipient, and status
- Minimizes operational complexity for self-hosted deployments
- Supports event sourcing patterns for audit trails
- Can scale from single-instance to multi-instance deployments
Pros:
- Zero operational overhead: single file, no separate server process
- Excellent for single-instance deployments and development
- ACID transactions with WAL (Write-Ahead Logging) mode for concurrent access
- Sufficient performance for typical stream volumes (thousands of streams)
- Easy backups: copy the database file
- No authentication/networking complexity
- Excellent TypeScript support via better-sqlite3
- Minimal dependencies
Cons:
- Limited concurrent write capacity (one writer at a time)
- Not ideal for high-throughput multi-instance deployments
- Requires shared storage in multi-instance setups (NFS, S3, etc.)
- No built-in replication or failover
Pros:
- Excellent multi-instance support with native replication
- High concurrent write throughput
- Advanced features (JSONB, full-text search, etc.)
- Mature ecosystem and tooling
- Scales to very large datasets
Cons:
- Requires separate server process and operational management
- More complex deployment (Docker, managed services, etc.)
- Additional authentication and networking configuration
- Higher resource overhead
- Overkill for typical stream volumes
- Adds operational burden for self-hosted deployments
Pros:
- Simplest possible implementation
- No external dependencies
Cons:
- No ACID guarantees
- Poor concurrent access patterns
- Inefficient querying
- Not suitable for production use
We choose SQLite with WAL mode for the initial implementation.
-
Deployment Simplicity: SQLite requires zero operational overhead, making it ideal for self-hosted deployments and development environments.
-
Sufficient for Current Scale: Stream volumes are expected to be in the thousands, well within SQLite's capabilities.
-
Development Velocity: SQLite enables rapid iteration without database setup complexity.
-
Multi-Instance Path: While SQLite has write limitations, we can:
- Use Redis for caching to reduce database load
- Implement read replicas with eventual consistency
- Migrate to PostgreSQL when needed (schema is database-agnostic)
-
Operational Simplicity: Single file backup, no authentication, no networking issues.
- Faster time to market with minimal operational complexity
- Excellent developer experience (no database setup required)
- Easy local development and testing
- Simple backup and restore procedures
- Lower resource requirements
- Limited to one concurrent writer (mitigated by Redis caching)
- Multi-instance deployments require shared storage or eventual consistency
- May need migration to PostgreSQL for very high-throughput scenarios
When the application outgrows SQLite:
-
Schema Compatibility: The current schema uses standard SQL compatible with both SQLite and PostgreSQL.
-
Migration Steps:
- Create PostgreSQL database with identical schema
- Implement dual-write pattern (write to both SQLite and PostgreSQL)
- Migrate historical data
- Switch reads to PostgreSQL
- Decommission SQLite
-
Timeline: Expected when stream volume exceeds 100k+ concurrent streams or write throughput exceeds SQLite's capacity.
- Database: SQLite with WAL mode enabled
- Location:
backend/data/streams.db - Schema: Defined in
backend/src/services/db.ts - Migrations: Inline schema creation on startup
streams: Main stream records with status trackingstream_events: Event history (created, claimed, canceled, start_time_updated)stream_archive: Completed streams > 30 days oldwebhook_deliveries: Pending webhook deliveries with retry trackingwebhook_dead_letters: Failed webhooks after max retriesindexer_cursor: Last processed ledger sequence
- WAL mode for concurrent reads during writes
- Indexes on frequently queried columns (sender, recipient, status)
- Connection pooling via better-sqlite3
- Redis caching layer for hot data (stream lists, stats)
To scale the application for multi-instance deployments, optional support for PostgreSQL has been implemented:
- Activation: PostgreSQL is automatically activated when the
DATABASE_URLenvironment variable is set. If not set, the application defaults to SQLite. - Synchronous Repository Adapter: To avoid altering the existing synchronous database access pattern in the backend services, a synchronous
PostgresDatabasewrapper class was created. It uses a backgroundWorkerthread to connect to PostgreSQL asynchronously via thepgdriver, communicating query requests and results back to the main thread synchronously using aSharedArrayBufferandAtomics.wait/Atomics.notify. - Dialect Compatibility Translation:
- Types:
INTEGERmaps toBIGINTandREALmaps toDOUBLE PRECISIONon the fly. - Primary Keys:
INTEGER PRIMARY KEY AUTOINCREMENTis rewritten toSERIAL PRIMARY KEY. - Conflict Resolution:
INSERT OR IGNOREclauses are translated to standard PostgreSQLON CONFLICT (...) DO NOTHINGclauses. - Full-Text Search: The SQLite
fts5virtual table indexing is bypassed on PostgreSQL, and query matches are executed using native, case-insensitiveILIKEclauses on the main table.
- Types:
- ADR 0002: Freighter wallet integration for recipient signing
- ADR 0003: Polling as the MVP refresh strategy
- ADR 0004: SQLite event history over an append-only log
- ADR 0005: Multi-asset support as a first-class stream property