Database
Maravilla Database is a document store with a MongoDB-style query API — find/findOne, insertOne/insertMany, updateOne/updateMany, replaceOne, deleteOne/deleteMany, count, distinct, and an aggregate pipeline (with $lookup joins across collections), plus rich query operators ($gt, $in, $regex, $elemMatch, …), update operators ($set, $inc, $push, …), secondary indexes, TTL, and native vector search. Every Maravilla project automatically gets one database — you can have as many collections inside it as you like. It runs at the edge in production and locally during maravilla dev; your code is identical in both.
Collections are schemaless and created automatically on the first insertOne. There is no provisioning step — you don’t pre-create a database, you don’t pre-declare collections, you don’t run migrations to add fields. In the example below, the users collection springs into existence on the insertOne call.
To see what’s inside your database, run maravilla platform db — an interactive terminal UI for browsing collections. By default it targets your local dev server (so maravilla dev needs to be running); pass --cloud to inspect the deployed project’s cloud database instead. Add --collection users to jump straight into a collection.
import { getPlatform } from '@maravilla-labs/platform';
const platform = getPlatform();
await platform.DB.insertOne('users', { name: 'Alice', age: 30 });
const user = await platform.DB.findOne('users', { name: 'Alice' });
maravilla.config.ts is only needed when you want to declare indexes — see Indexes below.
Database or KV? Reach for the Database when you need to query data — filter by field, sort, paginate, join multiple criteria, or run vector search. Reach for KV when you already know the exact key and just need fast read/write by that key (sessions, feature flags, cached blobs).
Document shape
find and findOne return documents typed as DbDocument<T> — your own fields (T) plus a stable string id the runtime injects on insert. The same value is mirrored as _id for MongoDB-style call sites, and server-managed timestamps appear when present.
type DbDocument<T> = T & {
id: string; // stable string row id, injected on insert
_id: string; // mirror of id — always equal to id
_created_at?: string; // server-managed, when present
_updated_at?: string;
};
const user = await platform.DB.findOne<{ name: string }>('users', { name: 'Alice' });
user?.id; // "..." — same value as user._id
insertOne returns that same id as a string. Use it (or _id) in later findOne / updateOne / deleteOne filters. The platform’s internal row ids are never raw database object ids — id is the only identifier you address records by.
API Reference
find(collection, filter?, options?)
Finds multiple documents matching a filter.
Parameters:
collection(string) — the collection namefilter(object, optional) — document query filteroptions(object, optional):sort(object) — field-to-direction map (1for ascending,-1for descending). Multiple keys are applied in the order you write them.limit(number) — maximum documents to return (default/max: 1000)skip(number) — number of documents to skipprojection(object) — choose which fields come back:{ field: 1, … }returns only the listed fields,{ field: 0, … }returns everything except them.id/_idis always included unless you set_id: 0.
Returns: an array of matching documents.
const users = await platform.DB.find('users', { active: true }, {
sort: { createdAt: -1 },
limit: 20,
skip: 0
});
Sorting accepts more than one key — { status: 1, createdAt: -1 } orders by status ascending, then newest-first within each status. Missing or null values sort first when ascending, last when descending.
Projection trims each returned document to just the fields you need:
// Only name + email come back (plus id); everything else is omitted
const contacts = await platform.DB.find('users', { active: true }, {
projection: { name: 1, email: 1 },
});
// Return the whole document except the large `embedding` field
const docs = await platform.DB.find('products', {}, {
projection: { embedding: 0 },
});
findMany is an exact alias for find — same arguments, same result — when you prefer the clearer name for “expecting many rows”.
findOne(collection, filter)
Finds a single document matching the filter. Returns the document or null.
const user = await platform.DB.findOne('users', {
email: 'alice@example.com'
});
insertOne(collection, doc)
Inserts a single document. Returns the generated document ID as a string.
const id = await platform.DB.insertOne('users', {
name: 'Alice',
email: 'alice@example.com',
age: 30,
tags: ['premium', 'early-adopter']
});
console.log(id); // generated document ID string
insertMany(collection, docs)
Inserts an array of documents in one call. Returns an array of the generated IDs, in input order.
const ids = await platform.DB.insertMany('users', [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 },
{ name: 'Carol', age: 41 },
]);
console.log(ids); // ['…', '…', '…'] — one id per inserted document
updateOne(collection, filter, update, options?)
Updates a single document matching the filter. Supports update operators.
// Simple field replacement
await platform.DB.updateOne('users',
{ email: 'alice@example.com' },
{ age: 31, lastLogin: Date.now() }
);
// Using update operators
await platform.DB.updateOne('users',
{ email: 'alice@example.com' },
{ $set: { status: 'inactive' }, $inc: { loginCount: 1 } }
);
Returns { matchedCount, modifiedCount }. Pass { upsert: true } as a fourth argument to insert a document when nothing matches the filter — the new document is seeded from the filter’s equality fields and then the update is applied on top:
// Increments the counter, or creates { name: 'signups', value: 1 } if absent
await platform.DB.updateOne('counters',
{ name: 'signups' },
{ $inc: { value: 1 } },
{ upsert: true }
);
replaceOne(collection, filter, replacement, options?)
Replaces the entire first matching document with replacement (keeping the same id). Pass { upsert: true } to insert the replacement when nothing matches. Returns { matchedCount, modifiedCount }.
await platform.DB.replaceOne('users',
{ email: 'alice@example.com' },
{ email: 'alice@example.com', name: 'Alice', plan: 'pro' },
{ upsert: true }
);
updateMany(collection, filter, update)
Updates every document matching the filter. Returns { matchedCount, modifiedCount }.
// Mark all unverified accounts older than the cutoff as dormant
await platform.DB.updateMany('users',
{ verified: false, createdAt: { $lt: cutoff } },
{ $set: { status: 'dormant' } }
);
deleteOne(collection, filter)
Deletes a single document matching the filter.
await platform.DB.deleteOne('users', {
email: 'alice@example.com'
});
deleteMany(collection, filter)
Deletes every document matching the filter. Returns { deletedCount }.
const { deletedCount } = await platform.DB.deleteMany('events', {
createdAt: { $lt: thirtyDaysAgo }
});
count(collection, filter?)
Returns the number of documents matching the filter (the whole collection when the filter is omitted) — without fetching the documents.
const open = await platform.DB.count('tickets', { status: 'open' });
const total = await platform.DB.count('tickets');
distinct(collection, field, filter?)
Returns the unique values of field across matching documents. Array-valued fields are flattened, so a tags field of ['a','b'] contributes 'a' and 'b' individually.
const regions = await platform.DB.distinct('orders', 'region');
// ['eu', 'us', 'apac']
const openTags = await platform.DB.distinct('tickets', 'tags', { status: 'open' });
Query Operators
Comparison Operators
// $eq -- equals (implicit or explicit)
await platform.DB.find('users', { age: 30 }); // implicit
await platform.DB.find('users', { age: { $eq: 30 } }); // explicit
// $ne -- not equals
await platform.DB.find('users', { status: { $ne: 'deleted' } });
// $gt -- greater than
await platform.DB.find('products', { price: { $gt: 100 } });
// $gte -- greater than or equal
await platform.DB.find('users', { age: { $gte: 18 } });
// $lt -- less than
await platform.DB.find('products', { stock: { $lt: 10 } });
// $lte -- less than or equal
await platform.DB.find('users', { score: { $lte: 100 } });
// Combining operators on the same field
await platform.DB.find('products', {
price: { $gte: 50, $lte: 200 } // between 50 and 200
});
Array & Set Operators
These cover set membership (matching a field against a list of values) and querying fields that hold arrays. A filter on a field that holds an array matches by containment — MongoDB-style — so the common cases need no special operator: a plain equality or $in matches when the array contains the value.
// doc: { title: 'Intro to Rust', tags: ['rust', 'edge', 'wasm'] }
// Equality on an array field matches when the array CONTAINS the value
await platform.DB.find('posts', { tags: 'rust' });
// $in -- the field equals any listed value, or an array field contains any of them
await platform.DB.find('posts', { tags: { $in: ['rust', 'go'] } }); // array contains 'rust' or 'go'
await platform.DB.find('users', { status: { $in: ['active', 'vip'] } }); // scalar field equals 'active' or 'vip'
// $nin -- inverse of $in (also excludes array fields containing any listed value)
await platform.DB.find('users', { role: { $nin: ['admin', 'moderator'] } });
// $ne on an array field excludes documents whose array contains the value
await platform.DB.find('posts', { tags: { $ne: 'draft' } });
// $all -- array contains ALL of the listed values (in any order)
await platform.DB.find('posts', { tags: { $all: ['rust', 'edge'] } });
// $size -- match arrays of an exact length
await platform.DB.find('posts', { tags: { $size: 3 } });
// $elemMatch -- at least one array element matches a sub-query.
// Match scalar elements directly:
await platform.DB.find('readings', { samples: { $elemMatch: { $gte: 100 } } });
// …or match fields of array-of-objects elements:
await platform.DB.find('orders', {
items: { $elemMatch: { sku: 'A1', qty: { $gte: 2 } } },
});
The comparison operators ($gt, $gte, $lt, $lte) are array-aware too — { scores: { $gt: 90 } } matches when any element of the array satisfies the comparison.
Logical Operators
// $or -- matches if any condition is true
await platform.DB.find('products', {
$or: [
{ category: 'electronics' },
{ price: { $lt: 20 } }
]
});
// $and -- matches if all conditions are true (explicit)
await platform.DB.find('users', {
$and: [
{ age: { $gte: 18 } },
{ status: 'active' }
]
});
// Implicit $and -- multiple fields in a single filter
await platform.DB.find('users', {
age: { $gte: 18 },
status: 'active',
verified: true
});
// $not -- negate an operator expression on a field
await platform.DB.find('products', {
stock: { $not: { $gt: 0 } } // out of stock (stock not greater than 0)
});
// $nor -- matches when none of the conditions are true
await platform.DB.find('users', {
$nor: [
{ status: 'banned' },
{ status: 'deleted' }
]
});
Element Operators
// $exists -- check if a field exists
await platform.DB.find('users', {
email: { $exists: true } // documents that have an email field
});
await platform.DB.find('users', {
phone: { $exists: false } // documents without a phone field
});
// $type -- match by value type: 'string', 'number', 'bool',
// 'object', 'array', 'null'
await platform.DB.find('events', {
payload: { $type: 'object' }
});
String Operators
// $regex -- pattern matching
await platform.DB.find('users', {
email: { $regex: '.*@company.com' }
});
Update Operators
Use these with updateOne to perform targeted modifications instead of replacing entire documents.
$set — Set Field Values
await platform.DB.updateOne('users',
{ id: '123' },
{ $set: { status: 'inactive', updatedAt: Date.now() } }
);
$unset — Remove Fields
await platform.DB.updateOne('users',
{ id: '123' },
{ $unset: { temporaryFlag: '' } }
);
$inc — Increment Numeric Values
await platform.DB.updateOne('users',
{ id: '123' },
{ $inc: { loginCount: 1, score: 5 } }
);
$push — Add to Array
await platform.DB.updateOne('users',
{ id: '123' },
{ $push: { tags: 'verified' } }
);
$pull — Remove from Array
await platform.DB.updateOne('users',
{ id: '123' },
{ $pull: { tags: 'unverified' } }
);
$addToSet — Add to Array (No Duplicates)
await platform.DB.updateOne('users',
{ id: '123' },
{ $addToSet: { tags: 'premium' } } // only adds if not already present
);
Both $push and $addToSet accept the $each modifier to add several elements at once:
await platform.DB.updateOne('users',
{ id: '123' },
{ $push: { tags: { $each: ['verified', 'beta'] } } }
);
$mul — Multiply a Numeric Field
await platform.DB.updateOne('products',
{ id: '123' },
{ $mul: { price: 1.1 } } // 10% price increase
);
$min / $max — Keep the Smaller / Larger Value
Only writes the field when the supplied value is lower ($min) or higher ($max) than the current one — or when the field is missing.
await platform.DB.updateOne('scores',
{ id: '123' },
{ $max: { highScore: 4200 } } // only updates if 4200 beats the current high
);
$rename — Rename a Field
await platform.DB.updateOne('users',
{ id: '123' },
{ $rename: { 'fullName': 'name' } }
);
Aggregation & Joins
aggregate(collection, pipeline) runs an array of stages, each transforming the stream of documents from the previous stage. Use it for grouping/rollups and for joining collections with $lookup.
// Total revenue per region, highest first
const byRegion = await platform.DB.aggregate('orders', [
{ $match: { status: 'paid' } },
{ $group: { _id: '$region', revenue: { $sum: '$amount' }, orders: { $sum: 1 } } },
{ $sort: { revenue: -1 } },
]);
// [{ _id: 'eu', revenue: 9120, orders: 42 }, … ]
Stages
| Stage | What it does |
|---|---|
$match | Filter documents — same syntax as find’s filter. |
$sort | Order documents by { field: 1 | -1, … }. |
$skip / $limit | Offset and cap the stream. |
$project | Select/reshape fields — same include/exclude rules as find’s projection. |
$group | Bucket by _id (a field reference like '$region', or null for the whole set) and compute accumulators. |
$unwind | Emit one document per element of an array field. |
$lookup | Join in matching documents from another collection. |
Accumulators for $group: $sum, $avg, $min, $max, $count, $first, $last, $push, $addToSet. ({ $sum: 1 } counts; { $sum: '$amount' } totals a field.)
$lookup — joining collections
There are no foreign-key constraints — documents are independent — but $lookup resolves references between collections at query time. Give it the collection to pull from, the local field, the matching foreign field, and the output array field:
// Attach each author's books (books.authorId === authors.id)
const authorsWithBooks = await platform.DB.aggregate('authors', [
{
$lookup: {
from: 'books',
localField: 'id',
foreignField: 'authorId',
as: 'books',
},
},
]);
// [{ id: 'a1', name: 'Ada', books: [ {…}, {…} ] }, … ]
as is always populated with an array (empty when nothing matches). Combine $lookup with $unwind and $match to filter on joined data.
Indexes
Indexes make reads fast. Without them, the database scans every document in a collection for each query. With them, lookups on indexed fields are instant — even on large collections.
The database also indexes sort fields automatically: the first time you sort a collection by a field, it provisions a matching index in the background so later sorts on that field are fast. You should still declare indexes for the fields you filter on, and for uniqueness, compound, and TTL behavior — those are the ones that need to exist up front.
You have two ways to create indexes: declare them in maravilla.config.ts (recommended — they provision automatically on deploy and when the dev server starts), or create them at runtime with the imperative API.
Declarative: maravilla.config.ts
// maravilla.config.ts — at your project root
import { defineConfig } from '@maravilla-labs/platform/config';
export default defineConfig({
database: {
indexes: [
// Lookup users by email, and enforce uniqueness
{ collection: 'users', keys: { email: 1 }, unique: true },
// Compound index for "posts by author, newest first"
{ collection: 'posts', keys: [['authorId', 1], ['createdAt', -1]] },
// Only index published posts (partial index)
{
collection: 'posts',
keys: { category: 1 },
partial: { status: 'published' },
},
// Auto-delete expired sessions after 1 hour
{
collection: 'sessions',
keys: { createdAt: 1 },
expireAfterSeconds: 3600,
},
],
},
});
When you run maravilla dev or deploy your app, Maravilla reconciles the declared indexes into the database. Declared indexes are upsert-only — existing indexes with matching configuration are left alone, and removing a declaration never auto-drops an index (use dropIndex() or the CLI for that).
Imperative: createIndex(), dropIndex(), listIndexes()
For ad-hoc or test-only indexes, or when you need to create an index after your app is already deployed:
// Simple single-field index
await platform.DB.createIndex('users', {
keys: { email: 1 },
unique: true,
});
// Compound index — key order matters for performance
await platform.DB.createIndex('posts', {
keys: [['authorId', 1], ['createdAt', -1]],
});
// Partial index — only includes rows matching the predicate
await platform.DB.createIndex('posts', {
keys: { category: 1 },
partial: { status: 'published' },
});
// Sparse index — only includes documents where every key field is non-null
await platform.DB.createIndex('users', {
keys: { phoneNumber: 1 },
sparse: true,
});
// TTL index — auto-deletes old documents
await platform.DB.createIndex('sessions', {
keys: { createdAt: 1 },
expireAfterSeconds: 3600,
});
// List every index on a collection
const indexes = await platform.DB.listIndexes('users');
// Drop an index by name
await platform.DB.dropIndex('users', 'users_email_unique');
Index Options
| Option | Type | Description |
|---|---|---|
keys | object or [field, direction][] | Field(s) to index. 1 for ascending, -1 for descending. Use the tuple array for compound indexes to guarantee key order. |
unique | boolean | Reject inserts/updates that would create a duplicate in the indexed columns. |
partial | object | MongoDB-style filter. Only documents matching the filter are indexed. Supports $eq, $ne, $gt/$gte/$lt/$lte, $in/$nin, $exists, $and, $or. |
sparse | boolean | Shorthand for “only index documents where every key field exists”. |
expireAfterSeconds | number | TTL in seconds. The field must hold a Unix timestamp (seconds). Requires a single-field index. |
name | string | Optional custom index name. Falls back to an auto-derived name based on fields and options. |
When to Use Which Index
- Single-field equality (
{ email: 1 }) — the bread and butter. Use forfindOne({ email }),find({ email }). - Compound (
[['tenantId', 1], ['createdAt', -1]]) — speeds up queries that filter by the first field and sort by the second. Also handles queries that filter by only the first field. - Unique — enforces data integrity at the database level. Faster than a read-before-write check in your application.
- Partial — when you frequently query only a subset of documents (e.g., only published posts). Smaller index, faster lookups.
- Sparse — when a field is only present on some documents (optional fields). Avoids indexing
nulls. - TTL — for session tokens, ephemeral cache entries, or any data that should auto-expire.
Complex Query Examples
Pagination
const pageSize = 20;
const pageNumber = 2;
const results = await platform.DB.find('users',
{ active: true },
{
sort: { createdAt: -1 },
limit: pageSize,
skip: pageSize * (pageNumber - 1)
}
);
Compound Filters
// Find adult users in specific cities with premium status
const premiumAdults = await platform.DB.find('users', {
age: { $gte: 18 },
city: { $in: ['New York', 'Los Angeles', 'Chicago'] },
status: 'premium',
active: true
});
Nested Logical Operators
// Cheap products OR highly-rated electronics in stock
const products = await platform.DB.find('products', {
$or: [
{ price: { $lt: 20 } },
{
$and: [
{ category: 'electronics' },
{ rating: { $gte: 4.5 } },
{ inStock: true }
]
}
]
});
Optional Field Queries
// Users with no email field, or with a verified email
const users = await platform.DB.find('users', {
$or: [
{ email: { $exists: false } },
{ emailVerified: true }
]
});
Vector Search
The database natively supports vector search — semantic similarity queries over embeddings, with optional metadata pre-filtering and support for quantization, matryoshka embeddings, and multi-vector (ColBERT-style) indexes.
// Declare a vector index
await platform.DB.createVectorIndex('products', {
field: 'embedding',
dimensions: 1536,
metric: 'cosine',
});
// Hybrid search: metadata filter + vector similarity in one call
const hits = await platform.DB.find('products',
{ category: 'electronics', inStock: true },
{
vector: { field: 'embedding', value: queryEmbedding, k: 10 },
},
);
See Vector Search for the full API — quantization, matryoshka embeddings, multi-vector (ColBERT), and declarative config.
Type Handling
The platform correctly handles all standard JavaScript types:
| JavaScript Type | Supported |
|---|---|
| String | Yes |
| Number | Yes |
| Boolean | Yes |
| Object | Yes |
| Array | Yes |
| null | Yes |
Best Practices
- Write queries once — your code runs in any environment
- Leverage query operators — use
$gte,$in, etc. instead of fetching all documents and filtering in JavaScript - Always use
limit— avoid unbounded queries on large collections - Batch reads with
find— use filters instead of multiplefindOnecalls - Use
$setfor partial updates — avoid overwriting entire documents when updating a few fields - Declare indexes in
maravilla.config.ts— for every field you query on often. Indexes are the single biggest performance knob available to you.
Limits
| Parameter | Value |
|---|---|
| Max documents per query | 1,000 |
Next Steps
- Vector Search — semantic similarity queries with embeddings
- Platform Services Overview — every service available to your project
- KV Store API Reference — for simple key-value storage
- Storage API Reference — for file and object storage