Skip to content

Hardik-369/NullBoard

Repository files navigation

Nullboard

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.

Why This Project Matters Today

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.

What The App Does

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.

Core Features

  • 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.

Tech Stack

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

How It Was Made

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.

System Architecture

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
Loading

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.

Important Design Decisions

No SQL 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.

No User Accounts

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.

Encrypted Content Blobs

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.

Real-Time Discussion

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.

Secure Defaults

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.

Mistakes And Lessons From Building It

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.

Mistake 1: Assuming Eventlet Works Everywhere

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_MODE can override the behavior.

Lesson: production and local runtime versions matter. A dependency can be correct in deployment and still fail locally.

Mistake 2: Creating SocketIO Before Locking The Async Mode

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.

Mistake 3: Expecting Redis To Be Installed Locally

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 MemoryRedis fallback was added,
  • LOCAL_REDIS_FALLBACK=true allows local development without Redis,
  • production still expects real Redis.

Lesson: great projects should be easy to run locally, even when production uses managed services.

Mistake 4: Making The First UI Look Too Generated

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.

Mistake 5: Putting Primary Navigation In The Sidebar

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.

Local Setup

Install Python dependencies:

pip install -r requirements.txt

Create an environment file:

cp .env.example .env

Run the app:

python app.py

Open:

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.

Python 3.14 Local Development

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=threading

Render production should use Python 3.12 and eventlet.

Production Command

Render uses:

gunicorn --worker-class eventlet -w 1 app:app

A gunicorn_config.py file is also included for deployments that prefer config files.

Render Deployment

The included render.yaml creates:

  • a Python web service,
  • a Redis service,
  • the correct build command,
  • the correct Gunicorn start command,
  • a REDIS_URL connection from the Redis service.

Deployment steps:

  1. Push the project to GitHub.
  2. Open Render.
  3. Choose New > Blueprint.
  4. Select the GitHub repository.
  5. Render reads render.yaml.
  6. Set a strong SECRET_KEY in Render.
  7. Enable GitHub auto deploy.

GitHub Auto Deploy

  1. Create a GitHub repository.
  2. Commit the project.
  3. Push to main.
  4. Connect the repository to Render.
  5. Keep auto deploy enabled.

Each push to GitHub will redeploy the app.

Environment Variables

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=true

API Documentation

All API responses are JSON.

GET /api/session

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
}

GET /api/posts

Lists encrypted posts.

Optional query parameters:

topic=general
cursor=1710000000

POST /api/posts

Creates an encrypted post.

{
  "topic": "news",
  "blob": "base64-ciphertext",
  "author_pubkey": "browser-key-fingerprint",
  "pow_challenge": "challenge-from-session",
  "pow_nonce": "12345"
}

GET /api/posts/<post_id>

Returns one encrypted post.

GET /api/posts/<post_id>/comments

Returns encrypted comments for a post.

POST /api/posts/<post_id>/comments

Creates an encrypted comment.

{
  "blob": "base64-ciphertext",
  "author_pubkey": "browser-key-fingerprint",
  "pow_challenge": "challenge-from-session",
  "pow_nonce": "12345"
}

GET /api/topics

Returns trending topics.

{
  "items": [
    { "topic": "news", "score": 8.4 },
    { "topic": "environment", "score": 2.1 }
  ]
}

POST /api/flags

Flags a post or comment.

{
  "type": "post",
  "id": "post_abc",
  "reason": "spam"
}

Supported reasons:

spam, harassment, illegal, doxxing, malware, other

WebSocket Event Schema

The browser connects to:

/socket.io/?EIO=4&transport=websocket

Client events:

  • topic:join
  • topic:leave

Server events:

  • session:ready
  • topic:joined
  • topic:left
  • post:new
  • comment:new
  • topics: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 Storage Model

Redis keys:

  • session:<sid> stores temporary anonymous session state.
  • post:<id> stores encrypted post blobs.
  • comment:<id> stores encrypted comment blobs.
  • feed:home stores recent post IDs.
  • feed:topic:<topic> stores topic post IDs.
  • comments:<post_id> stores comment IDs.
  • topics:trending stores topic scores.
  • flags:<type>:<id> stores community flag counters.
  • reports:ephemeral stores 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.

Security Model

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.

Recommended Hardening

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.

Project Structure

.
|-- 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

Final Note

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.

About

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.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors