Skip to Content
HostingManaged Data

Managed Data

Every hosting project can have its own data stores — a SQL database (D1), a key-value store (KV), and object storage (R2) — plus FlexDB, a MongoDB-like document layer your app code can use with no SQL or schema. You provision each store once, and your deployed Worker reads it through an env binding.

The one rule that trips everyone up

Provisioning a data store binds it on the next ship, not the current one.

Provisioning records the resource server-side, but the live Worker can’t see it until you re-ship. So the flow is always provision → re-ship → binding is live:

deploy.db_provision {projectId} # creates the D1, records it — env.DB not live yet yolo deploy # re-ship → env.DB is now bound into the Worker

Don’t expect env.DB, env.KV, or env.BUCKET to exist until you re-ship after provisioning. (Secrets are the one exception — see Secrets & Env.)

Provisioning stores

Provision a store from the project’s Resources tab in the hosting console, or through the deploy.* tools (an agent runs these for you):

StoreToolDefault binding
SQL database (D1)deploy.db_provision {projectId}env.DB
Key-value store (KV)deploy.kv_create {projectId}env.KV
Object storage (R2)deploy.bucket_create {projectId}env.BUCKET

You can override any binding name (uppercase letters, numbers, and underscores) if your code expects a different one or you provision two of a kind.

Reading them in the Worker:

// D1 (SQL) const { results } = await env.DB.prepare("SELECT * FROM todos WHERE done = ?").bind(0).all(); // KV await env.KV.put("session:" + id, JSON.stringify(data), { expirationTtl: 3600 }); const raw = await env.KV.get("session:" + id); // R2 await env.BUCKET.put("uploads/" + name, fileStream); const obj = await env.BUCKET.get("uploads/" + name); // obj.body is a stream

Plan limits

Plan caps on data stores are defined but not currently enforced on YOLO Host, so provisioning isn’t refused today. Once they are enforced, provisioning past your plan’s cap returns a quota-exceeded refusal: on the free plan you get one D1 database and one KV namespace, and R2 object storage requires a paid plan; paid plans raise these caps. KV is created lazily, so only provision it when your app actually uses it.

FlexDB: the default way to store data

For most apps that just need to “save some data” — todos, users, posts, settings — reach for FlexDB instead of writing SQL. It’s a MongoDB-like document store (@yolo-labs/flexdb) that runs inside your Worker on top of the project’s provisioned D1. No SQL, no schema, no migrations — you import it and write documents.

The flow is the same provision → write code → ship pattern, done in one ship so the binding and your code go live together:

deploy.db_provision {projectId} # creates the D1 that FlexDB stores into # ... write the Worker code below (no npm install needed) ... yolo deploy # one ship → env.DB binding + your FlexDB code both go live

No install step. When yolo deploy builds a plain source Worker (an entry like src/index.ts with no separate build step), it bundles FlexDB into your Worker for you — you do not npm install @yolo-labs/flexdb. Just import it. (Projects that run their own build first — Next.js, or a Vite/tsc build — should use raw D1 SQL for now.)

import { FlexDB } from '@yolo-labs/flexdb'; // bundled by yolo deploy — no install let db; // cached across requests export default { async fetch(req, env) { db ??= new FlexDB(env.DB); // env.DB = the project's provisioned D1 const todos = db.collection('todos'); await todos.insertOne({ title: 'ship it', done: false, createdAt: Date.now() }); const open = await todos.find({ done: false }).sort({ createdAt: -1 }).limit(20); return Response.json(open); }, };

Construct FlexDB inside the handler (where env exists), never at module top level. Write the FlexDB code before you ship — a ship only captures the code present at that moment, so adding the code after the first ship would mean a second deploy.

FlexDB API

FlexDB uses standard MongoDB syntax:

// reads await db.collection('users').findOne({ email: 'a@b.com' }); await db.collection('posts').find({ status: 'published' }).sort({ publishedAt: -1 }).limit(20); await db.collection('posts').countDocuments({ status: 'draft' }); // writes await db.collection('todos').insertOne({ title: 'one', done: false }); await db.collection('todos').updateOne({ _id: id }, { $set: { done: true } }); await db.collection('counters').findOneAndUpdate({ name: 'visits' }, { $inc: { value: 1 } }); await db.collection('todos').deleteMany({ done: true });
  • Query operators: $gt / $gte / $lt / $lte / $ne, $in / $nin, $exists, $regex, $and / $or / $not / $nor. Dotted field paths ("user.age") work in queries, sorts, and indexes.
  • Update operators: $set, $unset, $inc, $push, $pull, $addToSet.

FlexDB scale and limits

FlexDB targets small-to-medium scale — roughly 10,000 documents per collection. Only _id lookups use the database index; every other field query scans the collection and filters in memory, so performance degrades as a collection grows. You can declare indexes on a collection to mark your hot fields, but in this version that’s forward-looking and doesn’t yet speed up value queries. Past ~10k documents per collection, move to a heavier datastore.

FlexDB v1 does not support aggregation pipelines, cross-collection joins, or multi-document transactions. If you need those, use raw SQL on D1.

Raw SQL on D1

When you specifically want relational SQL — joins, aggregates, a hand-written schema — use the same provisioned D1 directly, both from your Worker and from the yolo deploy db query command for inspection and seeding.

# read rows yolo deploy db query "SELECT count(*) AS n FROM todos"

yolo deploy db query (and the deploy.db_query MCP tool) is read-leaning:

  • SELECT works by default. INSERT / UPDATE / DELETE require an explicit write opt-in (allowWrite: true via the MCP tool).
  • Schema statements (CREATE TABLE, ALTER, CREATE INDEX), PRAGMA, and ATTACH are always blocked here.

So create your tables inside the Worker at runtime, not through db query. The Worker’s own env.DB binding has no such restriction — run an idempotent CREATE TABLE IF NOT EXISTS on first request or from an init route:

// in the Worker — runs once, idempotent await env.DB.exec( "CREATE TABLE IF NOT EXISTS todos (id INTEGER PRIMARY KEY, text TEXT, done INTEGER DEFAULT 0)" );

Treat db query as an inspection and seed-data tool: read rows and insert seed data (once the table exists), not a place to define schema.

Always use bound parameters (? placeholders) for any user-supplied value — never string-concatenate SQL.

Last updated on