Complete reference for every admin feature, endpoint, env var, and operational procedure.
| Panel | URL | Purpose |
|---|---|---|
| Test Suite | /__admin/verify |
Core diagnostics β schema, RPCs, RLS, server health |
| Sync Repair | /__admin/sync |
Inspect best backup candidates and promote the safe backup |
| Storage Cleanup | /__admin/storage |
Preview backup cleanup and apply only after review |
| SQL Patch | /__admin/patch |
Apply community SQL patches to Supabase |
| Removed Events | /__admin/events* |
Removed-feature JSON response; Events admin is not active |
Normal local users only need SUPABASE_URL and SUPABASE_ANON_KEY. All /__admin/* routes show an "admin mode disabled" page unless owner/admin mode is explicitly enabled.
To enable owner/admin mode, set:
ENABLE_ADMIN_MODE=true
ADMIN_SECRET=
SUPABASE_SERVICE_ROLE_KEY=
ADMIN_EMAIL=
ADMIN_EMAILS=
SUPABASE_ACCESS_TOKEN=Access via:
- Header:
X-Admin-Secret: <secret> - Query string:
/__admin/verify?secret=<secret>
SUPABASE_ACCESS_TOKEN is only needed for SQL apply through /__admin/patch. The service-role key and PAT must stay private and server-side.
| Variable | Default | Purpose |
|---|---|---|
ENABLE_ADMIN_MODE |
false |
Enables /__admin/* tools when set to true |
ADMIN_SECRET |
(empty) | Optional local unlock secret for /__admin/* routes |
SUPABASE_SERVICE_ROLE_KEY |
(empty) | Required for owner/admin data management |
SUPABASE_ACCESS_TOKEN |
(empty) | Optional; applies SQL through Supabase Management API |
ADMIN_EMAIL |
(empty) | Optional Supabase admin email for browser unlock and auto-create/check |
ADMIN_EMAILS |
(empty) | Optional comma-separated Supabase admin email allowlist |
ADMIN_PASSWORD |
(empty) | Optional admin account password for auto-create |
Open /__admin/login in the browser. You can unlock with ADMIN_SECRET, or click Use Supabase Login after signing into the app as an allowed admin email or a user with an active owner, admin, or super_admin role in user_roles.
Important: Never put these values in commits, docs, screenshots, or frontend bundles.
Use this page when a user has rich backup data in old paths such as imports/latest.json or exports/latest.json, or when cloud-snapshot/latest.json needs to be rebuilt from a richer backup.
The page can:
- show the selected best backup
- show all backup candidates
- show collection counts
- dry-run repair
- apply repair by promoting the selected rich backup to:
{userId}/backups/latest.json{userId}/backups/history/{timestamp}-{hash}.json{userId}/cloud-snapshot/latest.json
JSON API:
POST /__admin/sync/repair-user-backupBody:
{
"user_id": "uuid",
"dry_run": true
}Set dry_run to false only after checking the selected backup.
Cleanup is preview-first.
Preview:
POST /__admin/storage/cleanup-previewApply:
POST /__admin/storage/cleanup-applyApply body must include:
{
"user_id": "uuid",
"confirm": true
}Cleanup never deletes canonical latest paths or the selected best backup.
URL: GET /__admin/verify
Auth: Admin unlock
Auto-refreshes: every 30 seconds
Runs diagnostics against your configured Supabase project. In admin mode, table/RPC/bucket checks can use the server-side service-role key. Results are grouped by schema, RPC, RLS, server health, community, removed-feature, and storage checks.
Checks every required table exists with all expected columns.
| Table | Key columns checked |
|---|---|
users |
id, email, username, plan_type, billing_status, coins, gems |
user_profiles |
user_id, profile_data |
user_points |
user_id, points, lifetime_points |
user_stats_summary |
user_id, total_study_seconds, streak_days, current_streak, β¦ |
daily_user_stats |
user_id, date, seconds_studied |
study_sessions_log |
id, user_id, duration_minutes, ended_at |
groups |
id, name, slug, member_count, owner_id, is_public, max_members |
group_members |
id, group_id, user_id, role, joined_at |
group_chat_messages |
id, group_id, user_id, content, message_type, created_at |
group_invites |
id, group_id, token, created_by, max_uses, uses_count |
group_challenges |
id, group_id, title, goal_type, goal_value, start_time, end_time |
group_challenge_participants |
challenge_id, user_id, progress, completed |
group_announcements |
id, group_id, author_id, content, pinned |
group_milestones |
id, group_id, milestone_type, earned_at |
notifications |
id, user_id, type, title, body, read_at |
user_presence |
user_id, status, last_seen |
| RPC | What it tests |
|---|---|
get_membership_snapshot() |
No-arg call β plan_type + is_premium |
get_membership_snapshot(p_user_id) |
Parameterized form |
get_membership_snapshot(target_user_id) |
Alias form |
accept_invite(p_code) |
Group invite acceptance |
get_invite_details(p_code) |
Invite metadata lookup |
finish_session_sync(...) |
Focus session cloud sync |
get_group_analytics_from_snapshots(...) |
Group stats |
Verifies anon key cannot cause infinite recursion on:
groups, group_members, group_challenges, group_challenge_participants, group_chat_messages, group_announcements, group_milestones
| Intercepted Route | What the server returns |
|---|---|
get-leaderboard |
Live leaderboard data from DB |
get-daily-leaderboard |
Daily leaderboard |
get-group-leaderboard |
Group leaderboard |
get-group-analytics |
Group analytics |
finish-session |
Forwards to finish_session_sync RPC |
create_checkout |
Disabled β {url:null, disabled:true} |
create_customer_portal_session |
Disabled β {url:null, disabled:true} |
redeem_membership_code |
Self-hosted β always redeems as "success" |
| Check | What is verified |
|---|---|
/__admin/patch UI |
HTTP 200 |
/ (app root) |
HTTP 200 |
/api/health |
HTTP 200 |
Supabase REST reachable |
HTTP < 500 |
Supabase Auth reachable |
HTTP < 500 |
| Check | What is verified |
|---|---|
Admin user |
Exists in auth.users with matching ADMIN_EMAIL |
Admin row in public.users |
Username + plan_type in public table |
_is_group_member(gid,uid) |
SECURITY DEFINER helper exists |
group_members table accessible |
HTTP 200 with service_role |
user_presence (realtime) |
Table accessible |
Events and Store checks are intentionally absent because those product surfaces were removed.
URL: GET /__admin/patch
Auth: Admin unlock
Serves the full contents of community-patch-v4.sql as a runnable SQL patch. Provides:
- One-click apply via Supabase Personal Access Token (PAT) β no copy-paste needed
- Download as
.sqlfile - Copy to clipboard for manual Supabase SQL Editor paste
- Go to
/__admin/patch - Get a PAT from supabase.com/dashboard/account/tokens
- Paste into the token field
- Click Apply All SQL Now
- Watch per-statement progress β "already exists" errors are safe (idempotent)
- Hard reload the app when done
| Version | What it adds |
|---|---|
| v1 | Core community tables: group_members, group_invites, group_challenges, etc. |
| v2 | group_challenge_participants, group_announcements, group_milestones, notifications, user_presence |
| v3 | _is_group_member() SECURITY DEFINER helper β eliminates RLS recursion |
| v4 | get_group_leaderboard, get_membership_snapshot, get_group_analytics_from_snapshots RPCs |
| v5 | finish_session_sync RPC β Focus session cloud sync to DB |
| v6 | RLS recursion fix β drop legacy gm_read_members etc. policies |
| v7 | Removed Events and Store cleanup retained as an idempotent compatibility patch |
POST /__admin/apply-sql β accepts {pat: string, sql: string} and executes via Supabase Management API. Used by the one-click apply button. Avoids browser CORS restrictions on api.supabase.com.
GET /__admin/patch.sql β raw SQL download.
Events and Store were removed from the product in v3.0.0. They are not active admin tools and should not be reintroduced without an explicit new implementation request.
Current behavior:
| Route | Response |
|---|---|
/__admin/events* |
Removed-feature 404 JSON |
/api/events* |
Removed-feature 404 JSON |
/api/community-events |
Removed-feature 404 JSON |
The cleanup SQL in community-patch-v4.sql and events-expansion.sql removes Events/Store tables, RPCs, policies, triggers, views, and storage bucket residue.
Supabase storage buckets provisioned for this project:
| Bucket | Visibility | Size Limit | Allowed Types | Purpose |
|---|---|---|---|---|
avatars |
Public | 5 MB | JPEG, PNG, WebP, GIF | User profile photos |
user-content |
Private | 50 MB | JPEG, PNG, WebP, PDF, TXT | User study files (pre-existing) |
notes |
Private | 10 MB | JPEG, PNG, PDF, TXT, JSON | User study notes |
avatars bucket:
- Anyone can read (public bucket)
- Authenticated users can upload to their own folder (
{user_id}/filename) - Owners can update or delete their own files
notes bucket:
- Authenticated users can read, write, update, delete their own folder only
- No public access
avatars/{user_id}/avatar.jpg
Public URL: https://{SUPABASE_URL}/storage/v1/object/public/avatars/{user_id}/avatar.jpg
All storage requests from the frontend go through the /__supa/storage/v1/* proxy on the server, which adds the apikey header automatically. The frontend does not need to know the Supabase URL directly.
These are not admin routes. They are user-facing auth routes protected by Supabase auth and local rate limits.
| Method | Endpoint | Payload | What it does |
|---|---|---|---|
POST |
/__auth/signup |
{username: email, password} |
Create Supabase user + public.users row + Supabase session |
POST |
/__auth/login |
{username: email|username, password} |
Sign in, return {session}. Supports legacy @isotope.local suffix |
POST |
/__auth/check |
{email} |
Check if email is taken β {available: bool} |
Rate limits:
- Signup: 5 attempts per IP per 60 seconds
- Login: 10 attempts per IP per 60 seconds
/auth-bridge.js is loaded synchronously from index.html before the React app starts. It defines window.__isoLogin(email, password) and window.__isoUp(email, password) for patched or stale Auth bundles that call those globals.
Do not test those globals in the Node REPL. Node has no window. Use the browser console on /auth:
typeof window.__isoLogin
typeof window.__isoUpBoth should return "function". For a Node smoke test, run:
npm run test:auth-bridgeFor cache/load-order proof against a running server, run:
npm run test:runtime-glueFor an end-to-end Supabase Auth + private user-content Storage sync proof, run:
npm run test:supabase-syncIf a stale PWA cache serves old runtime JavaScript, clear browser caches and reload:
caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k)))).then(() => location.reload())If the header sync button returns Authentication required, the app has no valid Supabase JWT for cloud sync. Sign out, sign in again through /auth, then confirm in the browser console:
typeof window.__isoGetValidJwt| Method | Endpoint | Auth | What it does |
|---|---|---|---|
GET |
/api/health |
None | Returns local server and Supabase health checks |
GET |
/api/version |
None | Returns package version, local Git SHA, and command metadata |
POST |
/api/restart |
None | Legacy no-op. Returns isotope update guidance and never stops the server |
GET |
/api/check-update |
None | Checks GitHub for a newer version, falling back to SHA when needed |
GET |
/api/community-events, /api/events* |
None | Removed-feature 404; Events has been removed |
All Supabase API calls from the frontend are proxied through /__supa/*. The server:
- Strips
/__supaprefix - Forwards to your
SUPABASE_URL - Passes through all request headers (Authorization, apikey, Content-Type)
- Adds
apikey: SUPABASE_ANON_KEYif missing - Streams the response back
This allows the app to work behind the local preview proxy without CORS issues, and makes swapping Supabase projects (via env vars) transparent to the frontend.
The server intercepts specific edge function calls before they hit Supabase:
| Intercepted | What happens instead |
|---|---|
finish-session |
Calls finish_session_sync RPC with user JWT |
get-leaderboard |
Live DB query for leaderboard rankings |
get-daily-leaderboard |
Daily rankings |
get-group-leaderboard |
Group-scoped rankings |
get-group-analytics |
Group stats from snapshots |
create_checkout |
Returns {url:null, disabled:true} |
create_customer_portal_session |
Returns {url:null, disabled:true} |
redeem_membership_code |
Returns {success:true, redeemed:true} (always grants premium) |
The server intercepts and patches selected compiled JavaScript bundles at request time:
| Bundle | What is patched |
|---|---|
App bundle (main JS) |
planType hardcoded β ranker; circuit breaker disabled; community URL patched |
AI store |
getApiKey() β reads from window.__IK__ (injected by server from env vars) |
Focus/AI bundle |
Replaces hardcoded production Supabase URL with self-hosted URL |
Auth bundle |
Username-as-email support; auth flow patches; stale IsotopeAI v2.0 badge β v3.1 |
Invites bundle |
token_input β p_code (accept_invite + get_invite_details) |
Community / CommunityHub bundles |
Store and Events navigation/cards removed |
PWAManager bundle |
Service-worker activation reload routed through window.__isoReloadGuard() |
| Store and Events chunks | Served as empty modules |
The server injects this object into every HTML response:
window.__IK__ = {
supa: "https://YOUR_SUPABASE_URL",
anon: "YOUR_ANON_KEY",
gemini: "GEMINI_API_KEY", // if set
groq: "GROQ_API_KEY", // if set
};This allows AI features to use server-configured API keys without them appearing in static JS files.
| Variable | Required | Default | Purpose |
|---|---|---|---|
SUPABASE_URL |
Yes | Built-in project | Your Supabase project URL |
SUPABASE_ANON_KEY |
Yes | Built-in key | Public/anon JWT key |
SUPABASE_SERVICE_ROLE_KEY |
Strongly recommended | (none) | Bypasses RLS; needed for community features |
SUPABASE_ACCESS_TOKEN |
Yes (for patch) | (none) | Management API PAT for one-click patch |
ADMIN_EMAIL |
Optional | (empty) | Supabase admin email for browser unlock and optional bootstrap |
ADMIN_EMAILS |
Optional | (empty) | Comma-separated Supabase admin email allowlist |
ADMIN_PASSWORD |
Optional | (empty) | Admin account password for optional bootstrap |
ADMIN_SECRET |
Optional | (empty) | Local admin unlock secret |
GROQ_API_KEY |
Optional | (none) | AI text features (Groq/LLaMA) |
GEMINI_API_KEY |
Optional | (none) | AI text features (Gemini) |
PORT |
Auto | 3000 |
HTTP server port (set by the local runtime) |
Setup env file behavior:
.envis preferred.yeh.envis copied to.envwhen.envis missing.ISOTOPE_ENV_FILEis supported, copied to.env, and then used.- Existing values are preserved. Setup does not overwrite Supabase values or blank out
ENABLE_ADMIN_MODE. - Missing
ENABLE_ADMIN_MODEis set tofalse.
The server logs [Config] β οΈ warnings at boot for:
- Using the built-in Supabase project (not your own)
ADMIN_PASSWORDat default value- admin unlock methods not configured
SUPABASE_SERVICE_ROLE_KEYmissing or shortADMIN_EMAILat placeholder default
On first start, the server automatically:
- Patches bundles β AI store, auth bundle, app bundle, invites bundle, Focus bundle
- Creates admin user β calls Supabase Admin API to create
ADMIN_EMAIL/ADMIN_PASSWORDuser inauth.usersif not already present - Creates public.users row β upserts
{id, email, username, plan_type:'ranker'}for the admin - DML backfills β any data-fixup statements that run idempotently on startup
- Starts HTTP server on
PORT
public.usersβ extended profile, plan_type, coins, gemspublic.user_profilesβ JSONB profile blobpublic.user_pointsβ XP/pointspublic.user_stats_summaryβ aggregated study statspublic.daily_user_statsβ per-day study secondspublic.study_sessions_logβ individual focus sessions
public.groups+public.group_membersβ study groupspublic.group_chat_messagesβ group realtime chatpublic.group_invitesβ invite links with token + use-countpublic.group_challenges+public.group_challenge_participantsβ group challengespublic.group_announcements+public.group_milestonesβ group activitypublic.notificationsβ user notification inboxpublic.user_presenceβ realtime online status
_is_group_member(group_id, user_id)β SECURITY DEFINER helper prevents RLS infinite recursion. Every group membership policy must use this function instead of directEXISTS (SELECT 1 FROM group_members β¦).get_membership_snapshot()β always returnsranker/active/2099in self-hosted mode. Overrides the SaaS subscription check.finish_session_syncβ RPC must be called with the user's own JWT (not service_role), because it usesauth.uid()internally.- Server-side Supabase proxy β uses
SUPABASE_SERVICE_ROLE_KEYonly inside server handlers. Browser bundles receive the anon key only.
- Go to
/__admin/patch - Enter Supabase PAT
- Click Apply All SQL Now
- Wait for all statements to complete (idempotent β safe to re-run)
- Hard-reload the app (
/__admin/verifyto confirm)
- Go to
/__admin/events - Click + New Event
- Fill in title, type, host, dates, image gradient or URL
- Check Active to make it visible immediately
- Click Save
- Go to
/__admin/events - Click π Refresh Past Dates
- Events with
start_time < now()are pushed forward with 1β14 day offsets
- Go to
/__admin/verify - Find the failing row
- If it says "MISSING β apply patch v7": go to
/__admin/patchβ Apply All SQL - If it says "RLS RECURSION": the
_is_group_memberhelper is missing or a legacy policy is still active β apply the patch
- Update
ADMIN_EMAILandADMIN_PASSWORDin your env vars /.env - Change the Supabase user password via Supabase Dashboard β Authentication β Users
- Restart the server (the startup block will find the existing user and skip re-creation)
| Route | Method | Auth | Description |
|---|---|---|---|
/__admin/verify |
GET | β | Full automated test suite (63 tests) |
/__admin/patch |
GET | β | SQL patch manager UI |
/__admin/patch.sql |
GET | β | Raw SQL download |
/__admin/apply-sql |
POST | β | Execute SQL via Management API proxy |
/__admin/events* |
Any | β | Removed-feature 404 JSON; Events admin is inactive |
β = protected by admin mode and ADMIN_SECRET
Last updated: v3.1.x β 2026-06-05