DEV Community

jstash for jstash

Posted on

Save per-user app data with Clerk and jstash

Your static site or single-page app already has Clerk sign-in. Next, you need to save each person's settings, drafts or progress in the cloud.

localStorage stays on one device, and clearing storage removes its contents. Clerk's user metadata is meant for small amounts of data, not app documents. The usual next step is a backend, a database and access rules for each user.

jstash Apps provides document storage for this setup. Your browser sends the signed-in person's Clerk session token. jstash checks it against your Clerk instance, and each person can only read and write their own documents.

Here's how to connect it.

1. Create a jstash app

Open the jstash dashboard at app.jstash.app and sign in with GitHub.

Create an app, choose Clerk and paste your Clerk publishable key. jstash reads the Frontend API URL from that key.

Tick Allow localhost for local testing. For production, website addresses must be listed: https, exact match, up to 5.

The Free plan includes 1 app and up to 100 users, with no card.

Don't put a jstash API key in the page: it would be refused. Requests use the signed-in person's Clerk session token instead.

2. Add save and load functions

The document endpoint is:

https://api.jstash.app/v1/apps/{appId}/me/{name}
Enter fullscreen mode Exit fullscreen mode

'clerk' is your Clerk instance after 'await clerk.load()' (in the example it's exported from src/clerk.js).

A PUT creates or replaces the whole document, which can be any JSON value. A GET returns it. A 404 with code DOC_NOT_FOUND means nothing has been saved yet.

Use your jstash app ID as APP_ID in the following code:

const JSTASH = `https://api.jstash.app/v1/apps/${APP_ID}/me`;

async function token() {
  const t = await clerk.session?.getToken();
  if (!t) throw new Error('Not signed in');
  return t;
}

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;
  throw err;
}

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 });
}
Enter fullscreen mode Exit fullscreen mode

Clerk tokens last 60 seconds. Get one right before each request with clerk.session.getToken(). Clerk returns the current token while it's valid, so call it for each request rather than keeping a token yourself.

Use the default session token, not a JWT template. In React, use getToken from useAuth().

Errors are JSON, with code and message inside an error object. The helper keeps the error code available to the caller. It treats DOC_NOT_FOUND as an empty starting point and throws other errors.

3. Load after sign-in, save on change

After sign-in, load the person's notes:

const notes = (await load('notes')) ?? { text: '' };
Enter fullscreen mode Exit fullscreen mode

When the notes change, save them:

await save('notes', notes);
Enter fullscreen mode Exit fullscreen mode

Debounce rapid edits, and drop a pending save when the user signs out.

Because save() replaces the whole document, pass the notes document you want stored, rather than just a changed field.

On Free, documents can be up to 100 KB, with 200 documents per user. Keep those limits in mind when deciding what belongs in each document.

4. Handle deletion and storage limits

For a “delete my account” flow, this request removes everything that person saved in the app:

DELETE https://api.jstash.app/v1/apps/{appId}/me
Enter fullscreen mode Exit fullscreen mode

jstash keeps no old versions. A PUT or DELETE can't be undone.

Don't store sensitive personal data such as health information.

Run the working example

The working Clerk and Vite example is MIT licensed.

In the example directory, copy the environment file:

cp .env.example .env.local
Enter fullscreen mode Exit fullscreen mode

Fill in VITE_JSTASH_APP_ID and VITE_CLERK_PUBLISHABLE_KEY, then run:

npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open http://localhost:5173.

The Apps docs cover the setup.

Before choosing it for your app, keep the service's stage in mind: jstash is an early-stage, small hosted service, and it is not open source. Clerk was tested end to end with a Clerk development instance (production Clerk instances not yet tested). Google sign-in is tested end to end. Supabase Auth and Firebase Auth have so far only been tested with simulated tokens.

I'm from jstash. AI helped draft this post; the code is from our tested example.

Top comments (0)