Sojorn Admin Panel — Comprehensive System Documentation
Last updated: February 6, 2026
Table of Contents
- Overview
- Architecture
- Authentication & Security
- Server Deployment
- Frontend (Next.js)
- Backend API Routes
- Database Schema
- Feature Reference
- Environment Variables
- Troubleshooting
Overview
The Sojorn Admin Panel is an internal tool for platform administrators to manage users, moderate content, review appeals, configure the feed algorithm, and monitor system health. It is a standalone Next.js 14 application that communicates with the existing Sojorn Go backend via a dedicated set of admin API endpoints.
Key characteristics:
- Separate frontend deployment from the main Flutter app
- Role-based access — only users with
role = 'admin' in the profiles table can log in
- All admin actions are logged to the
audit_log table
- Invisible Cloudflare Turnstile bot protection on login
- JWT authentication with 24-hour token expiry
Architecture
┌─────────────────────┐ HTTPS ┌──────────────────────┐
│ Browser │ ◄────────────► │ Nginx │
│ admin.sojorn.net │ │ (reverse proxy) │
└─────────────────────┘ └──────┬───────────────┘
│
┌──────────────────┼──────────────────┐
│ port 3002 │ port 8080 │
▼ ▼ │
┌─────────────────┐ ┌─────────────────┐ │
│ Next.js 14 │ │ Go Backend │ │
│ (sojorn-admin) │ │ (sojorn-api) │ │
│ SSR + Static │ │ Gin framework │ │
└─────────────────┘ └────────┬────────┘ │
│ │
┌────────▼────────┐ │
│ PostgreSQL │ │
│ sojorn database │ │
└─────────────────┘ │
│
┌─────────────────┐ │
│ Cloudflare R2 │───────┘
│ (media storage) │
└─────────────────┘
Tech Stack
| Component |
Technology |
| Frontend |
Next.js 14, TypeScript, TailwindCSS, Recharts, Lucide icons |
| Backend |
Go 1.21+, Gin framework, pgx (PostgreSQL driver) |
| Database |
PostgreSQL 15+ |
| Process management |
systemd |
| Reverse proxy |
Nginx |
| Bot protection |
Cloudflare Turnstile (invisible mode) |
| Auth |
JWT (HS256), bcrypt password hashing |
Authentication & Security
Login Flow
- User enters email + password on
/login
- Cloudflare Turnstile invisible widget generates a token in the background
- Frontend sends
POST /api/v1/admin/login with { email, password, turnstile_token }
- Backend verifies Turnstile token with Cloudflare API
- Backend checks
users table for valid credentials (bcrypt)
- Backend verifies account status is
active
- Backend checks
profiles.role = 'admin'
- Returns JWT (
access_token) with 24-hour expiry + user profile data
- Token stored in
localStorage as admin_token
Middleware Chain (Protected Routes)
All /api/v1/admin/* routes (except /login) pass through:
- AuthMiddleware — Validates JWT, extracts
user_id from sub claim, sets it in Gin context
- AdminMiddleware — Queries
profiles.role for the authenticated user, rejects non-admin users with 403
Security Measures
- Invisible Turnstile — Blocks automated login attacks without user friction
- Graceful degradation — If
TURNSTILE_SECRET is empty, verification is skipped (dev mode)
- bcrypt — Passwords hashed with bcrypt (default cost)
- JWT expiry — Admin tokens expire after 24 hours (vs 7 days for regular users)
- Auto-logout — Frontend redirects to
/login on any 401 response
- Audit logging — Status changes, post deletions, and moderation actions are logged
Granting Admin Access
UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';
Valid roles: user, moderator, admin
Server Deployment
Services
| Service |
systemd unit |
Port |
Binary/Entry |
| Go API |
sojorn-api |
8080 |
/opt/sojorn/bin/api |
| Admin Frontend |
sojorn-admin |
3002 |
node .../next start --port 3002 |
File Locations on Server
/opt/sojorn/
├── .env # Shared environment variables
├── bin/
│ └── api # Compiled Go binary
├── go-backend/ # Go source code
│ ├── cmd/api/main.go
│ ├── internal/
│ │ ├── handlers/admin_handler.go
│ │ ├── middleware/admin.go
│ │ └── ...
│ └── .env # Symlink or copy of /opt/sojorn/.env
├── admin/ # Next.js admin frontend
│ ├── .env.local # Frontend env vars
│ ├── .next/ # Build output
│ ├── node_modules/
│ ├── src/
│ │ ├── app/ # Page routes
│ │ └── lib/ # API client, auth context
│ └── package.json
└── firebase-service-account.json
systemd Service Files
/etc/systemd/system/sojorn-admin.service:
[Unit]
Description=Sojorn Admin Panel
After=network.target
[Service]
Type=simple
User=patrick
Group=patrick
WorkingDirectory=/opt/sojorn/admin
ExecStart=/usr/bin/node /opt/sojorn/admin/node_modules/next/dist/bin/next start --port 3002
Restart=on-failure
RestartSec=30
StartLimitIntervalSec=120
StartLimitBurst=3
Environment=NODE_ENV=production
Environment=NEXT_PUBLIC_API_URL=https://api.sojorn.net
[Install]
WantedBy=multi-user.target
Nginx Configuration
/etc/nginx/sites-available/sojorn-admin:
server {
listen 80;
server_name admin.sojorn.net;
location / {
proxy_pass http://127.0.0.1:3002;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
Common Operations
# Rebuild and deploy Go backend
cd /opt/sojorn/go-backend
go build -ldflags='-s -w' -o /opt/sojorn/bin/api ./cmd/api/main.go
sudo systemctl restart sojorn-api
# Rebuild and deploy admin frontend
cd /opt/sojorn/admin
npx next build
sudo systemctl restart sojorn-admin
# View logs
sudo journalctl -u sojorn-admin -f
sudo journalctl -u sojorn-api -f
# Check service status
sudo systemctl status sojorn-admin
sudo systemctl status sojorn-api
# SSL certificate (after DNS A record is pointed)
sudo certbot --nginx -d admin.sojorn.net
Frontend (Next.js)
Pages
| Route |
File |
Description |
/ |
app/page.tsx |
Dashboard with stats cards and growth charts |
/login |
app/login/page.tsx |
Admin login with invisible Turnstile |
/users |
app/users/page.tsx |
User list with search, filter by status/role |
/users/[id] |
app/users/[id]/page.tsx |
User detail — profile, stats, admin actions |
/posts |
app/posts/page.tsx |
Post list with search, filter by status |
/posts/[id] |
app/posts/[id]/page.tsx |
Post detail — content, media, moderation flags, admin actions |
/moderation |
app/moderation/page.tsx |
Moderation queue — AI-flagged content pending review |
/appeals |
app/appeals/page.tsx |
User appeals against violations |
/reports |
app/reports/page.tsx |
User-submitted reports |
/algorithm |
app/algorithm/page.tsx |
Feed algorithm & moderation threshold tuning |
/categories |
app/categories/page.tsx |
Category management — create, edit, toggle sensitive |
/system |
app/system/page.tsx |
System health, DB stats, audit log |
/settings |
app/settings/page.tsx |
Admin session info, API URL override |
Key Libraries
| Library |
Source |
api.ts |
src/lib/api.ts — Singleton API client with JWT token management |
auth.tsx |
src/lib/auth.tsx — React context provider for auth state |
API Client (src/lib/api.ts)
The ApiClient class provides typed methods for every admin API endpoint. Key behaviors:
- Token management — Stored in
localStorage as admin_token, attached as Bearer header
- Auto-logout — Any 401 response clears token and redirects to
/login
- Base URL — Configurable via
NEXT_PUBLIC_API_URL environment variable
- Error handling — Throws
Error with server-provided error message
Auth Context (src/lib/auth.tsx)
- Wraps app in
AuthProvider
- On mount, validates existing token by calling
getDashboardStats()
- Provides
login(), logout(), isAuthenticated, isLoading, user
Backend API Routes
All routes prefixed with /api/v1/admin/.
Authentication (no middleware)
| Method |
Path |
Handler |
Description |
POST |
/login |
AdminLogin |
Email/password login with Turnstile verification |
Dashboard (requires auth + admin)
| Method |
Path |
Handler |
Description |
GET |
/dashboard |
GetDashboardStats |
User/post/moderation/appeal/report counts |
GET |
/growth?days=30 |
GetGrowthStats |
Daily user & post creation counts for charts |
User Management
| Method |
Path |
Handler |
Description |
GET |
/users?limit=50&offset=0&search=&status=&role= |
ListUsers |
Paginated user list with filters |
GET |
/users/:id |
GetUser |
Full user profile with follower/post/violation counts |
PATCH |
/users/:id/status |
UpdateUserStatus |
Set status: active, suspended, banned, deactivated |
PATCH |
/users/:id/role |
UpdateUserRole |
Set role: user, moderator, admin |
PATCH |
/users/:id/verification |
UpdateUserVerification |
Toggle is_official and is_verified flags |
POST |
/users/:id/reset-strikes |
ResetUserStrikes |
Reset violation strike counter to 0 |
Post Management
| Method |
Path |
Handler |
Description |
GET |
/posts?limit=50&offset=0&search=&status=&author_id= |
ListPosts |
Paginated post list with filters |
GET |
/posts/:id |
GetPost |
Full post detail with moderation flags |
PATCH |
/posts/:id/status |
UpdatePostStatus |
Set status: active, flagged, removed |
DELETE |
/posts/:id |
DeletePost |
Soft-delete (sets deleted_at + status removed) |
Moderation Queue
| Method |
Path |
Handler |
Description |
GET |
/moderation?limit=50&offset=0&status=pending |
GetModerationQueue |
AI-flagged content awaiting review |
PATCH |
/moderation/:id/review |
ReviewModerationFlag |
Actions: approve, dismiss, remove_content, ban_user |
Appeals
| Method |
Path |
Handler |
Description |
GET |
/appeals?limit=50&offset=0&status=pending |
ListAppeals |
User appeals with violation details |
PATCH |
/appeals/:id/review |
ReviewAppeal |
Decision: approved or rejected, optional content restore |
Reports
| Method |
Path |
Handler |
Description |
GET |
/reports?limit=50&offset=0&status=pending |
ListReports |
User-submitted reports |
PATCH |
/reports/:id |
UpdateReportStatus |
Set status: reviewed, dismissed, actioned |
Algorithm & Feed Config
| Method |
Path |
Handler |
Description |
GET |
/algorithm |
GetAlgorithmConfig |
All key-value config pairs |
PUT |
/algorithm |
UpdateAlgorithmConfig |
Upsert a config { key, value } |
Categories
| Method |
Path |
Handler |
Description |
GET |
/categories |
ListCategories |
All content categories |
POST |
/categories |
CreateCategory |
Create { slug, name, description?, is_sensitive? } |
PATCH |
/categories/:id |
UpdateCategory |
Update name, description, or sensitive flag |
System
| Method |
Path |
Handler |
Description |
GET |
/health |
GetSystemHealth |
DB ping, latency, connection pool stats, DB size |
GET |
/audit-log?limit=50&offset=0 |
GetAuditLog |
Admin action history with actor handles |
Database Schema
Tables Created by Admin Migration
algorithm_config — Key-value store for feed and moderation tuning:
| Column |
Type |
Description |
key |
TEXT PRIMARY KEY |
Config identifier |
value |
TEXT NOT NULL |
Config value |
description |
TEXT |
Human-readable description |
updated_at |
TIMESTAMPTZ |
Last modification time |
Default seed values:
| Key |
Default |
Description |
feed_recency_weight |
0.4 |
Weight for post recency in feed ranking |
feed_engagement_weight |
0.3 |
Weight for engagement metrics |
feed_harmony_weight |
0.2 |
Weight for author harmony/trust score |
feed_diversity_weight |
0.1 |
Weight for content diversity |
moderation_auto_flag_threshold |
0.7 |
AI score threshold for auto-flagging |
moderation_auto_remove_threshold |
0.95 |
AI score threshold for auto-removal |
moderation_greed_keyword_threshold |
0.7 |
Spam/greed detection threshold |
feed_max_posts_per_author |
3 |
Max posts from same author per feed page |
feed_boost_mutual_follow |
1.5 |
Boost multiplier for mutual follows |
feed_beacon_boost |
1.2 |
Boost multiplier for beacon posts |
audit_log — Admin action history:
| Column |
Type |
Description |
id |
UUID PRIMARY KEY |
Unique entry ID |
actor_id |
UUID |
Admin who performed the action |
action |
TEXT NOT NULL |
Action type (e.g., post_status_change, admin_delete_post) |
target_type |
TEXT NOT NULL |
Entity type: user, post, comment, appeal, report, config |
target_id |
UUID |
ID of the affected entity |
details |
TEXT |
JSON string with action-specific metadata |
created_at |
TIMESTAMPTZ |
Timestamp |
Columns Ensured by Migration
The migration ensures these columns exist (added if missing):
| Table |
Column |
Type |
Default |
profiles |
role |
TEXT |
'user' |
profiles |
is_verified |
BOOLEAN |
FALSE |
profiles |
is_private |
BOOLEAN |
FALSE |
users |
status |
TEXT |
'active' |
users |
last_login |
TIMESTAMPTZ |
NULL |
Pre-existing Tables Used by Admin
| Table |
Admin Usage |
users |
Login validation, status management, growth stats |
profiles |
Role checks, user details, verification flags |
posts |
Content listing, status changes, deletion |
comments |
Moderation flag targets, appeal content restoration |
moderation_flags |
Moderation queue — AI-generated flags with scores |
user_violations |
Violation records linked to moderation flags |
user_appeals |
Appeal records linked to violations |
user_status_history |
Log of admin-initiated status changes |
reports |
User-submitted reports of other users/content |
categories |
Content categories managed by admins |
follows |
Follower/following counts on user detail page |
Feature Reference
Dashboard
Displays real-time aggregate stats:
- Users: total, active, suspended, banned, new today
- Posts: total, active, flagged, removed, new today
- Moderation: pending flags, reviewed flags
- Appeals: pending, approved, rejected
- Reports: pending count
- Growth charts: Daily user & post registrations (configurable 7/30/90 day window)
Moderation Workflow
- AI flagging — Posts/comments are automatically analyzed by the
ModerationService using OpenAI + Google Vision
- Three Poisons Score — Content is scored on Hate, Greed, Delusion dimensions
- Auto-flag — Content exceeding
moderation_auto_flag_threshold is flagged for review
- Admin review — Admin sees flagged content in the moderation queue with scores
- Actions available:
- Approve — Content is fine, dismiss the flag
- Dismiss — Same as approve (flag was a false positive)
- Remove content — Soft-delete the post/comment
- Ban user — Ban the author and action the flag
Appeal Workflow
- User receives a violation (triggered by moderation)
- User submits an appeal with reason and context
- Appeal appears in admin panel with violation details, AI scores, and original content
- Admin reviews and decides:
- Approve — Optionally restore the removed content
- Reject — Violation stands, include written reasoning (min 5 chars)
User Management Actions
- Change status:
active ↔ suspended ↔ banned ↔ deactivated (requires reason)
- Change role:
user ↔ moderator ↔ admin
- Toggle verification:
is_official and is_verified badges
- Reset strikes: Clear the violation counter
Environment Variables
Frontend (/opt/sojorn/admin/.env.local)
| Variable |
Value |
Description |
NEXT_PUBLIC_API_URL |
https://api.sojorn.net |
Go backend base URL |
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
0x4AAAAAAC... |
Cloudflare Turnstile site key (invisible mode) |
Backend (/opt/sojorn/.env)
The admin system uses these existing environment variables:
| Variable |
Description |
DATABASE_URL |
PostgreSQL connection string |
JWT_SECRET |
Secret for signing/verifying JWT tokens |
TURNSTILE_SECRET |
Cloudflare Turnstile server-side verification key |
PORT |
API server port (default: 8080) |
Troubleshooting
Admin login returns "Admin access required"
The user's profile doesn't have role = 'admin'. Fix:
UPDATE profiles SET role = 'admin' WHERE handle = 'your_handle';
Admin login returns "Invalid credentials"
- Verify the email matches what's in the
users table
- Password is validated against
encrypted_password via bcrypt
- Check the user's
status is active (not pending, suspended, or banned)
Admin login returns "Security verification failed"
Cloudflare Turnstile rejected the request. Possible causes:
TURNSTILE_SECRET in backend .env doesn't match the site key in frontend .env.local
- The Turnstile widget hostname doesn't include
admin.sojorn.net in Cloudflare dashboard
- Bot or automated request without a valid Turnstile token
sojorn-admin service won't start
# Check logs
sudo journalctl -u sojorn-admin -n 50 --no-pager
# Check if port 3002 is in use
ss -tlnp | grep 3002
# If port is taken, find and kill the process
sudo fuser -k 3002/tcp
sudo systemctl restart sojorn-admin
sojorn-api panics on startup
Check for duplicate route registrations in cmd/api/main.go. The Go backend will panic if two routes resolve to the same path (e.g., legacy routes conflicting with admin group routes).
Frontend shows "Loading..." indefinitely
- Check browser console for network errors
- Verify
NEXT_PUBLIC_API_URL in .env.local is correct and reachable
- Ensure the Go API is running:
sudo systemctl status sojorn-api
- Check CORS — the backend must allow the admin domain in
CORS_ORIGINS
Database migration errors
Run the migration manually:
export PGPASSWORD=your_db_password
psql -U postgres -h localhost -d sojorn -f /opt/sojorn/go-backend/internal/database/migrations/20260206000001_admin_panel_tables.up.sql
PM2 conflicts
Port 3001 is used by another PM2-managed site on this server. The admin panel uses port 3002 to avoid conflicts. Do not change it to 3001.