Skip to content

Local Development ​

This page is for people working on TeslaSync itself — adding a feature, fixing a bug, contributing a docs change. If you only want to run the platform, see Getting Started instead.

The development loop, at a glance ​

There are three workflows depending on what you're changing:

Changing…Run…Reload cadence
Frontend onlynpm run dev in web/ against containerised backendInstant via Vite HMR
Backend onlygo run ./cmd/teslasync against containerised data storesManual restart (~1 second)
BothTwo terminals: the above two side-by-sideFrontend instant, backend manual
Docs onlynpm run docs:dev in docs/Instant via VitePress
Helm charthelm lint + helm template against your valuesValidation, not runtime

We deliberately do not use a "rebuild everything on every change" workflow because the platform is large enough that a full rebuild is slower than a targeted restart.

Requirements ​

  • Go 1.27.1+ — match the minimum in go.mod; this includes required standard-library security fixes.
  • Node 26+ — matches the frontend engine requirement, CI, and container builds. Vitest workers use jsdom's browser storage rather than Node's process-wide Web Storage.
  • Docker + Compose v2 — for the data plane (postgres, redis, mosquitto) and the optional service profiles.
  • psql (optional but useful) — for inspecting the database while developing
  • gh (optional) — for the GitHub CLI workflow we use for PRs

Helm and Kubernetes tools are only needed if you're working on the chart.

Bring up just the dependencies ​

When you're iterating on Go or React code, you don't want the API or web container running — you want your local process to be the API or web. The dependencies still need to be up:

bash
docker compose up -d postgres redis mosquitto grafana prometheus

That gives you the data plane on the documented ports without conflicting with your local processes.

If you want optional sidecars too:

bash
docker compose --profile telemetry --profile commands --profile ai up -d \
  postgres redis mosquitto grafana prometheus \
  fleet-telemetry vehicle-command-proxy ollama

Backend development ​

bash
go mod download
go run ./cmd/teslasync

The API listens on :8080 and reads .env (or your shell environment). The four worker binaries follow the same pattern:

bash
go run ./cmd/notification-worker
go run ./cmd/export-worker
go run ./cmd/automation-worker

You usually only need to run the workers locally if you're actively changing them. The containerised versions can stay up and talk to your local API.

Useful backend checks ​

bash
go vet ./...
go test ./...
go test -race ./...
go test -run TestSpecific ./internal/api/...
go build ./...

The race-detector run is mandatory before opening a PR. Concurrency bugs in the telemetry pipeline are subtle and the race detector catches most of them.

Hot-reload-ish for Go ​

There is no Go HMR. The fastest loop is:

  1. Make your change
  2. Save
  3. Ctrl-C the running process
  4. Re-run go run ./cmd/teslasync

That's ~1 second on a warm build cache. If you want it shorter, air works but introduces its own footguns; we don't ship a config for it.

Backend rules that are not optional ​

  • Handlers are thin — no SQL inline, no business logic. Persistence goes through repositories.
  • All SQL is parameterised. No string concatenation, ever.
  • Errors are wrapped with context (fmt.Errorf("loading vehicle %d: %w", id, err)) so the log message identifies the operation.
  • Structured logs only — no fmt.Println. Use the package-level zerolog logger.
  • New environment variables are added in three places at once: internal/config, docker-compose.yml, and helm/teslasync/values.yaml + templates. If you only update one, you'll ship a feature that works in your environment and breaks in everyone else's.
  • New AI routes are wrapped by g.Wrap("<feature-id>", handler) and the feature is registered in internal/ai/features/registry.go with DefaultOn: false.

Frontend development ​

bash
cd web
npm install
npm run dev

Vite serves at http://localhost:3000. It proxies /api and /events to http://localhost:8080 so the SPA talks to your running backend without CORS dance.

Useful frontend checks ​

bash
npx tsc --noEmit          # type check
npm run build             # production build (catches lazy-load issues)
npm test                  # Vitest
npm run lint              # ESLint

The full pre-PR sequence is:

bash
npx tsc --noEmit && npm run lint && npm test && npm run build

