A small, self-hosted, read-only dashboard for TeslaMate. It connects to your existing TeslaMate Postgres database and renders a clean, map-first view of your driving and charging history. One static Go binary, one container, no second database, and nothing is ever written back to TeslaMate.
▶ Try the live demo — no install, running in demo mode with synthetic data.
The recording above (and the live demo) run in demo mode with synthetic data. Your real data never leaves your machine.
- Map-first. Every drive is drawn from its GPS trace and colored by speed (a light-to-deep blue ramp with a white casing), so you can read where you go fast and where you crawl at a glance.
- Presentation mode. Tick any drives and charges in the activity feed to isolate them on the map — build the exact route you want to walk someone through, then Focus map to frame it. Shift-click range-selects; Share serializes the selection into a URL you can send around.
- Detail panels. Click any drive — in the feed, or its route on the map — for a stat grid, state-of-charge bar, and a speed + battery chart over the trip. Click a charge (or its marker) for the real charging curve (power vs. state of charge) with peak/avg/min power and range added. Step through activities with ‹ › without leaving the panel.
- Trip summary. Turn a selection into a route-planner-style itinerary: departure → drive legs → charge stops (with SoC bars and energy) → arrival, plus drive/charge time totals.
- Activity feed. Drives and charging sessions merged newest-first, with type filters (All / Drives / Charging) and an eye toggle that hides home & destination charging from both the list and the map.
- Charge classification. Supercharger stops (DC fast charging), home charging (inside a named geofence), and destination charging are told apart from the data, not hardcoded, and get distinct markers.
- KPI cards. Distance, drives, energy, and efficiency for the range — each with a delta against the prior period of equal length and a sparkline of the trend.
- Any timeframe. All time, last year, 90 days, 30 days, or a custom From/To range with quick presets.
- Per-car. Multi-car aware; pick a vehicle in the header and the whole dashboard filters to it.
docker build -f docker/Dockerfile -t teslamate-dash .
docker run --rm -p 4001:4001 teslamate-dash
# open http://localhost:4001 (DEMO mode with synthetic data)Or with Go (1.22+) and Node (20+):
(cd src/web && npm install && npm run build) # build the embedded UI first
go run ./src # demo mode when no DATABASE_HOST/TC_DSN is setMulti-arch images (linux/amd64 and linux/arm64) are published to GHCR on every release:
docker run --rm -p 4001:4001 ghcr.io/gmaslowski/teslamate-dash:latest
# or pin a version: ghcr.io/gmaslowski/teslamate-dash:0.1.0Tags: latest (tip of main), X.Y.Z / X.Y / X (semver from git tags), and sha-<commit>.
Run it on the same Docker network as TeslaMate so it can reach Postgres. It reuses TeslaMate's
DATABASE_* variables, so usually you only set the password.
docker run --rm -p 4001:4001 \
--network teslamate_default \
-e DATABASE_HOST=database \
-e DATABASE_USER=teslamate_ro \
-e DATABASE_PASS=secret \
-e DATABASE_NAME=teslamate \
teslamate-dashOr add it to your existing TeslaMate docker-compose.yml as another service:
dash:
build: # or image: ghcr.io/youruser/teslamate-dash
context: ./teslamate-dash
dockerfile: docker/Dockerfile
environment:
- DATABASE_HOST=database
- DATABASE_USER=teslamate_ro
- DATABASE_PASS=${TM_DB_PASS}
- DATABASE_NAME=teslamate
- TC_UNITS=km
ports:
- 4001:4001
restart: unless-stoppedThe app forces read-only sessions, but a dedicated read-only role is the right belt-and-braces move:
CREATE ROLE teslamate_ro WITH LOGIN PASSWORD 'secret';
GRANT CONNECT ON DATABASE teslamate TO teslamate_ro;
GRANT USAGE ON SCHEMA public TO teslamate_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO teslamate_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO teslamate_ro;A minimal self-contained chart. Create a teslamate-dash/ directory with these four files:
teslamate-dash/Chart.yaml
apiVersion: v2
name: teslamate-dash
description: Read-only dashboard for TeslaMate
type: application
version: 0.1.0
appVersion: "0.1.0"teslamate-dash/values.yaml
image:
repository: ghcr.io/gmaslowski/teslamate-dash
tag: "0.1.0"
service:
port: 4001
# Non-secret settings (reuses TeslaMate's DATABASE_* names).
env:
DATABASE_HOST: teslamate-db
DATABASE_NAME: teslamate
DATABASE_USER: teslamate_ro
TC_UNITS: km
# DB password is read from an existing Secret (create it separately, see below).
dbPasswordSecret:
name: teslamate-dash-db
key: passwordteslamate-dash/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}
spec:
replicas: 1
selector:
matchLabels: { app: {{ .Release.Name }} }
template:
metadata:
labels: { app: {{ .Release.Name }} }
spec:
containers:
- name: teslamate-dash
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 4001
env:
{{- range $k, $v := .Values.env }}
- name: {{ $k }}
value: {{ $v | quote }}
{{- end }}
- name: DATABASE_PASS
valueFrom:
secretKeyRef:
name: {{ .Values.dbPasswordSecret.name }}
key: {{ .Values.dbPasswordSecret.key }}
readinessProbe:
httpGet: { path: /, port: 4001 }
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }teslamate-dash/templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}
spec:
selector: { app: {{ .Release.Name }} }
ports:
- port: {{ .Values.service.port }}
targetPort: 4001Create the DB password Secret, then install (use the read-only role from above):
kubectl create secret generic teslamate-dash-db --from-literal=password='secret'
helm install teslamate-dash ./teslamate-dash
# reach it: kubectl port-forward svc/teslamate-dash 4001:4001Add an Ingress (or set service.type: LoadBalancer) to expose it beyond the cluster. For SSL to the
database, set TC_DSN under env instead of the DATABASE_* parts.
Any reachable Postgres works. For a database that lives elsewhere (a remote host, a Kubernetes
cluster, etc.), expose it to the dashboard however you normally would (for example a port-forward to
localhost:5432) and point the variables below at it. Use TC_DSN if you need SSL.
All configuration is via environment variables. TC_-prefixed names override the plain TeslaMate ones.
| Variable | Default | Purpose |
|---|---|---|
DATABASE_HOST / TC_DATABASE_HOST |
(none) | TeslaMate Postgres host. Empty means demo mode. |
DATABASE_PORT / TC_DATABASE_PORT |
5432 |
Postgres port |
DATABASE_NAME / TC_DATABASE_NAME |
teslamate |
Database name |
DATABASE_USER / TC_DATABASE_USER |
teslamate |
DB user (use a read-only role) |
DATABASE_PASS / TC_DATABASE_PASS |
(none) | DB password |
TC_DSN |
(none) | Full postgres://... connection string, overrides the parts above (set sslmode here) |
TC_PORT |
4001 |
HTTP port |
TC_UNITS |
km |
km or mi |
TC_TITLE |
TeslaMate Dash |
Header title |
TC_MAP_STYLE_URL |
OpenFreeMap Positron | MapLibre style URL. Point at your own tiles for full privacy. |
TC_DEMO |
auto | Force synthetic data on or off |
This data is your home address, geofences, and full movement history. The app is built to keep it yours:
- Read-only. Sessions are forced read-only and it never writes to TeslaMate.
- No telemetry, no analytics, no outbound calls from the server.
- Local rendering. The one external request a browser makes is for basemap tiles from
TC_MAP_STYLE_URL. Point that at a self-hosted tile server if you do not want a third party to see which areas you pan to. - Coordinates are never logged. Keep your fork private if you commit any of your own data.
A single Go binary embeds the built web UI and talks to Postgres over a read-only pgx pool. The
frontend is React + TypeScript (Vite) with MapLibre GL; position traces are simplified server-side
(Douglas-Peucker) so map payloads stay small even for six-hour drives.
| File | Role |
|---|---|
src/main.go |
HTTP server, graceful shutdown, embeds src/web/dist |
src/config.go |
Environment configuration |
src/model.go |
Types, the Store interface, trace simplification |
src/db.go |
Read-only pgx pool and all SQL (the only file that touches Postgres) |
src/demo.go |
Synthetic data so it runs with no database |
src/handlers.go |
JSON API (/api/config, /api/cars, /api/summary, /api/activities, /api/activities/{id}) |
src/web/ |
React + Vite + TypeScript frontend, built to src/web/dist |
docker/ |
Dockerfile and its dockerignore |
(cd src/web && npm install && npm run build) # required once before go build/run
go mod tidy
go run ./src # demo mode if no DATABASE_HOST
# point at a database:
DATABASE_HOST=localhost DATABASE_PASS=secret go run ./srcFor frontend work, run the API and the Vite dev server side by side; Vite proxies /api to :4001:
go run ./src # terminal 1
cd src/web && npm run dev # terminal 2, open the printed URLThe Docker image always builds with the repo root as context (docker build -f docker/Dockerfile .);
the frontend is built inside the image, so no local npm run is needed for container builds. BuildKit
(the default builder since Docker 23) automatically applies docker/Dockerfile.dockerignore.
Tested against the TeslaMate schema tables drives, positions, charging_processes, charges,
addresses, geofences, cars. The app checks these exist on startup and fails with a clear message
if your TeslaMate version differs.
If this is useful to you, you can support the work here:
Not affiliated with, endorsed by, or sponsored by Tesla, Inc. or TeslaMate. "Tesla" is a trademark of Tesla, Inc. This project only reads data you already collect with TeslaMate.
MIT. See LICENSE.

