PotatoMesh federates mesh network dashboards across Meshtastic, Meshcore and Reticulum
A federated Meshtastic, Meshcore, and Reticulum node dashboard for your local community. No MQTT clutter, just local LoRa aether.
At a glance
- What is it?
- PotatoMesh is a Ruby Sinatra web dashboard with a Python ingestor that pulls nodes, messages and telemetry from local radio hardware and shares them with other communities by federation. It suits a community mesh operator who wants a map and chat for their neighbourhood without running an MQTT broker, and Reticulum support is still marked experimental.
- Who is it for?
- PotatoMesh fits a community running Meshtastic or Meshcore hardware that wants a shared map, chat and node list without standing up MQTT infrastructure, and that wants its instance to federate with neighbouring communities. It does not fit a Meshcore deployment that has not set both MESHCORE_PRESET and MESHCORE_FREQ, since the Meshcore line stays hidden until then, and Reticulum is announce-only.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Three parts, and what each one does
PotatoMesh is a federated Meshtastic, Meshcore and Reticulum node dashboard for your local community, with the tagline No MQTT clutter, just local LoRa aether.
The architecture is three pieces and the README is clear about the split.
The web app is the dashboard, offering a map plus chat, nodes, positions, neighbours, trace routes, telemetry and messages. It has an API to POST to, authenticated, and to GET nodes, messages and telemetry. New-node and telemetry notifications appear in chat, nodes can be searched and filtered in both the map and the table view, and the instance federates automatically with other communities running PotatoMesh.
The Python ingestor feeds the web app's POST APIs remotely. It supports multiple ingestors per instance, so a community can spread radio hardware across several houses and have each one push into the same dashboard.
The Matrix bridge posts Meshtastic messages to a Matrix channel, and the README notes no radio is required for it.
That third piece is the one that changes how a community uses mesh networking. A Matrix bridge means a radio message can reach people who have never installed a mesh client, which turns a technical tool into something you can put in front of a neighbourhood meeting.
The protocol support is tiered, and the badges say so: Meshtastic and Meshcore are marked supported, while Reticulum is marked experimental and announce-only.
The project is Apache-2.0 licensed, written in JavaScript and Ruby and Python by the primary language of the repository, with the homepage at potatomesh.net hosting a live demo for Berlin.
Running the web app locally
The web app needs Ruby and SQLite3, and the README gives the whole setup as a short sequence.
pacman -S ruby sqlite3
gem install sinatra sqlite3 rackup puma rspec rack-test rufo prometheus-client
cd ./web
bundle installThe gems tell you what the stack is: Sinatra for the application, SQLite3 for storage, rackup and Puma for serving, rspec and rack-test for the tests, rufo for formatting and prometheus-client for metrics.
Then you run it with an API token set.
API_TOKEN="1eb140fd-cab4-40be-b862-41c607762246" ./app.sh
== Sinatra (v4.1.1) has taken the stage on 41447 for development with backup from Puma
Puma starting in single mode...
[...]
* Environment: development
* PID: 188487
* Listening on http://127.0.0.1:41447Open 127.0.0.1:41447 for the dashboard. The API_TOKEN is required to authorize the API's POST endpoints, which is the only thing that has to be set for a development run.
The production invocation sets a few more.
RACK_ENV="production" \
APP_ENV="production" \
API_TOKEN="SuperSecureTokenReally" \
INSTANCE_DOMAIN="https://potatomesh.net" \
MAP_CENTER="53.55,13.42" \
exec ruby app.rb -p 41447 -o 0.0.0.0The README spells out the requirements: set RACK_ENV and APP_ENV to production, bind to a production port and all interfaces with -p 41447 -o 0.0.0.0, set a strong API_TOKEN, set INSTANCE_DOMAIN to the deployment's public URL, and set MAP_CENTER to your region's coordinates.
The configuration table, and the two settings that matter most
The web app is configured with environment variables, and the README publishes the full table with defaults. A few of them decide whether the dashboard behaves sensibly for your community.
INSTANCE_DOMAIN is the public hostname, optionally with a port, used for metadata, federation and generated API links. The note about the scheme is precise: with a scheme such as https://mesh.example.org, generated URLs keep that scheme whatever a proxy forwards. That matters because federation peers need to construct a URL for your instance and a reverse proxy in front of it will otherwise produce links that point at the wrong host.
MAP_CENTER defaults to 38.761944,-27.090833, which is somewhere in the Atlantic, and you set it to your region's coordinates. MAP_ZOOM, when set, is a fixed Leaflet zoom applied on first load and disables auto-fit, which is the setting to reach for when auto-fitting to a spread-out mesh keeps moving the view under you.
MAX_DISTANCE defaults to 42 and is the maximum distance in kilometres before node relationships are hidden on the map, which is how you stop a dashboard from drawing lines across the country to a node that can hear you faintly.
The privacy and federation switches are worth understanding together. FEDERATION defaults to 1 and announces your instance and crawls peers, or 0 disables federation, and private mode overrides it. PRIVATE defaults to 0, and setting it to 1 hides the chat UI, disables message APIs and excludes hidden clients and their data from the API and pages.
The live update stream has its own switch. EVENTS defaults to 1 and setting it to 0 disables the live-update SSE stream at GET /api/events, in which case clients fall back to polling at the refresh interval.
And the thread sizing note is the operational one to read carefully. MIN_THREADS defaults to 16 and MAX_THREADS to 96, and each active /api/events SSE stream pins one thread, so MAX_THREADS needs to be above your peak concurrent SSE clients plus API and ingest headroom.
Ingestor configuration, and the radio connection
The .env.example file is where the radio side is configured, and its comments are the most useful part.
Three settings are required. INSTANCE_DOMAIN, the public domain name for the instance, which has to resolve to the web service. API_TOKEN, the shared secret for ingestor communication, and the file gives a way to generate one with `openssl rand -hex 32`. And CONNECTION, the mesh radio target.
The CONNECTION value is flexible in a way that is worth knowing. It can be a serial path, with the common ones given as /dev/ttyACM0 and /dev/ttyUSB0 on Linux, /dev/cu.usbserial-* on macOS and /dev/ttyS* on Windows through WSL. It can also be an IP:PORT pair such as 192.168.1.20:4403, or a Bluetooth address such as ED:4D:9E:95:CF:60.
PROTOCOL selects between meshtastic, meshcore and reticulum, with meshtastic as the default. The Reticulum section is specific: RETICULUM_CONFIG_DIR points at the RNS configuration directory for the passive announce listener, defaulting to ~/.reticulum which is the stack rnsd already uses, and the file says to point it elsewhere only if the ingestor should read a different RNS config. There is also a setting for which RNS interfaces to ingest from, unset meaning RNode interfaces only, a wildcard meaning every interface including TCP, Backbone and AutoInterface, and otherwise comma-separated case-insensitive substrings of the names in rnstatus. Your own nodes are always ingested.
The passivity is the point. A Reticulum listener that only picks up announcements needs no radio to transmit, which is why the Reticulum path is announce-only rather than a full bidirectional protocol.
Docker and Nix deployment
The repository carries both a compose file and a flake, and the compose file is where the image architecture logic shows up.
The web service image is named from two variables, ghcr.io/l5yth/potato-mesh-web-${POTATOMESH_IMAGE_ARCH:-linux-amd64} with a tag from POTATOMESH_IMAGE_TAG defaulting to latest. The architecture variable existing at all is the giveaway that this is deployed on more than one kind of machine, which for a mesh network project means ARM boxes in people's homes.
The compose file also sets a default set of environment variables with the same pattern, APP_ENV and RACK_ENV defaulting to production, SITE_NAME defaulting to PotatoMesh Demo, and MAP_CENTER defaulting to that same Atlantic coordinate. The interesting detail is in the preset and frequency handling, with a comment that canonical names win while CHANNEL and FREQUENCY stay for existing .env files. That is a deliberate backwards-compatibility path for people who already had a .env from an earlier version, and it is the kind of detail that tells you the project has real users with existing configurations.
The Dockerfile shows the runtime choice. It is a multi-stage build on ruby:3.4-alpine, with BUNDLE_FORCE_RUBY_PLATFORM set so native extensions are built against musl libc rather than using glibc precompiled binaries, which the comment says fail on Alpine. The build installs the Meshtastic decoder dependencies in a dedicated venv with python3 -m venv /opt/meshtastic-venv and pip install meshtastic protobuf, kept separate from the Ruby gem set.
The file also carries a note about itself: it is kept for backward compatibility and the canonical build instructions live in web/Dockerfile, with the two to be kept in sync.
There is a Chromium build toggle, because the app generates a social preview image at /og-image.png. The OG_IMAGE_URL setting lets you replace it with your own absolute URL, and the linux-armv7 web image has no Chromium and serves the fallback image unless you set it.
Where PotatoMesh is the wrong choice
Four cases argue against it.
The first is a Meshcore deployment that has not set both preset and frequency. MESHCORE_PRESET and MESHCORE_FREQ are unset by default and the Meshcore line is hidden until both values are set. That is a reasonable default but it means a Meshcore user gets a dashboard with no Meshcore configuration and has to read the table to find out why.
The second is if you want Reticulum beyond announcements. The support badge says experimental and the ingestor handles it as a passive announce listener, so the Reticulum path is read-only over that protocol.
The third is a community that wants a hosted service rather than a self-hosted one. Everything here assumes you run the web app, the ingestor and the database. The live Berlin demo shows what the result looks like, but the product is the deployment, not the URL.
The fourth is a need for MQTT interoperability. The project positions itself against MQTT with the No MQTT clutter tagline, and it does not expose an MQTT bridge. If your existing mesh setup already publishes to MQTT and other tools consume that, this dashboard is a separate path rather than a reader for what you have.
The alternative in the same direction is the stock Meshtastic map, or one of the other community dashboards. The difference in approach is that PotatoMesh federates instances so two neighbouring communities share nodes and messages rather than each seeing only their own, it includes a Python ingestor so radio hardware can sit in several houses, and it includes the Matrix bridge so non-radio people can be reached. If you have one node in one room and no interest in federation, a single-node map is simpler and needs none of this.
Editorial conclusion
PotatoMesh fits a community running Meshtastic or Meshcore hardware that wants a shared map, chat and node list without standing up MQTT infrastructure, and that wants its instance to federate with neighbouring communities. It does not fit a Meshcore deployment that has not set both MESHCORE_PRESET and MESHCORE_FREQ, since the Meshcore line stays hidden until then, and Reticulum is announce-only. Run it with `API_TOKEN` set and `MAP_CENTER` pointed at your region, and read `MAX_THREADS` carefully, since each live SSE client pins a Puma thread.
Frequently asked questions
What is PotatoMesh?
It is a federated Meshtastic, Meshcore and Reticulum node dashboard for a local community. The Ruby web app provides a map, chat, node and telemetry views, the Python ingestor pulls data from radio hardware, and instances share data with other communities through federation.
How do I run the PotatoMesh web app locally?
Install Ruby and SQLite3, then the gems with `gem install sinatra sqlite3 rackup puma rspec rack-test rufo prometheus-client`, run `bundle install` in the web directory and start it with `API_TOKEN="..." ./app.sh`. The dashboard is then at 127.0.0.1:41447.
What does the API_TOKEN do in PotatoMesh?
It is the shared secret that authorizes ingestors and API clients making POST requests, and it is required. The .env.example suggests generating one with `openssl rand -hex 32`, and the README asks for a strong value in production.
Does PotatoMesh support Reticulum fully?
Not yet. The support badge marks Reticulum as experimental and announce-only, and the ingestor handles it with a passive announce listener, so there is no radio transmission on that path.
How does PotatoMesh handle live updates?
Through a server-sent events stream at GET /api/events, enabled by EVENTS which defaults to 1. Setting EVENTS to 0 disables the stream and clients fall back to polling. Each active SSE stream pins one Puma thread, so MAX_THREADS should be above your peak concurrent SSE clients plus API and ingest headroom.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/l5yth-potato-mesh)Was this useful?