Bear is a Django-based blogging platform. Users get a subdomain at *.bearblog.dev or can point a custom domain.
- Framework: Django 5.2, Python 3.13
- Server: Gunicorn (
conf.wsgi), 24s timeout, max 10k requests per worker - Database: Heroku Postgres. SQLite used as fallback locally.
- Cache / Sessions: RedisCloud (TLS). Falls back to no cache in dev.
- Static files: WhiteNoise (GZip compressed)
Bear runs on Heroku (bear-blog app). The Procfile runs migrations on release and starts Gunicorn on web.
Heroku Postgres is backed up locally on a daily schedule.
There is no staging server.
Cloudflare handles DNS and SSL for *.bearblog.dev. Cloudflare also does the heavy lifting for caching and bot deterrence. Cache is invalidated per-blog using Cloudflare cache tags (keyed on subdomain) whenever a blog or post is saved.
A Caddy server running on a Digital Ocean droplet handles custom domain SSL and reverse proxies to https://bearblog.dev. It uses on-demand TLS, verifying domains by querying https://bearblog.dev/ping/ before issuing a cert.
Caddy config: Caddyfile in repo root.
The MAIN_SITE_HOSTS env var (www.bearblog.dev,bearblog.dev) gates staff and discover routes. Requests to any other host are treated as blog subdomain/custom domain requests.
User-uploaded images go to a Digital Ocean Spaces S3 bucket. The DO CDN serves them.
Reviewed blog content (posts + blog metadata as CSV) is backed up to a separate DO Spaces bucket (bear-backup, region fra1) in a background thread whenever a blog is saved.
| Service | Purpose |
|---|---|
| Cloudflare | DNS, SSL (.bearblog.dev), caching, bot deterrence |
| Heroku Postgres | Primary database |
| RedisCloud | Cache + session store |
| Digital Ocean Spaces | Image CDN + blog content backups |
| Digital Ocean Droplet | Caddy reverse proxy for custom domains |
| LemonSqueezy | Subscription payments. Webhooks upgrade/downgrade UserSettings. |
| Mailgun | Transactional email (SMTP via smtp.eu.mailgun.org) |
| Sentry | Error tracking (production only, low sample rate) |
| JudoScale | Heroku autoscaling — passive unless under load |
| GeoIP2 | Geolocation for analytics |
Periodic jobs run via the Heroku Scheduler add-on. Each job calls a Django management command:
| Schedule | Command | Purpose |
|---|---|---|
| Every 10 min | python manage.py invalidate_cache |
Busts Cloudflare cache for posts that just went live |
| Daily | python manage.py scrub_hash_ids |
Anonymises Hit records older than 24h by replacing hash_id with 'scrubbed' |
Management commands live in blogs/management/commands/.
UserSettings— per-user upgrade status, LemonSqueezy order infoBlog— subdomain, custom domain, styles, discovery settings, dodginess scorePost— content, slug, tags, upvotes, HN-style score for discover feedHit— analytics hits (hash_id scrubbed after 24h for privacy)Subscriber— email subscribers per blogStylesheet— named CSS themesMedia— uploaded image URLs per blogPersistentStore— singleton for platform-wide settings (review terms, blacklist)
Unreviewed blogs get a dodginess_score computed from highlight/blacklist terms in PersistentStore. Upgraded users are auto-reviewed. Staff can approve, block, ignore, or flag blogs via /staff/.
Post scores use an HN-style algorithm (log of upvotes + time decay, capped at 30 upvotes to prevent permanent stickiness).
RateLimitMiddleware— 10 req/10s per IP, bans on.php/.envprobes and SQL injection patternsConditionalXFrameOptionsMiddleware—X-Frame-Options: DENYon main domains only- GZip, Security, WhiteNoise, Sessions, DebugToolbar, Common
AllowAnyDomainCsrfMiddleware— custom CSRF handling to support custom domains