Self-hosted intranet and ERP-style internal platform — run it on your own servers or private cloud, with your data, your branding, and no SaaS lock-in.
Firmgate is an open-source alternative to cloud employee portals and a tangle of separate internal tools. It combines intranet features (news, wiki, chat, documents) with operational modules (CRM, workforce directory, security clearance, timesheets-style workflows, and administration) in one deployable stack. You control where it runs, who can access it, and how it integrates with Office 365, LDAP, and email.
Built with Flask, with role-based access control, optional MFA, and integrations for document editing and outbound mail.
| Perk | What it means |
|---|---|
| Your infrastructure | SQLite (or your DB), uploads, and config live on your machine — not a vendor’s multi-tenant cloud |
| One system | Intranet + documents + CRM + workforce + compliance modules in a single app, instead of many subscriptions |
| Air-gap friendly | Suitable for LAN-only, VPN, or regulated environments where data must not leave the network |
| No per-seat cloud tax | Scale users on hardware you already pay for; optional commercial support later without mandatory SaaS |
| Full admin control | Users, roles, modules, backups, factory reset, and branding from Administration |
| Area | Highlights |
|---|---|
| Home | Configurable landing page and news |
| Blogs | Internal posts (admin authoring) |
| Events | Shared calendar (day / month / year), public holidays |
| Wiki | Internal knowledge base with sanitised HTML |
| Team Chat | Rooms, messaging, optional WebRTC voice |
| Workforce | Employee directory, presence, admin editing |
| Documents | Folders, uploads, sharing, PDF/image/EML viewers, Office editing |
| CRM | Leads, pipeline, companies, activities |
| Security | Clearance records, training library, officer reports |
| About | Editable company profile |
| Administration | Users, groups, roles, modules, integrations, backups, branding |
Modules can be enabled, restricted, or hidden per user from Administration → Modules.
Visual tour of Firmgate (demo data shown in some views). Each shot includes the default Firmgate logo and header label.
Configurable landing page with announcements and an editable hero section for administrators.
Internal blog posts with categories; authors with permission can create new entries.
Chat rooms, member management, shared files, and optional voice calls.
Track clearance levels, expiry, import/export, and compliance summaries.
Folder tree, uploads, sharing, favourites, and in-browser previews for common file types.
Pipeline dashboard, leads, companies, contacts, activities, and deals.
Built-in games (Chess, Lemmings, Sky Control) for informal team engagement.
User, group, and role management; integrations; backups; portal branding; and module toggles.
| Requirement | Notes |
|---|---|
| Python 3.10+ | 3.11 recommended |
| Git | Clone and deploy updates |
| SQLite | Default database (included with Python) |
| Disk space | Depends on document uploads (UPLOAD_ROOT) |
| Docker (Option 1) | Docker Engine + Docker Compose v2 |
Install from requirements.txt (Flask, SQLAlchemy, LDAP client, MFA, document libraries, etc.).
Production also uses Gunicorn (installed automatically by scripts/update.sh if missing).
| Feature | What you need |
|---|---|
| HTTPS reverse proxy | nginx, Caddy, or similar (strongly recommended in production) |
| OnlyOffice | ONLYOFFICE Document Server + public URLs for callbacks |
| Microsoft 365 editing | Azure app registration with Graph permissions |
| Outbound email | SMTP (custom, Microsoft 365, or Google Workspace) |
| LDAP / AD | Directory server for future SSO/sync integrations |
| Large uploads | Tune MAX_UPLOAD_MB and proxy client_max_body_size |
On a fresh install (empty database), the app creates one bootstrap administrator:
| Field | Value |
|---|---|
admin@example.com |
|
| Password | admin |
This account exists so you can sign in immediately and configure the portal. Change this password before going to production.
The bootstrap account is a temporary entry point, not a permanent second admin.
Once any other active user has full administration rights (admin.all), the bootstrap account is automatically deactivated (is_active = false). You can still see it in Administration → Users (marked as factory bootstrap); it cannot sign in until an administrator re-enables it.
This runs whenever users or role permissions are saved — for example after you:
- Sign in as
admin@example.com - Open Administration → Users
- Create your real administrator account and assign the admin role (or any role that includes
admin.all) - Sign out and sign in as the new account
The factory account is then disabled; use your real admin account from that point on.
Factory reset restores the same bootstrap credentials on a wiped portal. See Backup and factory reset below.
Two supported ways to deploy: Docker Compose (fastest) or a release ZIP (air-gap, no Git on the server). After either install, sign in with the bootstrap admin, create your real administrator, and change passwords before production use.
Runs Firmgate in a container with Gunicorn. SQLite and uploads persist in a named Docker volume.
Requirements: Docker Engine and Docker Compose v2.
git clone https://github.com/snooth/firmgate.git
cd firmgate
cp .env.example .env
# Edit .env — set SECRET_KEY (openssl rand -hex 32). Compose reads .env for ${VAR} substitution.
docker compose up -d --buildOpen http://127.0.0.1:5001/ (or the host port from FIRMGATE_HTTP_PORT in .env).
| Item | Location |
|---|---|
| App code | Inside the firmgate container image |
| Database + uploads | Docker volume firmgate_data → /data/instance in the container |
| Secrets / config | .env on the host (loaded by Compose) |
Useful commands:
docker compose logs -f firmgate # follow logs
docker compose ps # health status
docker compose down # stop (data kept in volume)
docker compose up -d --build # rebuild after git pullPut nginx or Caddy in front of the published port for HTTPS in production. The container exposes a health check on /health.
To back up data, copy the volume contents or use Administration → Backup and restore inside the app.
For servers without Git access or regulated networks: build a ZIP on a machine with the repo, copy it to the server, then install or upgrade through the admin UI.
From a clone of this repository:
./scripts/build_release_package.shOutput: dist/firmgate-release-<version>-<timestamp>.zip containing a firmgate/ folder with source, requirements.txt, manifest.json, and checksums.sha256.json.
- Unzip on the server, e.g.
/opt/firmgate - Create
.envfrom.env.exampleand setSECRET_KEY,DATABASE_URL, andUPLOAD_ROOT - Create a virtual environment and install dependencies:
cd /opt/firmgate
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
.venv/bin/python -c "from app import create_app; create_app()"- Run with Gunicorn (or use the production systemd steps)
.venv/bin/gunicorn --bind 127.0.0.1:5001 --workers 2 --timeout 300 "run:app"When Git pull is not available, enable package upgrades and upload the ZIP:
- Ensure
ENABLE_SOFTWARE_PACKAGE_UPGRADE=1in.env(default) - Sign in as an administrator → Administration → Software version
- Choose Upgrade from package and upload
firmgate-release-*.zip
The server keeps instance/, .env, and .venv, runs pip install -r requirements.txt, takes a light backup, and restarts the configured systemd service (SOFTWARE_UPGRADE_SERVICE_NAME, default intranet). Legacy packages tagged intranet-release-package are still accepted.
Follow these steps from an empty machine to a running portal on your laptop.
git clone https://github.com/snooth/firmgate.git
cd firmgatepython3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -r requirements.txtCreate a .env file in the repo root (gitignored):
cat > .env <<'EOF'
SECRET_KEY=replace-with-a-long-random-string
FLASK_DEBUG=1
EOFOr export variables in your shell — see Configuration.
source .venv/bin/activate
python run.pyOpen http://127.0.0.1:5001/ (port overridable with PORT=8080 python run.py).
On first start the app creates instance/secure_browser.db and the bootstrap admin if the database has no users.
- Sign in with
admin@example.com/admin - Go to Administration → Users and create your real administrator
- Assign the admin role (includes
admin.all) - Sign in as the new user — the bootstrap account is deactivated automatically
- Configure Integrations, Email, Portal customisation, and Modules as needed
Optional: run python seed_data.py on an empty database if you prefer an explicit seed step (same bootstrap user, no demo data).
After sign-in, the top bar lists modules your account may access. Availability depends on roles/permissions and Administration → Modules settings.
Typical workflow for end users:
- Home — announcements and links configured by admins
- Documents — upload, organise, share, and open files (Office formats use OnlyOffice or Microsoft 365 when configured)
- Events — view or create calendar entries (if permitted)
- Team Chat — join rooms and message colleagues
- Workforce — find people and org details
Power users and module owners use CRM, Wiki, Security Training, Resource Pool, etc., according to their permissions.
Built-in roles include Standard, Power, and admin. Fine-grained permissions (documents.read, wiki.write, admin.all, …) are managed under Administration → Roles & permissions.
Users can belong to groups; group roles grant permissions in bulk.
Open Administration from the main nav (requires admin.all or user-management permissions). Common tasks:
| Section | Purpose |
|---|---|
| Users / Groups / Roles | Accounts and access control |
| Registrations | Approve self-service sign-ups (Extranet theme) |
| Integrations | OnlyOffice, Microsoft 365, LDAP |
| Email Settings | Outbound SMTP |
| Portal customisation | Logo, theme, home content |
| Modules | Show/hide/restrict nav items |
| Backup and restore | Download zip backup, restore, factory reset |
| Software version | Git or package upgrade (when enabled) |
- Upload via Documents → + New or drag-and-drop
- Share folders/files with colleagues (permission permitting)
- Office files open in an embedded editor when OnlyOffice or Microsoft 365 is configured under Integrations → Document editor
- PDFs, images, and
.emlemail open in built-in viewers
A generic user manual lives in docs/User_Manual.html. Regenerate figures or Word export with:
python3 scripts/generate_manual_figure_images.py
python3 scripts/build_user_manual_docx.pyThis section walks from a fresh Linux server to a systemd-managed deployment behind HTTPS. Adjust paths and domain names for your environment.
Internet → nginx (TLS) → Gunicorn → Flask app
↓
instance/ (SQLite + uploads)
.env (secrets)
Keep code (git checkout) separate from runtime data (instance/, .env) so updates never wipe documents or settings.
Recommended layout (used by scripts/update.sh):
| Path | Purpose |
|---|---|
/root/intranet |
Git checkout (application code) |
/root/intranet_instance |
Database + uploads (symlinked as intranet/instance/) |
/root/intranet-backups |
Timestamped .env + DB backups before upgrades |
/etc/intranet-update.conf |
Optional override for update script |
sudo apt update
sudo apt install -y python3 python3-venv python3-pip git nginxsudo mkdir -p /root/intranet
sudo git clone https://github.com/snooth/firmgate.git /root/intranet
cd /root/intranetsudo mkdir -p /root/intranet_instance/uploads
sudo ln -sfn /root/intranet_instance /root/intranet/instancesudo tee /root/intranet/.env <<'EOF'
SECRET_KEY=CHANGE-ME-use-openssl-rand-hex-32
FLASK_DEBUG=0
DATABASE_URL=sqlite:////root/intranet_instance/secure_browser.db
UPLOAD_ROOT=/root/intranet_instance/uploads
MAX_UPLOAD_MB=4096
EOFGenerate a secret:
openssl rand -hex 32cd /root/intranet
python3 -m venv .venv
.venv/bin/pip install --upgrade pip
.venv/bin/pip install -r requirements.txt
.venv/bin/pip install gunicornStart once so create_all() runs and the bootstrap admin is created:
cd /root/intranet
.venv/bin/python -c "from app import create_app; create_app()"Or run briefly:
.venv/bin/python run.py &
sleep 3
kill %1Create /etc/systemd/system/intranet.service:
[Unit]
Description=Firmgate (Gunicorn)
After=network.target
[Service]
User=root
Group=root
WorkingDirectory=/root/intranet
EnvironmentFile=/root/intranet/.env
ExecStart=/root/intranet/.venv/bin/gunicorn \
--workers 2 \
--bind 127.0.0.1:5001 \
--timeout 300 \
"run:app"
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.targetEnable and start:
sudo systemctl daemon-reload
sudo systemctl enable intranet
sudo systemctl start intranet
sudo systemctl status intranetIncrease
--workerson multi-core hosts. For SQLite, avoid very high worker counts on heavy write loads.
Replace intranet.example.com with your hostname. Example /etc/nginx/sites-available/intranet:
server {
listen 80;
server_name intranet.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name intranet.example.com;
ssl_certificate /etc/ssl/certs/intranet.crt;
ssl_certificate_key /etc/ssl/private/intranet.key;
client_max_body_size 4096m;
location / {
proxy_pass http://127.0.0.1:5001;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}sudo ln -s /etc/nginx/sites-available/intranet /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxUse certbot or your CA for real TLS certificates.
- Browse to
https://intranet.example.com - Sign in with
admin@example.com/admin - Create your real admin user, assign admin role, sign in as that user
- Configure integrations (set App public URL to your HTTPS origin for OnlyOffice)
- Upload a logo under Portal customisation
- Disable or delete the factory bootstrap user if it remains visible (it should auto-deactivate)
sudo cp /root/intranet/scripts/root-update.sh /root/update.sh
sudo chmod +x /root/update.shOptional: copy scripts/update.conf.example to /etc/intranet-update.conf (default REPO_URL is https://github.com/snooth/firmgate.git).
From your development machine, commit and push:
./upload.sh "Describe your changes"On the server (run from /root, not inside the app tree):
sudo /root/update.shThe update script:
- Links external
instance/if configured - Light backup of
.envand SQLite DB to/root/intranet-backups/<timestamp>/ git fetch+ hard reset toorigin/mainpip install -r requirements.txt(and Gunicorn if needed)- Restarts the
intranetsystemd unit - Verifies upload file counts did not drop
Useful flags:
sudo /root/update.sh --dry-run # print steps only
sudo /root/update.sh --recreate-venv # rebuild .venv
sudo /root/update.sh --full # fresh clone + directory swap
sudo /root/update.sh --no-backup # skip pre-update backup
sudo /root/update.sh --backup-full # rsync entire instance/ (slow)Defaults live in config.py. Override with environment variables or .env (loaded when present).
| Variable | Purpose | Default |
|---|---|---|
SECRET_KEY |
Flask session signing | dev-change-me-in-production |
FLASK_DEBUG / DEBUG |
Debug mode | off |
DATABASE_URL |
SQLAlchemy URI | sqlite:///instance/secure_browser.db |
UPLOAD_ROOT |
Document blob storage | instance/uploads |
MAX_UPLOAD_MB |
Max upload size per request | 4096 |
PORT |
Dev server port (run.py) |
5001 |
MFA_ISSUER |
Name shown in authenticator apps | Firmgate |
PORTAL_PRODUCT_NAME |
Default header / shell name (core theme) | Firmgate |
ONLYOFFICE_APP_URL |
Public base URL for Document Server callbacks | (empty → request root) |
ENABLE_SOFTWARE_GIT_UPGRADE |
Admin Git upgrade API | enabled |
ENABLE_SOFTWARE_PACKAGE_UPGRADE |
Admin zip package upgrade | enabled |
DEPLOY_ROOT |
Git root for in-app upgrade | /root/intranet when present |
SOFTWARE_UPGRADE_SERVICE_NAME |
systemd unit to restart after upgrade | intranet |
VOICE_CALL_MODE |
Team Chat voice: webrtc or jitsi |
webrtc |
WEBRTC_STUN_URL |
STUN server for WebRTC | Google public STUN |
Example .env for local development:
SECRET_KEY=local-dev-secret-change-me
DATABASE_URL=sqlite:////absolute/path/to/instance/secure_browser.db
UPLOAD_ROOT=/absolute/path/to/instance/uploads
MAX_UPLOAD_MB=4096Configure under Administration → Integrations (and related tabs).
Choose the active editor, then configure the matching block:
- OnlyOffice — Document Server URL, JWT secret (if enabled on DS), App public URL (must be reachable from the Document Server)
- Microsoft 365 — Azure tenant, app ID, client secret, SharePoint site; requires Graph application permissions
Without a configured editor, Office files can still be downloaded; embedded editing routes return 404.
Administration → Email Settings — SMTP provider, from address, enable/disable outbound mail.
Stored for future bridges; Test verifies bind and search settings.
Administration → Backup and restore
| Action | Effect |
|---|---|
| Download backup | Zip of SQLite DB, uploads, branding |
| Restore | Replace DB/uploads from zip (destructive) |
| Factory reset | Wipes portal data; restores bootstrap admin admin@example.com / admin |
| Add demo data | Seeds ~20% sample content per module (development only) |
Factory reset requires typing FACTORY RESET to confirm. Download a backup first if you might need existing data.
The app uses db.create_all() on startup plus lightweight SQLite column helpers in app/__init__.py (ALTER TABLE … ADD COLUMN). No Alembic migration stack is bundled — for PostgreSQL or complex schema evolution, add Alembic or manage migrations separately.
| Symptom | What to check |
|---|---|
| Cannot sign in as bootstrap admin | Another active admin may have deactivated it; use your real admin account or factory reset |
| Upload HTTP 413 | Raise MAX_UPLOAD_MB and nginx client_max_body_size |
| OnlyOffice won’t open/save | App public URL must be HTTPS and reachable from Document Server; JWT secrets must match |
| Microsoft 365 test fails | Graph permissions, admin consent, SharePoint site hostname/path |
| Factory reset fails | Restart the app server so all workers release SQLite; retry |
| SQLAlchemy mapper errors | Usually a broken model relationship — see traceback model name |
| Service won’t start after update | journalctl -u intranet -n 50; try --recreate-venv |
| Docker container unhealthy | docker compose logs firmgate; confirm SECRET_KEY is set in .env |
| Package upgrade rejected | ZIP must include firmgate/manifest.json with tag firmgate-release-package |
app/ Flask application (blueprints, models, templates, static)
config.py Default configuration
run.py Development entrypoint (also used as Gunicorn `run:app`)
Dockerfile Container image (Option 1)
docker-compose.yml Docker Compose stack (Option 1)
.env.example Sample environment file (copy to .env)
seed_data.py Optional explicit seed (bootstrap admin only)
requirements.txt Python dependencies
scripts/ update.sh, build_release_package.sh, docker-entrypoint.sh, …
instance/ Runtime data (gitignored): DB, uploads, branding
docs/ User manual, screenshots, and reference images
docs/screenshots/ README gallery with Firmgate branding (home, blogs, chat, CRM, etc.)
LICENSE Apache 2.0 — Community Edition
COMMERCIAL.md Optional enterprise / support terms (not required to self-host)
- Backend: Flask, Flask-Login, Flask-SQLAlchemy
- Database: SQLite (default)
- Frontend: Jinja2 templates, vanilla JavaScript
- Production: Gunicorn + nginx (recommended)
Community Edition is licensed under the Apache License 2.0.
Optional paid support, hosting, and enterprise add-ons are described in COMMERCIAL.md. The Apache License allows free self-hosting; commercial terms apply only to separate offerings you choose to purchase.







