How to save user data in a static website
A static site is just files, so it has nowhere of its own to keep what people do on it. There are four ways to give it somewhere. Which one fits depends on whether the data has to follow the person to another device, and whether anyone else needs to see it.
Written by jstash, which makes one of the options compared here. Updated 4 October 2026.
Summary
- Only needs to stay in one browser? Use localStorage, or IndexedDB for larger data. No server, no account, nothing to secure.
- Should follow the person to their phone or another computer? Then you need a login, so you know who they are, and storage online.
- For each person’s own data behind a login, write a small backend, use Supabase or Firebase with security rules, or use a per-user store such as jstash Apps.
- Shared data, search, or an admin view of everyone’s data? Use a real database: Supabase, Firebase or your own.
Start with two questions
A static site is HTML, CSS and JavaScript served as files, from a host like GitHub Pages, Netlify or Cloudflare Pages. None of your own server code runs, so any saving happens in the browser or through a service the browser can call.
- Does the data need to leave this browser? A to-do list someone only uses on one laptop can live in the browser. Settings they expect to see on their phone too can’t.
- Is it one person’s data, or shared? Each person’s own settings, progress and drafts are simple to store safely. A shared board, comments or a leaderboard needs rules about who sees and changes what.
Option 1: keep it in the browser
Browsers give every site two places to keep data: localStorage for small things and IndexedDB for larger, structured data. Both are free, need no account and work offline.
// Save
localStorage.setItem('settings', JSON.stringify({ theme: 'dark' }));
// Load, with a default for first-time visitors
const settings = JSON.parse(localStorage.getItem('settings')) ?? { theme: 'light' };
- localStorage stores strings, so save JSON with
JSON.stringify. Browsers allow up to 5 MiB per origin. - IndexedDB holds much more and doesn’t block the page while it works, but its API is lower-level.
What you give up:
- It stays in one browser. The same person on their phone sees nothing.
- It can disappear. Private windows clear localStorage when the last private tab closes. With cross-site tracking prevention on, Safari deletes data a site’s scripts wrote once the person has gone seven days of Safari use without interacting with the site. And anyone can clear their site data.
- You can’t see it. That’s good for privacy, but you can’t help someone get it back.
If losing the data would only be annoying, this is the right choice, and it’s where most apps start. localStorage vs cloud storage covers the trade-offs in more detail.
To follow the person, you need a login
To show someone the same data on their laptop and their phone, something online has to know both are the same person. That’s what a login does. Clerk, Supabase Auth and Firebase Authentication each add sign-in to a static site, and each has a free plan: Clerk’s covers 50,000 monthly retained users per app, Supabase’s covers 50,000 monthly active users, and Firebase’s basic sign-in methods are free (phone sign-in is billed per SMS).
If you don’t want people to sign up, Firebase and Supabase can create anonymous accounts instead. They’re lost if the person signs out or clears their browser, unless they later link a real account, and Supabase warns that bad actors can abuse them to “increase your database size drastically”. jstash Apps accepts anonymous sign-ins only on Pro, which is invite-only for now.
With a login in place, there are three ways to store each person’s data.
Option 2: write a small backend
Add a serverless function next to your static site, with Netlify Functions, Vercel Functions or Cloudflare Workers. Your page sends the person’s login token. The function checks it with your login provider’s server library (Clerk’s, for example), then reads and writes a database under that person’s user ID.
- Good: full control. Any database, any query, your own validation, and you can see and export the data.
- Cost: server code to write, secure, deploy and keep running, plus a database to choose and pay for. Forget the token check in one function and that function is open to anyone.
Pick this when you need logic a browser can’t be trusted with, like taking payments, or you know you’ll grow into a real backend anyway.
Option 3: Supabase or Firebase, with security rules
Supabase and Firebase let your page read and write their database directly. In exchange, you write security rules that decide who can read or write each row or document. For each person’s own data, the rule is short. In Firestore, from Firebase’s docs:
service cloud.firestore {
match /databases/{database}/documents {
match /some_collection/{userId}/{document} {
allow read, write: if request.auth != null && request.auth.uid == userId
}
}
}
In Supabase, you turn on row level security (RLS) for the table and add a policy for each thing people may do, from Supabase’s docs:
alter table todos enable row level security; create policy "Individuals can view their own todos." on todos for select to authenticated using ( (select auth.uid()) = user_id ); -- and one each for insert, update and delete
- Good: both are mature, with free plans far larger than jstash’s, plus queries, shared data and live updates (Supabase Realtime, Firestore listeners). If you already sign in with Supabase or Firebase, their database is the natural next step.
- Cost: the rules are code, and a wrong rule doesn’t look like a bug. The app still works; the data is just open. Firebase calls its rules “the only safeguard blocking access for malicious users”. In Supabase, “a table in an exposed schema without RLS is readable and writable by any role with a grant on it”.
- It happens. In 2024, researchers found at least 900 Firebase websites set up wrongly, with at least 125 million user records open. On 1 October 2026, UpGuard reported 16,326 Supabase databases with tables anyone could read, among about 300,000 websites it checked.
- Quiet hobby apps: Supabase’s free projects pause after a week without enough database activity. You can restore them for up to a year.
Option 4: a per-user store that checks your login
jstash Apps, which we make, does one thing: it gives each signed-in person their own private JSON documents. Your page sends the login token it already has. jstash checks it with your login provider’s public keys, and the request reaches only that person’s documents. There are no rules to write: there’s one rule, built in, and you can’t turn it off. It works with Clerk, Supabase Auth, Firebase Authentication and Google sign-in.
// The app endpoint, from the jstash dashboard
const JSTASH = 'https://api.jstash.app/v1/apps/app_…/me';
// The login token of whoever is signed in. With Clerk:
const token = () => Clerk.session.getToken();
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);
}
async function load(name) {
const res = await fetch(`${JSTASH}/${name}`, {
headers: { Authorization: `Bearer ${await token()}` },
});
if (res.ok) return res.json();
const { error } = await res.json();
if (error.code === 'DOC_NOT_FOUND') return null; // nothing saved yet
throw new Error(error.message);
}
await save('settings', { theme: 'dark' });
await load('settings'); // { theme: 'dark' }, on any device they sign in on
The dashboard gives you a fuller version with your app’s address filled in, and the Apps guide has the token() line for each login.
- Good: no server, no rules and no jstash key in the page. It’s plain
fetch, with nothing to install. - Limits: Free includes 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. Pro has 3 apps and 1,000 people, and will be US$5 a month; it’s invite-only until paid plans open. See all limits.
- Not for: data people share, search or queries, live sync, apps with no login, or looking at your users’ data: jstash gives you no way to read their documents. Don’t store sensitive data such as health information.
- No backups. A replaced or deleted document can’t be recovered, so keep your own copy of anything that matters.
Which one to use
| Option | Follows the person | Needs a login | Server code | Access rules |
|---|---|---|---|---|
| localStorage or IndexedDB | No | No | No | None needed |
| Small backend | Yes | Yes | Yes | Checks in your code |
| Supabase or Firebase | Yes | Yes | No | Rules you write |
| jstash Apps | Yes | Yes | No | Built in, one rule |
- No login, or one device is fine: localStorage. Don’t add a service you don’t need.
- You already sign in with Supabase or Firebase: use their database with an owner-only rule like the ones above. Their free plans are bigger than jstash’s, and it’s one less service.
- Shared data, queries, live updates or an admin view: Supabase, Firebase or your own backend. jstash Apps doesn’t do these.
- You sign in with Clerk or Google, and each person just needs their own settings, progress or drafts: jstash Apps fits, with no server and no rules. See per-user storage for a Clerk app.
More guides
- localStorage vs cloud storage for small web apps
- How to add per-user cloud storage to a Clerk app
- How to save JSON from a frontend without exposing an API key
- How to add a database to a static website — and when you don’t need one