IsotopeAI / docs

Guides

Getting started Configuration Supabase setup Sync & backup Backup console Community

Reference

Architecture API reference Database CLI Android APK Admin console

Help

Troubleshooting FAQ Contributing Changelog

Legal

Security Privacy Terms License

Troubleshooting

Concrete symptoms, the actual cause, and the fix. Every entry here is a fault that has really occurred, not a hypothetical.

Black screen after opening a page #

Symptom. The page loads but stays black. The console shows Uncaught SyntaxError: Unexpected token pointing at a file under /assets/.

Cause. The service worker served a cached bundle that no longer matches the current build. Because /assets/ is cache-first, a stale copy can persist indefinitely.

Fix. The app now recovers automatically: the error boundary purges caches and reloads once per build. If that attempt is already spent you will see a Refresh required prompt with a Refresh now button.

Your data is safe during recoverycaches.delete() only clears HTTP responses. It cannot touch localStorage or IndexedDB, so your session and study data are untouched. You will not be signed out.

To clear it by hand:

// DevTools console
await caches.keys().then(k => Promise.all(k.map(n => caches.delete(n))));
location.reload();

Fix applied but the browser still shows old behaviour #

Cause. Same root as above. The cache name now includes a build token derived from VERSION plus the server.mjs modification time, so editing a patch rotates the cache automatically. If you are behind that change, one manual clear is needed.

Verify the browser and server agree:

# server-side: the cache name the server expects
curl -s http://127.0.0.1:3000/api/version | python3 -c 'import sys,json;print(json.load(sys.stdin)["pwa_cache"])'

# browser-side: what is actually cached
# DevTools → Application → Cache Storage

If they differ, the client is stale.

A feature silently does nothing #

Cause. A serve-time patch failed to match. The server rewrites minified bundles using exact string anchors; when an anchor misses, the bundle is served unpatched and whatever that patch added is simply absent.

Diagnose. A healthy start logs about 52 patch lines and zero failures:

bash bin/isotope restart && sleep 6

# any output here is a problem
grep -iE 'anchor not found|String not found' ~/.isotope/logs/server.log

# what did apply
grep -E '^\[[A-Za-z]+(Patch|CrashFix|KeyboardFix)' ~/.isotope/logs/server.log | sort -u

Failures are also surfaced in the UIMissed anchors are collected and rendered as a banner in the served HTML, so a silent mismatch cannot hide indefinitely.

Leaderboard is empty #

Cause. A leaderboard must read other users stats, which needs a public SELECT policy on user_stats_summary and daily_user_stats. An older stats_own policy used FOR ALL, which also blocked that read.

Fix. Run leaderboard-rls-fix.sql in the Supabase SQL editor. Fresh installs already have the corrected policy.

Confirm the policies exist:

select tablename, policyname, cmd
from pg_policies
where schemaname = 'public'
  and tablename in ('user_stats_summary','daily_user_stats')
order by tablename, policyname;
-- expect stats_read_all / daily_read_all with cmd = SELECT

Update button returns 403 #

Cause. The request did not arrive over loopback and carried no admin cookie. Most often the browser is on a different device reaching the server across the network.

Fix. Either open the app on the same machine as the server, or run isotope update in a terminal.

# what the server thinks
curl -s http://127.0.0.1:3000/api/update-status | python3 -m json.tool

authorized: false means neither condition was met. admin_available: false additionally means admin unlock is not an option on this install.

Update returns 409 confirmation_required #

This is intentional. isotope update runs git pull, which auto-stashes local modifications. The gate exists so that never happens silently.

Commit your work, or accept the stash and recover afterwards:

git stash list          # your work is here
git stash pop           # bring it back

Mobile keyboard closes while typing #

Cause. A React focus bug in the community modal. Its focus-trap effect depended on onClose, and every call site passed an inline arrow — so the effect re-ran on every keystroke and its cleanup moved focus off the input. Android dismisses the keyboard on blur.

Fix. Already patched at serve time. If you see it, your bundle is stale — see the stale bundle section.

Opening a group shows an error #

Two distinct causes with the same appearance.

ErrorCause and fix
Could not choose the best candidate functionTwo overloads of community_get_group exist — one taking text, one taking uuid. PostgREST cannot disambiguate. Drop the text version.
Cannot read properties of undefined (reading 'reduce')The RPC returned no members array. Apply community-patch-v6.sql.
-- find duplicate overloads
select p.proname, pg_get_function_identity_arguments(p.oid) as args
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname = 'public' and p.proname = 'community_get_group';

-- remove the text variant if both exist
drop function if exists public.community_get_group(p_group_id text, p_period text);

Port already in use #

# who holds 3000
isotope status

# stop the managed server
isotope stop

# or run on another port
PORT=4000 isotope start

Server stops when switching apps on Android #

Cause. Android kills background processes. This is OS behaviour, not a bug.

Fix.

Sync is stuck or not running #

Most likely cause. The sync engine is auth-blocked. When a JWT cannot be refreshed it stops retrying deliberately — retrying with a dead token cannot succeed.

Fix. Sign out and back in. Recovery is triggered by obtaining a valid session, not by waiting.

npm run test:supabase-sync

Data missing after reinstall #

Check the cloud copy before assuming loss. Sync selects by richness before recency, so an empty snapshot cannot overwrite real data.

# richest available backup, not merely the newest
curl -s http://127.0.0.1:3000/__auth/backup/best \
  -H "Authorization: Bearer $USER_JWT" | python3 -m json.tool | head -40

Admin mode also exposes /__admin/sync, which shows per user which backup would win and why.

Google sign-in does nothing, or is missing #

The button is not there at all. That is the default. server.mjs hides it (hideGoogleUI) because Google will not accept http://127.0.0.1:3000 as an authorised origin, so an unconfigured button would fail on click. It reappears once you complete Supabase setup → Google sign-in and remove the hiding function.

The button is there and the popup returns you signed out. Supabase finished the code exchange and then refused to redirect back, because your local origin is not in its allow-list. Add both forms — they are different origins to a browser:

Supabase → Authentication → URL Configuration → Redirect URLs

http://127.0.0.1:3000/dashboard
http://localhost:3000/dashboard

Exact paths, not patterns. Supabase does accept ** wildcards, but it recommends exact URLs outside preview-deployment use — and Google, on the other side of the same flow, rejects wildcards entirely. Listing the literal URL keeps both happy.

redirect_uri_mismatch from Google. The URI in Google Cloud must be your Supabase callback, not your local address, with no trailing slash:

https://<project-ref>.supabase.co/auth/v1/callback

You land on /dashboard and bounce straight back to /auth. The session was written under a different project's storage key than the one the guard reads. Check which key exists:

// DevTools console
Object.keys(localStorage).filter(k => /-auth-token$/.test(k))

The key is sb-<project-ref>-auth-token, and the ref must match SUPABASE_URL in .env. If it does not, the app is talking to one project while the session came from another — usually left over from a project migration.

Reaching the app over your LAN? That exact origin needs whitelisting too. If the device's IP changes with the network, the allow-list needs updating each time — which is a practical reason to stay with email sign-in on a phone-hosted install.

Reading the logs #

isotope logs                                  # last 80 lines, secrets redacted
tail -f ~/.isotope/logs/server.log            # follow
tail -20 ~/.isotope/logs/browser-errors.log   # errors posted by the browser
isotope doctor                                # full diagnostic