RF Index is a web app for comparing mesh networking and radio hardware (devices and antennas) with specs, pricing, test results, and purchase links. It is built and maintained by the Austin Mesh community.
- Framework: Next.js 15 (App Router) with React 19 and TypeScript
- Styling: Tailwind CSS with shadcn/ui (Radix primitives)
- Charts: Recharts
- Content: Decap CMS (Git-based, local) for editing
data/, validated against JSON Schema with AJV - Deployment: Cloudflare Workers via the OpenNext adapter
- Package manager: pnpm
The site runs entirely within the Cloudflare free tier. Please do not introduce features or dependencies that require paid services.
pnpm install
pnpm dev # generate device + antenna data, then start the dev serverOther useful commands:
pnpm build # production build via OpenNext (also the main verification step)
pnpm preview # build and run the Worker locally (closest to production)
pnpm lint # run ESLint
pnpm validate # check every data JSON file against its schema
pnpm cms # start the Decap CMS local backend (run alongside pnpm dev)There is no unit test suite. A clean pnpm build is the verification step: it
regenerates data, statically renders every device and antenna page, and
type-checks the full render tree.
All content lives in the data/ directory as JSON, plus images:
data/mesh_devices/- device JSON files and imagesdata/mesh_antennas/- antenna JSON files and images
At build time, lib/prebuild.ts reads this JSON, generates the typed arrays the
app imports, and copies images into public/. The generated files are not
committed; they are rebuilt from data/ on every build.
Each collection has a JSON Schema in schemas/. Run pnpm validate to check
every file against its schema; a GitHub Action runs the same check on every PR.
For why the data is compiled to TypeScript at build time rather than read from
data/ directly, see Design notes and tradeoffs.
There are three ways to contribute:
The easiest way to suggest a change is to open an issue using one of the templates:
- Add a device or antenna - suggest new hardware to list
- Remove a device or antenna - flag something that should come off
- Report an issue - incorrect data, a broken link, or a website bug
- Request an update - pricing, specs, or links that need refreshing
- Submit antenna test data - share a VNA sweep (VSWR / return loss) for a listed antenna
The repo includes a local Decap CMS for editing data/
through a web UI instead of hand-editing JSON.
pnpm dev
pnpm cmsThen open http://localhost:3000/admin/index.html. Changes are written straight
to the data/ JSON and images in your working tree, so you commit and open a PR
the same as any other change. The CMS is local-only today: the live /admin URL
exists but cannot authenticate yet.
To add or edit data directly in JSON:
Add a device
- Copy an existing file in
data/mesh_devices/as your starting point. Every field is defined inschemas/mesh_devices.json; theidfield becomes the URL slug (/mesh/devices/<id>). - Add the product image to
data/mesh_devices/images/as WebP, and reference it by bare filename in theimagefield (the same rule as antennas; a full/devices/...path also works, and is what the CMS writes). - The
manufacturerfield and everypurchase_urls[].supplierhold reference-collection slugs, not display names ("manufacturer": "lilygo", not"LilyGo"). For a new brand, add the reference file (withtitleandslug) todata/mesh_manufacturers/ordata/suppliers/in the same PR. - For stores with a referral program, use the referral link format documented
in
AFFILIATES.mdwhen adding purchase URLs.
A minimal complete device, covering every required field:
{
"id": "acme-mesh-node",
"title": "Acme Mesh Node",
"manufacturer": "acme",
"model": "Mesh Node v1",
"description": "A compact ESP32-S3 LoRa node with a 1000 mAh battery.",
"image": "acme-mesh-node.webp",
"category": ["Complete"],
"supported_firmware": ["Meshtastic"],
"features": ["Bluetooth"],
"purchase_urls": [
{ "supplier": "rokland", "url": "https://store.rokland.com/products/acme-mesh-node" }
],
"price": { "min": 39.99, "max": 44.99, "currency": "USD" },
"specifications": {
"lora_frequencies": ["915 MHz"],
"microcontroller": "ESP32-S3",
"lora_radio": "SX1262",
"power_consumption": "Low",
"battery": { "type": "LiPo", "capacity_mAh": 1000 },
"antenna": "External via SMA connector",
"interfaces": ["USB-C", "Bluetooth Low Energy (BLE)"]
}
}If something is off, pnpm validate names the exact file and field that is
wrong, and if you write a display name where a slug belongs it suggests the
slug.
When filling in the features array, reuse the values existing devices
already use (match spelling and casing exactly). The feature list is kept
deliberately short: it powers the filter checkboxes on the devices page, so
every new value lengthens and fragments that list. Only introduce a new
feature when it is genuinely needed, applies to more than one device, and does
not duplicate an existing value. Adding one should be a careful, reviewed
decision, not a quick addition. Put the LoRa radio in
specifications.lora_radio, not in features, and note that Solar (a
built-in panel) and Solar Input (you can connect one) are different.
Add an antenna
- Add a JSON file to
data/mesh_antennas/named after itsslugfield. Every field is defined inschemas/mesh_antennas.json. - Add the antenna image to
data/mesh_antennas/images/as WebP, and reference it by bare filename in theimagefield. - To include VSWR / return-loss test data, capture a Touchstone
.s1psweep with a VNA and add it underdata/mesh_antennas/touchstone/. Seedata/mesh_antennas/touchstone/README.mdfor how to capture a sweep (microSD or NanoVNASaver) and name the file.
Before you open the PR
- For data changes, run
pnpm validateto check your JSON against the schemas. - Run
pnpm buildlocally and confirm it completes without errors. - Leave any existing purchase or affiliate URL parameters intact. These fund the site's hosting; do not strip or alter them.
- Do not commit secrets, tokens, or
.envfiles.
By opening a pull request you agree that your contribution is licensed under the same terms as the project (inbound = outbound): code under the PolyForm Noncommercial License 1.0.0 and data under CC BY-NC-SA 4.0. See License below.
However you contribute, new devices and antennas appear automatically in listings, detail pages, and the sitemap after the next build.
A few architectural choices are deliberate. They are recorded here so they are not "simplified" away without weighing the cost.
lib/prebuild.ts does two jobs on every build: it transforms the per-file JSON
in data/ into generated TypeScript arrays (data/devices-generated.ts,
data/antennas-generated.ts), and it copies images from data/.../images/ into
public/.
The generated arrays are not just a convenience. The client filter components
(device-filters.tsx, antenna-filters.tsx) import derived constants
(allFeatures, allLoraFrequencies, and so on) from lib/data.ts, which means
lib/data.ts is bundled for the browser. A generated array is a pure data
literal, so lib/data.ts can compute those constants without importing fs or
path. If lib/data.ts read the JSON directly with fs, the client build would
fail with Can't resolve 'fs'. The codegen is what keeps the data importable
from both server and client.
Images are copied into public/ because data/ is not a served directory on
Cloudflare Workers (static assets are served from public/). Keeping the source
images under data/.../images/, next to their JSON, keeps the licensed data (see
data/LICENSE.md) as one self-contained unit and matches where
the CMS writes uploads.
Accepted tradeoff: the cost is a prebuild step, two gitignored generated
files, and a tsx lib/prebuild.ts && prefix on pnpm dev. In exchange we get a
clean client/server data boundary and data plus images colocated under data/.
Possible future simplification: the prebuild could be removed entirely by
(1) pointing the CMS media_folder at public/, (2) making lib/data.ts
server-only and reading the JSON directly, and (3) passing the derived constants
into the filter components as props instead of importing them. This is a real
refactor, not a deletion: it moves images out of data/ (which fragments the
colocated, separately licensed data and means hand-PR contributors touch two
trees) and reworks the client boundary. The half-measure, dropping the codegen
but keeping images in data/, is the worst of both: it still needs the script
for image staging and still requires the client refactor. If this is ever done,
do it all at once, not halfway.
pnpm validate checks every data file against its JSON Schema. It runs in GitHub
Actions on every PR that touches data/, schemas/, or the validator. There is
intentionally no pre-commit hook. CI is the authoritative gate because it covers
every contribution path, including edits made through the GitHub web UI, runs
server-side, and cannot be skipped or forgotten. A pre-commit hook would only
help contributors editing locally with the hook installed, and is bypassable with
--no-verify, so it would add a dependency without being a real guarantee.
RF Index is dual licensed because the code and the data have different terms. Both are noncommercial: you may not sell, or charge for the use of, the code or the data.
- Code is licensed under the PolyForm Noncommercial License 1.0.0. You may use, modify, and share the source for any noncommercial purpose.
- Data (everything under
data/, including specs, pricing, test results, and images) is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0.
For commercial licensing, contact the maintainers.