Guides

How to add per-user cloud storage to a Clerk app

Clerk signs people in. It doesn’t give you somewhere to keep each person’s settings, progress or drafts. Here are the ways to add that, what each one costs you, and which to pick.

Written by jstash, which makes one of the options compared here. Updated 4 October 2026.

Summary

  • A few small values the person may change themselves: Clerk’s own unsafeMetadata. Nothing else to set up, but 8 KB of metadata in total, and the person can edit it.
  • Data you need to trust, query or share: a database. Supabase accepts Clerk sign-ins and protects rows with policies you write. Convex and your own backend check the user in code you write.
  • Each person’s own JSON, saved from the browser, with no server or rules: jstash Apps checks the Clerk session token itself. Free covers 1 app and 100 users.
  • Firebase: Clerk’s Firebase integration is deprecated and can’t be turned on for new apps.

Option 1: Clerk user metadata

Every Clerk user has three metadata fields:

FieldBrowserYour server
publicMetadataReadRead and write
privateMetadataNo accessRead and write
unsafeMetadataRead and writeRead and write

Only unsafeMetadata can be written from the browser, so it’s the one a frontend-only app can use:

// Merged into the signed-in user's unsafeMetadata
await Clerk.user.updateMetadata({ unsafeMetadata: { theme: 'dark' } });

// Later, on any device they sign in on
const theme = Clerk.user.unsafeMetadata.theme;
  • Size: all of a user’s metadata together is limited to 8 KB. If you also copy it into the session token, Clerk recommends keeping that under 1.2 KB, because the token lives in a cookie. For more than that, Clerk suggests your own database (Clerk docs).
  • Trust: it’s called unsafe because “malicious users could potentially tamper with these values”. Fine for a theme or an onboarding flag. Not for a plan, credits, or anything else people shouldn’t set themselves.
  • Shape: one object per user. Good for a handful of preferences; awkward for lists that grow, like notes or saved items.

If a few preferences are all you need, stop here. It’s the simplest option, with nothing new to sign up for.

Option 2: Supabase, with Clerk as the login

Supabase can accept Clerk session tokens as a third-party login. You turn on the Supabase integration in Clerk, add Clerk as a provider in Supabase, then write row level security (RLS) policies that compare each row’s owner with the Clerk user ID in the token. From Clerk’s guide:

create table tasks(
  id serial primary key,
  name text not null,
  user_id text not null default auth.jwt()->>'sub'
);

alter table "tasks" enable row level security;

create policy "User can view their own tasks"
on "public"."tasks"
for select
to authenticated
using (
((select auth.jwt()->>'sub') = (user_id)::text)
);

-- plus a policy for insert, and for update and delete if people may do those

Option 3: Convex, or your own backend

Convex is a hosted backend with a realtime database that accepts Clerk sign-ins. Instead of rules, you write functions, and each one that touches private data checks who’s calling with ctx.auth.getUserIdentity() (Convex docs).

Your own backend works the same way. An API route or serverless function verifies the Clerk session token, with authenticateRequest() or against your instance’s public keys, checks its azp claim is one of your sites (Clerk docs), then reads and writes your database by user ID.

  • Good: any logic you like, and data you can query, share, validate and export.
  • Cost: server code to write and maintain, and a check in every function or route that touches someone’s data.

What about Firebase?

Firestore’s security rules see users through Firebase Authentication. Clerk’s Firebase integration, which bridged the two, is deprecated: “For new applications, you cannot enable the integration.” With Clerk as your login, pick one of the other options.

Option 4: jstash Apps

jstash Apps, which we make, keeps private JSON documents for each signed-in person and checks the Clerk session token itself. There’s no server, no table, no policy and no jstash key in your page.

Set it up

  1. Sign in to the jstash dashboard with GitHub and open Apps → New app.
  2. Choose Clerk and paste your Frontend API URL, from the Domains page in Clerk, or your publishable key.
  3. Add every website address where people sign in, 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 has your app’s address filled in. There’s also a ready-made prompt for your coding agent.

The code

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 });
}

Call load('settings') once someone is signed in (it returns null until they’ve saved something), and save('settings', value) when it changes. Each person reaches only their own documents.

Clerk details that matter

  • Get a token for each request. Clerk session tokens last 60 seconds. getToken() uses a cache and only asks Clerk for a new one once the old one has expired, so calling it every time is cheap. Use the default session token, not a JWT template.
  • Development and production are separate. Each Clerk instance has its own Frontend API URL and its own users, so each needs its own jstash app. Free includes 1 app, so you can connect one instance; Pro has 3.
  • List every site that signs people in. A Clerk token names the website it was made on, and jstash refuses tokens made on a website the app doesn’t list.
  • Not supported yet: Clerk’s proxy and satellite domains. People with a step left to finish in Clerk, like choosing an organization, are refused until they finish it.
  • Per person, not per organization. Each document belongs to one person. Data a team or organization shares needs a database.

Limits and trade-offs

  • Free: 1 app and up to 100 people across your account, each with up to 200 documents of up to 100 KB, within your account’s 10 MB; 100 saves per person and 500 per app a day. Pro has 3 apps, 1,000 people and 1 MB documents, and will be US$5 a month; it’s invite-only until paid plans open. See all limits.
  • Small on purpose. Clerk’s free plan covers 50,000 monthly retained users per app. If more than 1,000 people will save data in your app, jstash Apps is too small: use a database.
  • What it doesn’t do: jstash gives you no way to read your users’ documents, and there are no queries, live sync or backups. Don’t store sensitive data such as health information.
  • Tokens pass through jstash. Each request trusts jstash with a Clerk token that works for up to a minute. jstash uses it only to check who’s signed in, and doesn’t store or log it.

Which one to use

OptionRoom per personSaved from the browserServer codeAccess rules
Clerk unsafeMetadata8 KB, all metadataYes, and the person can edit itNoNone
Supabase with ClerkRows in PostgresYesNoRLS policies you write
Convex or your own backendYour databaseThrough your functionsYesA check in each function
jstash Apps200 documents of 100 KB (Free)YesNoBuilt in, one rule
  • A few preferences: Clerk metadata.
  • Shared data, queries, an admin view, or more than 1,000 people saving: Supabase, Convex or your own backend.
  • Each person’s own settings, progress or drafts, without running a server or writing policies: jstash Apps.

More guides

Using Clerk? The Clerk section of the Apps guide has the full setup and troubleshooting. Free includes 1 app with up to 100 users, no card. How jstash Apps works.