| backend | ||
| frontend | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| CHANGELOG.md | ||
| compose.yml | ||
| Dockerfile | ||
| README.md | ||
Malasaur Gooning
Version: 0.2.3
Self-hostable, read-only Reddit media wrapper built around local accounts, server feeds, saved posts, and a TikTok/Reels-style fullscreen viewer.
Run With Docker
- Copy the environment template:
cp .env.example .env
-
Put your Reddit API key in
.env, or leave it blank and enter it in the first-run web setup. -
Start the service:
docker compose -f compose.yml up --build
- Open
http://localhost:8080, create the first admin account, then configure feeds and account-login OAuth providers in the admin panel.
SQLite data is stored in ./data/malasaur.db.
Reddit Configuration
Create an app at https://www.reddit.com/prefs/apps and copy the API key shown there. Configure:
REDDIT_API_KEYREDDIT_USER_AGENT
The server uses that key to fetch read-only Reddit media. Malasaur accounts are local accounts. They are never linked to Reddit accounts, and the app does not upvote, comment, save, or otherwise mutate Reddit state.
Redgifs Configuration
Redgifs does not require a user-created API key for this app. Malasaur automatically requests a temporary Redgifs bearer token from https://api.redgifs.com/v2/auth/temporary, reuses it until expiry, and uses it only to resolve Redgifs watch URLs and stream Redgifs CDN media.
Redgifs ties temporary tokens to the requesting IP address and user agent, and direct CDN video/image requests often return 403 Forbidden when browsers hotlink them without the matching token headers. For that reason, only Redgifs media is served through a same-origin /api/v1/media/redgifs/... streaming endpoint with Range support. Reddit-hosted media still loads directly in the browser for speed.
Features
- Local signup/login/logout with opaque bearer sessions and HTTP-only browser cookies.
- First-run setup flow and admin panel.
- Admin-managed password signup, OAuth-only mode, adult-content confirmation gate, user management, Reddit config, OIDC providers, and server feeds.
- Generic OIDC login with issuer discovery or explicit endpoint configuration.
- Private user feeds, public browsable feeds, admin default feeds, and a global default feed.
- Feed sources from subreddits and Reddit users.
- Read-only media viewer for images, videos, GIFs, galleries, and external media links.
- Same-origin media proxy with browser cache headers and optional server-side media cache.
- Text-only posts are filtered out by the server.
- Per-user saved posts stored locally, separate from Reddit.
/api/v1JSON API suitable for non-web clients.
Local Development
Backend:
python -m venv .venv
. .venv/bin/activate
pip install -r backend/requirements.txt
MALASAUR_DATA_DIR=./data uvicorn backend.app.main:app --reload
Frontend:
cd frontend
npm install
npm run dev
The Vite dev server proxies /api to http://127.0.0.1:8000.
API Layout
All endpoints live under /api/v1.
GET /statusPOST /setupPOST /auth/loginPOST /auth/signupPOST /auth/logoutGET /auth/meGET /feedsPOST /feedsGET /feeds/{feed_id}/postsGET /me/saved-postsPOST /me/saved-postsPOST /me/streakGET /leaderboardGET /reddit/comments?permalink=/r/...GET/PATCH /admin/settingsGET/PATCH /admin/redditGET/POST/PATCH /admin/usersGET/POST/PATCH /admin/oauth/providers
Notes
This service is intentionally read-only toward Reddit. Local "saved" posts and feeds are stored only in your Malasaur database.
Performance Notes
Images, galleries, and Reddit videos are loaded directly by the browser. Redgifs videos are resolved by the backend and streamed through the app because Redgifs rejects most unauthenticated browser hotlinks. The backend keeps a short in-process Reddit JSON cache for repeated feed/source loads.
PostgreSQL/Redis are not required for media speed. The main bottleneck is upstream media delivery; the bundled client keeps Reddit media direct and uses server streaming only for hosts that require server-managed headers.
Example Caddy front-end:
goon.example.com {
encode zstd gzip
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"
@html path /
header @html Cache-Control "no-cache"
reverse_proxy localhost:8080 {
transport http {
dial_timeout 2s
response_header_timeout 30s
}
flush_interval -1
}
}
Keep personalized API routes private; do not shared-cache /api/v1/auth, /api/v1/me, or feed JSON. If you add external media caching in a browser extension, CDN, or custom reverse-proxy setup, keep range requests intact:
reverse_proxy localhost:8080 {
header_up Range {http.request.header.Range}
header_down Accept-Ranges bytes
flush_interval -1
}