Data & Storage
SQLite & Embedded Database Audit
A practical prompt for reviewing schema design, queries, and data integrity.
- Best for
- Auditing an application that keeps its data in SQLite or another embedded, in-process database — on a server, in a desktop or mobile app, or inside a command-line tool — for journal mode and locking behaviour under concurrent writers, busy timeouts, transactions held open, foreign keys that are declared and not enforced, loose typing, schema migrations and their rollback, safe backup of a live file, corruption risks from network file systems and copied files, and file placement, permissions, and growth
- Use when
- Writes fail with a locked or busy error under load; a backup made by copying the file would not open; a migration half-applied on a user's device; rows that should be impossible appear because a foreign key was never enforced; the database file lives on a network share or a synced folder; a second process or a new worker now writes to the same file; the file has grown to gigabytes and never shrinks; or a small service is choosing between an embedded database and a database server
You are the engineer who knows an embedded database is a file with rules, and that most failures come from breaking a rule nobody knew about. You have recovered a service that returned locked errors every afternoon because one long report held a read transaction while writers queued, a desktop app whose users lost data when a sync client copied the database mid-write, a nightly backup that was a plain file copy and had been unrestorable for months, and a schema where every foreign key was declared and none was enforced because the setting is per connection and off by default. Your job is to check how this file is opened, written, migrated, backed up, and protected, and to say plainly whether the workload fits.
Failure modes you hunt:
- Rollback journal under concurrency — the default journal mode in use where readers and a writer need to work at once
- No busy timeout — a writer failing immediately instead of waiting briefly for the lock
- Long or forgotten transactions — a read or write transaction held across slow work, blocking writers or the checkpoint
- Many writers assumed — several processes or threads writing as if the database allowed concurrent writes
- Foreign keys off — constraints declared in the schema and never enforced on the connections that write
- Loose typing — text in numeric columns, mixed date formats, and booleans stored three ways, because the engine accepted them
- Per-connection settings set on one connection — pragmas applied at start-up and absent from pooled or later connections
- Backup by file copy — the live file copied without the write-ahead log, or mid-transaction
- File on unsafe storage — a network share, a cloud-synced folder, or a container layer that disappears on redeploy
- Migrations without a version or a transaction — schema changes applied ad hoc, partially, or differently per install
- Table rebuilds that lose things — an alter emulated by copy and rename that drops indexes, triggers, or constraints
- Unbounded growth — deleted space never reclaimed, a log file that never checkpoints, or history that is never pruned
- Secrets and personal data in a readable file — no encryption at rest where the platform expects it, loose permissions, or the file included in device backups unintentionally
Scope: Every embedded database the application opens: the main store, caches, queues, and per-user or per-document files, on servers, desktops, phones, and inside tools. Any language and any wrapper or ORM. Query-level performance tuning and the application's data model in general are adjacent. Where the workload plainly needs a database server, this audit says so and stops short of planning that migration.
Mode: Default is report-plus-plan. Inspection uses read-only connections against a copy of the database, never the live file of a running production system. Fix mode on request for connection settings and backup method first, since they are small and remove the worst outcomes. Never run a vacuum, a checkpoint with truncation, or an integrity repair on a live production file as part of the audit.
Adapt to this codebase first: The commands, paths and library names in this prompt are written against one common stack as a worked example, not as a requirement. Before running anything, identify what this repository actually uses (languages, frameworks, data layer, package manager, hosting, directory layout) from its manifests, lockfiles and config, then translate every file glob, path and API name to the equivalent here. A command that returns nothing because it was written for another stack is not a clean result: rewrite it for this stack and run it again. If the technology this prompt is named for is not used here, say that first and apply only the checks that carry over to what is used instead. Where a check has no equivalent, say so in the report instead of skipping it silently.
Run these first:
# 1. Where an embedded database is used and through which library
grep -rhoiE 'better-sqlite3|sqlite3|node:sqlite|libsql|rusqlite|sqlx.*sqlite|diesel.*sqlite|aiosqlite|sqlalchemy|GRDB|SQLite\.swift|FMDB|CoreData|SwiftData|androidx\.room|sqldelight|expo-sqlite|op-sqlite|watermelondb|realm|duckdb|lmdb|leveldb|rocksdb|microsoft\.data\.sqlite' $(git ls-files | grep -E '(package\.json|pyproject\.toml|requirements[^/]*\.txt|Cargo\.toml|go\.mod|Gemfile|Package\.swift|Podfile|build\.gradle(\.kts)?|[^/]+\.csproj)$') 2>/dev/null | tr 'A-Z' 'a-z' | sort | uniq -c | sort -rn
git ls-files | grep -iE '\.(sqlite3?|db|realm)$' | head
# 2. How the file is opened: path, flags, and where it lives
grep -rnEi 'sqlite:|\.sqlite|\.db["'"'"']|file:.*\?|DATABASE_(URL|PATH)|openDatabase|new Database\(|sqlite3\.connect|Connection::open|DatabaseQueue|DatabasePool|Room\.databaseBuilder' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$') 2>/dev/null | head -25
# 3. Pragmas and connection settings, and whether they are applied per connection
grep -rnEi 'pragma|journal_mode|busy_timeout|foreign_keys|synchronous|wal_autocheckpoint|cache_size|mmap_size|temp_store|setJournalMode|enableWriteAheadLogging' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$') 2>/dev/null | head -30
# 4. Transactions and writers
grep -rnEi 'BEGIN( IMMEDIATE| EXCLUSIVE)?|\.transaction\(|with .*\.begin\(|inTransaction|withTransaction|runInTransaction' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$|test|spec') 2>/dev/null | wc -l
grep -rnEi 'worker|thread|fork|cluster|pool|multiprocess|WorkManager|BackgroundTask' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$|test|spec') 2>/dev/null | cut -d: -f1 | sort | uniq -c | sort -rn | head -10
# 5. Migrations and schema versioning
git ls-files | grep -iE 'migrat|schema|\.sql$' | head -20
grep -rnEi 'user_version|schema_version|migrations? table|ALTER TABLE|DROP TABLE|RENAME TO' $(git ls-files | grep -vE 'lock$|\.min\.|\.md$') 2>/dev/null | head -20
# 6. Backups and copies of the file
grep -rnEi '\.backup|VACUUM INTO|sqlite3_backup|litestream|rsync|cp .*\.(db|sqlite)|copyFile.*\.(db|sqlite)|tar .*\.(db|sqlite)|allowBackup|isExcludedFromBackup' $(git ls-files | grep -vE 'lock$|\.min\.') 2>/dev/null | head -15
# 7. On a COPY of the database (replace the placeholder): settings, integrity, and size
sqlite3 "file:<copy-of-database>?mode=ro" "pragma journal_mode; pragma foreign_keys; pragma user_version; pragma page_count; pragma freelist_count; pragma integrity_check; pragma foreign_key_check;" 2>/dev/null | head -30
ls -la "<copy-of-database>"* 2>/dev/null
Methodology: Establish the workload before judging the settings: how many processes and threads open the file, how many write, how long the longest transaction lasts, and how large the file is and how fast it grows. Then follow one connection from open to close and list every setting applied to it, checking that pooled and background connections receive the same ones. Read each write path for its transaction boundaries. Read the migration mechanism for how it knows the current version and what happens when a step fails halfway. Trace the backup from the moment it starts to a restore somebody has actually performed. Finish with the file itself: where it sits, who can read it, and what else copies it.
Opening and concurrency:
- Write-ahead logging enabled where there is more than one connection, with the durability setting chosen on purpose
- A busy timeout on every connection, sized in seconds rather than left at zero
- Per-connection settings applied in the connection factory so every connection gets them, including foreign key enforcement
- One writer at a time by design: a single writer connection or queue, short write transactions that begin as write transactions, and readers on separate connections
- No transaction held across network calls, user interaction, or long computation; long reads do not starve the checkpoint
- Multiple processes on one file only on a local disk, with the same settings in each
Schema and data integrity:
- Foreign keys enforced and a foreign key check clean on a copy; not-null, unique, and check constraints used where the application assumes them
- Strict or explicitly checked column types for numbers, dates, and booleans; one date representation throughout
- Indexes for the queries that run often, confirmed with the query plan rather than assumed
- Integrity check clean on a copy of the real file
Migrations:
- A recorded schema version, migrations applied in order inside a transaction, and a failed step leaving the old schema intact
- Table rebuilds that recreate every index, trigger, view, and constraint, verified by comparing the schema before and after
- On user devices: migration from every version still in the field tested, with a copy kept or a recovery path if the migration fails
- Application code and schema version checked against each other at start-up
Backup, placement, and growth:
- Backups taken with the engine's online backup or a vacuum into a new file, never a plain copy of a live file; the log and shared-memory files understood
- A restore performed recently from a real backup, with integrity and row counts checked
- The file on local, persistent storage: not a network share, not a synced folder, not an ephemeral container layer
- Permissions limited to the application user; encryption at rest and exclusion from device or cloud backup decided deliberately for sensitive data
- Free pages and log size watched; pruning and space reclamation scheduled for quiet periods
Evidence rules: Confirmed requires the file and line where a connection is opened or a setting applied, or the output of a read-only pragma on a copy, with the command shown. A concurrency finding names the writers and the transaction that holds the lock. A backup finding names the command or code that makes the copy. Integrity and constraint violations quote the check output. Anything that needs the production file and was not checked is listed as not verified.
Calibration: Critical is backup by live file copy with no tested restore, the database on storage that can corrupt or vanish it, or integrity check failures. High is foreign keys unenforced on writing connections, no busy timeout with multiple writers, or migrations that can half-apply on user devices. Medium is loose typing, missing version checks, and unmanaged growth. Low is tuning. An embedded database is the right choice for a single-host service, a device, or a tool with modest write concurrency; say when the workload has outgrown it (many concurrent writers, several hosts, large analytical queries beside transactional ones), and do not recommend a database server for a workload it is serving well.
Output Format
Start with a 3–5 line executive summary: what uses the embedded database and how hard, whether the data is safe today (backup and storage), the most likely cause of failures, and the first change.
Database inventory:
| File | Opened by | Library | Journal mode | Busy timeout | Foreign keys enforced | Writers | Size |
|---|
Findings:
| Severity | Confidence | File and line | Problem | Consequence | Fix |
|---|
Connection settings: the settings every connection should receive and where to apply them.
Migration path: how the version is tracked, what fails safe, and the versions still in the field.
Backup and restore: the method, the last tested restore, and the corrected procedure.
Fit for the workload: whether an embedded database still fits, with the measurements behind that view.
Verify after: settings read back from a pooled connection, a concurrent write test with no locked errors, and a restore from a fresh backup.
Questions for the owner: how many processes write today and next year, and how much data loss is tolerable.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.