Admin runbook
Operational reference for super-admins. Every routine action ("approve a sponsor," "promote a template," "recompute scores," "grant super-admin to a teammate") with the URL, the SQL, and what to verify after.
All admin operations require the
super_adminrole onprofiles.role. If you're not super-admin, the relevant page either renders a permission-denied state or returns 403 from the backend.
Quick links — admin pages
| Page | Use for |
|---|---|
| /dashboard/admin | Users, orgs/teams, imports, site settings, conversion rates, chat config, encouragement-drops templates, workout-window AI users |
| /dashboard/admin/tasks | Product roadmap (product_tasks table). Also managed via the Claude Code /tasks skill |
| /dashboard/admin/open-energy | Seed generators + stats for the Open Energy / DreamPro Citizen Science platform |
| /dashboard/admin/features | Feature flag table (admin-controlled feature visibility) |
| /dashboard/admin/kit-waitlist | DreamPro Junior waitlist signups + decision-gate dashboard |
| /dashboard/admin/health | Production health: backend status, DB latency, applied-vs-local migrations diff, commit SHA |
| Supabase SQL editor | One-off SQL when no admin UI exists |
Granting super-admin to a teammate
The super_admin role gates every admin operation in the platform.
Steps:
- Get the user's auth ID. Easiest path: have them sign up at
hivejournal.com, then look them up in/dashboard/admin(Users tab) and copy their ID. - Use the existing role-update endpoint via the admin Users tab (
Edit user → Role: super_admin → Save). This is preferred because it audit-logs through the backend. - Or run direct SQL if the UI is broken:
UPDATE public.profiles
SET role = 'super_admin'
WHERE id = '<user-uuid>';
Verify: the user signs out and back in, then visits /dashboard/admin — they should see the full admin nav strip including 🩺 Health, 📦 Kit Waitlist, ⚡ Open Energy, 📚 Feature Index. If they still see "Super-admin only," the cached session token is stale; signing out and back in forces a fresh JWT with the new role claim.
To revoke: same flow, set role = 'user'. Revocations are immediate but require the affected user's next request — kill any active session via Supabase Auth → Users → Sign out user.
Health page interpretation + applying missing migrations
The single most important admin page after a deploy.
/dashboard/admin/health shows:
- Status banner: ✅ Healthy / ⚠️ Degraded. Degraded means the backend's DB round-trip query failed.
- DB round-trip latency: should be under 200ms in production. Spikes past 500ms warrant a Supabase status check.
- Commit SHA: the deployed Railway commit. Match against
git rev-parse HEADafter a deploy. - Backend uptime: how long since the Railway process last booted. Resets on every deploy.
- Migrations diff: the critical bit. Files in
supabase/migrations/not yet applied in production show in an amber call-out.
When the diff shows missing migrations:
- Open the Supabase SQL editor.
- Open each missing migration file from the repo (e.g.
supabase/migrations/074_sponsor_outreach_reply_tracking.sql). - Paste the contents into the editor and Run. Any
IF NOT EXISTS/ON CONFLICT DO NOTHINGclauses make it safe to re-run. - Refresh the health page. The migration should disappear from the amber list and appear in "Most recent applied migrations."
When the migrations table itself can't be read (the diff shows "couldn't read applied migrations from Supabase"): the service role key in Railway env may not have access to the supabase_migrations schema. This is a Supabase project setting — check Project Settings → API → Service role key → permissions, or fall back to manually checking the SQL editor with SELECT version, name FROM supabase_migrations.schema_migrations ORDER BY version DESC LIMIT 20;
Recomputing competition scores
Two paths — manual button (for one competition) or the nightly cron (for all active competitions).
Manual recompute (e.g. after backfilling a batch of replications):
- Navigate to the competition leaderboard:
/dreampro/competition/<slug>(e.g./dreampro/competition/open-energy-2026). - The "Recompute scores" button at the top is super-admin-only — click it.
- Wait for the success toast; the leaderboard refreshes automatically.
Verify: reload the page and confirm the participant count and individual scores changed (or didn't, if the recompute was a no-op). Check the competitions.last_scored_at column in SQL.
Nightly cron: POST /api/cron/dreampro/recompute-competitions runs at 0 6 * * * (06:00 UTC). Iterates every competitions WHERE is_active AND entry_open and recomputes scores. No action needed unless cron itself stops firing. See docs/operations/crons/CRON_JOBS_SUMMARY.md for the full cron list.
Scoring weights (from competitions.scoring_config JSONB):
{
"clone": 1,
"completion": 5,
"replication_confirmed": 25,
"replication_refuted": 5,
"build": 10
}
To change a competition's weights, update the JSONB in Supabase SQL editor, then trigger a manual recompute.
Approving sponsors
Sponsors apply via /dreampro/sponsors/apply with status='pending'. They don't show on the public sponsor wall until approved.
Steps:
- Pull the pending list:
curl -X GET https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/sponsors/pending \
-H "Authorization: Bearer <your-jwt>"
Or open the Supabase SQL editor:
SELECT id, name, tier, contact_email, contact_name, applied_at
FROM public.sponsors
WHERE status = 'pending'
ORDER BY applied_at ASC;
-
Review each — confirm legitimacy (real organisation, contact email reachable, tier appropriate to the contribution).
-
Approve:
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/sponsors/<id>/approve \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json"
Or decline with a reason:
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/sponsors/<id>/decline \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"reason": "Could not verify organisation contact details"}'
Verify: approved sponsors appear on the public wall at /dreampro/sponsors. Decline reasons are stored in sponsors.decline_reason (private) and the applicant is not automatically notified — close the loop manually if needed.
Privacy reminder: contact_email and contact_name are admin-only and never returned by public endpoints. Don't leak them in screenshots or shared docs.
Approving opportunities (internships, scholarships, fellowships, mentorships)
Same workflow shape as sponsors. Opportunities live in the opportunities table.
# List pending
curl -X GET https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/opportunities/pending \
-H "Authorization: Bearer <your-jwt>"
# Approve
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/opportunities/<id>/approve \
-H "Authorization: Bearer <your-jwt>"
# Decline
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/opportunities/<id>/decline \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"reason": "..."}'
Verify: approved opportunities appear at /dreampro/opportunities.
Approving dream videos (Phase 8a)
Submitters post YouTube URLs at the template detail page. Status starts pending; approved videos render in the template's video section as embedded iframes.
# List pending videos
curl -X GET https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/videos/pending \
-H "Authorization: Bearer <your-jwt>"
# Approve
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/videos/<id>/approve \
-H "Authorization: Bearer <your-jwt>"
# Decline
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/videos/<id>/decline \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"reason": "Off-topic"}'
Pre-approval checklist:
- Watch the video. Off-topic / spam / promotional → decline.
- Confirm the YouTube ID is still live (oEmbed succeeded at submission, but channels can go private later).
- Confirm the submitter is not a
kind=classroomparticipant — classrooms are blocked from video submissions by the backend, but spot-check in case the rule is bypassed. - For first-time submitters, check the user's other public activity for context.
Featured videos: set the is_featured BOOLEAN column on dream_videos directly via SQL. Featured videos sort first on the template detail page.
Promoting a template to classroom-safe
dreams.safety_level defaults to 'adult_only'. K-12 classrooms can only run templates flagged 'classroom_safe' (or 'with_supervision' with teacher attestation). Promotion is super-admin-only.
Pre-promotion checklist:
- Read the template's procedure end-to-end. Confirm:
- No mains voltage, no flammable solvents, no high-current handling.
- All parts ship as kits or are commonly available without age verification.
- The instruction language is age-appropriate for the
min_grade_bandyou're going to set.
- Set
min_grade_bandtruthfully. A template suitable for high school but not for elementary should be9-12, notK-5.
Promote via API:
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/admin/templates/<id>/promote-to-classroom \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{"min_grade_band": "6-8"}'
Or direct SQL:
UPDATE public.dreams
SET safety_level = 'classroom_safe', min_grade_band = '6-8'
WHERE id = '<template-uuid>' AND is_template = TRUE;
Verify: the template appears at /dreampro/templates/classroom.
To demote: flip back to safety_level = 'adult_only'. Existing classroom progress reports remain in classroom_progress_reports but new clones from classroom accounts will be blocked.
DreamPro Junior — kit waitlist review
/dashboard/admin/kit-waitlist shows the decision-gate dashboard. Use it weekly during the validation period.
The decision gate: >200 signups within 30 days of launch = green-light to build the P3 kit-subscription tasks. The page surfaces the current count, day-since-first-signup, and projected-at-day-30. See docs/product/DREAMPRO_JUNIOR_UNIT_ECONOMICS.md for why 200 is the right threshold.
Per-week routine:
- Open the page Monday morning.
- Note the count delta from the prior week.
- Skim the country breakdown — significant new countries inform the international-shipping decision.
- Spot-check the most recent 10 signups: are they real (notes coherent, country plausible) or spam?
If spam appears, delete via SQL:
DELETE FROM public.kit_waitlist WHERE id = '<uuid>';
The endpoint is service-role-only insert, so no public DELETE policy exists.
Sponsor outreach — moderation and digest review
The Phase 3c relay flow at /dreampro/sponsorable is rate-limited (5 messages/24h per sender per recipient) and admin-blind under normal operation — admins don't read sponsor messages as a matter of policy.
When you do need to intervene (abuse complaint from a recipient):
-- Find every message from a specific sender
SELECT id, to_participant_id, subject, created_at, delivery_status
FROM public.sponsor_outreach
WHERE from_user_id = '<sender-uuid>'
ORDER BY created_at DESC;
-- Block a sender by setting is_active = false on their profile (heavy-handed)
UPDATE public.profiles SET is_active = false WHERE id = '<sender-uuid>';
Reviewing the weekly digest cron output:
-- Last 4 weeks of digests, with delivery success rate
SELECT week_start_at,
COUNT(*) AS digests_attempted,
SUM(CASE WHEN delivered THEN 1 ELSE 0 END) AS digests_delivered,
SUM(CASE WHEN delivered THEN 0 ELSE 1 END) AS digests_failed
FROM public.sponsor_outreach_digests
GROUP BY week_start_at
ORDER BY week_start_at DESC
LIMIT 4;
The cron is at POST /api/cron/dreampro/sponsor-outreach-digest, scheduled 0 9 * * 1 (Mondays 9am UTC). Idempotent — re-running in the same week is a no-op. To force-resend a week's digest, delete the matching sponsor_outreach_digests rows and re-trigger the cron via the Vercel dashboard.
Impersonating a user (debugging)
Super-admin only. Audit-logged in admin_impersonations.
From the admin panel: Users tab → click 👤 Impersonate next to the user. You're redirected through /auth/impersonate-callback and land in their dashboard.
Stop impersonating: click "Stop impersonating" in the navbar (visible during impersonation), or hit /auth/impersonate-callback?stop=1.
Audit log:
SELECT i.created_at, i.admin_id, p_admin.email AS admin_email,
i.target_id, p_target.email AS target_email, i.reason
FROM public.admin_impersonations i
LEFT JOIN auth.users p_admin ON p_admin.id = i.admin_id
LEFT JOIN auth.users p_target ON p_target.id = i.target_id
ORDER BY i.created_at DESC
LIMIT 50;
Don't impersonate without a stated reason — the row should explain why (debug ticket #N, support request, etc.). The reason field is mandatory; the UI prompts for it.
Cron job verification
Every scheduled job is configured in apps/frontend/vercel.json. Full per-cron details are in docs/operations/crons/CRON_JOBS_SUMMARY.md.
To verify a cron is firing:
-
Check the Vercel dashboard → the relevant project → Crons tab. Each cron shows its last invocation time and status.
-
Check the corresponding table for "did the cron actually do work":
sponsor_outreach_digests— should grow by 1 row per opted-in recipient per Mondaynote_analyses— should grow daily as the analysis cron runsweekly_reviewrecords injq_routine_runs— should grow weeklycompetitions.last_scored_at— should advance daily for active competitions
-
To manually trigger any cron from the admin terminal:
curl -X POST https://hivejournal.com/api/cron/<path> \
-H "Authorization: Bearer $CRON_SECRET"
Where $CRON_SECRET is the value of WORKOUT_WINDOW_CRON_SECRET (or the cron-specific secret) from the Vercel env.
Experiment calculators — running the optimizer + setting golden specs
The "Computational Companion" stack lets you pre-compute the optimal configuration for any experiment template that has a calculator, then publish it as a "confirm this prediction" invitation.
Step 1: Run the parameter sweep
Navigate to any research-grade template page (e.g. /dreampro/templates/<id>). Scroll to the "⚡ Find optimal parameters" section and click Run parameter sweep. The system sweeps up to 50K parameter combinations and ranks by the default objective:
| Calculator | Objective |
|---|---|
| Resonance | Maximize Q factor |
| Coil | Maximize self-resonant frequency |
| Impedance | Maximize Q factor |
| Electrolysis | Maximize energy efficiency |
| Pulsed DC | Maximize recovery efficiency |
| Plasma | Minimize breakdown voltage |
Or via API:
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/optimize \
-H "Content-Type: application/json" \
-d '{"calculator": "resonance", "top_n": 10}'
Step 2: Review and set the golden spec
The sweep returns the top N results ranked by objective. Review the #1 result. If it looks right, click "🎯 Set as golden spec" (super-admin only). This stores the optimal parameter set on the template's predicted_values JSONB column.
Or via API:
curl -X POST https://hivejournalbackend-production.up.railway.app/api/dreampro/templates/<template-id>/golden-spec \
-H "Authorization: Bearer <your-jwt>" \
-H "Content-Type: application/json" \
-d '{
"calculator": "resonance",
"optimal_inputs": {"inductance_uh": 4.7, "capacitance_nf": 100, "resistance_ohms": 0.5},
"calculator_outputs": {"outputs": [{"label": "Q factor", "value": 59.2, "unit": ""}]}
}'
Step 3: Verify the invitation card
Reload the template page in an incognito window. You should see the "🎯 Confirm this prediction" card with:
- Recommended build spec (the optimal inputs)
- Expected outcomes (the predicted outputs)
- Convergence bar (0/50 initially)
- "Clone + confirm this experiment" CTA
- Copy share link button
Step 4: Monitor convergence
As replications come in with structured measurements:
- The aggregate results section (📊 Community results) shows histograms + convergence bars per parameter
- When 50 independent confirmations land within ±5% of the prediction, the experiment reaches "robust"
- Anomaly flags (>2σ from mean) surface replications worth investigating
Verify via SQL:
SELECT
dr.id, dr.status, dr.measured_values, dr.delta_values, dr.created_at
FROM public.dream_replications dr
WHERE dr.dream_id = '<template-uuid>'
AND dr.measured_values IS NOT NULL
ORDER BY dr.created_at DESC
LIMIT 20;
Common one-off SQL operations
Adjust a user's drops balance (with audit row):
INSERT INTO public.drops_ledger (user_id, delta, reason, source)
VALUES ('<user-uuid>', 100, 'Manual admin grant: support compensation', 'admin');
-- Recompute balance from ledger
UPDATE public.profiles
SET drops_balance = (
SELECT COALESCE(SUM(delta), 0) FROM public.drops_ledger WHERE user_id = '<user-uuid>'
)
WHERE id = '<user-uuid>';
Force-delete a user's account + cascade: Supabase Auth → Users → Delete user. Most rows cascade via FK. Spot-check profiles, journal_entries, participants, dreams to confirm.
Promote a template from a private dream (so others can clone it):
UPDATE public.dreams
SET is_template = TRUE, is_public = TRUE
WHERE id = '<dream-uuid>' AND user_id = '<owner-uuid>';
Backfill journal_entries.mentions after a manual data import (idempotent — see migration 071):
curl -X POST https://hivejournalbackend-production.up.railway.app/api/journal/backfill-mentions \
-H "Authorization: Bearer <your-jwt>"
# Loop until {"remaining": 0}
When something breaks — first 5 minutes
- Visit /dashboard/admin/health. Status banner red? DB latency spiked? Migration diff non-empty?
- Check the BetterStack status page at status.hivejournal.com (configured per docs/setup-guides/UPTIME_MONITORING_SETUP.md). External monitor independent of the platform.
- Check Railway: https://railway.app → HiveJournal backend → Logs. Recent errors usually show the offending route + stack.
- Check Supabase status: https://status.supabase.com — if Supabase is degraded, everything else is downstream.
- Check Vercel status: https://vercel-status.com — if the frontend is down but the backend is up, that's a Vercel deploy issue.
If the issue is in code: revert the most recent commit, redeploy, then debug at leisure. Don't try to hot-fix in production unless you've reproduced the issue locally first. See docs/operations/RAILWAY_REDEPLOY.md for the rollback procedure.
Cross-references
- QUICKSTART.md — local dev setup
- DEPLOYMENT.md — Vercel + Railway deployment
- PRODUCTION_MIGRATIONS.md — safe migration procedures
- crons/CRON_JOBS_SUMMARY.md — every scheduled job
- docs/setup-guides/UPTIME_MONITORING_SETUP.md — external monitor config
- docs/ai/INDEX.md — full feature catalog (start here when adding a new admin operation)