{"id":50404564,"url":"https://github.com/samueldonovan/minimost","last_synced_at":"2026-06-30T03:00:53.718Z","repository":{"id":357037874,"uuid":"1123448597","full_name":"SamuelDonovan/minimost","owner":"SamuelDonovan","description":"A lightweight, self-hosted chat platform built for private networks.","archived":false,"fork":false,"pushed_at":"2026-06-30T02:14:21.000Z","size":8556,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-30T02:23:06.815Z","etag":null,"topics":["chat","chat-application","flask","lan","local-network","mattermost-alternative","messaging","no-database","pwa","python","real-time","screen-sharing","self-hosted","self-hosted-chat","slack-alternative","sqlite","team-chat","video-call","voice-chat","webrtc"],"latest_commit_sha":null,"homepage":"https://minimost.readthedocs.io/en/latest/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SamuelDonovan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-26T22:34:16.000Z","updated_at":"2026-06-30T02:13:49.000Z","dependencies_parsed_at":"2026-06-02T05:01:01.351Z","dependency_job_id":null,"html_url":"https://github.com/SamuelDonovan/minimost","commit_stats":null,"previous_names":["samueldonovan/minimost"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/SamuelDonovan/minimost","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamuelDonovan%2Fminimost","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamuelDonovan%2Fminimost/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamuelDonovan%2Fminimost/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamuelDonovan%2Fminimost/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SamuelDonovan","download_url":"https://codeload.github.com/SamuelDonovan/minimost/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SamuelDonovan%2Fminimost/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34950334,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-30T02:00:05.919Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chat","chat-application","flask","lan","local-network","mattermost-alternative","messaging","no-database","pwa","python","real-time","screen-sharing","self-hosted","self-hosted-chat","slack-alternative","sqlite","team-chat","video-call","voice-chat","webrtc"],"created_at":"2026-05-31T01:02:51.778Z","updated_at":"2026-06-30T03:00:53.705Z","avatar_url":"https://github.com/SamuelDonovan.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/SamuelDonovan/minimost/main/src/minimost/static/minimost-logo.png\" alt=\"MiniMost\" width=\"360\"\u003e\n\u003c/p\u003e\n\n---\n\n[![PyPI](https://img.shields.io/pypi/v/minimost.svg)](https://pypi.org/project/minimost/)\n[![Downloads](https://static.pepy.tech/badge/minimost)](https://pepy.tech/project/minimost)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/SamuelDonovan/minimost/blob/main/LICENSE)\n[![Python: 3.6+](https://img.shields.io/badge/python-3.6%20%7C%203.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11%20%7C%203.12%20%7C%203.13-blue.svg)](https://www.python.org/)\n[![Built with Flask](https://img.shields.io/badge/built%20with-Flask-000000.svg?logo=flask)](https://flask.palletsprojects.com/)\n[![Database: SQLite](https://img.shields.io/badge/database-SQLite%20only-003b57.svg?logo=sqlite\u0026logoColor=white)](https://www.sqlite.org/)\n[![PWA](https://img.shields.io/badge/PWA-installable-5a0fc8.svg)](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![Code style: prettier](https://img.shields.io/badge/code%20style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![Build](https://github.com/SamuelDonovan/minimost/actions/workflows/build.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/build.yml)\n[![Ruff](https://github.com/SamuelDonovan/minimost/actions/workflows/ruff.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/ruff.yml)\n[![ESLint](https://github.com/SamuelDonovan/minimost/actions/workflows/eslint.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/eslint.yml)\n[![Bandit](https://github.com/SamuelDonovan/minimost/actions/workflows/bandit.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/bandit.yml)\n[![Semgrep](https://github.com/SamuelDonovan/minimost/actions/workflows/semgrep.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/semgrep.yml)\n[![pip-audit](https://github.com/SamuelDonovan/minimost/actions/workflows/pip-audit.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/pip-audit.yml)\n[![CodeQL](https://github.com/SamuelDonovan/minimost/actions/workflows/codeql.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/codeql.yml)\n[![Documentation Status](https://readthedocs.org/projects/minimost/badge/?version=latest)](https://minimost.readthedocs.io/en/latest/)\n[![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=SamuelDonovan_minimost\u0026metric=alert_status)](https://sonarcloud.io/summary/overall?id=SamuelDonovan_minimost)\n[![Maintainability](https://sonarcloud.io/api/project_badges/measure?project=SamuelDonovan_minimost\u0026metric=sqale_rating)](https://sonarcloud.io/summary/overall?id=SamuelDonovan_minimost)\n[![Reliability](https://sonarcloud.io/api/project_badges/measure?project=SamuelDonovan_minimost\u0026metric=reliability_rating)](https://sonarcloud.io/summary/overall?id=SamuelDonovan_minimost)\n[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=SamuelDonovan_minimost\u0026metric=security_rating)](https://sonarcloud.io/summary/overall?id=SamuelDonovan_minimost)\n[![RPM](https://github.com/SamuelDonovan/minimost/actions/workflows/rpm.yml/badge.svg)](https://github.com/SamuelDonovan/minimost/actions/workflows/rpm.yml)\n[![Copr build status](https://img.shields.io/badge/dynamic/json?label=Copr\u0026logo=fedora\u0026logoColor=white\u0026color=blue\u0026query=%24.builds.latest.state\u0026url=https%3A%2F%2Fcopr.fedorainfracloud.org%2Fapi_3%2Fpackage%3Fownername%3Dsamuel-donovan-fas%26projectname%3DMinimost%26packagename%3DMinimost%26with_latest_build%3Dtrue)](https://copr.fedorainfracloud.org/coprs/samuel-donovan-fas/Minimost/package/Minimost/)\n\n**MiniMost** is a lightweight, self-hosted chat platform built for private networks. It runs entirely on Python and SQLite — no external database, no root access, no infrastructure required. Just Flask and a browser.\n\n---\n\n## Quick start\n\n```bash\npip install minimost   # 1. install (only needs Python 3.6+ and Flask)\nminimost               # 2. run → https://127.0.0.1:5000\n```\n\nOpen the URL, sign up with any username and password, and start chatting. To let others on your network join:\n\n```bash\nminimost --host 0.0.0.0          # listen on all interfaces\n```\n\nThen point a browser at `https://\u003cserver-ip\u003e:5000`, accept the self-signed certificate warning once, and you're done — no database to provision, no accounts to pre-create, no internet required.\n\n\u003e On Red Hat / Fedora? Skip pip and install the packaged systemd service straight from COPR — see [On Red Hat / Fedora (COPR)](#on-red-hat--fedora-copr).\n\n---\n\n## Screenshots\n\n![Login page](https://raw.githubusercontent.com/SamuelDonovan/minimost/main/docs/_static/screenshot-login.png)\n_The login page — clean, minimal, and version-tagged._\n\n![Chat interface](https://raw.githubusercontent.com/SamuelDonovan/minimost/main/docs/_static/screenshot-chat.png)\n_The main chat interface — channel list, direct messages, inline image attachments, and real-time typing indicators._\n\n![Message search](https://raw.githubusercontent.com/SamuelDonovan/minimost/main/docs/_static/screenshot-message-search.png)\n_Full-text message search with highlighted results._\n\n---\n\n## Features\n\n- ⚡ **Sign up in seconds** — pick a username and password and you're in; no email verification, no waiting. Changed your mind? Accounts can be deleted just as quickly.\n- 🔒 **Your password stays secret** — passwords are hashed and salted (PBKDF2) with enforced complexity, so not even the server admin can see what you typed.\n- 📁 **Messages are read-protected** — the message database is locked down on disk, so your conversations aren't sitting around for anyone to open.\n- 💬 **Channels \u0026 direct messages** — public channels (configurable), invite-only private channels you can rename and manage, and one-on-one or group DMs that keep going so anyone can catch up on what they missed.\n- 🔍 **Every message is saved \u0026 searchable** — nothing disappears, and full-text search finds any message in an instant. New users see the history from day one.\n- 📷 **Images show up inline** — paste, drag-and-drop, or use the paperclip button to attach any file. Images embed right in the conversation; everything else becomes a download link.\n- ✏️ **Replies, edits \u0026 reactions** — quote any message to reply in context, edit or delete your own messages, and react with emoji. Every change syncs to everyone in real time.\n- 👀 **Presence, typing \u0026 @mentions** — see who's online, watch the typing dots, and ping the right person with an @mention (or `@everyone`). Mentions alert you with a sound and desktop notification even while the tab is focused, and read receipts show who's seen your messages.\n- 📞 **Voice, video \u0026 screen sharing** — jump on a call or share your screen right from the chat in any DM or private channel. Calls grow into group calls with the in-call \"Add person\" button, and participant tiles reflow automatically with live speaking indicators.\n- 🛡️ **LAN-first, peer-to-peer media** — call and screen-share media is sent directly between participants over WebRTC and never touches the server. A small bundled STUN server means there's nothing external to configure — it even works on fully air-gapped LANs.\n- 🎨 **Make it yours** — upload a profile avatar (or use the default initials), pick a display-name colour, and hide DM threads you're done with (they reappear if a new message arrives).\n- 🔔 **Notifications, your way** — desktop and sound alerts are configurable per session and mutable with one click.\n- 🖥️ **Works everywhere** — runs right in your browser on Linux and Windows, with a touch-friendly, mobile-responsive layout, a drawer sidebar, and pinch-to-zoom font sizing.\n- 🌙 **Dark theme** — easy on the eyes.\n- 🧹 **Tidies up after itself** — a background thread automatically removes old messages and attachments past a configurable age (default: 770 days for messages, 30 for files) and trims the oldest content once the database or uploads grow past a size cap. Runs every 24 hours, no cron job required.\n- 🗑️ **Account self-deletion** — delete your own account from Settings. A soft delete re-attributes your messages to \"Deleted User\" while preserving chat history; a hard delete removes every message you ever sent. Both require password confirmation.\n- 🔑 **Admin password reset** — generate a one-time, time-limited reset URL from the CLI; the user gets an in-app notification when a reset is requested.\n\n---\n\n## Free, MIT licensed, and fully auditable\n\nMiniMost is released under the [MIT License](https://github.com/SamuelDonovan/minimost/blob/main/LICENSE) — free to use, free to modify, and free to redistribute, with no strings attached.\n\n- 💯 **Truly free** — there's no license to set up, no activation key, no seat count to track, and no paid tier hiding features behind a paywall.\n- 👥 **No user limit** — invite your whole team, your whole company, or your whole LAN. The software never counts heads or asks you to upgrade.\n- 🔍 **Every line is open to inspect \u0026 audit** — all of the code is right here in this repository. Unlike \"open core\" products that ship a stripped-down public version while keeping the real functionality closed, there is no hidden enterprise edition and nothing held back. What you read is exactly what you run.\n\n---\n\n## How MiniMost compares\n\nMiniMost trades breadth of features for something most chat platforms can't offer: it's fully open, runs on your own private network with no external services, and installs in one command. Here's how it lines up against common alternatives.\n\n|                              | **MiniMost**                 | **Mattermost**                           | **Cisco Jabber**                  | **Microsoft Teams**        | **Slack**                        | **Skype**¹                               |\n| ---------------------------- | ---------------------------- | ---------------------------------------- | --------------------------------- | -------------------------- | -------------------------------- | ---------------------------------------- |\n| **License**                  | MIT                          | Open core (source-available + paid EE)   | Proprietary                       | Proprietary                | Proprietary                      | Proprietary                              |\n| **Source open \u0026 auditable**  | ✅ Fully — every line public | ⚠️ Core only; enterprise features closed | ❌                                | ❌                         | ❌                               | ❌                                       |\n| **Cost**                     | Free, forever                | Free core; paid tiers                    | Paid (Cisco licensing)            | Paid (Microsoft 365 plans) | Freemium; paid per-seat tiers    | Was free                                 |\n| **User / seat limit**        | Unlimited                    | Free Team Ed. capped (≤250 users)        | Per-license                       | Per-license                | Free tier limited; paid per-seat | n/a                                      |\n| **Self-host on private LAN** | ✅                           | ✅                                       | ⚠️ On-prem, needs Cisco UC infra  | ❌ Cloud (SaaS)            | ❌ Cloud (SaaS)                  | ⚠️ Consumer cloud; SfB Server is on-prem |\n| **Works fully air-gapped**   | ✅                           | ⚠️ Possible, with effort                 | ⚠️                                | ❌                         | ❌                               | ❌                                       |\n| **Database**                 | SQLite (bundled, file-based) | PostgreSQL (external, you run it)        | Cisco UC backend                  | Microsoft cloud            | Slack cloud                      | Microsoft cloud                          |\n| **Runtime dependencies**     | Python + Flask               | Go binary + DB + reverse proxy           | CUCM / IM\u0026P servers               | Microsoft 365 tenant       | Internet account                 | Internet account                         |\n| **Ease of setup**            | One `pip`/`dnf` command      | Moderate (DB, config, proxy)             | Heavy (enterprise infrastructure) | Account signup             | Account signup                   | n/a                                      |\n| **Voice / video / screen**   | ✅ P2P WebRTC over LAN       | ⚠️ Via calls plugin / integrations       | ✅                                | ✅                         | ✅                               | ✅                                       |\n\n¹ Consumer Skype retired May 2025; Skype for Business Server is on-prem (end of support Oct 2025).\n\nThis isn't a feature-count contest — the big platforms have far more functionality (see the [FAQ](#faq)). MiniMost wins only where it's designed to: zero infrastructure, full auditability, and running on a network with no internet at all.\n\n---\n\n## Requirements\n\n- Python 3.6+\n- Flask (`pip install flask`)\n\nThat's it!\n\n---\n\n## Installation\n\n### From PyPI (recommended)\n\n```bash\npip install minimost\n```\n\n### From source\n\n```bash\ngit clone https://github.com/SamuelDonovan/minimost.git\ncd minimost\npip install -e .\n```\n\n### On Red Hat / Fedora (COPR)\n\nMiniMost is packaged for Fedora, RHEL, and their rebuilds (Rocky, Alma, CentOS Stream) via [COPR](https://copr.fedorainfracloud.org/coprs/samuel-donovan-fas/Minimost/). This installs MiniMost as a proper RPM with a hardened systemd service — no `pip`, no virtualenv.\n\n```bash\n# Enable the COPR repository (one-time). On RHEL/EL, first install the plugin:\n#   sudo dnf install -y dnf-plugins-core\nsudo dnf copr enable samuel-donovan-fas/Minimost\n\n# Install the package\nsudo dnf install minimost\n\n# Start it now and on every boot\nsudo systemctl enable --now minimost\n```\n\nThe RPM ships a locked-down systemd unit that runs MiniMost under a transient `DynamicUser`, keeping all state (databases, uploads, TLS material) under `/var/lib/minimost`. The service listens on **HTTPS port 6767** by default:\n\n```bash\nsystemctl status minimost          # check it's running\nsudo systemctl restart minimost    # apply settings.json changes\njournalctl -u minimost -f          # follow the logs\n```\n\nBrowse to `https://\u003cserver-ip\u003e:6767`. Clients can fetch the CA certificate to trust at `https://\u003cserver-ip\u003e:6767/ca.pem`. Remember to open the port (and UDP `3478` for calls) in `firewalld`:\n\n```bash\nsudo firewall-cmd --permanent --add-port=6767/tcp\nsudo firewall-cmd --permanent --add-port=3478/udp\nsudo firewall-cmd --reload\n```\n\n### From wheel (latest dev build)\n\nDownload the latest `.whl` from the [releases page](https://github.com/SamuelDonovan/minimost/releases/tag/dev), then:\n\n```bash\npip install minimost-*.whl\n```\n\n### Dependencies only (no internet access)\n\nDownload the Flask wheel and its dependencies, then:\n\n```bash\npip install --user *.whl\n```\n\n---\n\n## Running\n\n```bash\nminimost\n```\n\nOr without installing:\n\n```bash\npython3 -m minimost\n```\n\nOn first run MiniMost automatically generates a self-signed TLS certificate (`cert.pem` / `key.pem`) in pure Python (no `openssl` binary required) and serves over **HTTPS**. The server starts at [https://127.0.0.1:5000](https://127.0.0.1:5000) by default. Use `--host` and `--port` to change the bind address:\n\n```bash\n# Listen on all interfaces (accessible from other machines on the network)\nminimost --host 0.0.0.0\n\n# Listen on a specific IP and port\nminimost --host 192.168.1.10 --port 8080\n```\n\nTo reach the server from another machine, navigate to `https://\u003cserver-ip\u003e:\u003cport\u003e` in a browser. The generated certificate is self-signed, so your browser will show a security warning — add a permanent exception to dismiss it.\n\n\u003e **Note:** HTTPS is required for voice and video calling (browsers only allow camera/microphone and WebRTC access in secure contexts). The certificate is generated in pure Python (standard library only — no `openssl` binary required), so this works the same on Linux, macOS, and Windows.\n\n\u003e **Note:** Calls and screen shares connect peers directly over WebRTC. For this to work, peers must be on the **same LAN/subnet** and able to reach each other (and the bundled STUN server on UDP `3478`, configurable via `stun_port` in `settings.json`) over UDP. No public STUN/TURN server is used, so calls work on air-gapped networks, but they will not traverse the public internet or connect peers on different subnets.\n\n### Ports \u0026 firewall\n\n| Port                                  | Protocol | Open on          | Required for                                            | Notes                                                                                              |\n| ------------------------------------- | -------- | ---------------- | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |\n| `6767` (Gunicorn) / `5000` (dev)      | TCP      | Server (inbound) | Everything — web UI, chat, file uploads, call signaling | The only port needed for text chat. Set via `--port` or `gunicorn.conf.py`.                        |\n| `3478`                                | UDP      | Server (inbound) | Voice/video calls \u0026 screen sharing                      | Bundled STUN server. Set via `stun_port` in `settings.json`.                                       |\n| Ephemeral UDP (Linux `32768`–`60999`) | UDP      | Between clients  | WebRTC media (audio/video/screen)                       | Peer-to-peer, browser-chosen; only matters if clients run host firewalls or sit on segmented LANs. |\n\nNo outbound internet access is required (no external database, no public STUN/TURN) — MiniMost runs fully air-gapped. There is **no TURN relay**, so peers must be on the same LAN/subnet, and SQLite is file-based so there is no database port. See the [deployment docs](https://github.com/SamuelDonovan/minimost/blob/main/docs/deployment.rst) for an administrator setup checklist plus `firewalld` / `ufw` examples.\n\n### Production deployment\n\nThe built-in Flask server is suitable for development and small private networks. For a more robust deployment, run MiniMost behind a WSGI server such as Gunicorn:\n\n```bash\npip install gunicorn\n\n# From an installed package (wheel or `pip install -e .`) — uses the config\n# module shipped inside the package, so no source checkout is required:\ngunicorn \"minimost:create_app()\" -c python:minimost.gunicorn_conf\n\n# From a source checkout — equivalent thin shim that re-exports the same config:\ngunicorn \"minimost:create_app()\" --config gunicorn.conf.py\n```\n\nBoth forms handle automatic TLS certificate generation before Gunicorn starts. The bundled `gunicorn.conf.py` is a thin shim around the packaged `minimost.gunicorn_conf` module, so the two are interchangeable.\n\n### Configuration\n\nEdit `settings.json` (bundled with the package at `src/minimost/settings.json`) to configure MiniMost. All keys are optional and fall back to sensible defaults if omitted:\n\n```json\n{\n  \"channels\": [\"general\", \"software\", \"firmware\", \"systems\", \"off-topic\"],\n  \"image_retention_days\": 30,\n  \"file_retention_days\": 30,\n  \"message_retention_days\": 770,\n  \"max_message_db_size_mb\": 1024,\n  \"max_upload_dir_size_mb\": 2048,\n  \"max_upload_size_mb\": 25,\n  \"max_avatar_size_mb\": 5,\n  \"stun_port\": 3478,\n  \"max_login_attempts\": 5,\n  \"lockout_duration_minutes\": 15,\n  \"rate_limit_enabled\": true,\n  \"max_event_streams_per_user\": 12,\n  \"rate_limits\": {\n    \"login\": [60, 60],\n    \"signup\": [20, 3600],\n    \"password_reset\": [30, 3600],\n    \"send\": [240, 60],\n    \"avatar\": [60, 3600],\n    \"create_channel\": [60, 3600]\n  }\n}\n```\n\nMiniMost bounds disk usage two complementary ways: **age-based** retention deletes content once it gets old enough, and **size-based** caps delete content once a store grows past a limit (whichever triggers first). Size-cap eviction is **fair** — the account consuming the most space is trimmed first — so one user flooding messages or uploads cannot delete everyone else's data. Note the difference between the two upload settings: `max_upload_size_mb` is a per-file ceiling enforced at upload time, while `max_upload_dir_size_mb` caps the _total_ size of all stored attachments.\n\nMiniMost also includes built-in **denial-of-service throttles** (no external dependency): per-IP/per-user [rate limits](docs/security.rst) on abuse-prone routes (login, signup, message send, uploads) and a cap on concurrent live-update streams per user. They are generous enough that normal use never trips them; tune or disable them with the `rate_limit_enabled`, `max_event_streams_per_user`, and `rate_limits` keys below.\n\n| Key                          | Default       | Description                                                                                                                                                         |\n| ---------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `channels`                   | `[\"general\"]` | Public channel names shown in the sidebar. Restart required.                                                                                                        |\n| `image_retention_days`       | `30`          | Days before image attachments are auto-deleted. No restart needed.                                                                                                  |\n| `file_retention_days`        | `30`          | Days before non-image attachments are auto-deleted. No restart needed.                                                                                              |\n| `message_retention_days`     | `770`         | Days before messages are permanently deleted from the database. No restart needed.                                                                                  |\n| `max_message_db_size_mb`     | `1024`        | Total size cap (MB) for the message database `users/messages.db`; the heaviest sender's oldest messages are deleted when exceeded. `0` disables. No restart needed. |\n| `max_upload_dir_size_mb`     | `2048`        | Total size cap (MB) for the `uploads/` attachment directory; the heaviest uploader's oldest files are deleted when exceeded. `0` disables. No restart needed.       |\n| `max_upload_size_mb`         | `25`          | Maximum size in MB for a **single** file attachment, rejected at upload time. Restart required.                                                                     |\n| `max_avatar_size_mb`         | `5`           | Maximum size in MB for a profile avatar upload. Restart required.                                                                                                   |\n| `stun_port`                  | `3478`        | UDP port for the bundled STUN server used by WebRTC calls/screen share. Must be `1`–`65535`. Restart required.                                                      |\n| `max_login_attempts`         | `5`           | Consecutive failed logins before an account is locked. Set to `0` to disable lockout. No restart needed.                                                            |\n| `lockout_duration_minutes`   | `15`          | How long an account stays locked after too many failed logins. No restart needed.                                                                                   |\n| `rate_limit_enabled`         | `true`        | Master switch for the built-in DoS throttles (request rate limits + SSE stream cap). Restart required.                                                              |\n| `max_event_streams_per_user` | `12`          | Max concurrent live-update (`/events`) streams a single user may hold open. Excess connections get `429`. No restart needed.                                        |\n| `rate_limits`                | _(see above)_ | Per-route request limits as `{action: [max, window_seconds]}`. Omitted actions keep their default. Returns `429` when exceeded. No restart needed.                  |\n\n---\n\n## Keyboard Shortcuts\n\n### Messaging\n\n| Key             | Action                                                             |\n| --------------- | ------------------------------------------------------------------ |\n| `Enter`         | Send message                                                       |\n| `Shift + Enter` | New line                                                           |\n| `@`             | Open the mention dropdown (`↑`/`↓` navigate, `Enter`/`Tab` accept) |\n| `Esc`           | Unfocus input / close menus                                        |\n\n### Navigation\n\n| Key                     | Action                  |\n| ----------------------- | ----------------------- |\n| `i`                     | Focus message input     |\n| `o`                     | Start a new DM          |\n| `/` or `f`              | Search messages         |\n| `j` / `k`               | Scroll down / up        |\n| `d` / `u`               | Scroll down / up (2×)   |\n| `G`                     | Jump to bottom          |\n| `g`                     | Jump to top             |\n| `Ctrl + J` / `Ctrl + K` | Next / previous channel |\n| `?`                     | Open help menu          |\n\n### Visual Mode\n\nPress `v` in normal mode (input unfocused) to enter visual mode, which highlights a single message for direct keyboard actions. The topbar shows `-- visual --` while active.\n\n| Key       | Action                                          |\n| --------- | ----------------------------------------------- |\n| `v`       | Enter visual mode (selects most recent message) |\n| `j` / `↓` | Move selection to next (newer) message          |\n| `k` / `↑` | Move selection to previous (older) message      |\n| `d`       | Delete highlighted message                      |\n| `c`       | Edit highlighted message                        |\n| `o`       | Reply to highlighted message                    |\n| `y`       | Copy highlighted message text to clipboard      |\n| `e`       | React to highlighted message with emoji         |\n| `Esc`     | Exit visual mode                                |\n\n### Text Formatting\n\n| Key        | Action            |\n| ---------- | ----------------- |\n| `Ctrl + B` | **Bold**          |\n| `Ctrl + I` | _Italic_          |\n| `Ctrl + U` | Underline         |\n| `Ctrl + S` | ~~Strikethrough~~ |\n\nAll formatting uses Markdown syntax (underline uses `__text__`). Shortcuts work on selected text or toggle the format on/off while typing.\n\n### Media \u0026 Display\n\n- **Attach files** — paste from clipboard, drag onto the message box, or use the paperclip button; any file type is accepted\n- **Images** are displayed inline; all other file types appear as a download link showing the original filename\n- **File size limit** — configurable per-upload maximum (default 25 MB); the browser warns before attempting an oversized upload\n- **Font size** — pinch (mobile) or `Ctrl + Scroll` (desktop); preference is saved across sessions\n\n---\n\n## Security\n\n- Passwords are salted and hashed with PBKDF2 via Werkzeug — no plaintext or bare SHA-256 storage\n- Password complexity is enforced on both frontend and backend (8+ characters, uppercase, number, special character)\n- All database queries use parameterized statements — no SQL injection surface\n- Messages live in a single shared SQLite database; every read enforces channel access control (public channels, private-channel membership, and DM participation)\n- SAST scanning via [Bandit](https://bandit.readthedocs.io/), [Semgrep](https://semgrep.dev/), [CodeQL](https://codeql.github.com/), and [SonarCloud](https://sonarcloud.io/) in CI\n- Dependency vulnerabilities audited on every push with [pip-audit](https://github.com/pypa/pip-audit)\n- Flask debug mode is disabled in production\n\n---\n\n## FAQ\n\n**Are messages encrypted?**\n\nMessages are not end-to-end encrypted. All data lives in SQLite files on the server filesystem. These files are not world-readable, but an administrator with filesystem access can read/audit them. Treat this as an internal LAN tool, not a secure messenger.\n\n**What if a user forgets their password?**\n\nAn administrator can generate a one-time reset link from the command line:\n\n```bash\nminimost reset-password \u003cusername\u003e\n```\n\nThis prints a URL valid for 60 minutes (configurable with `--expires`) and sends the user an in-app notification via a system DM. Share the URL with the user through another channel (email, phone, etc.). When they open it, they can set a new password. The link expires after use or when the timer runs out — whichever comes first. Run `minimost reset-password --help` for all options.\n\n**Does it have feature X from Slack/Discord/Mattermost?**\n\nProbably not. Those products have hundreds of engineers and years of development. MiniMost is intentionally minimal — the goal is something that runs anywhere with zero infrastructure overhead.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamueldonovan%2Fminimost","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsamueldonovan%2Fminimost","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsamueldonovan%2Fminimost/lists"}