Skip to main content
Version: 2026-08-19 (archived)

App data storage

Voshi gives your app four JSON storage rows for every launch — scoped to the course, the placement, the student, and the student-on-placement. Many apps can use them as their entire persistence layer: no database, no user table, no schema migrations.

Each row holds one JSON object that is entirely yours. Voshi never reads or interprets it.

The four scopes​

Every launch JWT carries a storage claim with the URLs of the four rows relevant to that launch:

"storage": {
"context": "https://link.voshi.com/lti13/v1/contexts/IcT91mBxze/apps/I4ppXy/data",
"location": "https://link.voshi.com/lti13/v1/contexts/IcT91mBxze/apps/I4ppXy/locations/Il0c8n/data",
"member": "https://link.voshi.com/lti13/v1/contexts/IcT91mBxze/members/In4kDp7yZq/apps/I4ppXy/data",
"member_location": "https://link.voshi.com/lti13/v1/contexts/IcT91mBxze/members/In4kDp7yZq/apps/I4ppXy/locations/Il0c8n/data"
}

Each value is the row's full URL — read and write the row by calling that URL directly with GET, PUT, or PATCH. Use it exactly as given and treat it as opaque: don't parse it or construct these URLs yourself.

ScopeOne row per...Typical use
contextcourseCourse-wide settings: term dates, module visibility, instructor preferences
locationplacement (location × course)Per-assignment configuration: due date overrides, question pool selection
memberuser × courseThe student's course-wide state: profile choices, cumulative progress
member_locationuser × placementThe student's work on one assignment: answers, attempts, submission state

The rows are created automatically on first launch — the URLs in the claim always point at rows that exist.

Authenticating: the api claim​

The App Data API authenticates as the launching user, not as your app. The launch JWT's api claim gives you the credential:

"api": {
"domain": "api.link.voshi.com",
"token": "8f14e45fceea167a5a36dedd4bea2543"
}

Send the token as a Bearer header on every call:

Authorization: Bearer <api.token>
note

The token is tied to the launching user's Voshi session (which lasts about a week). Treat it as per-launch: capture it at launch time, use it server-side during the user's visit, and take a fresh one from their next launch. Don't embed it in client-side code — it acts as that user.

Who can access what​

Access is enforced per row, based on who the token belongs to:

  • Members of the course can read and write the shared context and location rows.
  • Only the user themself can access their member and member_location rows — one student can never touch another student's rows.
  • Your app's builders (members of the app in the dashboard) can access any row belonging to the app, which is handy for debugging.
warning

The shared context and location rows are writable by any member of the course — including students. Don't store anything there that a student shouldn't be able to change; keep instructor-only state guarded by your own role checks, or in your own database.

Requests for rows outside these rules return 403; a row URL that doesn't exist or doesn't belong to your app returns 404.

Semantics worth knowing​

  • Reads return your object exactly as you last wrote it. A never-written row returns {}.
  • Writes are atomic per row. Concurrent writers serialize — no torn writes. For read-modify-write flows, prefer PATCH so you only touch the keys you mean to change.
  • Course data rolls forward. When a school starts a new term and the LMS creates a fresh course from an old one, Voshi copies the previous course's context and location data into the new rows (once, on first launch). Instructor configuration survives the semester boundary. member and member_location rows do not roll forward — each student starts clean.
  • No fixed size limit is enforced on the JSON, but the practical request-size ceiling is a few megabytes. This is configuration-and-state storage, not a file store — keep rows small and lean.

Example: saving a student's progress​

import requests

def save_progress(launch, question, answer):
requests.patch(
launch["storage"]["member_location"],
headers={"Authorization": f"Bearer {launch['api']['token']}"},
json={"patch": [
{"op": "add", "path": f"/answers/{question}", "value": answer},
]},
).raise_for_status()

Full endpoint details: App Data API reference.