A modern, self-hosted radiation monitoring platform that makes environmental data accessible to everyone. Built to help communities understand radiation levels in their environment through open data and transparent mapping.
Live Demo: simplemap.safecast.org
Downloads: Latest releases for all platforms
English | Français | 日本語 | Deutsch (CH) | Italiano | 中文 | हिन्दी | فارسی | Русский | Монгол | Қазақша
This project provides a complete radiation mapping solution that runs on your own infrastructure. Whether you're monitoring environmental safety, conducting research, or providing public information, the platform handles everything from data collection to visualization.
Natural background radiation is typically low and safe. This map helps identify areas where levels rise above normal due to contamination, natural deposits, or other factors. Understanding these patterns helps communities make informed decisions about drinking water sources, agriculture, and land use.
Data Collection & Import
- Upload radiation measurements from multiple device formats (bGeigie, RadiaCode, AtomFast, and more)
- Automatic sync with Safecast's global database
- Import from files (.kml, .kmz, .json, .rctrk, .csv, .gpx) or URLs
- Real-time device monitoring (optional)
Visualization & Analysis
- Interactive map with multiple coloring schemes (scientific gradient or safety-focused)
- Speed-based layer separation (walking, driving, flying)
- Time-series analysis for specific locations
- Country-level statistics and reporting
- Print mode with QR codes for field marking
Data Management
- Multiple database backends (PostgreSQL, DuckDB, SQLite, ClickHouse)
- Automated JSON archive generation (daily/weekly/monthly)
- Track streaming for efficient large dataset handling
- RESTful API with rate limiting
Advanced Capabilities
- Gamma spectrum analysis (.spe, .n42 formats)
- User authentication with API key support
- Admin panel for content moderation and translation management
- Comprehensive authentication logging
- Short link generation for sharing
- Multi-language interface (29 languages) with PostgreSQL-backed translations and admin UI
- Auto-update system
Mirror notice: This repository is mirrored on Codeberg. All activity is managed on GitHub:
- Issues & bug reports: github.com/Safecast/safecast-new-map/issues
- Projects & roadmap: github.com/Safecast/safecast-new-map/projects
- Commits & pull requests: pushed to both GitHub and Codeberg simultaneously via dual-remote
Please open issues and contribute on GitHub. The Codeberg mirror exists for redundancy and to serve users who prefer FOSS hosting.
Download and run in seconds:
# Download from https://github.com/safecast/safecast-new-map/releases
chmod +x ./safecast-new-map
./safecast-new-mapStart with a complete dataset from simplemap.safecast.org:
./safecast-new-map -import-tgz-url https://simplemap.safecast.org/api/json/weekly.tgzThis imports all public tracks and starts the server. Future runs will use the cached data.
Deploy with HTTPS using Let's Encrypt:
./safecast-new-map -domain maps.example.org -db-type pgx -db-conn "postgres://user:pass@localhost/safecast"Requires ports 80 and 443 open for certificate validation.
Note for CloudFront/CDN deployments: If your domain uses CloudFront or another CDN for web traffic, SSH/rsync deployment must use the server's IP address directly, not the domain name. See CloudFront Setup Guide for details.
docker run -d -p 8765:8765 --name safecast-map safecastr/safecast-new-map:latestPostgreSQL (Recommended for Production)
Requires PostGIS extension for spatial indexing:
# Ubuntu/Debian
sudo apt install postgresql-16-postgis-3
# RHEL/CentOS/Rocky
sudo dnf install postgis34_16./safecast-new-map -db-type pgx -db-conn "postgres://user:pass@host:5432/dbname?sslmode=require"DuckDB (Fast Local Storage)
./safecast-new-map -db-type duckdb -db-path /path/to/dataSQLite (Simple Single-User)
./safecast-new-map -db-type sqlite -db-path /path/to/dataClickHouse (Large-Scale Analytics)
./safecast-new-map -db-type clickhouse -db-conn "clickhouse://user:pass@host:9000/dbname?secure=true"| Flag | Default | Description |
|---|---|---|
-port |
8765 | HTTP server port |
-domain |
- | Domain for HTTPS (enables Let's Encrypt) |
-db-type |
pgx | Database: pgx, duckdb, sqlite, chai, clickhouse |
-db-path |
. | Path for file-based databases |
-db-conn |
- | Connection string for network databases |
-default-lat |
44.08832 | Initial map latitude |
-default-lon |
42.97577 | Initial map longitude |
-default-zoom |
11 | Initial map zoom level |
-default-layer |
OpenStreetMap | Base map layer |
-admin-password |
- | Enable admin panel (track management) |
-allow-registration |
false | Enable user registration |
-require-auth |
false | Require authentication for uploads |
-smtp-host |
- | SMTP server for email (e.g., smtp.gmail.com) |
-smtp-port |
587 | SMTP server port |
-smtp-username |
- | SMTP authentication username |
-smtp-password |
- | SMTP authentication password |
-smtp-from |
- | Email "From" address |
-session-secret |
- | Secret key for session encryption |
-base-url |
- | Base URL for email links (e.g., https://example.com) |
-safecast-realtime |
false | Poll live Safecast device data |
-safecast-fetcher |
false | Auto-sync approved bGeigie imports |
-json-archive-frequency |
weekly | Archive generation: daily, weekly, monthly, yearly |
Import:
- KML/KMZ (Google Earth, Safecast bGeigie)
- JSON (exported tracks)
- RCTRK (RadiaCode)
- CSV (AtomFast, custom formats)
- GPX (GPS tracks)
- LOG (bGeigie Nano/Zen)
Export:
- JSON archives (compressed .tgz)
- Individual track JSON
- Legacy CIM format
Import from remote archive:
./safecast-new-map -import-tgz-url https://simplemap.safecast.org/api/json/weekly.tgzImport from local file:
./safecast-new-map -import-tgz-file /path/to/archive.tgz- Map/API app docs:
/map-api/(generated from Swaggo annotations) - MCP REST docs:
/mcp-api/(generated from Swaggo annotations) - Endpoint contracts should be maintained in route annotations, not duplicated in markdown endpoint lists.
- Both routes are served by the unified server and include cross-links in the UI.
The unified server includes an MCP (Model Context Protocol) server with a Claude-powered web chat interface, all in a single binary.
| Component | Source | Port | URL |
|---|---|---|---|
| Unified Server (Map + MCP) | cmd/unified-server/ |
8765 | /, /mcp-http, /assistant/ |
| MCP Server | cmd/unified-server/ |
8765 | /mcp-http, /mcp/sse |
| Web Chat | cmd/unified-server/ |
8765 | /assistant/ |
| REST API + Swagger | cmd/unified-server/ |
8765 | /api/, /map-api/ |
Build:
# Basic build (PostgreSQL only)
go build -o safecast-new-map ./cmd/unified-server
# With DuckDB analytics via DuckLake (requires CGO)
CGO_ENABLED=1 go build -tags duckdb -o safecast-new-map ./cmd/unified-serverRun:
# Map server with MCP (PostgreSQL required)
DATABASE_URL="postgres://user:pass@localhost/db" ./safecast-new-map
# With DuckLake analytics (in-memory DuckDB + PostgreSQL catalog + Parquet data)
DATABASE_URL="postgres://..." \
DUCKLAKE_PG_URL="dbname=ducklake_catalog host=localhost user=ducklake_rw" \
DUCKLAKE_DATA_PATH="/var/lib/safecast/ducklake/" \
./safecast-new-map
# With AI web chat (requires Anthropic API key)
DATABASE_URL="postgres://..." \
ANTHROPIC_API_KEY="your-key" \
CLAUDE_MODEL="claude-haiku-4-5-20251001" \
./safecast-new-mapConnect Claude to the live Safecast data:
# Claude Code CLI
claude mcp add --transport http safecast https://simplemap.safecast.org/mcp-http
# Claude.ai: Settings → Integrations → Add custom integration
# URL: https://simplemap.safecast.org/mcp-httpWeb Chat: Open http://localhost:8765/assistant/ in your browser.
Swagger API docs: simplemap.safecast.org/map-api/
Regenerate documentation after API changes:
# Main app API docs (canonical map docs for both binaries)
(cd cmd/unified-server && swag init -g doc.go -o docs/api --parseDependency --parseInternal --parseDependencyLevel 2 --instanceName unifiedapi)
# MCP API docs are generated from `cmd/unified-server` annotations and shared
# MCP registrars under `pkg/mcpserver`.CI verifies generated output is committed for cmd/unified-server/docs/api.
Analyze gamma spectra for isotope identification:
.spe- Maestro spectrum format.n42- ANSI N42.42 standard.rctrk- RadiaCode with embedded spectra
Add spectrum support to existing databases:
# PostgreSQL
psql -d your_database -f migrations/add_spectrum_support.sql
# SQLite
sqlite3 data.db < migrations/add_spectrum_support_sqlite.sql
# DuckDB
duckdb data.duckdb < migrations/add_spectrum_support_duckdb.sqlSee SPECTRAL_MIGRATION_GUIDE.md for details.
- Automatic spectrum extraction during upload
- Energy calibration support
- Peak detection and isotope identification
- Visualization on map markers
- API access for external analysis
Architecture diagram: docs/spectral-data-flow.mmd — full pipeline from upload → parsers → DB storage → REST API → browser chart construction.
Enable the admin panel with:
./safecast-new-map -admin-password your-secure-passwordAccess the admin panel at /admin/users?password=your-secure-password or log in as an admin user.
Admin pages:
| Page | URL | Description |
|---|---|---|
| Users | /admin/users |
Manage user accounts, roles, and API keys |
| Uploads | /admin/uploads |
View uploads, import from Safecast API, delete tracks |
| MCP Analytics | /admin/mcp |
Monitor MCP tool usage and AI query logs |
| Realtime | /admin/realtime |
Manage real-time sensor device data |
| Translations | /admin/translations |
Edit, add, and delete UI translations for all 29 languages |
Translations are stored in PostgreSQL and loaded into memory at startup. The admin translations page provides:
- Filter by language (29 languages: ar, bg, cs, da, de, el, en, es, fa, fi, fr, he, hi, hu, id, it, ja, ko, ms, nl, no, pl, pt, ru, sv, th, tr, uk, vi, zh)
- Search across keys and values
- Inline editing with save
- Add new translation keys
- Reload into Memory button to apply changes without restarting the server
All UI components are translated: map legend, AI assistant widget, login/register modals, search bar, spectrum viewer, profile page, and coordinate input dialog.
How it works:
- Language selection: The
?lang=URL parameter takes priority, then the browser'sAccept-Languageheader, defaulting to English - Incremental seeding: On startup, any new keys in the embedded
translations.jsonare inserted into the DB (ON CONFLICT DO NOTHINGpreserves existing edits) - Performance: Only the active language + English fallback are embedded in the page HTML (~30KB vs ~850KB for all 30 languages), keeping the AI widget within Claude's token limits
- Branding: "Safecast" must remain untranslated as a brand name in all languages
Enable user registration and authentication with email support:
./safecast-new-map \
-allow-registration \
-smtp-host smtp.gmail.com \
-smtp-port 587 \
-smtp-username your-email@gmail.com \
-smtp-password your-app-password \
-smtp-from your-email@gmail.com \
-session-secret "your-random-secret-key" \
-base-url "https://your-domain.com"Features:
- Email-based user registration with verification
- Session-based authentication (30-day sessions)
- Password reset via email
- User profile pages with upload history
- Upload tracking per user
Every registered user receives a unique API key that can be used for:
- Web login - Alternative to password authentication
- Programmatic uploads - Automated data submission
- API access - Protected endpoint authentication
Two ways to authenticate:
-
Password Login (traditional):
POST /api/auth/login { "email": "user@example.com", "password": "your-password" }
-
API Key Login (new):
POST /api/auth/login { "email": "user@example.com", "api_key": "your-20-char-api-key" }
Using API Keys with HTTP requests:
# Header method (recommended)
curl -H "X-API-Key: your-api-key" https://your-domain.com/api/protected-endpoint
# Query parameter method
curl "https://your-domain.com/api/protected-endpoint?api_key=your-api-key"- Log in to your account
- Click your username → "Profile"
- Your API key is displayed in the "API Key" section
- Click "Copy" to copy it to clipboard
Security Note: Treat your API key like a password. Don't share it or commit it to version control.
Admins can regenerate API keys for any user:
# Regenerate API key for user ID 42
curl -X POST "https://your-domain.com/api/admin/users/42/regenerate-api-key?password=admin-password"Response:
{
"message": "API key regenerated successfully",
"api_key": "new-20-char-key",
"user_id": 42
}The user will receive an email with their new API key. The old key becomes invalid immediately.
All authentication events are logged for security monitoring:
Logged Events:
- ✅ Successful logins (with method: password or api_key)
- ✅ Failed login attempts (with detailed failure reasons)
- ✅ User registrations
- ✅ Email verifications
- ✅ Password changes
- ✅ User logouts
- ✅ API key regenerations
Log Format:
AUTH: Successful login - user_id=42 email=user@example.com method=password ip=192.168.1.100 user_agent=Mozilla/5.0...
AUTH: Failed login attempt - email=user@example.com method=password reason=invalid_password ip=192.168.1.100
AUTH: New user registered - user_id=43 email=new@example.com username=john_doe ip=192.168.1.100
Filtering logs:
# View all authentication events
grep "AUTH:" /var/log/safecast.log
# View failed login attempts
grep "AUTH: Failed login" /var/log/safecast.log
# Monitor for brute force attacks
grep "AUTH: Failed login.*ip=192.168.1.100" /var/log/safecast.log | wc -lAdd user authentication to existing databases:
PostgreSQL:
psql -h 127.0.0.1 -U postgres -d safecast -f migrations/create_users_table.sql
psql -h 127.0.0.1 -U postgres -d safecast -f migrations/link_historical_uploads_to_users.sql
psql -h 127.0.0.1 -U postgres -d safecast -f migrations/add_ui_translations.sqlNote: The translations table is auto-created at startup and seeded from the embedded translations.json if no DB rows exist. The add_ui_translations.sql migration adds the extended UI translations (AI widget, auth modals, search, spectrum, profile page).
Key columns in users table:
id- Unique user identifieremail- User email (unique)password_hash- Bcrypt hashed passwordapi_key- 20-character unique API keyusername- Display nameemail_verified- Email verification statusis_active- Account statusis_admin- Admin privilegescreated_at,updated_at,last_login_at- Timestamps
Customize map views with URL parameters:
| Parameter | Values | Description |
|---|---|---|
place |
City, country, or location name | Navigate to a specific location by name |
lat |
Latitude (-90 to 90) | Latitude coordinate for map center |
lon |
Longitude (-180 to 180) | Longitude coordinate for map center |
zoom |
1-18 | Zoom level (used with lat/lon) |
minLat, minLon, maxLat, maxLon |
Coordinates | Define map bounds (legacy format) |
coloring |
safecast, chicha | Scientific gradient vs. safety bins |
unit |
uSv, uR | Display units (microsieverts or microroentgen) |
legend |
1, 0 | Show/hide legend |
lang |
en, ja, de, fr, etc. (29 langs) | Interface language (server-side + client-side) |
layer |
OpenStreetMap, Google Satellite | Base map |
show |
rt | Filter to show only realtime sensors |
Location Examples:
- Direct to Tokyo:
/?place=Tokyo - Coordinates with zoom:
/?lat=48.8566&lon=2.3522&zoom=13 - Share search result:
/?place=São%20Paulo(auto-generated when searching)
Display Examples:
- Safety view:
/?coloring=chicha&unit=uR - Clean embed:
/?legend=0 - Russian interface:
/?lang=ru - Realtime sensors only:
/?show=rt
Parameter Priority: When loading URLs, place takes priority over lat/lon, which takes priority over bounds (minLat/maxLat).
Poll live sensor data:
./safecast-new-map -safecast-realtimeSensors appear on the map in real-time with current readings.
Automatically import approved bGeigie measurements:
./safecast-new-map -safecast-fetcher \
-safecast-fetcher-interval 5m \
-safecast-fetcher-batch-size 10 \
-safecast-fetcher-start-date 2024-01-01git clone https://github.com/Safecast/safecast-new-map.git
cd safecast-new-map
go build -o safecast-new-map ./cmd/unified-server
./safecast-new-mapgo test ./...The cmd/tools/ directory contains standalone utilities for database maintenance and migration:
| Tool | Usage |
|---|---|
fix-pg-sequence |
Reset PostgreSQL markers sequence |
fix-sequence |
Alternative sequence sync utility |
add-internal-user-id |
Add internal user IDs to uploads |
cleanup-test-users |
Remove test users from database |
import-api-keys |
Import API keys from CSV |
migrate-to-postgres |
Migrate from SQLite to PostgreSQL |
migrate-users |
Migrate user data |
Run any tool with:
go run ./cmd/tools/fix-pg-sequence
export DATABASE_URL="postgres://user:pass@localhost/db" && go run ./cmd/tools/fix-pg-sequence# See scripts/crosscompile/crosscompile.go
go run scripts/crosscompile/crosscompile.goFor deploying to production servers with CloudFront/CDN setup, see:
- Deployment Guide - Complete deployment procedures and troubleshooting
- GitHub Actions Guide - Automated CI/CD setup
- CloudFront Setup - CDN configuration details
- Architecture Diagram - System overview (Mermaid)
- Spectral Data Flow - Spectral upload/parse/store/render pipeline (Mermaid)
Important: When using CloudFront, SSH and rsync must use the server's IP address directly, not the domain name.
Analytics: The platform uses DuckLake for analytics (in-memory DuckDB with a PostgreSQL catalog and Parquet data files), allowing multiple services to share analytics tables concurrently. Set DUCKLAKE_PG_URL and DUCKLAKE_DATA_PATH environment variables to configure.
PostgreSQL Tuning: Use connection pooling and appropriate work_mem settings for large batch imports.
Rate Limiting: API endpoints are rate-limited by default. Adjust in code if needed for high-traffic deployments.
After data migration or restore, you may encounter "duplicate key value violates unique constraint" errors. This happens when PostgreSQL sequences are out of sync with existing data.
Fix the markers sequence:
psql -h 127.0.0.1 -U postgres -d safecast -c \
"SELECT setval('markers_id_seq', (SELECT MAX(id) FROM markers) + 1);"Fix all sequences at once:
./tools/reset_postgres_sequences.shIf you see errors like column X does not exist, the database schema may need updating:
psql -h 127.0.0.1 -U postgres -d safecast -c "
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS recording_date TIMESTAMPTZ;
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS detector TEXT;
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS username TEXT;
ALTER TABLE uploads ADD COLUMN IF NOT EXISTS internal_user_id TEXT;
"| Script | Purpose |
|---|---|
tools/reset_postgres_sequences.sh |
Reset all PostgreSQL sequences after migration |
tools/reset_postgres_marker_sequence.sh |
Reset only the markers sequence |
tools/refresh_track_stats.sh |
Refresh track statistics materialized view |
tools/populate_usernames.sh |
Fetch usernames from Safecast API for uploads |
This project is developed and maintained by the Safecast community with contributions from:
- Matvey Gladkikh (primary designer/developer)
- Rob Oudendijk (developer)
- Safecast volunteers worldwide
- AtomFast community
- RadiaCode community
- Open dosimetry researchers
Contributing: We welcome contributions! See issues for areas needing help.
History: Inspired by Dmitry Ignatenko's field research and built on Safecast's decade of open radiation monitoring. The goal remains simple: make environmental radiation data accessible to everyone who needs it.
Code: Apache 2.0
Data: CC0 1.0 Universal
The map data is provided as-is for public benefit. While we strive for accuracy, always consult professional advice for health and safety decisions.