Examplary
Start for free
Developer docs

Storage

Your app can keep data on Examplary with storage, from any hook. Every call names whose data it is:

ScopeShared byFor example
storage.orgEveryone in the workspace the hook runs forWhich courses a workspace linked
storage.userThe user the hook runs for, in that workspaceA teacher's dismissed notices
storage.appEvery workspace that has your appA catalogue, or a list of workspaces to sync

Each scope has collections of small JSON documents, and blobs for files.

Collections

A collection is a named set of JSON documents, each with a key you choose. Give it a type to get that type back from every method:

lib/storage.ts
import { storage } from "@examplary/apps";
 
type LinkedCourse = { courseId: string; linkedAt: string };
 
export const linkedCourses =
  storage.org.collection<LinkedCourse>("linked-courses");
await linkedCourses.set("exam_123", {
  courseId: "abc",
  linkedAt: new Date().toISOString(),
});
await linkedCourses.get("exam_123"); // LinkedCourse | undefined
await linkedCourses.update("exam_123", { courseId: "def" }); // merges fields, throws when there's no document
await linkedCourses.delete("exam_123");
 
const { items, cursor } = await linkedCourses.list({ limit: 50 }); // sorted by key
for await (const { key, value } of linkedCourses.all()) {
  // fetches the next page when it needs to
}
  • Collection names use lowercase letters, digits and dashes, and start with a letter.
  • Keys are strings of up to 512 characters. Use crypto.randomUUID() when there's no natural one.
  • A document can be up to 100 KB of JSON.

Blobs

Blobs are files of any size, like exports or generated reports. Examplary gives every blob an ID when you store it, and that ID is the only way to reach it again, so keep it, for example in a collection:

import { storage } from "@examplary/apps";
 
const { id } = await storage.org.blobs.put(pdf, {
  contentType: "application/pdf",
});
await reports.set("2026-11", { blobId: id });
 
const blob = await storage.org.blobs.get(id); // { id, size, contentType, updatedAt, text(), arrayBuffer() } | undefined
await storage.org.blobs.delete(id);

A hook can store files of up to 100 MB itself. To hand a file to the browser, or let the browser upload a bigger one, use a signed link from an action:

// A download link, valid for 5 minutes (at most 24 hours)
const url = await storage.org.blobs.url(id, { expiresIn: 300 });
 
// An upload link without a size limit: the browser PUTs the file to `url`,
// with the same Content-Type, and the file then has this `id`
const upload = await storage.org.blobs.uploadUrl({ contentType: "text/csv" });

Where you can use it

Storage works in every hook while it runs, for the workspace and user it runs for. storage.user is only there when a user started the hook, like a dashboard page action; a cron job has no user.

Components can't use storage directly. Read and write it through your page's actions, so your app's code always decides what the current user may see, and storage.app never reaches a browser.

When data is removed

When a workspace removes your app, its storage.org and storage.user data goes with it, and a user who removes their own installation loses their storage.user data. storage.app data stays until you delete the app.

Looking at your data

While you run dev, the App storage panel of the developer toolbar shows your app's documents for your workspace, for you, and for every workspace.

To seed or check the documents every workspace shares, use the CLI in your app's folder. Add --scope org for your own workspace's documents, and --dev to use the copy that dev runs:

npx @examplary/cli storage set projects project-42 '{"title": "Feedback study"}'
npx @examplary/cli storage set projects project-43 --file project.json
npx @examplary/cli storage get projects project-42
npx @examplary/cli storage list projects
npx @examplary/cli storage delete projects project-42