A web-based browser for NomadNet nodes, packaged in Docker. Browse distributed NomadNet content, send LXMF messages, and manage identities, all from a standard web browser.
NomadPortal runs on Reticulum and LXMF.
This software is provided "as is," without warranty of any kind, express or implied. The author accepts no risk and no liability for any use, misuse, or consequence from this project.
Anyone who installs, runs, hosts, or otherwise uses this software does so entirely at their own risk and on their own responsibility. This includes compliance with applicable laws, the content accessed or sent through it, and any harm from its operation.
NomadPortal is in active trial-release development. Features, configuration, and behavior may change between releases while the project matures. Feedback, bug reports, and pull requests are welcome. Open an issue for any problem you find. A 1.0 release will follow once the main surfaces settle.
- Browse NomadNet pages rendered from Micron markup to HTML
- File downloads from NomadNet nodes with a confirm dialog showing filename, MIME type, real-time byte progress, and the source URL
- Optional virus scanning for downloaded files through ClamAV (off by default. The app confirms with the user before download when no scan ran)
- Send and receive LXMF messages, with a per-user inbox and a per-user contact book
- Chat attachments — attach images, audio, or files to outbound LXMF
messages with the 📎 button in the chat composer. Inbound images render
inline in the bubble, audio plays with the browser's
<audio>control, and files download as a link. The limit is 500 KB per attachment and per message, overridable withLXMF_ATTACHMENT_MAX_BYTES. This matches MeshChat'sFIELD_IMAGE/FIELD_AUDIO/FIELD_FILE_ATTACHMENTSwire format. - Manage RNS identities and announce to the mesh
- Contact book with MeshChat icon support
- Node discovery through Reticulum announces
- Host a NomadNet node with an auto-generated unique name
(
NomadPortal-<2 hex>). Silent by default, so a plain install does not add noise to the announce stream. An operator who publishes content can opt in to broadcasting. - HTTPS with an auto-generated self-signed certificate, with an optional HTTP→HTTPS redirector that survives port conflicts
- Local username/password login or OIDC/SSO (Keycloak, Authentik, Auth0, Google)
- Admin panel for interface configuration, user management, diagnostics, and a one-click RNS state reset that clears a stale destination table
- RNS-aware
/healthzendpoint. The Docker healthcheck reflects real routing availability, not just "gunicorn is listening." - Micron-formatted application title with a live preview
- Node blocklist for operator abuse response
- Mobile-responsive layout
- Custom 404/500 error pages
- Rate limiting and CSRF protection throughout
- Path-based URLs. A browser refresh preserves the current page, standard bookmarks work, and a share-link copied from the URL bar reaches the right node.
- Docker and Docker Compose v2
- A Reticulum interface reachable from the host (TCP, LoRa, etc.)
git clone https://github.com/JamesM92/NomadPortal.git
cd NomadPortalEdit docker-compose.yml and set at minimum:
ADMIN_PASSWORD: your-strong-password-here
FLASK_SECRET_KEY: some-random-string-herePre-built image — every tagged release is also published to the GitHub Container Registry. The default
docker-compose.ymlalready points atimage: ghcr.io/jamesm92/nomadportal:latest. Pin a specific version, such as:v0.9.4, for a reproducible deployment.git cloneis still the easiest way to get the docker-compose file and theconfig/skeleton.
cp config/config.yml.example config/config.ymlEdit config/config.yml to enable the interfaces you want (TCP, LoRa through RNode, I2P, AutoInterface, etc.). All interfaces are disabled by default. Set enabled: true on the ones you want, or use the Admin → Settings UI after the first start. The Reticulum-side config/reticulum/config is regenerated from this file on every start, so do not edit it directly.
./start.shThen open https://localhost:8443 in your browser. Accept the self-signed certificate warning. HTTP on port 8080 redirects to HTTPS automatically.
To rebuild after a code change:
./start.sh --buildTo stream logs to the terminal instead of running in the background:
./start.sh --fgAll options are set through environment variables in docker-compose.yml.
| Variable | Default | Description |
|---|---|---|
ADMIN_USERNAME |
admin |
Local admin login username |
ADMIN_PASSWORD |
(unset) | Local admin login password — required to enable local login |
FLASK_SECRET_KEY |
(auto-generated) | Session signing key. Set explicitly so sessions survive restarts |
WEB_PORT |
8080 |
HTTP redirect port |
WEB_PORT_HTTPS |
8443 |
HTTPS port (main app) |
CACHE_TTL |
300 |
Page cache TTL in seconds |
LOG_LEVEL |
INFO |
Python logging level (DEBUG, INFO, WARNING, ERROR) |
| Variable | Default | Description |
|---|---|---|
RNS_CONFIG_DIR |
/config/reticulum |
Reticulum configuration directory |
CONFIG_YML |
/config/config.yml |
Optional YAML config file (overrides env vars) |
| Variable | Default | Description |
|---|---|---|
TRUSTED_PROXIES |
0 |
Number of upstream proxy hops to trust for X-Forwarded-For. Set to 1 when behind nginx/Caddy |
HTTPS_REDIRECT |
false |
Set to true when TLS is terminated by a reverse proxy and you want the app to redirect HTTP → HTTPS at the application layer |
WEB_PORT |
8080 |
The plain-HTTP port. When WEB_PORT_HTTPS is also set, this port runs an HTTP→HTTPS redirector. When WEB_PORT_HTTPS is empty, this is the main port and gunicorn binds plain HTTP here (use behind a reverse proxy). |
WEB_PORT_HTTPS |
8443 |
The TLS port. When set, gunicorn binds HTTPS here and the entrypoint auto-generates a self-signed cert if /config/ssl/cert.pem does not exist. Set to "" (empty) to disable TLS and run plain HTTP only. |
| Variable | Description |
|---|---|
OIDC_CLIENT_ID |
OIDC application client ID |
OIDC_CLIENT_SECRET |
OIDC application client secret |
OIDC_DISCOVERY_URL |
Provider discovery URL (see examples below) |
OIDC_ALLOWED_EMAILS |
Comma-separated email allowlist (empty = any authenticated user) |
OIDC_ALLOWED_SUBJECTS |
Comma-separated subject claim allowlist |
OIDC_ADMIN_EMAILS |
Comma-separated list of admin emails |
OIDC_ADMIN_SUBJECTS |
Comma-separated list of admin subject claims |
OIDC_INSECURE_SKIP_VERIFY |
true/false — when true, NomadPortal skips TLS verification for the OIDC provider's hostname only. Use this only for a trusted-LAN setup with a self-signed Authentik/Keycloak. Logs a warning at startup. |
If both
OIDC_ADMIN_EMAILSandOIDC_ADMIN_SUBJECTSare empty, OIDC users log in as non-admins by default. Use the local admin account (ADMIN_PASSWORD) to bootstrap, then promote users from the Users page, or set the env-var allowlists. The local admin user is always admin.
Discovery URL examples:
| Provider | URL |
|---|---|
| Keycloak | https://auth.example.com/realms/<realm>/.well-known/openid-configuration |
| Authentik | https://auth.example.com/application/o/<slug>/.well-known/openid-configuration |
| Auth0 | https://<tenant>.auth0.com/.well-known/openid-configuration |
https://accounts.google.com/.well-known/openid-configuration |
- Create the Provider. Applications → Providers → Create → OAuth2/OpenID Provider. Set client type to Confidential, then copy the auto-generated Client ID and Client Secret. Redirect URIs must include
https://<your-nomadportal-host>/auth/callback(one per line in Strict mode, or Regex mode for multiple hosts). - Create the Application. Applications → Applications → Create. Set Provider to the one above. The Slug becomes part of the discovery URL.
- Get the Discovery URL. Authentik shows it on the provider page as the "OpenID Configuration URL." Paste the full URL into
OIDC_DISCOVERY_URL. - Paste credentials into
docker-compose.yml(OIDC_CLIENT_ID,OIDC_CLIENT_SECRET,OIDC_DISCOVERY_URL). - Set a stable
FLASK_SECRET_KEY(openssl rand -hex 32) so sessions survive restarts. - Restart NomadPortal and click Sign in. Authentik then redirects you back after login.
If your Authentik runs on a self-signed cert (the default for a Docker install on port 9443), set OIDC_INSECURE_SKIP_VERIFY: "true" for a trusted-LAN setup, or import Authentik's CA into the NomadPortal container for a stricter setup.
Common pitfalls:
redirect_uri_mismatch— the redirect URI registered in Authentik must exactly match what NomadPortal sends, including the scheme. Behind a reverse proxy, setTRUSTED_PROXIES: "1"andHTTPS_REDIRECT: "true"so URLs come out ashttps://.certificate verify failed: self-signed certificate— seeOIDC_INSECURE_SKIP_VERIFYabove.- Stuck logged out after a restart —
FLASK_SECRET_KEYis empty or keeps changing.
If you run NomadPortal behind nginx, Caddy, or a similar proxy:
- Set
TRUSTED_PROXIES: "1"so client IPs are read fromX-Forwarded-For. - Set
HTTPS_REDIRECT: "true"if you want the app to redirect plain HTTP. - The self-signed certificate from the entrypoint is only for direct access. Your proxy can terminate TLS with its own certificate instead.
Example Caddy snippet:
nomadnet.example.com {
reverse_proxy localhost:8443 {
transport http {
tls_insecure_skip_verify
}
}
}
When NomadPortal reaches Reticulum hubs through a VPN with an MTU under 1500, TCP connections can silently blackhole after about 30–60 seconds. Fetches work right after a container restart, then every later fetch times out with "No response from node" or "Link closed before response." At the RNS level, the interface logs Connection reset by peer, reconnects, works briefly, and dies again. Nothing in Reticulum or the application code is broken. This is a path-MTU-discovery blackhole, or fragmentation from Reticulum's default TCP hardware-MTU (8192) writing chunks the tunnel cannot carry.
Two different setups cause this, each with its own fix:
The container's eth0 sits at MTU 1500 (the Docker default). Traffic leaves the container, hits the host's VPN interface at a smaller MTU, and packets stall. Fix this by matching the docker network MTU to whatever the upstream VPN uses:
services:
nomadportal:
# ... existing config ...
networks: [default]
networks:
default:
driver: bridge
driver_opts:
com.docker.network.driver.mtu: 1280 # match your VPN's actual MTUOr share a VPN container's namespace directly. This is the same option as Setup B below, and it avoids the docker-network-MTU adjustment entirely.
Setup B — container IS sharing a VPN namespace (for example, network_mode: "container:gluetun") with a low-MTU tunnel
Here the tunnel interface (tun0 / wg0) sits inside the container's namespace, but its MTU is low. Many providers land in the 1170–1300 range on WireGuard. Even with MSS clamping active, Reticulum's default 8192 hardware-MTU produces payloads that get fragmented at the tunnel. Fix this by constraining Reticulum itself:
interfaces:
tcp_clients:
- name: MichMesh
host: rns.michmesh.net
port: 7822
enabled: true
fixed_mtu: 1000 # ← under whatever your tun MTU isfixed_mtu: 1000 is a safe default under any tunnel MTU of 1100 or more (it leaves room for IP, TCP, and IFAC headers). If throughput matters and your tunnel MTU runs comfortably higher, you can raise it. As a rule of thumb, tunnel_mtu − 80 is a good ceiling. Set this per tcp_clients entry, so hubs reached over different paths can be tuned independently.
Inside the container:
# List interfaces with their MTUs
for i in /sys/class/net/*; do
n=$(basename $i); echo "$n mtu=$(cat $i/mtu 2>/dev/null)"
done
# Show the default route(s)
awk 'NR==1 || $2=="00000000"' /proc/net/routeIf you see a tun0 / wg0 interface at MTU under 1400, you have Setup B. If you see only eth0 at MTU 1500 and the default route runs through it, you have Setup A.
entrypoint.sh runs this check at boot and prints a warning that matches whichever setup it detects. Silence it with NOMADPORTAL_SKIP_MTU_WARNING=true on a deployment where neither shape applies.
On the first start, entrypoint.sh generates a self-signed RSA-2048 certificate at /config/ssl/cert.pem, valid for 10 years. To use your own certificate, place cert.pem and key.pem in config/ssl/ before starting.
NomadPortal can run as a full NomadNet node, serving your own pages and files to any NomadNet client on the mesh.
- Add your pages to
./site/pages/. Create at leastindex.muas your home page. - Optionally, add downloadable files to
./site/files/. - Set your node name in
docker-compose.yml:SITE_NAME: "My Node"
- Start, or restart, the container.
NomadPortal detects the node automatically when ./site/pages/ exists. No extra flag is needed.
When hosting is active, the web UI loads your site's home page (index.mu) for every visitor automatically, instead of the default welcome screen.
site/
├── pages/ ← .mu pages, served at /page/<filename>
│ ├── index.mu ← home page (required for custom landing)
│ ├── about.mu
│ └── subdir/
│ └── page.mu ← reachable at /page/subdir/page.mu
├── files/ ← downloadable files, served at /file/<filename>
│ └── document.pdf
├── lib/ ← pip install target (managed by NomadPortal)
├── data/ ← writable directory for SQLite, app state
└── requirements.txt ← optional, see "Adding Python packages"
Pages are written in Micron markup. Basic example:
#!bg=1a1a2a
#!fg=cccccc
>My NomadNet Node
Welcome to my node on the mesh.
`[About`/page/about.mu] `[Files`/file/]
A page with the execute bit set (chmod +x) runs as a script. The script's stdout serves as the page content. This is useful for dynamic content. Available environment variables:
| Variable | Value |
|---|---|
node_destination |
This node's destination hex hash |
link_id |
Hex ID of the RNS link (unset for local browsing by the node owner) |
remote_identity |
Hex hash of the requesting identity (set if identified, including for local NomadPortal users) |
field_* / var_* |
Form field values submitted with the request |
PYTHONPATH |
/site/lib, so packages from requirements.txt import |
For a deeper walkthrough of env vars, form handling, persistence, and the trust model, see docs/AUTHORING.md.
Drop a site/requirements.txt file in the volume:
requests>=2.32
psycopg2-binary>=2.9
On the next container start, the entrypoint runs
pip install --target /site/lib -r site/requirements.txt. Packages persist
across restarts. The first install is slower, since it downloads the
packages. Later starts skip entries that are already satisfied. An
executable .mu page can import these packages directly, because
PYTHONPATH=/site/lib is exported.
For local persistence in your scripts, use site/data/ and Python's stdlib
sqlite3. No install is needed. For an external database, add the driver to
requirements.txt and connect over the host network.
The node's RNS identity, and so its mesh address, is stored at config/reticulum/site_identity.id and persists across container restarts. The node re-scans the pages and files directories every 5 minutes. Announces are off by default, so a plain install does not broadcast. Set SITE_ANNOUNCE=true to publish to the mesh every 6 hours, or use Admin → Dashboard → "Announce now" for a one-time announce.
| Variable | Default | Description |
|---|---|---|
SITE_NAME |
(auto: NomadPortal-<2 hex> from hash) |
Display name announced/served. Leave empty for the auto-suffixed default so 20 installs on the same network are individually addressable. |
SITE_PAGES_DIR |
/site/pages |
Path to pages directory inside the container |
SITE_FILES_DIR |
/site/files |
Path to files directory inside the container |
SITE_HOSTING |
true |
Set false to disable hosting entirely (pure browser mode — no destination registered, no served pages) |
SITE_ANNOUNCE |
false |
Set true to broadcast the node's existence to the mesh every 6 hours. Off by default to keep readers off the announce stream. The "Announce now" button always works regardless. |
Clicking a /file/ link on a NomadNet page opens a confirm dialog showing the filename, MIME type, and source URL, then transfers the file with byte-level progress reporting. Once the transfer completes, the browser's native save dialog opens.
NomadPortal can scan files through ClamAV before they reach the user. This is off by default. When it is off, the frontend confirms with the user that "no virus scan was performed" before saving. To turn it on, set VIRUS_SCAN=clamd (or required to fail closed) and point at a clamd instance.
| Variable | Default | Description |
|---|---|---|
VIRUS_SCAN |
off |
off | clamd (fail-open if scanner unreachable) | required (fail-closed) |
CLAMD_SOCKET |
/var/run/clamav/clamd.ctl |
Unix socket path. Empty when using TCP. |
CLAMD_HOST |
(unset) | clamd hostname or docker-compose service name (e.g. clamav). Mutually exclusive with CLAMD_SOCKET. |
CLAMD_PORT |
3310 |
clamd TCP port |
VIRUS_SCAN_MAX_BYTES |
104857600 |
Files larger than this pass through without scanning (verdict: too-large). The no-scan confirm still fires |
Two common deployment shapes:
# (A) Sidecar in the same docker-compose stack — recommended.
services:
clamav:
image: clamav/clamav:latest
restart: unless-stopped
volumes:
- clamav-db:/var/lib/clamav
nomadportal:
depends_on: [clamav]
environment:
VIRUS_SCAN: "clamd"
CLAMD_HOST: "clamav"
CLAMD_PORT: "3310"
CLAMD_SOCKET: "" # empty → forces TCP
volumes:
clamav-db:
# (B) Existing host clamd via Unix-socket bind-mount.
services:
nomadportal:
volumes:
- /var/run/clamav/clamd.ctl:/var/run/clamav/clamd.ctl
environment:
VIRUS_SCAN: "clamd"
CLAMD_SOCKET: "/var/run/clamav/clamd.ctl"Docker container
├── entrypoint.sh — generates SSL cert, starts redirect + gunicorn
├── redirect_http.py — HTTP :8080 → HTTPS :8443 redirect
└── Gunicorn (HTTPS :8443)
└── Flask app (1 worker, 8 threads)
├── Reticulum — network stack + node discovery
├── LXMF — per-user messaging routers
└── /config — volume-mounted persistent storage
Run directly without Docker (this needs Reticulum and its dependencies installed):
pip install -r requirements.txt
python app.pyFor a more complete native dev loop with persistent state and venv handling:
./run-local.sh # http://127.0.0.1:8080 with empty config
DOCKER_VOL=/var/lib/docker/volumes/<your-vol>/_data ./run-local.sh # seed from an existing Docker deployment
FRESH_CONFIG=1 ./run-local.sh # wipe and start over
PORT=9000 LOG_LEVEL=INFO ./run-local.sh # custom port / quieter logsCI runs a Docker image build on every push and pull request. See .github/workflows/build.yml. Tests for the Micron rendering library live in the Micron2HTML repository.
/healthz— returns 200 when at least one RNS interface is online, 503 otherwise. The DockerHEALTHCHECKuses this, so a hung-RNS container reportsunhealthyinstead of a false-positivehealthy./api/status— a service-level snapshot (uptime, node count, cache stats). Always returns 200 if gunicorn is up.
- Admin → Cache → "Clear all cached pages" — drops the in-memory
PageCache, so the next fetch re-pulls from the source node. - Admin → Cache → "Reset RNS cache" — moves
config/reticulum/storage/aside to a timestamped backup, triggers a graceful gunicorn worker reload, and reinitializes RNS against an empty state directory. Use this when RNS hangs during startup — a stale, multi-megabytedestination_tableis the usual cause.
The browser URL reflects the page you are viewing, so a refresh preserves state and a standard browser bookmark works for any NomadNet page. The scheme collapses the default node's hash for a cleaner URL and reserves an /n/ prefix for everything else:
| URL | Target |
|---|---|
/ |
default node home |
/page/foo.mu |
default node's page/foo.mu |
/file/x.pdf |
default node's file/x.pdf |
/n/<hash> |
external node's home |
/n/<hash>/page/foo.mu |
external node's page |
/?url=hash%3A//<hash>/... |
legacy entry point, transparently rewritten to the pathname form on first navigation |
Reserved prefixes (not treated as SPA paths): /api, /admin, /auth, /static, /healthz, /robots.txt.
NomadPortal stores all persistent data in ./config/ (volume-mounted into the container):
| Path | Contents |
|---|---|
config/reticulum/ |
Reticulum config, routing tables, identity keys |
config/reticulum/lxmf/ |
LXMF message router storage (per-user) |
config/identities/ |
Named RNS identity keypairs |
config/ssl/ |
Auto-generated TLS certificate |
config/messages.json |
Sent/received LXMF message history |
config/contacts/ |
Per-user contact books (one file per user) |
config/nodes.json |
Discovered NomadNet nodes |
config/blocklist.json |
Admin-managed node blocklist |
config/favorites.json |
Per-user node favorites |
config/users.yml |
User account records |
Back up
config/reticulum/identities/regularly. These files hold your RNS private keys. You cannot recover them if you lose them.
Keep config/ on local disk. Bind-mounting config/ from network storage (a NAS over NFS/SMB/CIFS, remote block storage, or anything else with real write latency) can degrade mesh reachability, and in extreme cases can effectively break it. Symptoms range from occasional link handshake timeouts to a complete inability to reach other nodes, even though the container receives their announces fine.
Why this happens: Several persistent stores (the LXMF peer database, the discovered-nodes registry, message history) update as mesh events arrive. Even after the batching improvements in the 0.9.x line, writes still land on the config directory in the hot path. If those writes take tens or hundreds of milliseconds per operation, which is typical for a network filesystem under real load, Python holds the GIL through the write. This starves every other thread, including the ones that should be sending RNS Link handshakes on the wire.
What to do:
- Use a local SSD, or on a constrained host a local rotational disk, for the
config/bind mount. - If your data-lifecycle policy requires the state to live on shared storage, snapshot from local disk to your NAS on a schedule (nightly rsync, ZFS send, etc.) instead of serving directly from it.
- If you are already on network storage and cannot move it, watch for these signals:
ss -tnpon the host shows a largeRecv-Qon the container's connection to your hub.- The interface's
txbin/api/_debug/stateruns much smaller thanrxb, for example more than 10 to 1. - The logs show frequent "Link establishment timed out" messages, even though the destination just announced.
- Other clients (MeshChat, Sideband) on the same LAN reach the same peers fine.
Volumes for site/ (hosted pages and files) can safely live on network storage. That content is read-only from the server's point of view, and it updates on the operator's schedule, not on every mesh event.
- Change
ADMIN_PASSWORDbefore you expose the service to any network. - Set
FLASK_SECRET_KEYto a stable random value, so sessions survive restarts. - The self-signed certificate triggers browser warnings for direct access. This is expected. Use a reverse proxy with a real certificate for a public deployment.
- CSRF tokens are required on every state-changing request.
- Rate limiting applies to page fetches and message sends.
- Session cookies are always marked
Secure, so the service must be accessed over HTTPS. robots.txtis served automatically, and it blocks search-engine indexing of API and admin paths.- See SECURITY.md for vulnerability reporting and full hardening guidance.
NomadPortal is a single-operator application. The security boundary sits between the operator (you) and the network (everything else):
- Logged-in users can submit forms, send LXMF messages, and, depending on the access mode, browse external nodes. They cannot upload content or change configuration unless they are also admins.
- Admin users can edit configuration, change interfaces, and reach the admin panel. Treat the admin role like root on the host.
- Executable pages in
site/pages/and packages listed insite/requirements.txtare FULLY TRUSTED. They run as the NomadPortal process. Anything in those locations is effectively code you wrote. Do not accept user-uploaded.mupages orpippackages. - External NomadNet nodes are untrusted. Their content is HTML-escaped and rendered through Micron2HTML, which has no JavaScript execution path. Field submissions to external nodes go through
_can_interactaccess-control gating.
NomadPortal acts as a conduit to the NomadNet mesh network. Content fetched from other nodes passes through your server, but you do not create, select, or modify it. The following guidance applies if you expose this service to the public internet.
The defaults are conservative and fit a site-hosting use case:
- Access mode: Gated — an unauthenticated visitor is restricted to your hosted site. The server refuses to fetch pages from other nodes for them. A logged-in user has full access.
- Nodes/Messages sidebar: Logged-in users only — this reduces surface area for guests.
- Address bar: Hidden for guests — a guest cannot enter an arbitrary node address.
The three access modes (Admin → Settings):
| Mode | Guests | Logged-in users | Admins |
|---|---|---|---|
| Public | Browse anywhere | Browse anywhere | Browse anywhere |
| Gated (default) | Restricted to your site | Browse anywhere | Browse anywhere |
| Locked | Restricted to your site | Restricted to your site | Browse anywhere |
Setting the access mode to "Public" means a visitor can browse the full NomadNet network freely. That network is unmoderated. Content from other nodes may be unsuitable, offensive, or illegal in some jurisdictions. A logged-in user sees a content warning dialog before they navigate to an external node.
This is not legal advice. Consult a lawyer in your jurisdiction before you operate a public-facing instance.
- You are a conduit, not a publisher. Under most safe-harbor frameworks (US CDA §230, DMCA §512, EU E-Commerce Directive equivalents), an operator who does not start, select, or modify transmitted content has reduced liability. The key is to not cache content longer than necessary, and to respond to abuse reports promptly.
- Set an abuse contact in Admin → Settings. This gives users a way to report a problem and shows good-faith operation.
- Use the node blocklist (Admin → Settings) to block a node that serves harmful content. Keep a record of your responses to abuse reports.
- Page cache (
CACHE_TTL, default 300 seconds) briefly stores fetched content on your server. A shorter TTL reduces the time cached content stays on your server. ConsiderCACHE_TTL: 60for a public instance. - LXMF message storage — NomadPortal stores sent and received messages in
config/messages.json. Tell your users about this in your terms of service. - Private operation, such as a closed group with an OIDC allowlist, carries far lower risk than open public access.
- Jurisdiction matters. What is legal to transmit in one country may not be legal in another. Know your local laws before you go public.
MIT — see LICENSE.
- NomadNet — the NomadNet node software
- Reticulum — the network stack
- LXMF — the messaging protocol
- Micron2HTML — the Micron → HTML library used by this project
- Ansi2MicronMU — converts ANSI terminal output to Micron. Use it to expose an existing CLI tool as an executable
.mupage on your hosted site:#!/bin/bash git -C /repo log --color=always -n 20 | ansi2micron
