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

Architecture

IsotopeAI is one Node process with no runtime dependencies. It serves a pre-built React application, rewrites those bundles on the way out, and proxies data to your Supabase project.

Request flow: the browser talks to a Node server on the same device, which patches JavaScript bundles from disk and proxies data requests to the user's own Supabase project. The service-role key stays on the server.
Two trust zones. The service-role key never crosses into the browser.

One file, no dependencies #

server.mjs is around 10,000 lines and imports nothing outside the Node standard library plus two local modules. package.json lists zero dependencies and zero devDependencies, which is why install needs no build toolchain and cannot break from a transitive update.

Serve-time bundle patching #

The frontend in public/assets/ is a pre-built, minified Vite output. The server does not modify those files on disk. Instead, each patched bundle has a function that reads the file, performs exact string replacements, memoises the result and returns a Buffer:

function getPatchedCommunityBundle() {
  if (patchedCommunityBundle) return patchedCommunityBundle;   // memoised
  let raw = fs.readFileSync(COMMUNITY_BUNDLE_ABS, 'utf8');
  if (raw.includes(ANCHOR_FROM)) {
    raw = raw.replace(ANCHOR_FROM, ANCHOR_TO);
    console.log('[CommunityPatch] applied');
  } else {
    console.warn('[CommunityPatch] anchor not found');
    _criticalPatchFailures.push('community-anchor');   // surfaces as a UI banner
  }
  patchedCommunityBundle = Buffer.from(raw, 'utf8');
  return patchedCommunityBundle;
}

There are 23 patched bundles and around 52 individual patches. They fix upstream crashes, neutralise a demo-data gate, disable a circuit breaker that could lock all Supabase calls for five minutes, and add features such as group chat and the leaderboard tab.

Anchors are matched against minified codeA patch anchor is an exact substring of the built bundle. Any frontend rebuild changes both the content hashes in the filenames and the minified identifiers, so anchors must be re-verified. Every patch logs on success and warns on failure, and failures also render a banner in the served HTML — a silent mismatch is the one failure mode worth engineering against.

Verifying patch health #

bash bin/isotope restart && sleep 6

# empty output means every anchor matched
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

HTML script injection #

Every HTML response is rewritten. Scripts that must run before the app are placed before </head>; everything else is deferred to just before </body> so first paint is not blocked.

InjectedPositionPurpose
Origin constantsheadSupabase URL and anon key for the runtime.
Local-data guardheadPer-user isolation of localStorage and IndexedDB keys.
Auth guardheadSynchronous redirect for unauthenticated deep links, before React mounts.
Error boundarybodyCatches bundle load failures and offers recovery.
Premium scriptbodyFetch interceptor and leaderboard builder.
Reload guardbodyAllows one service-worker reload per session.
Sync enginebodyJWT refresh, auto-sync, notification polling.

Service worker caching #

/assets/ is served cache-first, which is correct for content-hashed files but created a subtle failure: because bundles are patched at request time, editing a patch changed the response without changing the filename. A cached copy would be served indefinitely.

The cache name therefore includes a build token — a digest of VERSION plus the server.mjs modification time:

const CACHE_SUFFIX = APP_VERSION + '-' + APP_SHA.slice(0, 12) + '-' + BUILD_TOKEN;
const SHELL_CACHE   = 'isotope-local-shell-'   + CACHE_SUFFIX;
const RUNTIME_CACHE = 'isotope-local-runtime-' + CACHE_SUFFIX;

Any edit to a serve-time patch now rotates both caches on the next load. The live value is reported by /api/version as pwa_cache.

Recovery when a stale bundle still slips throughThe error boundary detects a failed /assets/ load, purges caches and reloads once — stamped per build in sessionStorage so it can never loop. If that attempt is spent, it shows a "Refresh required" prompt instead of a black screen. Auth tokens and study data are untouched: caches.delete() only clears HTTP responses, not localStorage or IndexedDB.

Supabase proxy #

/__supa/* forwards to your project. Escalation to the service-role key happens only for an authenticated admin:

const useServiceKey = ADMIN_MODE_READY && isAdminAuthed(req);

Every other caller is forwarded with their own Authorization header plus the anon key, so row-level security still applies.

Feature-to-Supabase map #

Which features actually reach Supabase, which are local-first, and which are local on purpose. The distinction matters: “local-first” means the feature works offline and syncs later; “local only” means it never touches the network and that is intended, not a gap.

FeatureLocal storeSupabase objectOffline
AuthSupabase session keys, isotope-auth-tokenauth.users, public.usersCached session restored
Session restoreisotope-bootstrap-cacheuser_profiles, user_onboardingCached bootstrap fallback
Onboardingisotope-onboardinguser_onboarding, user_profilesLocal cache
Profile & settingsisotope_user_profile_v2user_profiles.profile_data, user_settingsLocal profile stands
Avatarisotope_user_profile_v2avatars bucketLocal avatar stands
Focus sessionsIndexedDB sessions, isotope_sessions_v2finish_session_sync RPC, study_sessions_logPending queue
Daily statsisotope_daily_logs_v2daily_user_statsLocal analytics
Tasks, subjects, habits, exams, timerIndexedDB + isotope_*_v2Backup JSON in user-contentFully local-first
Backup / import / smart syncisotope_sync_metadatauser-content/backups/*Skipped offline
Community & notificationsGroup cachegroups, group_members, notificationsOnline required
Admin routesNoneService role / Management APIUnavailable offline
Update check, install scripts~/.isotopeNone — GitHub API onlySkipped, by design

The pattern worth noticing: everything a student produces while studying — tasks, sessions, subjects, habits, exams, timer state — is local-first and reaches Supabase only as a backup blob. Only community features genuinely require a connection, because they involve other people. That is why the app is usable on a phone with no signal.

SQL run order #

For a fresh project, in this order:

  1. isotope-complete.sql — the whole schema. Idempotent, safe to re-run.

  2. Compatibility migrations in sql/only when upgrading an older install.

  3. sql/verify-security.sql — verification queries, not changes.

Upgrade-only patches, which a fresh install should skip:

PatchWhen you need it
leaderboard-rls-fix.sqlThe leaderboard renders empty. A legacy stats_own FOR ALL policy blocked public SELECT on user_stats_summary; this replaces it with public read plus own-row writes. Fresh installs already get the correct policy.
community-patch-v6.sqlThe cumulative community patch, and what /__admin/patch serves. Supersedes v4.
performance-patch.sql, performance-indexes.sqlIndexes for RLS membership subqueries and leaderboard date sorts.

Startup sequence #

  1. Load and validate configuration

    .env then host environment. Exits on a malformed SUPABASE_URL.

  2. Bind the port

    Listens on 0.0.0.0 so a phone on the same network can reach it.

  3. Warm the patch cache

    All patchable bundles are patched and 11 are pre-gzipped, sequentially to avoid a CPU spike.

  4. Ensure storage buckets

    Creates or reconciles the four buckets. Requires admin mode.

  5. Run backfills

    Seeds satellite rows for existing users. Requires admin mode.