jstash Apps · Free to try

Your users already log in.
Now give them somewhere to save their data.

jstash gives every signed-in user private JSON storage. No database. No backend. No security rules.

Works with Clerk, Google, Supabase Auth and Firebase Auth.

Sign in with GitHub. Free includes 1 app with up to 100 users. No card.

Or try it first: Cloud Notes is a live demo. Sign in with Google, save a note, and open it on your phone.

app.js
// save() and load() are below

// Signed in with Clerk, on their laptop:
await save('settings', { theme: 'dark' });

// Later, signed in on their phone:
await load('settings');
// → { theme: 'dark' }, theirs alone
Why

You have sign-in. Where do you keep each person’s stuff?

Sooner or later, an app with a login needs to remember things for each person: their settings, their progress, their drafts. That data should follow them from their laptop to their phone. The usual ways to do it each cost something.

localStorage

Save it in the browser

Easy, but it stays in that one browser. It doesn’t follow the person to their phone, and private windows forget it when they close.

server + database

Build a small backend

Write a server or serverless function that checks who’s signed in and talks to a database. It works, but now a static site has server code to host, secure and keep running.

security rules

Let the browser talk to a database

Hosted databases like Supabase and Firestore let your page read and write directly. In return you write security rules: short pieces of code that say who may read or write each piece of data. Firebase’s own docs call them “the only safeguard blocking access for malicious users.”

Those rules are easy to get wrong, and a wrong rule doesn’t show up as a bug: the app still works. On 1 October 2026, security firm UpGuard reported checking about 300,000 websites that appear to use Supabase. It found 16,326 databases with tables anyone could read. It notes that tables a coding agent creates through Supabase’s API don’t have security rules switched on by default. In 2024, researchers found at least 900 Firebase websites set up wrongly, with at least 125 million user records open to the public. The tools aren’t the problem. The rules are one more thing to get right.

jstash Apps takes the rules away. There’s one rule, built in, and you can’t turn it off: each person reaches only their own documents.

How it works

Your login says who. jstash keeps what.

  1. 1

    A person signs in with your login

    Clerk, Supabase, Firebase or Google, exactly as today. jstash never sees their password and never runs the sign-in.

  2. 2

    Your page sends their login token

    A login token is a short-lived note, signed by your login provider, that says who is signed in. Your page already has one. It goes in the Authorization header of each save and load.

  3. 3

    jstash checks the token

    It checks the signature with the public keys your login provider publishes for checking its tokens. It also checks that the token comes from your login setup and hasn’t expired. jstash holds no secret of yours.

  4. 4

    They get their own documents

    Documents are filed under your app and that person’s ID, so a request only ever reaches the signed-in person’s own documents. Nobody else’s token gets there, and a jstash API key is refused.

How jstash Apps works A person signs in with your login. Your web page sends their login token with each save or load. jstash checks the token against your login provider’s published keys, then reads or writes only that person’s documents. A person signs in Clerk, Supabase, Firebase or Google login token Your web page static, no server · plain fetch token + JSON jstash checks the token with your login’s public keys only theirs Their documents settings · progress · drafts, private to them
Setting up

Plain fetch. Nothing to install.

Setting up takes a few minutes. Then this is all the code you need. The dashboard gives it to you with your app’s address filled in. This copy is set up for Clerk.

Set up in the dashboard

  1. 1

    Create an app

    In the dashboard, open Apps → New app and give it a name. In jstash, an app is a setting you make once: which login to trust and which websites may call it.

  2. 2

    Pick your login

    Choose the login your app uses and paste one value from it (see the table below).

  3. 3

    Add your website

    Add its address, like https://my-app.netlify.app. Turn on Allow localhost while you develop.

  4. 4

    Copy the code

    Copy the starter code from the app’s page. Or copy the ready-made prompt and paste it into your coding agent.

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

// In React, use getToken from useAuth() instead
function token() {
  if (!Clerk.session) throw new Error('Not signed in');
  return Clerk.session.getToken(); // Clerk refreshes it for you
}

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 await failure(res);
}

