Skip to content

Documentation

Database Methods Interface

Comprehensive reference for the IDBAdapter interface and its modular namespaces, including findPage, count modes, and shared count caching across MongoDB, PostgreSQL, MariaDB, and SQLite.

8/4/2026
9 min read Edit on GitHub

🎯 Architectural Vision

SveltyCMS leverages a Modular Namespace pattern to organize its database capabilities. Instead of a single monolithic adapter, functionality is divided into specialized domains. This ensures that as the system grows, the database layer remains maintainable, testable, and strictly type-safe.

Core Principle: Write once (in the application layer), deploy everywhere (MongoDB, SQL, etc.).

graph TD SDK[LocalCMS / API] --> Adapter[IDBAdapter] Adapter --> Auth[auth.*] Adapter --> Coll[collection.*] Adapter --> CRUD[crud.*] Adapter --> Cont[content.*] Adapter --> Media[media.*] Adapter --> Syst[system.*] Adapter --> Batch[batch.*]

πŸ› οΈ Global Interface Contract

All methods in the SveltyCMS database layer follow the DatabaseResult<T> pattern. They MUST NOT throw exceptions for expected failures; instead, they return a structured object.

export type DatabaseResult<T> =
  | { success: true; data: T; meta?: QueryMeta }
  | { success: false; message: string; error: DatabaseError };

πŸ›‘οΈ auth.* Namespace

Responsibility: Identity, Session Management, and RBAC.

Method Description
createUser(data, options) Creates a new user with encrypted password.
getUserById(id) Retrieves a user by their primary key.
getUserByEmail(email) Retrieves a user for login flows.
validatePassword(userId, password) Securely verifies password hashes.
createSession(userId, data) Generates a persistent session.
invalidateSession(sessionId) Revokes a specific session.
listUsers(options) Paginated retrieval of system users.

πŸ—‚οΈ collection.* Namespace

Responsibility: Dynamic Schema and Model Management.

Method Description
createModel(schema) Provisions tables or collections for a new content type.
listSchemas(tenantId) Retrieves all registered collection definitions.
updateModel(schema) Handles bi-directional schema synchronization.
deleteModel(id) Drops the physical storage and definition.

⚑ crud.* Namespace

Responsibility: High-performance, standardized data operations.

Method Description
find(collection, criteria, options) Advanced filtering (fused mapQuery β€” no IR).
findMany(collection, query, options) Multi-row read with limit/offset/fields/sort.
findPage(collection, query, options) Preferred list API β€” limit+1 β†’ { items, hasMore, nextCursor?, total? }.
findOne(collection, criteria) Single record retrieval fast-path.
findByIds(collection, ids, options) Batch PK lookup (relational widgets / loaders).
count(collection, query, options) Cardinality with mode: "exact" \| "estimate" \| "auto".
exists(collection, query, options) Existence check (LIMIT 1 / lean projection) β€” prefer over count > 0.
insert / insertMany Standard and bulk create.
update / updateMany Scoped updates with multi-tenant safety.
delete / deleteMany Permanent or soft-deletion.
upsert / upsertMany Atomic update-or-insert.
streamMany Async-iterable scan for export / migration.

findPage (shared product path)

Use for admin tables, media grids, and REST/GraphQL list connections. Avoids a default COUNT(*).

import type { FindPageResult } from "@src/databases/db-interface";

const res = await db.crud.findPage<Post>("posts", filter, {
  limit: 50,
  tenantId,
  sort: { updatedAt: -1 },
  fields: ["_id", "title", "status", "updatedAt"],
  total: "none", // or "exact" | "estimate" | "auto"
});

if (res.success) {
  const { items, hasMore, nextCursor, total, totalEstimated }: FindPageResult<Post> = res.data;
}
options.total Effect
"none" (default) No count query β€” use hasMore only
"exact" Always exact count (after page fetch)
"estimate" Stats/metadata when unfiltered + untenanted; else exact
"auto" Same eligibility as estimate

Files: core/sql-adapter-core.ts, mongodb/crud-methods.ts, core/page-utils.ts.

count modes & short-lived cache

// Exact (billing / RBAC)
await db.crud.count("posts", { status: "publish" }, { tenantId, mode: "exact" });

// Estimate when empty + no tenant (dashboard global size)
await db.crud.count("posts", {}, { mode: "estimate" });

// Force skip L1/L2 count cache
await db.crud.count("posts", filter, { tenantId, bypassCache: true });
Layer Module Role
Modes core/page-utils.ts β†’ shouldUseEstimateCount Shared eligibility (tenant-safe)
SQL estimate SqlAdapterCore.estimateTableRows PG reltuples, Maria TABLE_ROWS, SQLite sqlite_stat1
Mongo estimate MongoCrudMethods.count estimatedDocumentCount()
Cache core/count-cache.ts β†’ createCountCachedCrud 30s L1/L2; wired in db.ts after tenant guard
Invalidation BaseAdapter.invalidateQueryCache Clears count:{collection}:* + tag count:{collection}
Important

Tenant-scoped counts never use whole-table estimates. Prefer findPage + hasMore for list UIs; use cached exact counts for badges; use estimate only for unscoped dashboards.

See Performance Architecture β€” Product-layer list & count.