Frontend rules that are not optional ​

  • Data loading goes through web/src/api/hooks/*. No fetch() in components, no useEffect-driven fetch(). Use TanStack Query hooks.
  • Hook URLs omit /api/v1 because the request client adds it.
  • Query params are snake_case.
  • Pages render loading, error, and empty states. Always. Even for tiny widgets.
  • Shared UI comes from @/components/ui / @/components/charts / @/components/maps. Don't import Recharts or Leaflet directly in feature code.
  • Units, dates, currency render through useUnits() / useFormatting() / useDateFormat(). Hardcoded "km" / "$" / toLocaleString() calls fail the lint.
  • User-visible strings go through i18n with English fallbacks.
  • Helix AI components wrap with withAiFeature('<feature-id>') — the HOC handles the off-by-default rendering for you.

Service-worker behaviour in dev ​

Production builds register a service worker that caches assets aggressively. Development does not unless you set VITE_PWA_DEV=true. If you previously enabled dev mode and now have stale behaviour on localhost, clear the service worker in your browser's devtools once.

Keeping the Helix AI feature mirror in sync ​

The frontend's web/src/ai/features.ts is auto-generated from internal/ai/features/registry.go. If you add or modify an AI feature:

bash
go run ./tools/aigen          # regenerate
go run ./tools/aigen --check  # CI mode: fail if regen would change the file

CI also runs tools/aivet which enforces:

  • Every registered feature ID is referenced by exactly one wrapped backend route
  • Every wrapped backend route references a registered feature ID
  • Every feature is registered with DefaultOn: false
  • Every AI React component is wrapped by withAiFeature

Don't edit web/src/ai/features.ts by hand; the next aigen run will overwrite it.

Docs development ​

bash
cd docs
npm install
npm run docs:dev    # local preview with hot reload
npm run docs:build  # production build into docs/.vitepress/dist

The docs config (docs/.vitepress/config.ts) controls navigation, sidebar, and which directories are included. Internal-only docs (audits, runbooks, signal-audits, observability, architecture ADRs, dev guidelines) are excluded from the published site via srcExclude — they're still in the repo, just not built.

Tests ​

SuiteHow to runWhat it covers
Go unit testsgo test ./...Pure-Go logic, repositories with an in-memory DB
Go race testsgo test -race ./...Concurrency bugs in the telemetry/FSM/typed-rule paths
Go integration testsgo test -tags=integration ./...Tests that need real PostgreSQL / Redis / MQTT
Frontend unit testscd web && npm testVitest + React Testing Library for components and hooks
TypeScript type checkcd web && npx tsc --noEmitType errors across the SPA
Phase-50 final-gatego test ./test/phase50/...Off-by-default + wrapping contract for Helix features
Helm linthelm lint helm/teslasyncChart syntax and convention checks
Helm templatehelm template ... -f values.yamlRenders the chart against your values to spot issues
audit-violations skillbash .github/skills/audit-violations/audit.shInline styles, raw HTML, wrong imports

The pre-PR baseline is "everything in this table passes". CI runs the same set.

CI parallel execution ​

CI scales out across hosted runners rather than oversubscribing one machine:

  • Go race tests use eight disjoint package shards, balanced by test-source size. Packages without tests are retained. Each shard has its own TimescaleDB service.
  • Vitest uses eight native shards with two workers per runner. The merge job checks all shard artifacts against full test-file discovery before merging Vitest's coverage maps. Go profiles merge atomic block counts, not percentages.
  • Lint, architecture checks, individual Go binary builds, frontend builds, database replay/rollback, and all five Docker image builds start alongside unit tests. The final CI gate requires generated artifacts, backend, frontend, and Docker to succeed; failed tests or incomplete coverage merges still fail CI without delaying image builds.
  • Browser jobs reuse one hermetic build. Chromium responsive/smoke tests and Windows visual snapshots each use four shards. Accessibility and performance run on separate runners with one worker each; Firefox and WebKit remain independent.

The existing Backend (lint + test + build), Frontend (lint + test + build), Chromium quality, and Windows snapshot check names remain aggregate gates. The new CI (generated + backend + frontend + Docker) gate requires all four outcomes to succeed. Failed, cancelled, skipped, or incomplete required shards cannot turn those gates green. backend-coverage and frontend-coverage still contain the merged reports; raw shard artifacts remain available for diagnosis. The pre-existing telemetry replay continue-on-error exception is unchanged; this parallelization adds no new waivers and does not change coverage thresholds.

To reproduce a frontend shard locally from web/:

bash
npx vitest run --shard=1/8 --maxWorkers=2 --coverage --coverage.reporter=json --reporter=blob

To reproduce a browser shard using the existing build/preview wrapper:

bash
npm run e2e:quality -- --shard=1/4

Changes to shard counts must update the workflow matrix and corresponding count together. The report validators deliberately reject missing artifacts rather than publishing partial coverage as a complete run.

Debugging tips ​

  • The backend isn't seeing my env change — Go binaries snapshot the environment at startup. Restart the process.
  • The frontend isn't seeing my code change — check the Vite terminal for HMR errors. Some files (e.g., new context providers) need a full reload.
  • Tests pass locally but fail in CI — most often a race condition or timezone assumption. Run with -race and with TZ=UTC locally.
  • A migration won't apply — schema_migrations.dirty=true blocks subsequent migrations. Inspect, fix the underlying issue, set dirty=false and re-run.
  • tools/aivet is failing — you added an AI route without registering the feature, or vice versa. Re-run go run ./tools/aigen and check git diff web/src/ai/features.ts.

Commit and PR conventions ​

  • Branch from main, name descriptively (feat/cost-forecast-narration, fix/charging-null-safety, docs/rewrite-readme-helix).
  • Conventional-commit-ish prefixes are common in the history (feat:, fix:, docs:, chore:, refactor:) — match the surrounding style.
  • Include a Co-authored-by trailer when working with the Copilot CLI.
  • Update docs in the same PR as behaviour, configuration, or route changes.
  • Open the PR with a description that includes the problem, the approach, and the verification you ran. CI will block on the test suite; do the verification yourself first so review can focus on the change.

Self-hosted Tesla intelligence · MIT licensed · Privacy policy