export async function load(name) {
  const res = await fetch(`${JSTASH}/${name}`, {
    headers: { Authorization: `Bearer ${await token()}` },
  });
  if (res.ok) return res.json();
  const err = await failure(res);
  if (err.code === 'DOC_NOT_FOUND') return null; // nothing saved yet
  throw err;
}

// jstash errors are JSON: {"error": {"code", "message"}}
async function failure(res) {
  const { error } = await res.json()
    .catch(() => ({ error: { message: `jstash returned ${res.status}` } }));
  return Object.assign(new Error(error.message), { code: error.code });
}
app.js
await save('settings', { theme: 'dark', units: 'metric' });
const settings = (await load('settings')) ?? { theme: 'light' };

Using another login? Only token() changes.

Login (you paste)Token from
Clerk
Frontend API URL or publishable key
Clerk.session.getToken()
Supabase
Project URL
supabase.auth.getSession(), then data.session.access_token
Firebase
Project ID
auth.currentUser.getIdToken()
Google
OAuth client ID
response.credential from your Sign In With Google callback

Full setup for each login is in the guide.

Using a coding agent?

Copy this prompt into Claude Code, Codex, Cursor or a chat assistant. It asks you for your app ID and login, then follows the Markdown reference. Once you’ve created an app, its page in the dashboard has a prompt like this with your app’s details filled in.

prompt
Help me add per-user cloud save to this app with jstash Apps. Each person who signs in gets their own private JSON documents and can read and write only their own. The app keeps the login it already has: the browser sends the signed-in person's login token, and jstash checks it. There's no server to write, no SDK to install, and no jstash API key in the page.

Before you write any code:
- Read the reference at https://jstash.app/docs/apps.md if you can fetch it. The essentials are below.
- Ask me for my jstash app ID. It starts with app_ and is part of the endpoint at the top of the app's page in the jstash dashboard: https://api.jstash.app/v1/apps/app_…/me. Never make one up. If I don't have an app yet, tell me to create one at https://app.jstash.app/apps (Apps → New app): pick the login, paste the one value it asks for, and add the app's website address; then, on the app's page, add any other addresses and turn on Allow localhost for local development.
- Ask me which login the app uses, unless the code already makes it clear. jstash works with Clerk, Supabase Auth, Firebase Auth and Sign In With Google only; GitHub sign-in works through Clerk, Supabase or Firebase. If the app uses something else, stop and tell me.

Then:
1. Look at the app: how sign-in is set up, and what each person's data is (settings, progress, drafts...). Tell me in a sentence or two what you'll save, under which document names.
2. Add one small module like the starter below, adapted to the app's framework and style. Call load once someone is signed in (null means nothing is saved yet, so use the app's defaults), and call save when the data changes (debounce rapid edits), never on a timer. Drop any pending save when the person signs out, switches account or deletes their account, so their data is never saved under someone else or after deletion. Don't change how sign-in works.
3. Get the login token right before each request; don't store it:
   - Clerk: Clerk.session.getToken() (in React, getToken from useAuth()). Use the default session token, not a JWT template. Tokens last 60 seconds; getToken() returns a fresh one when needed. Clerk tokens name the website they were made on, so every address where people sign in must be in the app's website addresses. Clerk development and production instances have different Frontend API URLs; each needs its own jstash app.
   - Supabase: (await supabase.auth.getSession()).data.session.access_token, using the app's existing client. data.session is null when nobody is signed in, so don't call jstash then.
   - Firebase: auth.currentUser.getIdToken(), using the app's existing getAuth() instance. currentUser is null until sign-in finishes, so wait for onAuthStateChanged.
   - Google: the ID token from the Sign In With Google callback (response.credential), kept in memory. It lasts one hour and can't be refreshed in the page, so on 401 AUTH_INVALID ask the person to sign in again.
