A central, community-maintained directory of amateur radio and GMRS nets.
Companion project to Netcontrol-Online.
Net Repository is a lightweight FastAPI service that maintains a curated public database of amateur radio and GMRS nets — their frequencies, schedules, modes, and geographic coverage. It provides a public read API that any tool (including Netcontrol-Online) can query to discover known nets, and a submission API that allows trusted instances to contribute net listings for moderation.
This is the missing piece from Netcontrol-Online's issue #8: a community-run, API-accessible net directory that other tools can publish to and consume.
- Public directory — browse and search published nets with no login required
- Manual submissions — anyone can add their net via a public form
(
/nets/new), no Netcontrol-Online integration or API key required; enters the same moderation queue as an API submission - Submission API — Netcontrol-Online (or any trusted tool) can submit nets via a long-lived API key; submissions enter a moderation queue before publish
- Verified Netcontrol-Online badge — an API submitter can claim its net runs on Netcontrol-Online, but the badge only shows once an admin verifies the claim
- Admin moderation — approve, reject, or edit submissions; retire stale entries
- Duplicate prevention — submissions from the same source instance + net ID update the existing net instead of creating a duplicate
- Net session stats — a source can log a session (e.g. when a net closes)
via
POST /nets/stats; the directory shows each net's session count, average check-ins, and last-session date - API key management — admins create/revoke
nr_-prefixed keys for submitters; keys are stored as SHA-256 hashes, shown raw only at creation - Self-service key requests — prospective submitters request a key via
POST /keys/request, an admin approves it through a browser dashboard, and the requester automatically retrieves the issued key by polling — no manual key-copying required - Admin accounts — named username/password logins (bcrypt-hashed) protect
the browser admin UI, separate from the
ADMIN_TOKENused by the JSON API - SEO — every published net has its own crawlable page (
/nets/{id}), withrobots.txt, a dynamicsitemap.xml, canonical URLs, meta descriptions, and Open Graph/Twitter Card tags throughout - Same tech stack as Netcontrol-Online — FastAPI + SQLAlchemy + PostgreSQL, same deployment patterns (systemd + Apache), so operators of both projects share familiar tooling
- Backend: Python 3.11+, FastAPI, SQLAlchemy (sync), PostgreSQL
- Auth: Admin Bearer token for the JSON API (env var); session-cookie login (bcrypt-hashed passwords) for the browser admin UI; submitter API keys (SHA-256 hashed)
- Deployment: systemd + Apache reverse proxy + Let's Encrypt
git clone https://github.com/LadyHwesta/Net-Repository.git
cd Net-Repositorypython3 -m venv venv
source venv/bin/activate
pip install -r requirements.txtsudo -u postgres psqlCREATE USER netrepo WITH PASSWORD 'yourpassword';
CREATE DATABASE netrepo OWNER netrepo;
\qcp .env.example .env
nano .envSet DATABASE_URL, ADMIN_TOKEN, SESSION_SECRET_KEY, and PORT. Keep
SESSION_COOKIE_SECURE=true in production (behind TLS); set it to false
only for local HTTP development. Also set APP_BASE_URL (e.g.
https://nets.example.com) — it's used for canonical URLs, sitemap.xml,
and Open Graph tags; without it they fall back to the request's own URL,
which can report http:// instead of https:// behind a reverse proxy.
python3 -c "from database import init_db; init_db()"python3 create_admin.py --username YOUR_CALLSIGNSee Admin Accounts below.
Development:
uvicorn main:app --host 127.0.0.1 --port 8001 --reloadProduction: see Deployment below.
Fresh installs create the full schema automatically via init_db().
For existing installs, run migrations manually or let deploy.sh handle it:
python3 migrate.pymigrate.py is the single source of truth for schema changes. Every statement
is idempotent and safe to re-run. When adding a column to models.py, add the
corresponding ALTER TABLE … ADD COLUMN IF NOT EXISTS to MIGRATIONS in
migrate.py.
New tables (like admin_users or api_key_requests) don't need a migration
entry — init_db() creates any missing table automatically on startup.
The browser admin UI at /admin is protected by named username/password
accounts (separate from the ADMIN_TOKEN used by the JSON /admin/* API,
which keeps working unchanged for scripts and the Swagger UI).
Create or reset an account:
python3 create_admin.py --username YOUR_CALLSIGNYou'll be prompted for a password (not echoed). Running it again for an existing username updates that account's password — this is also how you reset a forgotten password.
Log in at /admin/login. Sessions last 12 hours and are signed with
SESSION_SECRET_KEY — keep that value private and stable across restarts
(if unset, a random key is generated at startup, which logs everyone out on
every restart).
Interactive docs are available at /docs (Swagger UI) when the service is running.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Health check / service info |
GET |
/nets |
List/search published nets |
GET |
/nets/{id} |
Get a single net by ID |
GET |
/directory |
Alias for GET /nets |
GET |
/robots.txt |
Crawler rules |
GET |
/sitemap.xml |
Sitemap of /, /nets, and every published net |
GET |
/nets/new |
Manual net submission form |
POST |
/nets/new |
Submit the manual form |
A browser hitting /, /nets, or /nets/{id} gets a server-rendered HTML
page (with its own <title>, meta description, canonical URL, and Open
Graph/Twitter Card tags) instead of raw JSON — same content-negotiation
pattern as the rest of the site: send Accept: application/json for the API
response instead.
Query parameters for GET /nets:
| Parameter | Type | Description |
|---|---|---|
q |
string | Full-text search on name, description, region |
net_type |
ham | gmrs |
Filter by type |
state |
string | Filter by US state |
band |
string | Filter by band (e.g. 2m, 70cm) |
mode |
string | Filter by mode (e.g. FM, DMR) |
is_ares |
bool | Filter ARES/ACES nets |
skip |
int | Pagination offset (default 0) |
limit |
int | Page size, 1–200 (default 50) |
GET /nets/new shows a public form for anyone to add their net — no
Netcontrol-Online integration or API key needed. POST /nets/new accepts
the standard form submission (name, type, frequency, up to 3 recurring
schedule slots, etc.) and enters the same moderation queue as an API
submission; once approved it publishes with source: "manual". There's no
way to set claims_netcontrol_online through this form — that's API-only
(see below).
POST /nets/submit
Authorization: Bearer nr_<your-key>
Content-Type: application/json
Request body:
{
"name": "Monday Night Net",
"net_type": "ham",
"frequency": "146.520 MHz",
"band": "2m",
"mode": "FM",
"ctcss_tone": "100.0",
"description": "Weekly check-in net for the valley.",
"region": "Snohomish County",
"state": "WA",
"country": "US",
"website": "https://example.club",
"contact_callsign": "W7XYZ",
"is_ares": false,
"submitted_by_callsign": "W7XYZ",
"source_net_id": 12,
"claims_netcontrol_online": true,
"schedules": [
{ "day_of_week": 0, "start_time": "19:30", "timezone": "America/Los_Angeles" }
]
}source_net_id is the net's ID in the submitting system, used to match this
submission against anything already sent from the same source. Returns
202 Accepted with a submission_id for tracking.
claims_netcontrol_online is optional (default false) and only meaningful
here — the public manual form (below) has no way to set it. A true claim
doesn't show a badge by itself: an admin has to verify it first (see
Admin Endpoints — JSON API).
Re-submitting the same source_net_id (e.g. after editing a net's
schedule or frequency) behaves differently depending on where it is:
- If that net is already published here, the update is applied directly — no new moderation, since the API key already proves the source's identity and this exact net was approved once already.
- If it's still pending review, the queued submission's payload is updated in place rather than creating a second one to review.
- Otherwise, it's a normal new submission that enters the moderation queue.
Log a session against a net this source already has published here — e.g. call this when the net closes:
POST /nets/stats
Authorization: Bearer nr_<your-key>
Content-Type: application/json
{
"source_net_id": 12,
"checkin_count": 9,
"session_date": "2026-08-18T19:30:00Z"
}
The net is matched the same way as /nets/submit — by this API key's
instance_url + source_net_id — and must already be published (404 if
not found). No moderation needed; returns 201 with the logged session's
id. The directory's GET /nets response includes each net's rolled-up
session_count, avg_checkins, and last_session_at, and the public
directory page displays them on each net's card.
Instead of an admin manually creating a key via POST /admin/keys, a
prospective submitter can request one and retrieve it automatically once
approved:
POST /keys/request
Content-Type: application/json
{
"name": "My Netcontrol-Online Instance",
"contact_callsign": "W7XYZ",
"instance_url": "https://mytracker.example.com",
"request_notes": "Would like to publish nets from our club."
}
Returns 201 with a claim_token — shown once, store it. Poll for status
(and, once approved, the issued key) with:
GET /keys/request/status
Authorization: Bearer <claim_token>
Response status is one of pending, rejected, or claimed. The first
poll after an admin approves the request transitions it to claimed and
includes the raw api_key (nr_...) in that one response only — subsequent
polls report claimed with no key. If the key is lost in transit, submit a
new request.
All admin JSON endpoints require Authorization: Bearer <ADMIN_TOKEN>.
| Method | Path | Description |
|---|---|---|
GET |
/admin/submissions |
List submissions (?status=pending) |
GET |
/admin/submissions/{id} |
Get a single submission |
POST |
/admin/submissions/{id}/approve |
Approve → publish net |
POST |
/admin/submissions/{id}/reject |
Reject submission |
PATCH |
/admin/nets/{id} |
Edit a published net |
DELETE |
/admin/nets/{id} |
Retire a net (soft-delete) |
GET |
/admin/nets/{id}/sessions |
List a net's raw logged sessions |
GET |
/admin/nets/netcontrol-online-claims |
Nets claiming Netcontrol-Online, unverified |
POST |
/admin/nets/{id}/verify-netcontrol-online |
Verify the claim (shows the badge) |
POST |
/admin/nets/{id}/dismiss-netcontrol-online-claim |
Reject the claim |
GET |
/admin/keys |
List API keys |
POST |
/admin/keys |
Create a new API key directly |
DELETE |
/admin/keys/{id} |
Revoke an API key |
GET |
/admin/key-requests |
List key requests (?status=pending) |
GET |
/admin/key-requests/{id} |
Get a single key request |
POST |
/admin/key-requests/{id}/approve |
Approve a key request |
POST |
/admin/key-requests/{id}/reject |
Reject a key request |
Session-cookie login, separate from ADMIN_TOKEN — see
Admin Accounts.
| Method | Path | Description |
|---|---|---|
GET |
/admin/login |
Login form |
POST |
/admin/login |
Authenticate |
POST |
/admin/logout |
Clear session |
GET |
/admin |
Dashboard: submissions, Netcontrol-Online claims, key requests, API keys |
POST |
/admin/ui/submissions/{id}/approve |
Approve a submission (from dashboard) |
POST |
/admin/ui/submissions/{id}/reject |
Reject a submission (from dashboard) |
POST |
/admin/ui/nets/{id}/verify-netcontrol-online |
Verify a claim (from dashboard) |
POST |
/admin/ui/nets/{id}/dismiss-netcontrol-online-claim |
Reject a claim (from dashboard) |
POST |
/admin/ui/key-requests/{id}/approve |
Approve a key request (from dashboard) |
POST |
/admin/ui/key-requests/{id}/reject |
Reject a key request (from dashboard) |
POST |
/admin/ui/keys/{id}/revoke |
Revoke an API key (from dashboard) |
Net Repository is designed to work alongside Netcontrol-Online. The integration has two directions:
Netcontrol-Online → Net Repository (publish):
When a net owner checks "List in Public Net Directory" in Netcontrol-Online, the
app can optionally submit that net to Net Repository via POST /nets/submit.
This requires a Net Repository API key configured in Netcontrol-Online's .env.
Net Repository → Netcontrol-Online (discover):
When creating a new net, Netcontrol-Online can query GET /nets to let the
operator find and import an existing listing rather than typing everything from
scratch.
Both integration hooks are tracked in Netcontrol-Online issue #8.
Create /etc/systemd/system/netrepo.service:
[Unit]
Description=Net Repository
After=network.target postgresql.service
[Service]
Type=simple
User=netrepo
WorkingDirectory=/opt/net-repository
EnvironmentFile=/opt/net-repository/.env
ExecStart=/opt/net-repository/venv/bin/uvicorn main:app --host 127.0.0.1 --port ${PORT}
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=netrepo
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload
sudo systemctl enable netrepo
sudo systemctl start netrepoSee apache/net-repository.example.conf for a ready-to-use vhost with HTTPS.
sudo cp apache/net-repository.example.conf /etc/apache2/sites-available/net-repository.conf
# Edit ServerName, then:
sudo a2enmod proxy proxy_http headers rewrite
sudo a2ensite net-repository
sudo systemctl reload apache2
sudo certbot --apache -d nets.example.comsudo -u netrepo bash deploy.shdeploy.sh pulls the latest code, installs dependencies, runs migrations, and
restarts the systemd service.
Tests run against an in-memory SQLite database — no PostgreSQL required.
pip install -r requirements-dev.txt
python -m pytest tests/Pull requests are welcome. For major changes please open an issue first.
- Fork the repository
- Create a feature branch (
git checkout -b feature/my-feature) - Commit your changes
- Open a pull request
GNU General Public License v3.0 — see LICENSE for the full text.
Built for the ham radio community as a companion to Netcontrol-Online.