jstash Apps

Give every person who signs in to your app their own private JSON documents. Your app keeps the login it already has; jstash checks it.

  • The one rule: a person’s documents can be read and written by that person, and nobody else. There’s no rules language, so there’s nothing to get wrong.
  • No server, no secret in the page. Browser code sends the login token your app already has. You never put a jstash API key in a web page.
  • Works with Supabase, Firebase and Google sign-in. GitHub sign-in works through Supabase or Firebase.
  • jstash never sees a password and never runs a sign-in. It only checks a signature your login provider already made, using the keys that provider publishes.
Apps are part of Pro, which is invite-only for now and free for invitees. Request an invite.

Set up an app

  1. In the dashboard, open Apps → New app and give it a name.
  2. Pick the login your app uses and paste one value you already have: your Supabase Project URL, Firebase Project ID, or Google OAuth client ID.
  3. Add your website address, like https://my-app.netlify.app (up to 5). Turn on Allow localhost while you develop.
  4. Copy the starter code from the app’s page. It already has your app’s address in it.

Each app has an endpoint like https://api.jstash.app/v1/apps/app_…/me. Documents live under it by name: …/me/players, …/me/settings.

Supabase

Paste your Project URL (the Connect button, or Project Settings → Data API). Then send the signed-in user’s access token:

const JSTASH = 'https://api.jstash.app/v1/apps/app_…/me';

async function token() {
  const { data } = await supabase.auth.getSession();
  return data.session.access_token;
}

export async function save(name, value) {
  const res = await fetch(`${JSTASH}/${name}`, {
    method: 'PUT',
    headers: { Authorization: `Bearer ${await token()}` },
    body: JSON.stringify(value),
  });
  if (!res.ok) throw new Error((await res.json()).error.message);
}

export async function load(name) {
  const res = await fetch(`${JSTASH}/${name}`, {
    headers: { Authorization: `Bearer ${await token()}` },
  });
  if (res.status === 404) return null; // nothing saved yet
  if (!res.ok) throw new Error((await res.json()).error.message);
  return res.json();
}
  • Your project needs JWT signing keys. Older Supabase projects sign logins with a shared secret, which jstash would have to hold to check. If yours does, the dashboard says so when you create the app: in Supabase, open Project Settings → JWT Keys and migrate to signing keys.
  • Anonymous sign-ins are refused unless you turn on Allow anonymous users for the app. Anyone can create those, and each one gets its own storage.
  • Supabase refreshes the token for you, so calling getSession() before each request is enough.

Firebase

Paste your Project ID (Project settings → General). Use the same save and load as above, with this token():

import { getAuth } from 'firebase/auth';
const auth = getAuth();

function token() {
  return auth.currentUser.getIdToken(); // Firebase refreshes it every hour
}

Firebase anonymous auth is refused unless you turn on Allow anonymous users.

Google

Paste the OAuth client ID you use for Sign In With Google. Its callback hands you an ID token in response.credential:

let idToken;

function onSignIn(response) {   // your Sign In With Google callback
  idToken = response.credential;
}

function token() {
  return idToken;
}

Google ID tokens last one hour and the page doesn’t refresh them, so this suits short sessions. For apps people keep open, use Supabase or Firebase — both offer “Sign in with Google” and refresh tokens for you.

No login yet?

Add one with Supabase Auth or Firebase Authentication. Both have free tiers, take minutes to add, and offer email, Google and GitHub sign-in. jstash doesn’t run sign-in itself: that’s the part best left to a specialist, and it means jstash never holds your users’ passwords or email addresses.

GitHub sign-in goes through Supabase or Firebase. GitHub gives apps an access key to the person’s GitHub account rather than a signed login token, so jstash can’t check it directly — and shouldn’t hold it.

Endpoints

Your users’ calls, each with Authorization: Bearer <login token>. They reach only the signed-in person’s documents.

GET/v1/apps/{appId}/meLogin token · Lists their documents: {"items": [{"name", "bytes", "version", "etag", "updated_at"}]}.
GET/v1/apps/{appId}/me/{name}Login token · The document, raw, with its ETag. 404 DOC_NOT_FOUND if nothing is saved yet.
PUT/v1/apps/{appId}/me/{name}Login token · Creates (201) or replaces (200) it. If-Match for safe updates, If-None-Match: * to create only.
DELETE/v1/apps/{appId}/me/{name}Login token · Deletes it. Safe to repeat.
DELETE/v1/apps/{appId}/meLogin token · Deletes all of their documents — for your app’s “delete my account”.

Names are 1–64 characters: letters, digits, -, _ and ., not starting with a dot. Bodies follow the same rules as bins: any JSON value, any Content-Type, stored byte for byte. If-Match works as described in Updating safely.

Your own calls, with your API key or the dashboard:

