Database Adapters
How Auvyra stays independent of its database.
Business logic talks to repository interfaces, never to a database driver.
Layout
packages/database/src/
interfaces/ DatabaseAdapter and the 12 repository contracts
schemas/ Zod schemas and the types inferred from them
repositories/ Shared helpers and the contract test suite
migrations/ The migration runner
adapters/ mongodb, postgresql, and sql for MySQL, MariaDB and SQLiteRepositories
Users, sessions, stations, news, articles, pages, presenters, shows, applications, settings, activity logs, integrations, media assets, listener samples, track likes, the song log and station records.
Station records hold everything listeners and staff create together: polls and votes, shoutouts, comments, competitions and entries, events, subscribers, newsletters and the email queue, alerts, listener accounts and their favorites. They share one store with a kind, a status and a JSON payload, so a new feature of this sort needs no migration on any engine. Each feature validates its payload in its own file under apps/web/lib/services/.
Each adapter also has a transfer, which reads and writes a station's rows as plain data. It is what lets a station move between engines, because rows keep their IDs and a row that clashes on an ID or a unique key is skipped. See Moving a Station.
Shared rules
- IDs are application-generated UUIDs
- Validation happens in the adapter with shared Zod schemas, so bad data and query operators are rejected the same way everywhere
- Pagination is
{ page, perPage }, withperPagecapped at 100 - Errors are
NotFoundError,ConflictErrorandDatabaseConnectionError, which never contain credentials - Methods reject rather than throwing synchronously
Indexes, transactions and migrations belong to each adapter.
Differences that adapters hide
| Concern | MongoDB | PostgreSQL | MySQL and MariaDB | SQLite |
|---|---|---|---|---|
| Client | Official driver | Drizzle ORM on postgres | mysql2 | Built into Node.js |
| Nested data | Documents | jsonb columns | JSON stored as text | JSON stored as text |
| Dates | Native | timestamptz | DATETIME(3) in UTC | Milliseconds as an integer |
| Expired sessions | TTL index | Cleared at each sign-in | Cleared at each sign-in | Cleared at each sign-in |
| Null ordering | Nulls sort last when descending | Requested with NULLS LAST | Nulls sort last when descending | Nulls sort last when descending |
| Text search | Escaped regular expression | Escaped ILIKE | Escaped LIKE on lowercased text | Escaped LIKE on lowercased text |
| Migration safety | Idempotent operations | Advisory lock in a transaction | GET_LOCK | One writer at a time |
No engine uses foreign keys or cascades, so behavior stays identical. Rules such as "a presenter with shows cannot be deleted" live in the services.
Contract tests
packages/database/src/repositories/contract-suite.ts defines behavior every adapter must satisfy, including CRUD, unique constraints, pagination, filtering, query-operator rejection, concurrent owner claims and connection failures. An engine is only advertised as supported once it passes.