Nullboard is a production-style MVP for an anonymous, privacy-focused social discussion platform. It is built with Python Flask, Flask-SocketIO, Redis, PyNaCl/libsodium utilities, vanilla JavaScript, self-hosted CSS, and Render deployment support.
The project is designed around one clear idea: people should be able to discuss sensitive topics without creating permanent accounts, giving away personal details, or leaving plaintext content sitting in a database.
Modern social platforms are usually built around identity, tracking, analytics, recommendation systems, and permanent user profiles. Nullboard goes in the opposite direction.
It focuses on:
- anonymous posting,
- temporary identities,
- encrypted content blobs,
- minimal metadata,
- Redis TTL expiration,
- real-time discussion,
- no user registration,
- no SQL database,
- no third-party trackers,
- no analytics scripts,
- no external frontend CDNs.
That makes this one of the strongest MVP ideas for today's internet: a lightweight discussion platform built for privacy first, not engagement farming first.
In that sense, this is the greatest kind of project to build right now: practical, socially relevant, technically interesting, and deployable. It combines real-time social UX with privacy engineering instead of making another normal login-based CRUD app.
Nullboard lets visitors open the site and immediately participate anonymously.
There are no accounts, emails, passwords, phone numbers, OAuth providers, or user profiles. Each browser session receives an ephemeral anonymous name such as:
shadow-fox-4821
silent-raven-9922
cipher-node-1840
Users can:
- create anonymous encrypted posts,
- comment anonymously,
- browse topic channels,
- view trending topics,
- receive live post and comment updates,
- locally hide content,
- flag content for community reporting,
- use the app without installing Redis locally during development,
- deploy the full stack to Render with Redis.
- No user registration.
- Random anonymous usernames.
- Ephemeral Flask session identities.
- Client-side browser encryption for post/comment content.
- PyNaCl/libsodium helper utilities for future native encryption workflows.
- Redis-only temporary storage.
- Redis TTL expiration for posts, comments, sessions, reports, and proof-of-work challenges.
- Flask-SocketIO real-time updates.
- Topic channels such as
/topic/news,/topic/politics,/topic/environment. - Trending topic scoring.
- Community flagging.
- Local content hiding.
- Strict Content Security Policy.
- Secure HTTP headers.
- No analytics.
- No trackers.
- No Google Fonts.
- No third-party frontend scripts.
- Rate limiting.
- Proof-of-work anti-spam.
- Packet padding utilities.
- Randomized response delays.
- Render deployment support.
Backend:
- Python 3.12 for Render production
- Flask
- Flask-SocketIO
- Gunicorn
- eventlet on Python 3.12 production
- threading fallback for Python 3.14 local development
- Redis
- PyNaCl
Frontend:
- HTML5
- self-hosted CSS
- vanilla JavaScript
- Web Crypto API
- lightweight Socket.IO-compatible WebSocket client
- inline self-hosted SVG icons
Deployment:
- Render web service
- Render Redis service
- GitHub auto deploy
render.yaml
Storage:
- Redis only
- encrypted content blobs
- expiring sorted sets
- expiring hashes/lists/strings
- local in-memory fallback for development only
The app was built as a modular Flask project instead of a single messy file.
The backend is organized around:
routes/for API and page routes,topics/for post, comment, feed, and trending logic,websocket/for live Socket.IO events,security/for headers, rate limiting, and proof-of-work,crypto/for PyNaCl utilities,moderation/for community flagging,utils/for Redis, IDs, usernames, timing, padding, and local fallback storage,templates/for Flask HTML templates,static/for CSS and JavaScript.
The frontend started as a dark anonymous forum and was then redesigned into a more professional light interface. The latest UI uses:
- a clean top navigation bar,
- centered primary navigation tabs,
- a left sidebar for identity and topics,
- a main feed column,
- a right sidebar for trending and privacy notes,
- responsive mobile topic navigation,
- professional spacing and restrained colors.
flowchart TB
User["Anonymous visitor"] --> Browser["Browser UI<br/>HTML, CSS, Vanilla JS"]
Browser --> Crypto["Client-side encryption<br/>Web Crypto AES-GCM"]
Browser --> WSClient["Self-hosted WebSocket client"]
Browser --> HTTP["HTTP JSON API calls"]
subgraph Render["Render deployment"]
Web["Flask app<br/>Gunicorn + eventlet"]
SocketIO["Flask-SocketIO<br/>real-time events"]
Security["Security layer<br/>CSP, headers, rate limits, PoW"]
Routes["Routes<br/>pages + API"]
Topics["Topic/feed service<br/>posts, comments, trending"]
Moderation["Community moderation<br/>flags + reports"]
Redis["Render Redis<br/>TTL-only temporary storage"]
end
HTTP --> Security
Security --> Routes
Routes --> Topics
Routes --> Moderation
WSClient --> SocketIO
SocketIO --> Topics
Topics --> Redis
Moderation --> Redis
Security --> Redis
SocketIO --> Browser
Crypto --> HTTP
Redis -. stores only .-> Blobs["encrypted blobs<br/>post/comment/session TTL data"]
subgraph NoStorage["Intentionally not stored"]
Accounts["accounts"]
Emails["emails"]
Passwords["passwords"]
Plaintext["plaintext posts/comments"]
Analytics["analytics identifiers"]
end
At a high level, the browser encrypts content before sending it to Flask. Flask validates rate limits and proof-of-work, stores only encrypted blobs in Redis, and broadcasts real-time updates through Socket.IO. Redis is treated as temporary infrastructure, not a permanent database.
The app intentionally avoids PostgreSQL, MySQL, SQLite, MongoDB, or any permanent database.
Redis is used because the product is designed around temporary content. Posts, comments, sessions, proof-of-work challenges, and reports all expire automatically.
There is no account model. No passwords. No emails. No login page.
The user identity is temporary and generated automatically. This keeps the app simple and reduces the amount of personal data the server can leak or lose.
The server accepts encrypted blobs for posts and comments. It does not need plaintext content to store or broadcast discussions.
The browser uses Web Crypto AES-GCM for content encryption. PyNaCl utilities are included for future libsodium-compatible clients and server-side encryption workflows.
Flask-SocketIO powers live post, comment, and trending-topic updates. The frontend uses a small self-hosted WebSocket client instead of loading a third-party Socket.IO script from a CDN.
The app includes:
- strict CSP headers,
X-Frame-Options,Referrer-Policy,Permissions-Policy,X-Content-Type-Options,- no third-party scripts,
- no external fonts,
- no analytics,
- no IP-based application logging logic,
- rate limiting,
- proof-of-work.
This project also exposed several real mistakes that commonly happen while building production-style apps. These are important because they made the final project stronger.
The first version used eventlet directly at app startup. That is correct for Render with Python 3.12, but it broke on local Python 3.14 because eventlet does not yet support Python 3.14's newer threading internals.
The fix:
- eventlet is only selected when compatible,
- Python 3.14 local development automatically falls back to
threading, - Render production still uses eventlet with Python 3.12,
SOCKETIO_ASYNC_MODEcan override the behavior.
Lesson: production and local runtime versions matter. A dependency can be correct in deployment and still fail locally.
Flask-SocketIO auto-detected eventlet because eventlet was installed. That caused another crash before the app could choose the safer local mode.
The fix:
SocketIO(async_mode=select_async_mode())is now set directly when the SocketIO object is created.
Lesson: library auto-detection is convenient, but production apps should be explicit.
The app was designed for Redis on Render, but local development failed when Redis was not installed on the machine.
The fix:
- a development-only
MemoryRedisfallback was added, LOCAL_REDIS_FALLBACK=trueallows local development without Redis,- production still expects real Redis.
Lesson: great projects should be easy to run locally, even when production uses managed services.
The UI went through multiple visual directions. The first versions looked too cyberpunk, then too glossy and AI-generated.
The fix:
- decorative gradients were removed,
- shadows were reduced,
- border radius was made more restrained,
- navigation was reorganized,
- the layout became closer to a professional social/product interface.
Lesson: professional UI is usually quieter, cleaner, and more deliberate than flashy mockup UI.
The earlier navigation made the app feel like a prototype dashboard instead of a polished social product.
The fix:
- Home and Trending moved into the top navigation bar,
- the sidebar now focuses on identity and topics,
- mobile navigation became cleaner and easier to understand.
Lesson: navigation hierarchy matters. Primary actions should be immediately visible and predictable.
Install Python dependencies:
pip install -r requirements.txtCreate an environment file:
cp .env.example .envRun the app:
python app.pyOpen:
http://localhost:5000
Redis is recommended locally, but not required. If Redis is not installed, the app uses the local in-memory fallback when LOCAL_REDIS_FALLBACK=true.
The in-memory fallback is only for development. It loses all data when the server stops.
If you use Python 3.14 locally, eventlet may fail. The app handles this automatically by using threading mode.
You can also force local threading mode:
SOCKETIO_ASYNC_MODE=threadingRender production should use Python 3.12 and eventlet.
Render uses:
gunicorn --worker-class eventlet -w 1 app:appA gunicorn_config.py file is also included for deployments that prefer config files.
The included render.yaml creates:
- a Python web service,
- a Redis service,
- the correct build command,
- the correct Gunicorn start command,
- a
REDIS_URLconnection from the Redis service.
Deployment steps:
- Push the project to GitHub.
- Open Render.
- Choose New > Blueprint.
- Select the GitHub repository.
- Render reads
render.yaml. - Set a strong
SECRET_KEYin Render. - Enable GitHub auto deploy.
- Create a GitHub repository.
- Commit the project.
- Push to
main. - Connect the repository to Render.
- Keep auto deploy enabled.
Each push to GitHub will redeploy the app.
Example values are in .env.example.
Important variables:
SECRET_KEY=replace-with-a-long-random-value
REDIS_URL=redis://localhost:6379/0
SESSION_TTL_SECONDS=21600
POST_TTL_SECONDS=86400
COMMENT_TTL_SECONDS=43200
RATE_LIMIT_WINDOW_SECONDS=60
RATE_LIMIT_MAX_REQUESTS=50
POW_DIFFICULTY=4
MAX_BLOB_BYTES=8192
MAX_COMMENT_BLOB_BYTES=4096
ENABLE_RANDOM_DELAYS=true
LOCAL_REDIS_FALLBACK=trueAll API responses are JSON.
Creates or refreshes an ephemeral session and returns a proof-of-work challenge.
{
"sid": "opaque-session-id",
"username": "silent-raven-9922",
"pow_challenge": "random-challenge",
"pow_difficulty": 4,
"ttl_seconds": 21600
}Lists encrypted posts.
Optional query parameters:
topic=general
cursor=1710000000
Creates an encrypted post.
{
"topic": "news",
"blob": "base64-ciphertext",
"author_pubkey": "browser-key-fingerprint",
"pow_challenge": "challenge-from-session",
"pow_nonce": "12345"
}Returns one encrypted post.
Returns encrypted comments for a post.
Creates an encrypted comment.
{
"blob": "base64-ciphertext",
"author_pubkey": "browser-key-fingerprint",
"pow_challenge": "challenge-from-session",
"pow_nonce": "12345"
}Returns trending topics.
{
"items": [
{ "topic": "news", "score": 8.4 },
{ "topic": "environment", "score": 2.1 }
]
}Flags a post or comment.
{
"type": "post",
"id": "post_abc",
"reason": "spam"
}Supported reasons:
spam, harassment, illegal, doxxing, malware, other
The browser connects to:
/socket.io/?EIO=4&transport=websocket
Client events:
topic:jointopic:leave
Server events:
session:readytopic:joinedtopic:leftpost:newcomment:newtopics:trending
Example:
{
"event": "post:new",
"payload": {
"id": "post_abc",
"topic": "news",
"author": "shadow-fox-4821",
"blob": "base64-ciphertext",
"created_at": 1710000000,
"expires_at": 1710086400
}
}Redis keys:
session:<sid>stores temporary anonymous session state.post:<id>stores encrypted post blobs.comment:<id>stores encrypted comment blobs.feed:homestores recent post IDs.feed:topic:<topic>stores topic post IDs.comments:<post_id>stores comment IDs.topics:trendingstores topic scores.flags:<type>:<id>stores community flag counters.reports:ephemeralstores temporary report events.rl:<action>:<subject>stores rate-limit counters.pow:<sid>:<challenge>stores proof-of-work challenges.
All important content and session keys expire with TTLs.
Nullboard minimizes metadata. It does not claim to make users magically anonymous from every network observer.
The app does not intentionally store:
- emails,
- passwords,
- phone numbers,
- account profiles,
- plaintext posts,
- plaintext comments,
- analytics IDs.
Important reality:
- Render and network providers may still observe connection metadata.
- Browser and device security still matter.
- Public topic keys make public discussion usable, but private rooms should use invite-only keys or user-supplied passphrases.
- For stronger anonymity, deploy behind Tor or another privacy-preserving network layer.
Before using this for high-risk real-world speech:
- Put the service behind Tor or an onion gateway.
- Add private room keys or passphrase-based topic encryption.
- Add encrypted key import/export.
- Add privacy-preserving abuse controls.
- Audit hosting logs.
- Increase proof-of-work difficulty carefully.
- Shorten post and comment TTLs.
- Add transparent community moderation rules.
- Add automated security tests.
.
|-- app.py
|-- gunicorn_config.py
|-- render.yaml
|-- requirements.txt
|-- README.md
|-- config/
| |-- __init__.py
| `-- settings.py
|-- crypto/
| |-- __init__.py
| `-- nacl_utils.py
|-- moderation/
| |-- __init__.py
| `-- community.py
|-- routes/
| |-- __init__.py
| |-- api.py
| `-- pages.py
|-- security/
| |-- __init__.py
| |-- headers.py
| |-- pow.py
| `-- rate_limit.py
|-- static/
| |-- css/
| |-- fonts/
| |-- images/
| `-- js/
|-- templates/
| `-- index.html
|-- topics/
| |-- __init__.py
| `-- service.py
|-- utils/
| |-- __init__.py
| |-- async_mode.py
| |-- ids.py
| |-- memory_redis.py
| |-- padding.py
| |-- redis_client.py
| |-- timing.py
| `-- usernames.py
`-- websocket/
|-- __init__.py
`-- events.py
Nullboard is not just another Flask demo. It is a complete privacy-first social MVP with real-time discussion, temporary data, anonymous identities, encrypted blobs, Redis TTL architecture, Render deployment, and a professional UI.
It is the kind of project that shows practical engineering judgment: not only building features, but also fixing runtime compatibility issues, improving local development, tightening security defaults, and refining the user experience until it feels like a real product.