ichibaseichibase

Collections & queries

Read and write MongoDB collections through a familiar document API. Every call goes through your project's HTTP gateway — no native driver, no connection string, no replica set to manage. The gateway enforces your access policy and broadcasts changes over realtime.

Default-deny. Mongo refuses every request until you deploy a Mongo policy. With no _mongo_policy configured, all reads and writes are rejected. Write one first — see Mongo policies.

Enabling Mongo

Mongo is a per-project flavor. Create the project with the mongo flavor (or a flavor that includes Mongo) in the dashboard, and you get a Mongo-backed database alongside the rest of your project. Authenticate with one of your project keys, sent in the apikey header: a publishable key ich_pub_… (role anon) for client apps, or a secret key ich_admin_… (role service_role) for your own backend and Edge Functions. To act as a signed-in end user, attach their JWT with asUser(token) — it is sent as Authorization: Bearer so your policy and realtime rules see the real user identity.

const ichi = createClient('https://<project>.ichibase.net', 'ich_pub_…');

// Every collection op runs as: ichi.mongo.collection(name).<op>(...)
const orders = ichi.mongo.collection('orders');

// Acting as the signed-in user — policy sees $auth.uid
const me = ichi.mongo.asUser(session.accessToken);
await me.collection('orders').insertOne({ total: 42 });
Every method returns { data, error } — it never throws on an HTTP error. Check error first; error is { code, detail?, status }.

Insert

insertOne(doc, opts?) returns { insertedId }; insertMany(docs, opts?) returns { insertedIds }. Both accept an optional { realtime: false } to skip the change broadcast for that write (honored only for the secret key).

const { data, error } = await ichi.mongo
  .collection('orders')
  .insertOne({ total: 42, customer: 'alice' });
if (error) return console.error(error.code, error.detail);
console.log(data.insertedId);

await ichi.mongo
  .collection('orders')
  .insertMany([{ total: 10 }, { total: 20 }]);

Find

find(filter?, opts?) returns an array; opts accepts projection, sort, limit, and skip. findOne(filter?) returns a single document or null. finddefaults to a limit of 50 and is hard-capped at your plan's max-docs ceiling.

const { data: orders } = await ichi.mongo
  .collection('orders')
  .find(
    { total: { $gt: 10 } },
    { sort: { created_at: -1 }, limit: 20, projection: { notes: 0 } },
  );

const { data: one } = await ichi.mongo
  .collection('orders')
  .findOne({ _id: '...' });

Count & distinct

count(filter?) returns { count }. distinct(field, filter?) returns the distinct values of a field across matching documents as { values, truncated }truncated is true when the result hit the plan cap.

const { data: c } = await ichi.mongo
  .collection('orders')
  .count({ customer: 'alice' });

const { data } = await ichi.mongo
  .collection('orders')
  .distinct('status', { customer: 'alice' });
console.log(data.values, data.truncated);

Update & replace

updateOne(filter, update, opts?) returns { matched, modified, upsertedId? } and accepts { upsert: true }; updateMany(filter, update, opts?) returns { matched, modified }. Both take update operators like $set and $inc. replaceOne(filter, replacement, opts?) swaps in a full document — no $set / $inc; keys starting with $ are rejected.

await ichi.mongo
  .collection('orders')
  .updateOne({ _id: 1 }, { $set: { status: 'paid' } }, { upsert: true });

await ichi.mongo
  .collection('orders')
  .updateMany({ customer: 'alice' }, { $set: { archived: true } });

await ichi.mongo
  .collection('orders')
  .replaceOne({ _id: 1 }, { total: 99, customer: 'bob' });

Delete

deleteOne(filter, opts?) and deleteMany(filter, opts?) both return { deleted }.

deleteMany() with an empty filter is refused by the gateway — always pass a real filter.
await ichi.mongo.collection('orders').deleteOne({ _id: 2 });

await ichi.mongo
  .collection('orders')
  .deleteMany({ status: 'cancelled' });

Atomic read-modify-write

findOneAndUpdate(filter, update, opts?) and findOneAndDelete(filter, opts?) both return { doc } (the document or null). findOneAndUpdate returns the post-update document by default; pass returnDocument: 'before' for the pre-update snapshot, and supports projection, sort, and upsert. Use these for atomic counters and claim-a-row patterns on a single document.

const { data } = await ichi.mongo
  .collection('orders')
  .findOneAndUpdate(
    { _id: 1 },
    { $inc: { views: 1 } },
    { returnDocument: 'after' },
  );
console.log(data.doc);

await ichi.mongo
  .collection('orders')
  .findOneAndDelete({ _id: 999 });

Bulk writes

bulkWrite(ops, opts?) sends multiple writes in one HTTP round trip. Each op is one of insertOne, updateOne, updateMany, replaceOne, deleteOne, or deleteMany. The whole batch shares one policy check. Pass { ordered: true }to stop on the first error (default is unordered). The batch length is capped at your plan's max-docs ceiling.

const { data } = await ichi.mongo.collection('orders').bulkWrite(
  [
    { op: 'insertOne', doc: { total: 5 } },
    { op: 'updateOne', filter: { _id: 1 }, update: { $set: { status: 'paid' } } },
    { op: 'deleteOne', filter: { _id: 2 } },
  ],
  { ordered: false },
);
console.log(data.inserted, data.modified, data.deleted);

Aggregate

aggregate(pipeline)runs a read-only aggregation pipeline and returns the result documents. Results are bounded by your plan's max-docs ceiling.

const { data: stats } = await ichi.mongo
  .collection('orders')
  .aggregate([
    { $match: { status: 'paid' } },
    { $group: { _id: '$customer', spent: { $sum: '$total' } } },
    { $sort: { spent: -1 } },
  ]);

The realtime write flag

Every write method accepts { realtime: false } in its options object to skip the change broadcast for that one write — useful for bulk imports or system writes. This is honored only for the secret key (service_role); a publishable-key write can't silence other subscribers, so the flag is ignored there. The default is to broadcast.

await ichi.mongo
  .collection('orders')
  .insertMany(thousandsOfDocs, { realtime: false });

Banned operators

The gateway rejects server-side JavaScript and aggregation-write stages anywhere in the request body, and your policy cannot re-enable them:

$where  $function  $accumulator  $out  $merge
$currentOp  $collStats  $indexStats  $planCacheStats
$listSessions  $listLocalSessions

There are no multi-document transactions or sessions — each op is independent. Use bulkWrite for batched writes and findOneAndUpdate for atomic read-modify-write on a single document. Direct mongodb:// connections are not available; all access is through the HTTP gateway and your policy. And as noted above, deleteMany() with an empty filter is refused outright.

Realtime

Realtime on Mongo collections works via change emits: the gateway broadcasts an insert / update / delete event on every write it handles — no MongoDB change streams or replica set required. Mark a collection listenable, then subscribe over a WebSocket. A write made directly to MongoDB outside ichibase will not broadcast. Full setup, subscription, and per-subscriber authorization: Realtime.

Authorization is code. The publishable key only sets anon vs service_role — what each request can actually do is decided by your Mongo policy, which runs before every operation to allow, deny, or scope it. With no policy configured, every request is denied. Keep the secret key (ich_admin_…) server-side only.