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.
Set up an app
- In the dashboard, open Apps → New app and give it a name.
- 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.
- Add your website address, like
https://my-app.netlify.app(up to 5). Turn on Allow localhost while you develop. - 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.
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.
/v1/apps/{appId}/meLogin token · Lists their documents: {"items": [{"name", "bytes", "version", "etag", "updated_at"}]}./v1/apps/{appId}/me/{name}Login token · The document, raw, with its ETag. 404 DOC_NOT_FOUND if nothing is saved yet./v1/apps/{appId}/me/{name}Login token · Creates (201) or replaces (200) it. If-Match for safe updates, If-None-Match: * to create only./v1/apps/{appId}/me/{name}Login token · Deletes it. Safe to repeat./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:
/v1/appsAPI key · Your apps, each with its users, documents, storage and today’s saves./v1/apps/{appId}API key · One app./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://localhostandhttp://127.0.0.1on 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
- 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}/mewith 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.
Authorization: Bearer <token> once the person has signed in.If-Match), or already exists (If-None-Match: *).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.