Admin Guide
The admin-only surfaces of Wanderlearn, and what BAM does day to day as the sole admin in Phase 1.
If you're a creator or teacher, you want CREATOR_GUIDE.md instead. This doc covers only what requires the admin role.
0. Your admin surfaces
Four routes behind requireAdmin():
| Route | What it's for |
|---|---|
| /en/admin/users | Promote users to creator / teacher / admin roles |
| /en/admin/courses | Review + approve + unpublish courses |
| /en/admin/support | Triage every support thread across all users |
| /en/admin/support/<thread-id> | Read + reply to a specific thread, change its status |
Everything else on the site runs through the same UI creators and learners see. There's no separate admin-only "god mode" dashboard; the admin powers are narrowly scoped to role-gated actions.
1. User roles
Wanderlearn has four roles:
| Role | Can do |
|---|---|
learner |
Browse catalog, enroll in courses, take lessons. Default on sign-up. |
creator |
Everything a learner can do, plus upload media, build destinations/scenes/courses, submit for review. |
teacher |
Same as creator today. Reserved for future differentiation (e.g. institutional permissions). |
admin |
Everything creators/teachers can do, plus the four surfaces above. |
Promoting a user
- Open /en/admin/users.
- Find the user by email or name.
- Pick the new role from the dropdown. Save.
Or via CLI: pnpm db:promote <email> <role>. Requires your .env.local DATABASE_URL to point at whichever DB you want to update.
When to promote to creator: someone who will build Wanderlearn courses (MUCHO partners, invited educators).
When to promote to teacher: same as creator for now. Revisit when we actually differentiate them (institutional dashboards, class cohorts, etc.).
When to promote to admin: yourself, and only yourself in Phase 1. Adding another admin gives them full access to all support threads, every course's review gate, and every user's role. High-trust action.
2. Reviewing and approving courses
Creators build a course in draft, then click Submit for review on their course detail page. That transitions the course to in_review status. Status is only visible to the creator and to admins.
Your review inbox
/en/admin/courses shows courses in in_review + published status, most-recently-updated first. Filter by status with the pills at the top.
Reviewing a specific course
Click into any course to land on /en/admin/courses/<id>. You'll see:
- Header: title, creator (name + email), current status
- Review controls: the publish checklist (violations, if any) + approve / unpublish buttons
- Lessons: a summary of every lesson in the course with status and summary
The publish checklist is the same one the creator saw when they hit submit. If it's all green, the course is ready to publish. If it has violations, those are blocking; clicking Approve and publish will refuse with the same gate error.
Approving a course
- Skim the course content in the creator view: open the course as a learner via
/en/courses/<slug>(you're an admin, so access is unrestricted), click through every lesson, verify quality, transcripts, accuracy. - Open any
virtual_tourblock and verify the referenced destination's scenes render correctly. If the destination mixes photo_360 and video_360 scenes, the viewer only renders the photos. An amber warning on the destination edit page flags this, but worth spot-checking from the learner view. - If the creator has toggled the destination to public (
/en/tours/<slug>is reachable), confirm the public version looks right before approving; public share links live independently of course publish state. - Back in the admin review page, click Approve and publish. Course status becomes
published,publishedAtis set. Course appears in the public catalog.
There's no signed reviewer record or audit trail beyond the updatedAt timestamp in Phase 1; you're the only admin, so it's implicit. When a second admin is added, add an audit log as a follow-up feature.
Unpublishing
If a published course turns out to have a problem (copyright, incorrect content, safety):
- Open /en/admin/courses/<id>.
- Click Unpublish. Status becomes
unpublished, removed from public catalog,publishedAtcleared.
Learners who already enrolled keep their enrollment (they paid for it, you don't take it back unilaterally) but the course is no longer purchaseable and no longer in the catalog. If the issue is severe enough to require revoking enrollments, that's a DB-level manual action today, tracked as a future admin-tool feature.
Re-submitting: the creator can revise and submit for review again. Status flows unpublished → in_review → published.
3. What the publish gate enforces
Full source in src/lib/publish-gates.ts. Five violation kinds:
| Violation | Meaning |
|---|---|
no_lessons |
Course has zero lessons |
lesson_empty |
A lesson has zero blocks |
video_missing_transcript |
A video or video_360 block's media has no transcript_media_id linked |
media_not_ready |
A media-backed block points at media still processing |
media_missing |
A block points at media that's been deleted |
The gate runs on submit for review (creator-side) AND on approve (admin-side). You can't bypass a gate violation by clicking approve harder; the check re-runs server-side every time.
What the gate does NOT check (yet): audio descriptions on videos, color-contrast of embedded images, length of content, language consistency with defaultLocale. Those are either out of scope for Phase 1 or tracked as follow-up accessibility gates.
4. Support inbox
As sole admin, you're also on-call for support threads.
/en/admin/support lists every thread across all users, sorted by most recent activity. Filter by status:
| Status | Meaning |
|---|---|
open |
New thread or any thread freshly created |
waiting_admin |
User replied; your turn to respond |
waiting_user |
You replied; user's turn |
resolved |
You marked it resolved (sets resolvedAt) |
closed |
Archived. Replying is blocked. |
How threads flow
- A user (any role) clicks the "Get help" floating button from any page, or navigates directly to /en/support/new.
- They submit subject + category + body. Thread is created,
status=open, first message attached withauthorRole=user. - You get a Mailgun email at
ADMIN_NOTIFY_EMAILwith the thread subject and excerpt + a deep link to the admin thread page. - You click the link, read the full thread, reply using the Reply form at the bottom.
- Submitting a reply auto-flips the status to
waiting_userAND emails the user via Mailgun. - Repeat until resolved. Click the Status dropdown to mark
resolvedorclosedwhen done.
Attachments in threads: not in Phase 1. Users can only write text. If a screenshot is needed, instruct them to upload it to their media library (if they're a creator) or host it elsewhere and paste a link.
Response time expectations
Plan 00 doesn't set a hard SLA. Best practice: respond within 24 hours during weekdays. If a thread is a genuine outage / user-locked-out / payment issue, prioritize.
5. Ecosystem + shared infrastructure
Wanderlearn isn't standalone forever. It shares Cloudinary with Fly.WitUS (future), Tour Manager OS (future), and CentenarianOS (future). You're the ecosystem admin for Wanderlearn's share.
Read these before changing any Cloudinary or cross-app config:
- docs/CLOUDINARY_FOLDER_CONVENTION.md: top-level folder prefixes per app,
public_idrule,contextmetadata keys, tags, BVC→Wanderlearn hand-off contract - docs/INFRA.md: what every third-party service does, required env vars, R2 fallback plan
- docs/CLOUDINARY_SETUP.md: signing, webhook, poster-frame generation
Two things to know as admin:
- Wanderlearn owns the
wanderlearn/folder prefix in the shared Cloudinary tenant. Don't let another app's signer drop uploads intowanderlearn/; that's the security boundary. The signer at src/app/api/media/cloudinary-sign/route.ts hard-codes our prefix, so a compromised client can't exfiltrate. - Reserved prefixes (
bvc/,tour/,cent/) belong to other apps. If you see content there, it's not yours; don't delete or modify.
6. Handling abuse
Inappropriate course content
- Unpublish the course from /en/admin/courses/<id>.
- If the course references a destination whose creator has toggled public (a shareable
/en/tours/<slug>link exists), you can't unpublish that directly from admin today. Ask the creator to flip it private, or, as a break-glass, manually setdestinations.is_public = falseagainst the DB. A dedicated admin control is a follow-up. - Contact the creator via support chat or direct email explaining why.
- If egregious (illegal content, clear TOS violation), delete the course. The reference blocker will surface any media that needs to be deleted separately.
- Log the action in a private note (no admin audit log exists yet, so keep your own record).
Abusive support threads
A user spamming support threads:
- Mark the threads
closed(replying is blocked inclosedstate). - If it persists, revoke their account. There's no "ban" UI today; you'd manually null their session + delete their user row, or set
role=learnerand rely on rate-limiting. Log as a future admin tool need.
Media reference blocker
If you try to delete media from /en/creator/media that a scene, destination hero, or course cover points at, you'll see a list of references and the delete is blocked. This is a safety feature, not a bug. Click through to each reference, replace the media with something else, then delete.
7. What doesn't exist yet
Honest list:
- Admin audit log: who approved what course, when. Today it's just
updatedAt. - Revenue dashboard: Stripe has your data, but Wanderlearn doesn't surface it per-course in the admin UI. View in Stripe dashboard directly.
- Bulk actions: you can't approve 10 courses at once or mark-all-read a batch of threads.
- User detail page: you can change a role, but there's no "view this user's courses and progress" page.
- Scheduled content: you can't schedule a course to publish at a specific time; you click approve when you want it live.
- Refund UI: refunds are manual via the Stripe dashboard for now. Add a refund action as a future admin tool.
- Admin override for a creator's public-share toggle: you can't un-share a public destination from admin; the creator has to flip it, or you DIY via SQL. Small follow-up.
- PostHog analytics surfacing: events aren't wired yet; waiting on the event taxonomy decision.
Each is a small feature, and none is hard. They're just not in Phase 1 because you're the only admin and you can work around each with the Stripe dashboard + direct DB queries + your own notes.
8. Emergency operations
If something goes badly wrong:
The site is down (all routes 500):
- Check Vercel dashboard → Deployments → last deploy state.
- If a recent deploy introduced the break, promote the previous successful deploy (Deployments → ⋯ on the known-good row → Promote to Production).
- Open an incident in your notes. File a bug. Fix on a
fix/*branch.
A specific user is locked out:
- Check /en/admin/users for their account. Still there? Role correct?
- If their sign-in method is stuck (magic-link not arriving), check Mailgun logs for bounces.
- As a last resort: manually delete their
sessionsrow in the DB and have them sign in fresh.
Data loss panic:
- Don't run
db:migrateordb:seedagainst production until you've assessed. - Neon Pro keeps 7 days of point-in-time snapshots. Restore from the Neon dashboard if needed.
- Cloudinary assets are append-only for us (deletes are a creator action); they're not lost unless someone deleted them.
Stripe webhook loop or duplicate charge:
- Check Stripe dashboard → Webhooks → recent deliveries to see what actually fired.
- Cross-reference with the
purchasestable in the DB. Each Stripepayment_intent.succeededshould create exactly oneenrollmentsrow. - If a duplicate got through, manually adjust in Stripe (refund the duplicate) and in the DB (delete the extra
enrollmentsrow). Log the incident.
9. The rule set you agreed to
You're the admin, and also the person who agreed to how Wanderlearn operates. Read these when you hire a second admin:
- plans/STYLE_GUIDE.md: engineering standards, launch gates, commit conventions
- plans/00-wanderlearn-phase-1-mvp.md: the plan you built against
- docs/CLOUDINARY_FOLDER_CONVENTION.md: shared-infrastructure rules
The no-AI-content rule applies to admin actions too. Don't use AI to draft course approvals, support replies, or legal policy text. Your name stands behind what you publish.
When in doubt
Ask yourself: "Is this something a creator could reasonably need me to do?" If yes, do it. If no, escalate to a human stakeholder (legal, accounting, cofounder if you have one) before taking an action that's hard to reverse.