maravilla-db
Maravilla DB
platform.env.DB is a MongoDB-shaped document store with secondary indexes and first-class vector search. Two backends: a Mongo-mode default and a per-tenant SQLite mode (set via the production-sqlite feature flag — vector search runs there).
import { getPlatform } from '@maravilla-labs/platform';
const db = getPlatform().env.DB;
Layer 1 (tenant isolation) is unconditional. If you've declared a resource in maravilla.config.ts whose name matches the collection name, Layer-2 policies gate every op too — including find, where rows that fail the policy are silently filtered out of the result set. See maravilla-policies.
CRUD basics
Insert
const id = await db.insertOne('users', {
name: 'Alex',
email: 'alex@example.com',
createdAt: Date.now(),
active: true,
});
// id is the auto-assigned _id (string)
Find
// All matching docs
const active = await db.find('users', { active: true });
// With pagination + sort
const recent = await db.find('posts',
{ published: true },
{ limit: 10, sort: { createdAt: -1 }, skip: 0 },
);
// Single doc
const me = await db.findOne('users', { email: 'alex@example.com' });
Filter operators supported: $eq (default), $ne, $gt / $gte / $lt / $lte, $in / $nin, $exists, $and, $or. Not supported: $regex, $where, $text.
Update
const result = await db.updateOne('users',
{ email: 'alex@example.com' },
{
$set: { lastLogin: Date.now() },
$inc: { loginCount: 1 },
$push: { ipHistory: '1.2.3.4' },
},
);
// result.modified — number of docs modified
Update operators: $set, $unset, $inc, $push, $pull, $addToSet. Field paths support dotted notation.
Delete
const { deleted } = await db.deleteOne('posts', { _id: postId });
const { deleted: many } = await db.deleteMany('users', { active: false });
Indexes — declarative + imperative
The recommended path is to declare indexes in maravilla.config.ts so they're versioned with your code and reconciled on deploy. See maravilla-config.
// maravilla.config.ts
database: {
indexes: [
{ collection: 'users', keys: [['email', 1]], unique: true },
{ collection: 'posts', keys: [['authorId', 1], ['createdAt', -1]] },
{ collection: 'sessions',
keys: [['createdAt', 1]],
expireAfterSeconds: 3600 },
],
},
You can also create indexes imperatively at runtime:
await db.createIndex('users', { keys: { email: 1 }, unique: true });
await db.createIndex('posts', { keys: [['authorId', 1], ['createdAt', -1]] });
await db.createIndex('sessions', { keys: { createdAt: 1 }, expireAfterSeconds: 3600 });
// Drop
await db.dropIndex('users', 'email_1');
// Inspect
const all = await db.listIndexes('users'); // includes both document and vector indexes
IndexSpec options:
keys—Array<[field, 1 | -1]>(preferred) orRecord<field, 1 | -1>unique— enforce uniqueness across the indexed columnssparse— shorthand forpartial: { <field>: { $exists: true } }partial— predicate using inline-literal operators only (no$regex)expireAfterSeconds— TTL on a single-field index over a unix-seconds field
Use tuple form for compound keys — JS object key order is not guaranteed across runtimes.
Vector search — first-class, with quantization
Vector indexes are real first-class citizens, not a roadmap item. The v1 surface includes int8 and 1-bit quantization, matryoshka (truncated query vectors), and multi-vector / late-interaction (ColBERT-style).
Register a vector index
// Declarative (preferred)
database: {
vectorIndexes: [
{ collection: 'products', field: 'embedding',
dimensions: 1536, metric: 'cosine', storage: 'int8' },
],
},
// Imperative
await db.createVectorIndex('products', {
field: 'embedding',
dimensions: 1536,
metric: 'cosine', // 'cosine' | 'l2' | 'hamming'
storage: 'int8', // 'float32' (default, 4B/dim)
// 'int8' (1B/dim, ~4× smaller, <2% accuracy loss)
// 'bit' (1bit/dim, 32× smaller; requires metric='hamming')
matryoshka: false, // allow query vectors shorter than `dimensions` (req. matryoshka-trained embeddings)
multiVector: false, // each doc holds an array of vectors (chunk/token level)
});
Inserts thereafter automatically sync the value at field into the ANN-indexed table inside the same SQLite transaction as the document write — your inserts stay atomic.
Pure vector search
const hits = await db.findSimilar('products', {
field: 'embedding',
value: queryEmbedding, // number[] for single-vector
k: 10,
metric: 'cosine', // optional override
minScore: 0.7, // drop hits below this normalized score
filter: { category: 'shoes' }, // optional metadata filter
limit: 10,
});
// hits: VectorSearchHit[] — your doc fields plus _score and _distance
Hybrid metadata + vector via find
const hits = await db.find('products',
{ category: 'shoes', stock: { $gt: 0 } }, // metadata filter
{
limit: 20,
vector: {
field: 'embedding',
value: queryEmbedding,
k: 50,
minScore: 0.6,
},
},
);
The metadata filter is applied alongside (or after, depending on the index type) the vector ranking.
Multi-vector / late interaction
await db.createVectorIndex('passages', {
field: 'token_embeddings',
dimensions: 128,
metric: 'cosine',
multiVector: true,
});
const hits = await db.findSimilar('passages', {
field: 'token_embeddings',
value: queryTokenEmbeddings, // number[][] — one vector per query token
k: 10,
queryMode: 'late-interaction',
aggregation: 'max-sim', // 'max-sim' (default) or 'sum'
});
Listing & dropping vector indexes
const vIxs = await db.listVectorIndexes('products');
await db.dropVectorIndex('products', 'embedding');
// { removed: true } if it existed
listIndexes(collection) returns both regular and vector indexes in a unified shape (kind: 'document' | 'vector').
Query patterns
"Get my stuff"
Combine the auth contract with a filter:
// Inside a route, after hooks.server.ts ran setCurrentUser(token):
const me = platform.auth.getCurrentUser();
const myPosts = await db.find('posts',
{ ownerId: me.user_id },
{ sort: { createdAt: -1 }, limit: 50 },
);
If your posts resource has policy auth.user_id == node.ownerId || auth.is_admin, you don't even need the explicit filter — the policy filters the result set for you.
Pagination
Use cursor-style on a stable sort key plus _id:
const page = await db.find('posts',
{ ...filter, ...(cursor ? { createdAt: { $lt: cursor } } : {}) },
{ sort: { createdAt: -1 }, limit: 20 },
);
const nextCursor = page.length === 20 ? page.at(-1)!.createdAt : null;
Atomic counters
await db.updateOne('users', { _id: userId }, { $inc: { visits: 1 } });
Single-document updates are atomic. Cross-document atomicity requires a workflow — see maravilla-workflows.
TTL'd documents
// In maravilla.config.ts
{ collection: 'sessions', keys: [['createdAt', 1]], expireAfterSeconds: 3600 }
A background sweeper deletes documents whose indexed unix-seconds field is older than expireAfterSeconds.
Pitfalls
- Empty results despite valid data? Almost always a missed
setCurrentUser(token)in your hook — the policy denies becauseauth.user_idis empty. See maravilla-auth. findreturns a subset of expected docs. Layer-2 silently filters denied rows. Useplatform.policy.setEnabled(false)in a known-safe block to confirm whether the policy is the cause.- Compound indexes from object literals. Prefer
[['a', 1], ['b', -1]]tuples — object key order is implementation-dependent. - Vector index storage mismatch.
storage: 'bit'requiresmetric: 'hamming'. Mixing them errors out at index creation.
Related skills
- maravilla-config — declarative index management
- maravilla-policies — gate per-collection ops
- maravilla-events —
onDb({ collection, op })handlers for derived state - maravilla-kv — when to prefer KV over DB
Full reference: https://www.maravilla.cloud/llms-full.txt.
More from maravilla-labs/maravilla-cli
maravilla-auth
Maravilla Cloud authentication. Use whenever wiring login/register/session, OAuth callbacks, resource policies, or hitting `platform.auth.*` APIs. Critical: the 3-step request-scoped contract (validate → setCurrentUser → can) — skipping any step silently breaks Layer-2 policies and owner-scoped reads return empty with no error.
12maravilla-events
Maravilla Cloud event handlers — files in `events/*.ts` auto-discovered by the framework adapter. Use to react to data changes (`onKvChange`, `onDb`), auth lifecycle (`onAuth`), schedule (`onSchedule`), queue messages (`onQueue`), realtime publishes (`onChannel`), deploy phases (`onDeploy`), object storage (`onStorage`), or arbitrary REN events (`defineEvent`). Run inside the Maravilla runtime with full platform access via `ctx`.
12maravilla-workflows
Maravilla Cloud durable workflows — replay-based, multi-step processes that survive restarts. Use whenever you need sleeps spanning minutes/hours/days, multi-step pipelines where each step's output feeds the next, waiting for external events, or strict step-history audit. `defineWorkflow` from `@maravilla-labs/functions/workflows/runtime` with `step.run`, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, `step.invoke`.
12maravilla-media-transforms
Async media + document derivations via `platform.media.transforms` and the declarative `transforms` block in `maravilla.config.ts`. Media: transcode video, thumbnail extraction, image resize/variants, OCR. Documents (.docx/.odt/.pptx/.xlsx/...): convert to PDF, render page thumbnails, generic format conversion, Markdown extraction (RAG-ready), single-file HTML with inlined images, image-replacement templating ({{TAG}} swap + named-object swap), QR-code injection. Use when ingesting user uploads that need normalised renditions, generating contracts/invoices from templates, or extracting structured content for LLMs. Critical: derived keys are content-addressed — `keyFor(srcKey, spec)` is known up front, before the worker starts, so clients can render placeholder UI without round-trips. Declarative config is the default; imperative `transforms.*` calls are for one-offs.
12maravilla-config
The `maravilla.config.ts` declarative project file. Use whenever creating or modifying auth resources, groups, relations, registration fields, OAuth providers, password/session policy, branding, database indexes, or media transforms. Reconciled into delivery on every deploy — partial adoption is supported (omit a section to leave it untouched).
11maravilla-push
Maravilla Cloud Web Push — server `platform.push.send/schedule/cancelScheduled/listScheduled` with idempotent keys and recurring `everySeconds`, browser `registerPush({ topics, userId, swPath })` from `@maravilla-labs/platform/push`. Use for browser notifications, scheduled reminders, recurring digests, and per-user fan-out by topic.
11