GET/v1/appsAPI key · Your apps, each with its users, documents, storage and today’s saves.
GET/v1/apps/{appId}API key · One app.
DELETE/v1/apps/{appId}/users/{userId}API key · Deletes one person’s documents, for deletion requests. {userId} is their ID in your login provider (the token’s sub).

Creating, changing and deleting apps happens in the dashboard only, so a leaked API key can’t open your app to other websites. You can’t read your users’ documents, and neither can jstash staff through the dashboard.

Website addresses

Browsers send the address of the page making a request. jstash refuses requests from addresses your app doesn’t list, with an error naming the address to add. List the scheme and host only — https://my-app.netlify.app, no path.

  • Allow localhost accepts http://localhost and http://127.0.0.1 on any port. Turn it off when you launch.
  • Mobile apps and servers don’t send an address, so the list doesn’t affect them. The login token is what protects your users.

Limits

Per appLimit
Apps per account3 (Pro)
Users5,000 per app
Saves50,000 a day per app
StorageShares your account’s 1 GB
Per userLimit
Documents200
Size1 MB per document
Saves60 a minute, 1,000 a day
Reads300 a minute
  • A user counts once they first save something. Reading doesn’t make someone a user.
  • Daily limits reset at 00:00 UTC. Saving an identical document doesn’t count.
  • If you leave Pro, your apps stay readable but stop accepting saves until you’re back.

Deleting data

  • A person deletes their account in your app: call DELETE /v1/apps/{appId}/me with their token.
  • Someone asks you to delete their data: use Delete user data on the app’s page, or DELETE /v1/apps/{appId}/users/{userId} with your API key.
  • You delete the app: it stops answering straight away, and every user’s documents are deleted within two hours.
  • You delete your jstash account: your apps go with it.

jstash keeps no backups or old versions of app documents.

Errors

Same format as the rest of the API: a stable code and a message that says what to fix. The messages are safe to show in your app’s console, but are written for you, not your users.

Status · codeMeaning and what to do
401 · AUTH_REQUIREDNo login token. Send Authorization: Bearer <token> once the person has signed in.
401 · AUTH_INVALIDThe token is expired, from a different project, edited, or not a login token at all — the message says which. A jstash API key here is refused on purpose.
403 · FORBIDDENThe website isn’t listed for the app, anonymous sign-ins are off, or the app is read-only because its owner left Pro.
403 · APP_SUSPENDEDjstash moderators took the app offline.
403 · APP_LIMIT_REACHED200 documents for this person, 5,000 users for the app, or 3 apps for the account.
404 · APP_NOT_FOUNDNo app with that ID. Copy it from the dashboard.
404 · DOC_NOT_FOUNDNothing saved under that name yet. Usually you treat it as empty.
412 · ETAG_MISMATCHThe document changed since you read it (If-Match), or already exists (If-None-Match: *).
429 · WRITE_LIMIT_REACHEDA daily save limit is used up. It resets at 00:00 UTC.
503 · LOGIN_PROVIDER_UNAVAILABLEjstash couldn’t fetch your provider’s keys. Retry shortly.

Other codes, like INVALID_JSON and PAYLOAD_TOO_LARGE, mean the same as for bins — see all error codes.

Troubleshooting

“Requests from https://… aren’t allowed for this app.”

Add that exact address under Website addresses on the app’s page. While developing on your own machine, turn on Allow localhost instead.

“This login token expired…”

Get the token just before each request rather than storing it: supabase.auth.getSession() or auth.currentUser.getIdToken() return a fresh one. Google ID tokens can’t be refreshed in the page; sign the person in again.

“…signs logins with the legacy JWT secret.”

Your Supabase project needs JWT signing keys. In Supabase: Project Settings → JWT Keys, then migrate, and try creating the app again.

“This token is from …, but this app uses …”

The token came from a different Supabase or Firebase project than the app is set up for. Check the app ID in your code, and the Project URL or Project ID on the app’s page.

I changed login provider and my users’ data disappeared.

Documents belong to a person’s identity in one login, so the login can’t change once people have saved data. If you’re moving providers, keep the old app until people have moved, or export through your app first.

Privacy

  • jstash stores each person’s documents and when they were saved, under a one-way hash of their user ID. It never stores their password, email address or login token.
  • Your users’ login tokens pass through jstash. A token your app sends also works with your login provider — for example your Supabase project — until it expires, usually within an hour. jstash uses it only to check who is signed in. It isn’t stored, and request logs record the header as redacted. That’s the trade-off of not running a server: you trust jstash with the token for each request.
  • You’re responsible to your users: tell them what your app stores and why, and get any consent the law needs — see the Terms.
  • Don’t store sensitive personal data in an app yet. We don’t offer a data processing agreement (DPA) yet; apps that hold that kind of data need one first. You can read the draft DPA.