πŸ“¦ content.* Namespace

Responsibility: CMS Workflows (Nodes, Drafts, Revisions).

Method Description
nodes.getStructure(tenantId) Retrieves the hierarchical content tree.
nodes.upsertNode(node) Persists structural nodes (Menus, Categories).
drafts.create(data) Manages work-in-progress content nodes.
revisions.list(contentId) Retrieves historical audit trail and snapshots.

πŸ–ΌοΈ media.* Namespace

Responsibility: File Metadata and Organization.

Method Description
saveMetadata(data) Persists file size, dimensions, and SHA-256 hash.
getFilesByFolder(folderId) Paginated retrieval of media items.
updateMetadata(id, data) Updates Alt text, tags, or focal points.

βš™οΈ system.* Namespace

Responsibility: Preferences, Jobs, Multi-Tenancy, and external API credentials.

Method Description
preferences.get(key, scope) Retrieves User or System settings.
preferences.set(key, value) Persists settings with automatic cache invalidation.
tenants.create(data) Provisions a new isolated workspace.
themes.ensure(theme) Idempotent theme registration during setup.
websiteTokens.create(data, tenantId?) Creates a website token; returns plaintext once.
websiteTokens.getAll(options, tenantId?) Paginated list; hashes scrubbed from response.
websiteTokens.getByToken(token, tenantId?) Auth lookup by bearer value (hashed at adapter).
websiteTokens.getByName(name, tenantId?) Resolves a token record by display name.
websiteTokens.delete(id, tenantId?) Hard-deletes the credential (SQL) or permanent delete (Mongo).

πŸ” Credential Storage (Website Tokens & API Keys)

SveltyCMS treats website tokens and sck_ API keys as credentials, not user records. The database layer enforces the same invariants on all four engines while allowing engine-native optimizations.

Shared contract (ISystemAdapter.websiteTokens)

Invariant Behavior
Hash at rest Plaintext is hashed with hashCredentialSha256Hex() (src/utils/security/credential-hash.ts) before insert.
One-time reveal create() returns the raw token in the response; only the digest is persisted.
List scrubbing getAll() omits the token field from every row in the response.
Tenant scope Optional tenantId on all methods; SDK and handlers pass request context.
Auth alignment handle-authentication.ts calls getByToken(value, locals.tenantId) β€” same pattern as auth.getApiKey(hash, { tenantId }).

Per-engine implementation

Engine Module Storage Soft-delete List performance
MongoDB mongodb/website-token-methods.ts system_website_tokens collection; strict: true schema with tenantId, isDeleted safeQuery + { $ne: true } via MongoCrudMethods crud.findPage (limit+1 hasMore) + total: "exact" (count hits 30s L1 cache)
PostgreSQL / MariaDB / SQLite core/relational-system.ts website_tokens table via Drizzle Hard delete (no isDeleted column) Parallel select limit+1 + COUNT(*) (Promise.all); trim sentinel row

Indexes (all SQL engines + MongoDB)

  • Unique on token (hashed value) β€” O(1) bearer authentication lookup
  • Compound { tenantId, name } β€” tenant-scoped name resolution and admin list filters

MongoDB-specific notes

  • Mongoose strict: true requires every persisted field to be declared on the schema. Undeclared fields (e.g. tenantId) are silently stripped on insert and break tenant-scoped list queries.
  • Credential lookup without request tenant context uses bypassTenantCheck only (not bypassSafeQuery), so safeQuery still applies soft-delete boundaries.
  • Do not use bypassSafeQuery for routine website-token CRUD; it is reserved for other internal fast-paths documented in the MongoDB implementation guide.

SQL migration note

After pulling schema changes that add the { tenantId, name } compound index, run:

bun run db:push

πŸš€ Implementation Matrix (Status 2026)

Namespace MongoDB PostgreSQL MariaDB SQLite
Auth βœ… Production βœ… Production 🟑 Planned βœ… Production
CRUD βœ… Production βœ… Production 🟑 Planned βœ… Production
Content βœ… Production βœ… Production 🟑 Planned βœ… Production
Media βœ… Production βœ… Production 🟑 Planned βœ… Production
System βœ… Production βœ… Production 🟑 Planned βœ… Production
Batch βœ… Production βœ… Production 🟑 Planned βœ… Production
SCIM βœ… ProductionΒΉ βœ… ProductionΒΉ 🟑 PlannedΒΉ βœ… ProductionΒΉ

ΒΉ SCIM 2.0 endpoints are production-ready (RFC 7644: Users, Groups, Bulk, filters, PATCH, Okta/Azure) but route through the auth.* adapter directly rather than a dedicated IScimAdapter namespace.


πŸ““ Developer Best Practices

  1. Always use the Interface: Never cast IDBAdapter to a concrete class like SQLiteAdapter unless performing emergency maintenance.
  2. Favor LocalCMS: Use the SDK bridge in server files for 0ms internal latency.
  3. Respect tenantId: Ensure every query includes a tenant scope to maintain data isolation. For bearer auth, pass locals.tenantId into websiteTokens.getByToken() when the request tenant is already resolved.
  4. Error Checking: Always check result.success before accessing result.data.

Related Documentation

databaseinterfacearchitecturemethodscrud
Was this page helpful?