Off-grid, local-first messaging.
Run it on a laptop, a Raspberry Pi, or your phone. Everyone nearby scans a QR code and starts talking — no internet, no accounts, no cloud.
loamnet.com · Quick start · Docs
LOAM is a messaging system that works when the internet doesn't. One person becomes the host — they run LOAM on any device and turn it into a small WiFi hotspot. Everyone nearby scans a QR code, the messaging app opens in their browser, and the group can post to channels, reply in threads, send direct messages, react, and — if the host enables it — chat with a local AI, all over the local network. Nothing leaves the immediate area and nothing touches a server you don't control.
It's built for the moments when normal networks are unavailable, unreliable, or unsafe: emergencies and outages, large events and remote sites, and communities that simply want to talk without an account, a data plan, or anyone in the middle.
LOAM's priorities, in order:
- Simplicity — no accounts, no installs, no setup. Scan and go.
- Privacy — identities are anonymous and ephemeral by default.
- Resilience — designed for very low bandwidth and intermittent connectivity.
It is transport-agnostic: WiFi today, with low-bandwidth radio relay (LoRa) a stated design goal, so a message can eventually hop device-to-device across a wider area with the same experience.
- 📡 Off-grid by design — a local hotspot is the whole network; no internet required at any point.
- 📱 Nothing to install — joiners open a link (or scan a QR); the host can run it from a laptop, Pi, or an Android phone.
- 🕶️ Anonymous & ephemeral — every joiner gets a deterministic, memorable display name and avatar derived from a random id. No email, no phone number.
- 💬 Real messaging — public channels, threaded replies, direct messages, and reactions.
- 🤖 Optional local AI — point it at a local Ollama model and a bot appears as a DM contact; replies stream in. Entirely local, entirely optional.
- 🔌 Works offline — the client is an installable PWA that keeps working against its local cache when the connection drops.
- 🌍 Minimal by design — an intentionally sparse interface that stays out of the way and renders text in any language.
- 🌗 Light & dark — the client follows your system theme automatically.
- 🔒 Optional encryption at rest + a kill switch — the host can encrypt the on-disk database and wipe everything in one action (see Security).
You'll need Node 24.13.1 and pnpm 10 (corepack enable will
set pnpm up for you).
pnpm install
pnpm devpnpm dev starts the server and client together and prints a join QR code plus a LAN URL to your
terminal. On any device connected to the same network, scan the QR or open the URL — that's the whole
join flow. (Under the hood the client runs on :3000 and proxies the API to the server on :3001.)
In production LOAM is a single origin: the client is built to static files and served by the server with an SPA fallback.
pnpm build
pnpm --filter @loam/server start # serves the built client + API, defaults to PORT 3000apps/app is an Expo/React-Native host that runs the LOAM server embedded on the phone, brings
up a local-only WiFi hotspot, and shows the join QR — turning a single phone into a complete,
internet-free LOAM node. See docs/04-android-host-app.md for
building and running it.
Host device (laptop / Pi / phone) Nearby people
┌───────────────────────────────────┐
│ LOAM server (Fastify) │ 📱 scan QR ─┐
│ • REST + WebSocket │ 📱 scan QR ─┤
│ • local database (SQLite) │◀── WiFi ─── ├─▶ open the PWA
│ • serves the PWA client │ 📱 scan QR ─┘ in a browser
│ • optional local LLM (Ollama) │
└───────────────────────────────────┘
no internet · no cloud · no accounts
The host runs a small Fastify server that stores everything locally and serves a Preact PWA. Joiners never install anything — they load the app in a browser and talk to the host over the local network via REST and a WebSocket. The client and server share a single Zod schema package as their contract and both validate every message against it, so the wire format can't drift.
A pnpm workspace (apps/*, packages/*).
| Path | What it is |
|---|---|
apps/server |
Fastify backend: REST + WebSocket, SQLite persistence (optionally encrypted), optional Ollama LLM. |
apps/client |
The Preact + Vite PWA everyone connects to. |
apps/app |
Expo/React-Native Android host — runs the server on a phone and brings up a hotspot. |
apps/site |
The loamnet.com landing site (static Vite build). |
packages/schema |
The client↔server contract: shared Zod schemas + inferred types. |
packages/display-name |
Deterministic anonymous display names from an id. |
packages/avatar |
Deterministic SVG avatars from an id. |
packages/qr |
Dependency-free QR encoder + renderers used by the join flow. |
LOAM runs with no config file at all. Optional identity and LLM features are enabled by creating
.loam/config.json (or pointing LOAM_CONFIG_FILE at another JSON path). See
config.example.json for the full set. A minimal example, assuming
Ollama is running locally:
{
"identity": {
"allowUserDisplayNameEdit": true,
"allowUserAvatarEdit": true,
"allowUserAvatarUpload": true
},
"llm": {
"ollama": {
"enabled": true,
"baseUrl": "http://localhost:11434",
"model": "gemma4",
"botId": "llm.ollama.gemma4",
"botDisplayName": "Gemma"
}
}
}When the LLM is enabled the bot appears as a DM contact and its replies stream into the conversation.
With allowUserAvatarUpload on, users can pick an image, crop it locally in the browser, and
upload only the final 256×256 avatar — the original file never leaves the device. If the config is
absent, none of these features are active.
LOAM can encrypt its on-disk database so that a lost or seized host device doesn't readily give
up stored messages. It's off by default and controlled by the LOAM_DB_KEY environment variable:
- unset — no encryption; the database is a plain SQLite file.
- a passphrase — encrypted at rest with SQLCipher (AES-256); the same passphrase is required on every start.
ephemeral— a random key is generated in memory and never written to disk. Data is readable only while the process runs; a reboot loses the key forever, and the kill switch rotates to a fresh key so anything still physically on flash becomes unreadable.
LOAM_DB_KEY=ephemeral pnpm --filter @loam/server startHonest limitations. This raises the bar; it is not a guarantee of safety. The host processes
messages in plaintext while running, so a compromised host, a device seized while powered on with the
key in memory, or coercion of a known passphrase can still expose data. Anyone whose safety depends
on this should seek a professional security review and not treat LOAM as sufficient on its own. See
SECURITY.md and the docs for the full threat model.
A note on intent. These protections exist to protect ordinary people — activists, journalists, people organising where that is dangerous, communities cut off from the internet. They are deliberately not marketed as a way to hide wrongdoing, and LOAM's other design choices (a trusted host who can read and wipe everything, admin moderation) reflect that.
Design notes, threat models, and initiative briefings live in docs/:
- Roadmap & how the initiatives interlock
- SQLite migration · Kill switch · Admin UI
- Android host app · Authentication · LLM
- Transport security · Security profiles
CLAUDE.md— architecture baseline for contributors (and AI agents).
pnpm install # install workspace deps
pnpm dev # run server + client, print the join QR
pnpm build # build every package, then server and client (also type-checks via tsc)
pnpm test # run the workspace test suiteCI runs pnpm build then pnpm test on every push and PR to master. There's no separate lint or
typecheck script — type-checking happens as part of pnpm build. If you're new to the codebase,
CLAUDE.md is the fastest way in, and a server or client test harness is a
high-value first contribution.
LOAM is licensed under the GNU Affero General Public License v3.0. Copyright © Magic Zebra Ltd. The AGPL's network-use clause means that if you run a modified LOAM as a service, you must offer your users the corresponding source.