10Docs
Files & search
Two collection features for content-heavy apps: storage collections back files in R2 with the same path-scoped auth as data, and search declares full-text indexing on chosen fields.
Files, type: storage
A collection declared "type": "storage" is R2-backed: each document is a file (blob) addressed by its path, with metadata fields you declare. Auth is exactly the same rules model as any collection, so file access is scoped by path just like data.
{
"users/$userId/files/$fileId": {
"type": "storage",
"fields": { "name": "String", "owner": "String!", "size": "UInt" },
"rules": {
"read": "@user.id != null && $userId == @user.id",
"create": "@user.id != null && $userId == @user.id && @newData.owner == @user.id",
"update": "false",
"delete": "@user.id != null && $userId == @user.id"
}
}
}- The path scopes the file.
users/$userId/files/$fileIdwith$userId == @user.idmeans a user can only touch files under their own id. The path is the access boundary, enforced by the runtime rule. - Declared
fieldsare the file’s metadata (name, size, content-type, whatever you need); the bytes are stored separately in R2 and streamed. - Mark
owner(and any set-once metadata)!so it can’t be reassigned. - Storage collections are offchain.
Upload and download go through the SDK: setFile(path, file, { metadata }) uploads a File (or null to delete); getFiles(path) lists files under a path. The same path-scoped rules apply.
import { getCurrentUser, setFile, getFiles } from "@bounded-sh/client";
const user = getCurrentUser();
if (!user) throw new Error("Sign in first");
await setFile(`users/${user.id}/files/avatar`, file, {
metadata: { name: file.name, owner: user.id },
});
const { data: files } = await getFiles(`users/${user.id}/files`);Search, search: { fields: [...] }
Declare which String fields are full-text indexed. The runtime maintains the index; you query it through the data plane.
{
"orgs/$orgId/docs/$docId": {
"fields": { "org": "String", "title": "String", "body": "String" },
"tier": "durable",
"search": { "fields": ["title", "body"] },
"rules": {
"read": "@user.id != null",
"create": "@user.id != null",
"update": "@user.id != null",
"delete": "@user.id != null"
}
}
}fieldsmust be a non-empty array of valid field names.- Index only the fields you actually search; each adds write-time index cost.
- Search respects
readrules, results a caller can’t read are not returned.
Search is a query mode on the collection, combinable with filters and paging:
// SDK
import { search } from "@bounded-sh/client";
const hits = await search("orgs/o1/docs", "quarterly revenue", { fields: ["title", "body"] });
// CLI
// bounded data search --app-id <id> --path orgs/o1/docs --query "quarterly revenue"