Storage
Your tool has four places to keep data, and most tools need no other persistence at all — no database, no accounts, no schema. Each is one JSON object that is entirely yours; Voshi never reads or interprets it.
// Save a student's answer
await this.api.storage.memberLocation.update({ q3: 'B', lastSaved: Date.now() })
// Read it back on the next visit
const work = await this.api.storage.memberLocation.get() // {} if nothing was ever saved
The four scopes
| Scope | One object per… | Typical use |
|---|---|---|
api.storage.course | course | Course-wide settings the instructor made: term dates, which modules are open |
api.storage.location | location × course | The instructor's setup of one activity: the word list, the due date, the question pool |
api.storage.member | person × course | The student's state across your whole tool in this course: preferences, cumulative progress |
api.storage.memberLocation | person × location × course | The student's work on one activity: answers, attempts, the saved draft |
The two member scopes are the student's own. The course and location scopes are shared by everyone in the course, which is why only staff can write them.
Every scope points at the right object for this launch: this course, this location, this person. Your tool never says which course or which student it means — the launch already did.
Who can write what
student | instructor / assistant / manager | |
|---|---|---|
course, location | read | read and write |
member, memberLocation | read and write (their own) | read and write |
A student writing course or location throws a VoshiPermissionError. So course-wide state that students need to change has to live in their own member object instead.
Course staff read and write the same member and memberLocation objects — their own, in a staff launch. Reading a particular student's work from an instructor launch is not something the client does today; design an instructor's review view around the shared scopes, or around what the student's own launch wrote into them.
The methods
Each scope has the same four methods. All are async and throw on failure.
get()Promise<object>The stored object, or {} when nothing has been stored yet.
set(data)Promise<void>Replace the whole object with data.
update(changes)Promise<void>Merge changes into the object: each top-level key in changes is set, and other keys are left alone. A key whose value is undefined is removed. Prefer this over get-then-set for small changes — it is one request, and it does not overwrite a key another tab or the instructor changed in between.
patch(operations)Promise<void>Apply a JSON Patch (RFC 6902): an array of operations like { op: 'add', path: '/answers/-', value: 4 }. For changes deeper than the top level, such as appending to a list or setting one key inside a nested object. Missing intermediate paths are created for you, and a remove or replace of a path that does not exist is tolerated.
// An instructor saves the activity's setup
if (this.api.isStaff) {
await this.api.storage.location.set({ words: ['asset', 'liability', 'equity'], shuffle: true })
}
// A student appends an attempt without touching anything else
await this.api.storage.memberLocation.patch([
{ op: 'add', path: '/attempts/-', value: { score: 0.8, at: Date.now() } },
])
Size
The JSON object is limited to 512 KB on the two member scopes and 1 MB on the course and location scopes, measured as compact JSON. That is configuration and state, not a file store. Anything larger — an image, a recording, an essay as a PDF, a big dataset — belongs in files, which each scope keeps beside its object.
Things worth knowing
- Reads return exactly what you last wrote. No envelope, no metadata.
- Writes are atomic. Two writes to the same object serialize; a
patchorupdatenever half-applies. - Copied courses keep their setup. When a school copies a course into a new term, the new course's
courseandlocationobjects start as copies of the old course's. The instructor's setup carries forward; students'memberobjects do not, because the students are new. - Nothing is ever torn down. Objects persist for as long as the course does. There is no API to delete them; the Sandbox's Reset is the one place data is discarded.
- Versions share storage. A course that upgrades to a newer version of your tool keeps its objects, so new code must read what old code wrote.