Overview
The data layer uses SQLite with the better-sqlite3 driver, abstracted through the Repository pattern. This design provides database independence while maintaining simplicity for small to medium deployments.Database Architecture
Technology Choice: SQLite
Rationale:- Simplicity: Zero configuration, embedded database
- Reliability: ACID compliance, proven stability
- Performance: Sufficient for thousands of voters
- Deployment: Single file, easy backups
- Development: No separate database server required
- Single-writer concurrency model
- Limited horizontal scaling
- Not ideal for distributed systems
Connection Management
Singleton Pattern:- Lazy Initialization: Database connection created on first access
- Foreign Keys: Enabled for referential integrity
- Single Connection: All repositories share one connection (SQLite limitation)
Schema Migrations
Version-Controlled Migrations:- Repeatable Deployments: Same migrations run in all environments
- Version Tracking: Know which schema version is deployed
- Rollback Safety: Migrations never re-run
- Team Coordination: Avoid schema conflicts
Repository Pattern Implementation
Architecture
Interface Layer
Repositories implement focused interfaces:- Domain-Oriented: Methods operate on domain entities, not database rows
- CRUD Operations: Standard create, read, update, delete
- Custom Queries: Add domain-specific finders (e.g.,
findByEmail) - Null Safety: Return
nullwhen not found (explicit handling)
Concrete Implementation
Example: VoterRepositoryObject-Relational Mapping
Manual Mapping Approach: Rather than using an ORM (TypeORM, Prisma), Consensus uses manual mapping:| Aspect | Manual Mapping | ORM |
|---|---|---|
| Learning Curve | Minimal (just SQL) | Steep (ORM API) |
| Transparency | See exact SQL | Hidden queries |
| Performance | Optimal queries | May generate inefficient SQL |
| Flexibility | Full SQL control | Limited to ORM features |
| Debugging | Straightforward | ORM abstraction complexity |
- More boilerplate code
- No automatic migrations from entity changes
- Manual handling of relationships
- No runtime ORM overhead
- Easier to optimize queries
- Simpler stack for small team
Database Schema
Core Tables
Voters Table:Audit and Admin Tables
Audit Logs:Data Access Patterns
Transaction Support
SQLite supports transactions for atomic operations:Prepared Statements
All queries use prepared statements for security and performance:- SQL Injection Prevention: Parameters sanitized
- Performance: Statement compilation cached
- Type Safety: TypeScript types for parameters
Data Type Handling
SQLite Type Mappings:| TypeScript Type | SQLite Type | Storage |
|---|---|---|
string | TEXT | UTF-8 string |
number | INTEGER | 64-bit integer |
boolean | INTEGER | 0 or 1 |
Date | TEXT | ISO 8601 string |
enum | TEXT | Enum value as string |
Array | TEXT | JSON serialized |
Repository Catalog
VoterRepository
Purpose: Manage voter accounts Key Methods:save(voter)- Create new voterfindById(voterID)- Lookup by IDfindByEmail(email)- Lookup for loginupdate(voter)- Update voter detailsdelete(voterID)- Remove voterfindAll()- List all voters
ElectionRepository
Purpose: Manage elections Key Methods:save(election)- Create new electionfindById(electionID)- Get specific electionfindActive()- Get currently active electionsupdate(election)- Update election (status changes)delete(electionID)- Remove draft electionfindAll()- List all elections
CandidateRepository
Purpose: Manage candidates Key Methods:save(candidate)- Add candidate to electionfindById(candidateID)- Get specific candidatefindByElectionId(electionID)- Get all candidates for electionupdate(candidate)- Update candidate detailsdelete(candidateID)- Remove candidate
BallotRepository
Purpose: Store anonymous votes Key Methods:save(ballot)- Store a cast ballotfindByElectionId(electionID)- Get all ballots for resultscountByElectionId(electionID)- Get vote countdeleteByElectionId(electionID)- Clear votes (admin function)
findByVoterId - ballots are anonymous!
VoteConfirmationRepository
Purpose: Track voter participation Key Methods:save(confirmation)- Record vote confirmationfindByVoterId(voterID)- Get voter’s confirmationsfindByElectionId(electionID)- Get all confirmations for electionexists(voterID, electionID)- Check if voted
VoterEligibilityRepository
Purpose: Track eligibility and voting status Key Methods:addEligibility(voterID, electionID)- Make voter eligiblemarkVoted(voterID, electionID)- Record that voter votedhasVoted(voterID, electionID)- Check voting statusgetEligibleVoters(electionID)- List eligible voters
AuditLogRepository
Purpose: Immutable event logging Key Methods:save(entry)- Record audit eventfindAll()- Get full audit trailfindByElectionId(electionID)- Get election-specific logscount()- Get total audit entries
AdminRepository
Purpose: Manage admin accounts Key Methods:save(admin)- Create admin userfindById(adminID)- Get admin by IDfindByUsername(username)- Lookup for loginfindAll()- List all adminsupdate(admin)- Update admin detailsdelete(adminID)- Remove admin
SettingsRepository
Purpose: Store system configuration Key Methods:set(key, value)- Update settingget(key)- Retrieve setting valuegetAll()- Get all settings as object
- Maintenance mode
- Banner messages
- Feature flags (signup enabled, login enabled)
Testing Strategy
In-Memory Databases
- Fast test execution (no disk I/O)
- Isolated test environment
- Automatic cleanup (in-memory database discarded)
Repository Mocking
For unit tests, mock repository interfaces:Performance Considerations
Indexing Strategy
Indexes to Add:Query Optimization
Current Approach:- Simple queries - optimization not critical at current scale
- Prepared statements cache compilation
- Foreign keys enforce referential integrity
- Add indexes as data volume grows
- Use
EXPLAIN QUERY PLANfor slow queries - Consider materialized views for complex reports
Connection Pooling
Not Applicable: SQLite uses a single connection (file-based locking). Connection pooling is not beneficial and can cause lock contention.Database Backups
Simple Backup Strategy:- Before each election goes active
- Daily during active voting periods
- After election closes and results are finalized
Migration to Production Database
When to Switch from SQLite:- Concurrent voters > 100 simultaneous
- Need for horizontal scaling
- Geographical distribution
- Regulatory requirements for specific DBMS
- Implement PostgreSQL repositories (same interfaces)
- Update DatabaseConnection to use pg/postgres driver
- Convert schema migrations to PostgreSQL syntax
- Export data from SQLite, import to PostgreSQL
- Update deployment to include database server
Next Steps
- Domain Model - Entities persisted by repositories
- Design Patterns - How patterns use repositories