A Next.js dashboard for visualizing and managing LoRaWAN GPS pings, managing user access, and importing field data from remote sources.
It combines PostgreSQL-backed storage, role-based permissions, local login, and optional Keycloak (OIDC) authentication for a robust and secure data management experience.
- Features
- Requirements
- Tech Stack
- Quick Start
- Docker Compose (Published Image)
- Environment Variables
- Keycloak Configuration
- License
- Interactive Maps: Displays LoRaWAN pings on a map (markers, heatmap, hexagons) using Leaflet.
- Role-Based Access Control: Supports dynamic roles (
admin,user,guest). - Flexible Authentication: Offers local username/password login and optional Keycloak (OIDC) sign-in.
- Data Ingestion: Ingests data via MQTT from ChirpStack and/or TTN (The Things Network), with optional remote log polling as fallback.
- Granular Permissions: Restricts non-admin users to view only their assigned boards.
- Node.js: 20+
- npm: 10+
- PostgreSQL: 14+ (or compatible)
- Framework: Next.js 16 (React 19)
- Language: TypeScript
- Database: PostgreSQL (
pg) - Authentication: NextAuth (
next-auth) with Keycloak provider - Mapping: Leaflet & React Leaflet
Clone this repository and install the project dependencies:
git clone https://github.com/Joshua154/lorawan-website.git
cd lorawan-website
npm installCreate your local environment configuration:
cp example.env.local .env.localRequired Variables:
DATABASE_URL: Connection string for your PostgreSQL instance.AUTH_SECRET: A secure random string for NextAuth.LORAWAN_LOG_URL: The Url of the Log File
Optional Keycloak Variables:
KEYCLOAK_ID,KEYCLOAK_SECRET,KEYCLOAK_ISSUER
Ensure you point DATABASE_URL to a reachable PostgreSQL instance. You can use Docker to spin one up quickly.
Example .env.local snippet:
DATABASE_URL=postgresql://lorawan:lorawan@postgres:5432/lorawanstart running a Postgres Instance e.g.:
docker compose up -d postgresStart the development server:
npm run devOpen http://localhost:3000 in your browser.
On startup, pending database migrations in src/server/migrations/ will be applied automatically.
If you want to run the app from the published container image (instead of building locally), use a dedicated compose file like this:
services:
postgres:
image: postgres:16-alpine
container_name: lorawan-postgres
restart: unless-stopped
environment:
POSTGRES_DB: lorawan
POSTGRES_USER: lorawan
POSTGRES_PASSWORD: lorawan
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lorawan -d lorawan"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
lorawan-dashboard:
image: ghcr.io/joshua154/lorawan-website:latest
build:
context: .
dockerfile: Dockerfile
args:
NEXT_PUBLIC_BASE_PATH: ${NEXT_PUBLIC_BASE_PATH}
container_name: lorawan-dashboard
restart: unless-stopped
pull_policy: always
depends_on:
postgres:
condition: service_healthy
ports:
- "3000:3000"
env_file:
- .env.local
volumes:
postgres_data:Save it as docker-compose.published.yml, then run:
cp example.env.local .env.local
printf "NEXT_PUBLIC_BASE_PATH=/lora-scanner\n" > .env
docker compose -f docker-compose.published.yml pull
docker compose -f docker-compose.published.yml up -dFor immutable deploys, you can replace :latest with a specific commit image tag (the workflow also publishes SHA tags).
| Variable | Required | Description |
|---|---|---|
DATABASE_URL |
Yes | PostgreSQL connection string |
AUTH_SECRET |
Yes | Secret used by NextAuth |
KEYCLOAK_ID |
Keycloak only | Keycloak client ID |
KEYCLOAK_SECRET |
Keycloak only | Keycloak client secret |
KEYCLOAK_ISSUER |
Keycloak only | Keycloak realm issuer URL |
KEYCLOAK_ADMIN_ROLE |
Optional | Keycloak role name required to grant admin rights (defaults to lorawan-admin) |
LORAWAN_ADMIN_USERNAME |
No | Initial admin username when no admin exists |
LORAWAN_ADMIN_PASSWORD |
No | Initial admin password when no admin exists |
APP_URL |
Recommended | Trusted origin for origin validation |
NEXTAUTH_URL |
Recommended | Canonical public origin used by NextAuth/Auth.js (without /api/auth path) |
AUTH_TRUST_HOST |
Recommended (reverse proxy) | Trust forwarded host/proto headers |
NEXT_PUBLIC_APP_URL |
Optional | Fallback trusted origin if APP_URL is missing |
NEXT_PUBLIC_BASE_PATH |
Optional | Base path for the application (e.g. /lorawan) if hosted under a sub-path |
LORAWAN_LOG_URL |
No | Remote log file URL (legacy/fallback ingestion) |
MQTT_BROKER |
No | ChirpStack MQTT broker hostname |
MQTT_PORT |
No | ChirpStack MQTT broker port (default: 8883) |
MQTT_USERNAME |
No | ChirpStack MQTT username |
MQTT_PASSWORD |
No | ChirpStack MQTT password |
MQTT_TOPIC |
No | ChirpStack MQTT topic (default: application/+/device/+/event/up) |
TTN_MQTT_BROKER |
No | TTN MQTT broker hostname (e.g. eu1.cloud.thethings.network) |
TTN_MQTT_PORT |
No | TTN MQTT broker port (default: 8883) |
TTN_MQTT_USERNAME |
No | TTN MQTT username ({application-id}@ttn) |
TTN_MQTT_PASSWORD |
No | TTN MQTT password (API key from TTN Console) |
TTN_MQTT_TOPIC |
No | TTN MQTT topic (e.g. v3/{application-id}@ttn/devices/+/up) |
RELEASE_TIMESTAMP |
No | Build/release timestamp metadata |
If you need to host the application under a sub-path instead of the domain root (e.g., https://example.com/lorawan), you can use the NEXT_PUBLIC_BASE_PATH environment variable.
- Set
NEXT_PUBLIC_BASE_PATH=/lorawanin your.env.local(runtime) and in.env(build-time variable for Docker Compose substitution). - The Next.js application will prefix all internal routes (both frontend and API) with this path.
- Keep in mind that when running behind a reverse proxy (like Nginx or Traefik), you'll need to route requests starting with this base path to the Next.js container without stripping the
/lorawanprefix.
For Docker image builds, pass the value as a build arg so Next.js can read it during npm run build:
docker build --build-arg NEXT_PUBLIC_BASE_PATH=/lorawan -t your-image:tag .The dashboard supports two independent MQTT connections for receiving LoRaWAN pings in real-time. Both are optional and can run simultaneously.
Set the MQTT_BROKER, MQTT_USERNAME, and MQTT_PASSWORD variables to connect to a ChirpStack MQTT broker. Pings received this way are stored with network: "chirpstack".
MQTT_BROKER=your-chirpstack-host.example.com
MQTT_PORT=8883
MQTT_USERNAME=your-username
MQTT_PASSWORD=your-password
MQTT_TOPIC=application/your-app-id/#To receive pings from TTN v3, configure the TTN_MQTT_* variables. Pings are stored with network: "ttn".
- In the TTN Console, go to your Application > API Keys and create a key with "Read application traffic" permission.
- Add the following to
.env.local:
TTN_MQTT_BROKER=eu1.cloud.thethings.network
TTN_MQTT_PORT=8883
TTN_MQTT_USERNAME={application-id}@ttn
TTN_MQTT_PASSWORD=NNSXS.your-api-key
TTN_MQTT_TOPIC=v3/{application-id}@ttn/devices/+/upReplace {application-id} with your actual TTN application ID and use eu1 (or us1, au1, etc.) matching your TTN cluster.
Each MQTT connection automatically tags pings with the correct network ("ttn" or "chirpstack"). No payload formatter changes are needed on the device side. The dashboard UI allows filtering by network.
This app uses NextAuth with the Keycloak provider from src/server/next-auth.ts.
In Keycloak Admin Console:
- Create or choose a realm.
- Create a client (OIDC).
- Set
Client IDto matchKEYCLOAK_ID. - Set
Client authenticationto enabled (confidential client) so a client secret is issued.
Use your app base URL from APP_URL/NEXTAUTH_URL.
- Development redirect URI:
http://localhost:3000/api/auth/callback/keycloak - Production redirect URI:
https://your-domain/api/auth/callback/keycloak
If you deploy under a base path (for example /lora-scanner), include it in the callback path:
https://your-domain/lora-scanner/api/auth/callback/keycloak
Recommended Keycloak client values:
- Valid redirect URIs:
http://localhost:3000/api/auth/callback/keycloakhttps://your-domain/api/auth/callback/keycloakhttps://your-domain/lora-scanner/api/auth/callback/keycloak
- Web origins:
http://localhost:3000https://your-domain
Populate these in .env.local:
KEYCLOAK_ID=your-client-id
KEYCLOAK_SECRET=your-client-secret
KEYCLOAK_ISSUER=https://keycloak.example.com/realms/your-realm
NEXTAUTH_URL=https://your-domain
APP_URL=https://your-domain
NEXT_PUBLIC_BASE_PATH=/lora-scanner
AUTH_SECRET=replace-with-strong-random-secretWith this setup, the app computes the OAuth redirect proxy URL as:
APP_URL + NEXT_PUBLIC_BASE_PATH + /api/auth
Example:
https://your-domain/lora-scanner/api/auth
Issuer format must be the realm issuer URL (not just the Keycloak root). e.g.:
https://localhost:8080/realms/master
- Start the app using
npm run dev. - Open the login page.
- Click "Sign in with Keycloak".
- After successful authentication, you will be redirected to the dashboard (
/).
To grant a Keycloak user admin privileges in the dashboard, you need to assign them the specific admin role. By default, this role is lorawan-admin, but you can customize it by setting the KEYCLOAK_ADMIN_ROLE environment variable.
- In the Keycloak Admin Console, navigate to your Realm -> Clients.
- Select your client (the one matching
KEYCLOAK_ID). - Go to the Roles tab and click Create Role.
- Name the role exactly
lorawan-admin(or match yourKEYCLOAK_ADMIN_ROLEsetting) and save. - Go to Users, select the user you want to make an admin, and navigate to the Role mapping tab.
- Click Assign role, filter by clients, and assign the
lorawan-adminrole (or your custom role) to the user. - Note: Ensure your client scopes are configured to map client roles into the user profile/token so the application can read them during login.
- Endpoint:
/api/auth/login - Creates a server-managed session cookie
- Best for internal users managed from the admin panel
- Configured in
src/server/next-auth.ts - Callback URL pattern:
<NEXTAUTH_URL>/api/auth/callback/keycloak - Can coexist with local auth users
If no admin user exists, one local admin account is seeded on first startup.
Defaults (from example.env.local):
- Username:
admin - Password:
admin1234
Override before first run in .env.local.
- Migrations:
src/server/migrations/ - Applied automatically during server startup
Schema diagram:
erDiagram
USERS {
integer id PK
text username
text password_hash
text role
timestamp created_at
text auth_type
text oauth_provider
text oauth_subject
}
SESSIONS {
text id PK
integer user_id FK
timestamp created_at
timestamp expires_at
}
USER_BOARDS {
integer user_id FK
text board_id FK
}
PING_FEATURES {
integer feature_id PK
text board_id
integer counter
text gateway_name
integer rssi
float snr
timestamp observed_at
float longitude
float latitude
integer rssi_stabilized
integer rssi_bonus
text network
}
USERS ||--o{ SESSIONS : has
USERS ||--o{ USER_BOARDS : owns
USER_BOARDS }o--|| PING_FEATURES : references_board
Build and run with Docker Compose:
docker compose up -d --buildNotes:
- Container exposes port
3000 - Runtime image includes
src/server/migrations/so migrations can run - You still need a reachable PostgreSQL database (
DATABASE_URL)
npm run dev
npm run build
npm run start
npm run lintsrc/app/api/auth/*: Local login/logout and NextAuth handlersrc/app/api/pings/*: Ping retrieval, summaries, manual imports, update triggerssrc/app/api/users/*: Admin user management
- Mutating API routes validate request origin (
APP_URLor fallback host headers) - JSON endpoints enforce
application/json - Passwords are hashed with
bcryptjs - Non-admin users only receive pings for boards they are assigned to
- English:
src/i18n/locales/en.json - German:
src/i18n/locales/de.json
Copyright (c) 2026. All Rights Reserved.
This application is proprietary and confidential. No part of this software, including source code, documentation, or associated files, may be reproduced, distributed, or transmitted in any form or by any means, without the prior written permission of the owner.