Database

Maravilla Database is a document store with a MongoDB-style query APIfind/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 name
  • filter (object, optional) — document query filter
  • options (object, optional):
    • sort (object) — field-to-direction map (1 for ascending, -1 for 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 skip
    • projection (object) — choose which fields come back: { field: 1, … } returns only the listed fields, { field: 0, … } returns everything except them. id/_id is 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

StageWhat it does
$matchFilter documents — same syntax as find’s filter.
$sortOrder documents by { field: 1 | -1, … }.
$skip / $limitOffset and cap the stream.
$projectSelect/reshape fields — same include/exclude rules as find’s projection.
$groupBucket by _id (a field reference like '$region', or null for the whole set) and compute accumulators.
$unwindEmit one document per element of an array field.
$lookupJoin 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

OptionTypeDescription
keysobject or [field, direction][]Field(s) to index. 1 for ascending, -1 for descending. Use the tuple array for compound indexes to guarantee key order.
uniquebooleanReject inserts/updates that would create a duplicate in the indexed columns.
partialobjectMongoDB-style filter. Only documents matching the filter are indexed. Supports $eq, $ne, $gt/$gte/$lt/$lte, $in/$nin, $exists, $and, $or.
sparsebooleanShorthand for “only index documents where every key field exists”.
expireAfterSecondsnumberTTL in seconds. The field must hold a Unix timestamp (seconds). Requires a single-field index.
namestringOptional 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 for findOne({ 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 }
  ]
});

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 TypeSupported
StringYes
NumberYes
BooleanYes
ObjectYes
ArrayYes
nullYes

Best Practices

  1. Write queries once — your code runs in any environment
  2. Leverage query operators — use $gte, $in, etc. instead of fetching all documents and filtering in JavaScript
  3. Always use limit — avoid unbounded queries on large collections
  4. Batch reads with find — use filters instead of multiple findOne calls
  5. Use $set for partial updates — avoid overwriting entire documents when updating a few fields
  6. Declare indexes in maravilla.config.ts — for every field you query on often. Indexes are the single biggest performance knob available to you.

Limits

ParameterValue
Max documents per query1,000

Next Steps