4. If the app lets people delete their account, also call DELETE on the endpoint (https://api.jstash.app/v1/apps/{appId}/me) with their token as part of it.
5. Browsers are refused from any address that isn't listed under Website addresses on the app's page. Addresses match exactly (scheme, host and port), must use https, have no wildcards, and there can be up to 5; localhost works only with Allow localhost on. If an address the app runs on is missing, tell me exactly which one to add. Code can't change these settings.
6. Add a short jstash section to AGENTS.md (or CLAUDE.md, if that's what this repo uses): where the module is, that it sends the login token and never a jstash API key, and the reference link.
7. Tell me how to test it.

If you can't see or edit this project's files (for example, you're a chat assistant), ask me for the files you need, then give me complete code to paste and say where each piece goes.

Starter module (shown with Clerk; for another login, only token() changes):

```js
const JSTASH = 'https://api.jstash.app/v1/apps/APP_ID/me'; // APP_ID: the app ID I give you

function token() {
  if (!Clerk.session) throw new Error('Not signed in');
  return Clerk.session.getToken();
}

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 await failure(res);
}

export async function load(name) {
  const res = await fetch(`${JSTASH}/${name}`, {
    headers: { Authorization: `Bearer ${await token()}` },
  });
  if (res.ok) return res.json();
  const err = await failure(res);
  if (err.code === 'DOC_NOT_FOUND') return null; // nothing saved yet
  throw err;
}

// jstash errors are JSON: {"error": {"code", "message"}}
async function failure(res) {
  const { error } = await res.json()
    .catch(() => ({ error: { message: `jstash returned ${res.status}` } }));
  return Object.assign(new Error(error.message), { code: error.code });
}
```

## jstash Apps essentials
Every call sends Authorization: Bearer <the signed-in person's login token> and reaches only that person's documents. Never send a jstash API key here: it's refused, and it must never be in browser code.
GET    {endpoint}         List their documents: {"items": [{"name", "bytes", "version", "etag", "updated_at"}]}.
GET    {endpoint}/{name}  The document, raw, with an ETag header. 404 DOC_NOT_FOUND: nothing saved yet.
PUT    {endpoint}/{name}  Create (201) or replace (200) the whole document; the body is any JSON value.
DELETE {endpoint}/{name}  Delete it. 204, safe to repeat.
DELETE {endpoint}         Delete all of their documents.

Document names: 1-64 characters of letters, digits, - _ and ., not starting with a dot. Use a few fixed names like "settings" or "progress", never user input.
PUT replaces the whole document; there are no partial updates. If the same person might save from two devices at once, send the ETag from your last GET or PUT as If-Match. 412 ETAG_MISMATCH means it changed: load it again, reapply the change and retry.

Limits on Free (Pro in brackets): 200 documents per person, 100 KB each (1 MB). Per person: 100 saves a day (1,000) and 1,000 reads (5,000); 60 saves and 300 reads a minute. Per app: 500 saves a day (50,000) and 5,000 reads (50,000). Up to 100 people across all my jstash apps (1,000). Daily limits reset at 00:00 UTC.

Errors are JSON: {"error": {"code": "...", "message": "..."}, "request_id": "..."}. Branch on code, never on message. Log the message: it says what to fix, but it's written for developers, not for the people using the app.
- 400 INVALID_REQUEST: the document name isn't allowed. 400 INVALID_JSON: the body isn't JSON.
- 401 AUTH_REQUIRED, AUTH_INVALID: no token, or it's expired or not from the app's login.
- 403 FORBIDDEN: the website isn't one of the app's addresses (the message names it), anonymous sign-ins are refused, or the app is read-only.
- 403 APP_LIMIT_REACHED, STORAGE_LIMIT_REACHED: a plan limit. 404 APP_NOT_FOUND: wrong app ID.
- 403 APP_SUSPENDED: the app was taken offline; don't retry.
- 413 PAYLOAD_TOO_LARGE: the document is over the size limit.
- 429 RATE_LIMITED: wait a minute and retry. 429 WRITE_LIMIT_REACHED: a daily limit is used up; don't retry before 00:00 UTC.
- 500 INTERNAL_ERROR, 503 LOGIN_PROVIDER_UNAVAILABLE, R2_UNAVAILABLE, DATABASE_UNAVAILABLE: retry shortly.

jstash keeps no backups or old versions of documents. Don't store sensitive personal data such as health information.
Logins

Works with the login you already have

Clerk

Paste your Frontend API URL, or your publishable key. Development and production instances have different users, so set up a jstash app for each. Proxy and satellite-domain setups aren’t supported yet.

Supabase

Paste your Project URL. Your project needs Supabase’s newer JWT signing keys, not the legacy JWT secret. jstash tells you if it’s still on the old one. Anonymous (guest) sign-ins are refused unless you’re on Pro and turn them on.

Firebase

Paste your Project ID. Firebase refreshes tokens for you. Anonymous (guest) sign-ins are refused unless you’re on Pro and turn them on.

Google

Paste your OAuth client ID, for Sign In With Google. Its tokens last an hour and can’t be refreshed in the page, so it suits short sessions.

GitHub sign-in works through Clerk, Supabase or Firebase. Each Clerk instance, Supabase project, Firebase project or Google client needs its own jstash app. No login yet? See the guide.

What you get

Small, safe, and hard to get wrong.

…/me/settings

One rule, built in

Your page only ever calls …/me, which means the person whose token it is. They read and write only their own documents. There’s no rules language, so there’s nothing to get wrong.

Authorization: Bearer <login token>

No server, no secret in the page

Your page sends the login token it already has. A jstash API key never goes in a web page. If you try, it’s refused.

{"theme":"dark"}

Any JSON, by name

Save any JSON value under a name like settings or progress. Each person can keep up to 200 documents of up to 100 KB each (1 MB on Pro), within your account’s storage. Optional version checks stop two devices overwriting each other.

https://my-app.netlify.app

Only your websites

Your jstash app accepts browser requests only from the website addresses you list (up to 5), plus localhost while you develop, if you turn it on.

This login token expired 5 minutes ago.

Errors that say what to fix

An expired token, a website you haven’t listed, the wrong Clerk instance: the error names the problem and how to fix it.

APP_LIMIT_REACHED

Hard limits, no surprise bills

Free has 1 app and up to 100 people; Pro has 3 apps and up to 1,000 people across them. Each person can save 60 times a minute, and 100 times a day on Free (1,000 on Pro). Each app takes up to 500 saves and 5,000 reads a day on Free (50,000 of each on Pro). Your apps and bins share your account’s storage: 10 MB on Free, 1 GB on Pro. Limits are hard caps, not a meter: at a limit the save is refused with an error that names it, and nothing extra is billed. All limits →

Not a fit

What Apps isn’t for

Apps does one thing: private data for each signed-in person. If you need any of these, use a full database with its own rules.

Data people share

A team’s task list or a group chat needs many people on the same data. In Apps, each document belongs to one person.

Search and queries

You load a document by its name, or list a person’s document names. There’s no search, filtering or sorting across documents or people.

Looking at your users’ data

jstash gives you no way to read your users’ documents: no API, no dashboard screen, no admin view. If your app needs one, you need a database you control.

Live sync

Changes aren’t pushed to other devices. Your app loads the latest copy when it needs it.

Apps with no login

Every request needs a signed-in person. For public data anyone can read, use a jstash bin: a JSON document at its own URL.

Big data or sensitive data

Even on Pro, each person gets up to 200 documents of up to 1 MB, your apps have up to 1,000 people between them, and they share your account’s 1 GB. Don’t store health, biometric or similar sensitive data (Terms).

Privacy

What jstash keeps, and what it doesn’t

jstash keeps as little as it can about the people who use your app.

  • What’s stored: each person’s documents, with their names, sizes and save times, when the person first and last saved, and daily counts of their saves and reads. It’s all filed under a one-way hash of the person’s user ID: a fingerprint of the ID, not the ID itself.
  • What’s never stored: passwords, email addresses or login tokens.
  • Tokens pass through. jstash uses each token only to check who’s signed in, and doesn’t store or log it. A token still works with your login provider until it expires (Clerk tokens last a minute; the others usually last an hour). So each request trusts jstash with a working token for that long. That’s the trade-off of not running your own server.
  • jstash gives you no way to read your users’ documents. Your API key can’t, there’s no dashboard screen for it, and jstash’s admin console doesn’t show them either. You see counts: people, documents, storage, and saves and reads today. We don’t look at documents except to keep jstash secure or when the law requires it (Terms).
  • Where it lives: on Cloudflare, stored in its Oceania region. Each request is handled at the Cloudflare data centre nearest the caller. Cloudflare is the only company that handles this data (subprocessors). HTTPS only, and data is encrypted at rest.
  • Data Processing Agreement: our DPA (Common Paper’s standard DPA, version 1.1) is part of the Terms. There’s nothing separate to sign.
  • Your users, your promise: tell them what your app stores and why, and get any consent the law needs.
No backups. jstash keeps no backups or old versions of documents. A deleted or replaced document can’t be recovered, so keep your own copy of anything that matters.
Availability

Free to try. More room on Pro.

Free: every jstash account can create 1 app with up to 100 users. Sign in with GitHub to start. No invite, no card.

Pro: 3 apps and up to 1,000 users across them, 1 MB documents, higher daily limits, and anonymous sign-ins if you turn them on. Pro is invite-only until paid plans open, and we aren’t sending new invites yet. Request one and we’ll email you when we do.

At launch, Pro will be US$5 a month or US$48 a year. People already invited use it free until then, with no card needed. We’ll tell you before any charge. If you go back to Free with more than 1 app, your apps keep their data and can still load it, but stop accepting saves until you delete down to 1. All limits · See all plans.

Request a Pro invite

3 apps, 1,000 users, private bins and higher limits. We’ll email you when invites open.

Use the email on your jstash account. No account yet? Sign in with GitHub. It’s free.

Questions

Anything else, email support@jstash.app.

Do I need a server?

No. Your page calls jstash directly with plain fetch. The login token your page already has proves who’s signed in, so there’s no secret to hide on a server.

Can I see my users’ data?

Not through jstash. You see counts (people, documents, storage, and saves and reads today), not what’s in the documents. There’s no API or dashboard screen for that, and jstash’s admin console doesn’t show them either. Your own app’s code can read a person’s documents while they’re signed in, since it has to in order to show them. If your app needs an admin view of users’ data, Apps isn’t the right fit.

A user wants their data deleted. What do I do?

If your app has a “delete my account” button, have it call DELETE …/me with the person’s token. That removes all their documents. For a request by email, use Delete user data on the app’s page in the dashboard, with their user ID from your login provider. There are no backups of documents, so a deleted document can’t be recovered.

What happens if I change login provider?

Each person’s documents belong to their identity in one login, so an app’s login can’t change once people have saved data. Create a new app for the new login. Keep the old one running until people have moved, or have your app export their data first.

Is it a database?

Not really. It’s a place to keep each person’s JSON documents by name: save, load, list and delete. There’s no querying, no sharing between people and no schema. That’s on purpose: it’s what keeps it simple and safe. If you need those things, use a database.

What if jstash shuts down?

The Terms promise at least 30 days’ notice, by email where we have one and on this site, so you can take your data. Because jstash gives you no way to read your users’ documents, your app moves them: while each person is signed in, it loads their documents and saves them somewhere new. jstash is a small service and has no uptime guarantee.

I use Clerk. Can’t I keep this in Clerk’s user metadata?

For a few small values, yes. Clerk limits metadata to 8 KB and suggests your own database for more than 1.2 KB. Apps gives each person up to 200 documents of up to 100 KB each (1 MB on Pro), within your account’s storage, without you setting up a database or writing rules.

Give each signed-in user their own JSON.

PUT https://api.jstash.app/v1/apps/app_…/me/settings