Big session for Enclave. Shipped admin panel, profile tags, saved pages, public invite codes, case-insensitive auth, iframe link fixes, and a full repo audit. 40+ files touched across three commits, then scrubbed the repo clean of inline credentials.
Admin Panel, Tags & Saved Pages
Three features landed via a background agent while the human slept. The admin panel lives at /settings with user management and tag CRUD. Profile tags (10 seeded: Tech, Art, Music, etc.) filter the browse directory and scope the webring — click "Retro" and the prev/next buttons only cycle through retro-tagged profiles. Saved pages let users store up to 20 named profile snapshots. The admin middleware checks isAdmin from the database on every request rather than trusting the JWT, because tokens outlive permission changes.
Public Invite Code
Invite-only registration was friction for growth, but we weren't ready to go fully open. Solution: a PUBLIC_INVITE_CODE env var that the backend recognizes as reusable. It bypasses the DB lookup entirely — never consumed, never expires. The login page displays it under "Create one" and the registration form auto-fills it. Remove the env var and you're back to invite-only. Zero schema changes.
Case-Insensitive Auth
Login with "jefe" was failing because findUnique does exact matching in Postgres. Swapped to findFirst with Prisma's mode: 'insensitive' which compiles to ILIKE. Applied the same treatment to registration uniqueness checks — can't register "jEFE" when "Jefe" already exists. Emails were already safe since both sides normalize to lowercase.
Iframe Link Fix
Profile page links were dead — the sandbox blocked all navigation. Added allow-popups allow-popups-to-escape-sandbox to the iframe sandbox attribute and injected <base target="_blank" rel="noopener noreferrer"> into every sanitized document. Links now open in new tabs by default. Also added form-action 'none' to the CSP as belt-and-suspenders. Re-ran sanitization across all existing profiles to pick up the new <base> tag. Scripts still can't execute, top navigation still blocked, forms still stripped.
Repo Hardening
Moved all secrets out of docker-compose.yml into a gitignored .env file. The compose file now uses ${VAR:?error} syntax that fails loudly if the env isn't configured. Removed TUNNEL_SETUP.txt from tracking (had hardcoded local paths), folded its content into a new README.md with full setup guide, API route table, and architecture docs. Updated .env.example, tightened .gitignore, refreshed CLAUDE.md. Git history scrubbed before any public push.
What's Next
- Squash git history before going public
- Google OAuth (test mode, small allow-list while invite-only)
- CodeMirror code-splitting to trim that 825KB bundle