SQLite Implementation
SQLite adapter implementation via Drizzle ORM for local and edge deployments.
On this page
SQLite support is the default for local development, testing, and edge environments. It provides a zero-config database experience using Drizzle ORM with the node:sqlite or bun:sqlite driver.
🎯 Implementation Architecture
The SQLite adapter enables a seamless transition from local development to production by implementing the same IDBAdapter interface as MongoDB and PostgreSQL.
Architecture Overview
Current Status
| Feature | Status | Notes |
|---|---|---|
| Adapter Class | 🟢 Complete | Environment-aware (Bun/Node) implementation |
| Schema Mapping | 🟢 Complete | SQLite-specific type mapping (INTEGER/TEXT) |
| Migrations | 🟢 Complete | Auto-creation of folders and tables |
| Setup Wizard | 🟢 Complete | selection and connection test implemented |
| Seeding | 🟢 Complete | Default data injection verified |
| Performance | 🟢 Platinum | 0.000 ms L1 hit; 0.338 ms miss; Atomic batching |
🚀 Performance Benchmarks (Verified)
SQLite delivers exceptional response times for local and edge deployments. For detailed performance metrics, stress test results, and cross-database comparisons, please refer to the Performance Benchmarks document.
Key SQLite highlights:
- Cache L1 hit at 0.000 ms, miss at 0.338 ms. INSERT 0.038 ms (14.2k RPS via raw
INSERT…RETURNING), FIND ONE 0.008 ms (100–119k RPS), UPDATE 0.080 ms (10.5k RPS). - WAL Mode enabled by default for concurrent read/write performance.
- Runtime Agnostic: Optimized for both
bun:sqliteandnode:sqlite. - findPage / count modes: Shared
SqlAdapterCorepath — FIND PAGE 0.073 ms vs legacy LIST+COUNT 0.191 ms (~2.6×); COUNT CACHED 0.024 ms (2026-08-04). See Performance Architecture. - Composite list index:
createModelprovisions(tenantId, status, updatedAt)— the canonical tenant list query (WHERE tenantId=? AND status=? ORDER BY updatedAt DESC LIMIT n) is served from one index. Measured at 100k rows: 194 → 68,346 RPS (EXPLAIN QUERY PLAN:SEARCH … USING INDEXvsSCAN … USE TEMP B-TREE FOR ORDER BY). - Raw
findById+ findMany id ultra path: pure{_id}reads go through prepared raw SELECT (~111k RPS viafindMany), bypassing Drizzle AST building.
Key Optimizations
-
WAL Mode: Write-Ahead Logging enabled by default for concurrent read/write performance.
-
Performance PRAGMAs: 9 optimized PRAGMAs applied on connect:
journal_mode = WAL— concurrent read/writesynchronous = NORMAL— 2-5x faster writes (safe with WAL)foreign_keys = ON— enforce referential integritypage_size = 8192— 8KB pagesbusy_timeout = 30000— 30s wait vs immediate SQLITE_BUSY errorstemp_store = MEMORY— temp tables in RAMmmap_size— RAM-tiered memory-mapped I/O (SQLITE_MMAP_SIZE, 64MB–512MB)cache_size— RAM-tiered page cache (SQLITE_CACHE_SIZE, 16MB–256MB)wal_autocheckpoint = 0— SQLite’s synchronous auto-checkpoint is disabled by default; an idle-gated background scheduler runswal_checkpoint(PASSIVE)off the request path instead (see the Scheduled WAL checkpoints bullet below). SetSQLITE_WAL_AUTOCHECKPOINT=<pages>to restore classic auto-checkpointing.
-
Scheduled WAL checkpoints:
wal_autocheckpoint=0plusstartWalCheckpointScheduler(src/services/background/wal-checkpoint.server.ts) keeps the checkpoint cost off waiting requests. The policy checkpoints when the server is idle, when the WAL exceeds a size cap (SVELTY_WAL_MAX_BYTES, default 64 MB) even under sustained load, and forces one after 60 s without an idle window. On by default; opt out withSVELTY_WAL_CHECKPOINT=0. Durability is unchanged —synchronous=NORMALstays the default. -
Prepared-Statement Cache: Drizzle’s
SQLiteBunSession.prepareQueryre-compiles SQL on every query; the adapter wrapsclient.preparewith a per-connection SQL-text cache (2,000 statements) invalidated on DDL (createModel/clearDatabase). Measured: ~4µs/query re-prepare tax eliminated, 2× on write ops. -
Projection (
options.fields): when all requested fields are physical columns, the rawfindByIdand sharedfindManyskip the JSONdatablob entirely — noJSON.parse+flattenDataColumnon list/detail reads. -
Atomic increments bind timestamps:
atomicIncrementbindsupdatedAtas a parameter instead of interpolatingnow.getTime()into SQL text — keeps the statement cache warm. -
Partial updates merge the JSON blob: the
datacolumn’sSETexpression becomesjson_patch(COALESCE("data", '{}'), ?)for scalar/array patches (one statement, statement-cache keyed on the merge flag); nested-object or explicit-nullpatches read the singledatacolumn and merge in JS first, becausejson_patchis RFC 7396 (recursive, and it deletes keys patched withnull) while the contract is MongoDB’s shallow, null-keeping$set. The merge is shared withcrud.updateMany,batch.bulkUpdateand query-builderupdateMany. See Write Path. -
Dynamic-field filters stay on the extraction form (
json_extract(data, '$."field"')): SQLite has no index that serves arbitrary JSON paths, so the engine-native answer for a filtered field is column materialization — declare itindexed: true(ormaterialize: true) and the row-store hybrid gives it a real column with a real index.json_extractis typed, so a numeric probe does not match a text-stored"101"(same as PostgreSQL containment and MongoDB; MariaDB is the looser exception). Paths are quoted per segment (json_extract(data, '$."meta"."lang"')), so a dotted field resolves the nested key instead of the single literal key"meta.lang"that previously matched nothing. -
Drizzle Integration: Type-safe querying without the overhead of a large ORM.
-
Typed Collection Proxy: Fully-typed access via
locals.cms.collections.typed.Posts.find(). -
Platinum SQL Batching: Raw multi-VALUES
insertMany(single atomic statement, chunked under the 999-param limit) — BULK INSERT (100) 177 → 248 RPS (+40%). -
Website Tokens:
website_tokenstable via sharedrelational-system.ts— hash-at-rest, tenant-scoped queries, parallel list+count. See Credential Storage. -
Mongo-only fast-path:
bypassSafeQueryapplies to selected MongoDB internal lookups, not SQL website-token operations. -
Zero-Allocation Dates: Optimized
relational-utilswith dirty-bit check. -
Atomic Versioning: Native
getVersionandincrementVersionsupport. -
Targeted partial UPDATE: shared
prepareUpdateValues()omits emptydatacolumns and never writescreatedAton UPDATE —SETis modified fields +updatedAtonly. RawUPDATE…RETURNINGstays one prepared statement. -
crud.aggregate(): MongoDB-style pipelines compile to one statement viacore/aggregation-translator.ts—$match,$group(scalar_idplus$sum/$avg/$min/$max/$count),$sort,$skip,$limit,$countand include-only$project. Stages SQL cannot express ($lookup,$unwind, compound$group._id, reordered stages) fail closed witherror.code === "NOT_SUPPORTED";$min/$maxstay type-correct here becausejson_extractkeeps numbers as numbers.
📂 Storage & File Management
SQLite is a file-based database. When running SveltyCMS with SQLite, the system defaults to storing the database in /config/database/SveltyCMS.db.
| File | Purpose | Git Status |
|---|---|---|
SveltyCMS.db |
The main database file containing all collections, users, and settings. | Ignored |
*.db-shm |
Shared Memory file used for WAL mode concurrency. | Ignored |
*.db-wal |
Write-Ahead Log containing recent uncommitted transactions. | Ignored |
Version Control Best Practices
Database files are included in .gitignore by default.
Committing binary database files to version control is discouraged as it leads to repository bloat and potential exposure of sensitive data.
Provisioning System & Self-Healing
The SQLite adapter implements a robust provisioning system that handles the complete lifecycle of the database file and schema:
- Idempotent Provisioning: The
provision()method checks the internal_provisionedstate and only executes schema creation (CREATE TABLE IF NOT EXISTS) if necessary. - Explicit Reset: During integration tests or system resets,
clearDatabase()drops all tables and resets the_provisionedflag, ensuring the next access re-creates a clean schema. - Path Auto-Creation: The adapter automatically detects if the
DB_HOSTdirectory exists and creates it recursively if missing, preventing “File not found” errors on new installations. - TTL-Equivalent Cleanup:
cleanupExpiredData()purges expired sessions/tokens (with consumed-token GC after 7 days) via parameter-bound DELETE — replicating MongoDB’s TTL indexes for SQL (same as MariaDB/PostgreSQL). - Boot Migration Lock: Schema migrations run once under
withMigrationLockatprovision()— multi-instance boots cannot race DDL. - Schema Fingerprint:
bootstrapSystemSchemacompares a sha-256 ofSYSTEM_SCHEMA(per dialect) stored insvelty_schema_stateand skips the whole DDL pass on a match (skipped: true) — measured on PostgreSQL: 118 statements / 124 ms per boot → ~2 ms. A spec change re-runs the pass, and a pass with a failed statement does not store the marker, so a half-applied schema is retried.
ESM-First Driver Loading
To comply with SvelteKit 5’s strict ESM requirements and avoid “require is not defined” errors:
- The adapter uses Dynamic Imports to load binary drivers.
- It detects the runtime environment (
BunvsNode.js) and imports the appropriate library (bun:sqliteornode:sqlite) at runtime. - This allows a single codebase to run seamlessly across local development (Bun) and production (Node.js) environments.
WAL Mode & Performance PRAGMAs
SveltyCMS enables WAL mode and 9 performance PRAGMAs on every connection. This provides concurrent read/write operations, 2-5x faster writes, resilience under load (busy_timeout), and enforced referential integrity.
🛠️ Setup & Configuration
1. Using the Setup Wizard (Recommended)
When using the Setup Wizard, select SQLite (via Drizzle). The wizard will automatically set:
- Host:
/config/database(The directory where the database file will be stored) - Database Name:
SveltyCMS.db(The filename)
2. Manual Configuration
If you are configuring the system manually, ensure your config/private.ts (or environment variables) contains:
// Example config/private.ts
export const privateEnv = {
DB_TYPE: "sqlite",
DB_HOST: "/config/database", // Directory path
DB_NAME: "SveltyCMS.db", // Filename
};
The adapter will resolve the final path as process.cwd() + DB_HOST + "/" + DB_NAME.
Test/benchmark mode fails closed without DB_NAME — when TEST_MODE, VITEST, BUN_TEST, or BENCHMARK is set and no explicit name is derivable (config DB_NAME or process.env.DB_NAME), the adapter refuses to connect instead of silently falling back to the live sveltycms.db name. Env DB_NAME is honored (the E2E/integration harnesses pass it when no private.test.ts exists yet, e.g. the setup-wizard boot); DB_PATH also short-circuits the check. See the benchmark isolation note in Benchmark Matrix Safety.
3. Implementation Pattern
The SQLite adapter follows the modular pattern established in the core infrastructure, ensuring all seeding and user data updates work seamlessly across the agnostic codebase.
🔗 Related Documentation
- Core Infrastructure - Unified architecture
- PostgreSQL Implementation - Similar Drizzle pattern
- Drizzle ORM Documentation