Skip to content

Latest commit

Β 

History

57 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


Divetracx

The self-hosted dive log you actually own.
Import from DiveMate and Garmin, log by hand, export anywhere. Keep every dive on your own infrastructure.

Live demo Β Β·Β  Import Β Β·Β  Export Β Β·Β  Quick start Β Β·Β  Documentation

Checks and Build License: MIT Self-hosted Bun 1.4


Tip

Try it in ten seconds. The read-only demo runs entirely from seven anonymized sample dives with complete profiles β€” no account, no database, nothing to install.

Divetracx overview with logbook statistics and recent dives

🀿 What it is

Divetracx is a self-hosted home for your dives, sites, buddies, equipment, cylinders, gases, profiles, pictures, and certifications.

Dive computers, phone apps, and exchange formats each keep their own copy of your dives, with their own schemas and their own gaps. Divetracx pulls them together into one canonical logbook: run a full historical import once, then let incremental syncs pick up new dives β€” or skip the import entirely and log dives straight from the UI. Either way the data lives on your own PostgreSQL and storage, and you can export it again in the format of your choice.

✨ Why Divetracx

  • πŸ“₯ Import from multiple sources. DiveMate .ddb backups (with photos and certification cards) and Garmin dive computers via the official FIT SDK.
  • πŸ” Reliable ongoing sync. Full imports are manual, validated, and transactional. Incremental imports skip unchanged records and never delete a dive just because it disappeared from a later feed.
  • ✍️ Log anything by hand. Dives, tanks, sites, buddies, gear, and certifications have full editors in the UI. No dive computer required β€” manual dives take paper-logbook fields (pressure groups, surface interval, safety stop, residual nitrogen time) and render a logbook-style schematic instead of an empty profile chart.
  • 🧩 One canonical record. Different source schemas map into one durable model with full provenance. Vendor IDs and raw payloads stay in a separate integration layer, so your manual edits survive every re-import.
  • πŸ“ˆ Complete dive detail. Profiles, tanks, gases, pressure, temperature, decompression ceiling, ppOβ‚‚, notes, ratings, and media in one place.
  • πŸ—ΊοΈ Maps and statistics. Interactive profiles, a site map with a click-to-pin coordinate picker, calendar activity, trends, personal records, and aggregate statistics.
  • πŸ“€ Your data, portable. Export to DiveMate .ddb, Subsurface .ssrf, UDDF 3.2.3, CSV, or versioned Divetracx JSON at any time.
  • πŸ€– Ask your agent. An optional OAuth-protected MCP endpoint with owner-controlled read, write, and delete tools lets your own AI tools search dives, load details, list sites, and read statistics.
  • πŸ”’ Yours, end to end. Self-hosted on your own PostgreSQL and your own storage, behind your own auth proxy. No accounts, no telemetry, no upsell.
  • πŸš€ Deploys in one command. Docker Compose for a home server, a Helm chart for a cluster.

πŸ“₯ Getting dives in

Three import sources are supported today. All are run from Settings β†’ Integrations and from the command line; DiveMate and Garmin can also run on a schedule. Every run lands in Import history, which reports discovered, new, changed, unchanged, and failed source records.

Source Full import Incremental sync What comes across How it connects
DiveMate .ddb βœ… βœ… Dives, profiles, tanks, sites, buddies, equipment, equipment sets, certifications with card images, shops, dive types, diver profile, pictures Reads DiveMate.ddb plus its Media and Cards files from a Google Drive folder via a service account
Garmin dive computers βœ… βœ… Dives, profiles, tanks, gases; gear and certifications from the Garmin Dive app Divetracx signs in with your account (MFA supported), pulls FIT activities from Garmin Connect, and reads gear from the Garmin Dive service
Subsurface .ssrf / .xml β€” βœ… Dives, profiles, cylinders, gas changes, sites with GPS, buddies and dive guides, weights, tags Upload a logbook file from the browser or the CLI

Note

Not yet supported: importing UDDF, CSV, or Divetracx JSON. Those three formats are export-only for now. If your dives live elsewhere, log them by hand, or export them from Subsurface β€” which reads most dive computers and logbook formats β€” and upload that file.

Full and incremental mean different things:

  • A full import is a deliberate, manual operation. It re-reads the whole source, validates it, and applies the result in a single transaction. It is destructive by design for records the source previously produced, but it never touches dives you created by hand or that only matched an imported profile.
  • An incremental import is safe to run as often as you like. It compares stable source identities and content hashes, applies only new or changed records, and never deletes anything.

Each integration card has a What to sync list where you can switch off the kinds of data you don't want brought across β€” say, certifications from DiveMate while you keep its dives. Everything is on by default. A switched-off kind is left out of later imports but nothing already imported is removed, and dives keep pointing at sites, people, and gear that came across earlier. Some kinds are tied together and the list says so: profiles and tanks belong to dives, gear sets are made of gear, and a Garmin activity or Subsurface dive is the thing its profile and people are read from, so those dives are always on. Switching an entity back on imports its records as new.

DiveMate details

Configure DIVEMATE_GOOGLE_DRIVE_FOLDER_ID and GOOGLE_APPLICATION_CREDENTIALS for a service account with read access to the Drive folder holding your backup. The importer uses SQLite row IDs and content hashes for idempotent incremental detection. Images are copied to configured storage with immutable originals and generated WebP thumbnails.

DiveMate buddy IDs import as buddies. Its legacy free-text Divemaster value imports as a linked person with the Divemaster role. Divetracx exports Divemaster, Instructor, and Guide assignments back into that field with role labels, so those richer assignments survive a Divetracx β†’ DiveMate β†’ Divetracx round trip.

bun run sync:divemate                              # incremental, from the CLI
bun run import:incremental --integration=divemate  # same thing, generic entry point
Garmin details

Garmin Connect runs directly in Divetracx’s server process. Connect your Garmin account once from Settings β†’ Integrations; MFA is supported. Passwords and verification codes are used only for the login, while the resulting OAuth tokens are retained in Divetracx’s server-only database for later imports.

Dives are listed through the Garmin Dive service (the backend of the Garmin Dive app), so dives you logged by hand in the app come across too, and each dive’s original FIT file is downloaded for everything the computer recorded: the depth, temperature, and ceiling profile with remaining no-deco limit, time to surface, CNS and nitrogen loading per sample; gases with their planned role (bottom, deco, travel); the decompression model and gradient factors; CNS and OTU totals; fresh or salt water; every alert and gas switch as a timeline of computer events; the recording device with serial and firmware; and β€” when the watch recorded it β€” heart rate. Wrist heart rate underwater is off by default on Descent computers; enable it in the watch’s dive settings if you want it in your profiles.

A dive read from more than one source shows all of them under Recorded by on its page, with what each contributed. No source is treated as primary: each one fills in whatever the dive did not have yet.

Activities are reconciled against existing log entries by start time within 45 minutes, so a computer-recorded profile attaches to the dive you already logged instead of creating a duplicate.

Gear and certifications you keep in the Garmin Dive app come across too. They live in a separate, undocumented Garmin service (the one the Dive app talks to), which Divetracx reaches by exchanging the stored Connect token for a Dive-scoped one β€” no second login. Gear brings brand, model, serial number, purchase date, price and shop, weight, retirement, and the next service date; Garmin stores certifications as a kind of gear, so those arrive with name and date only. Both are matched to existing gear and certifications by name before anything new is created, so a DiveMate import of the same item is enriched, not duplicated. Because this API is unofficial, a failure there is recorded in the run's diagnostics and never blocks the dive import. Switch either off under What to sync to skip the Dive service entirely.

The Probe Dive API button under the Garmin account exercises those endpoints read-only and shows what each returned, which is useful when Garmin changes something.

bun run import:incremental --integration=garmin
Subsurface details

Divetracx reads the native Subsurface XML in all three layouts it has used over the years β€” the original <dives> files, <divelog version='2'> with inline <location> elements, and the current <divelog version='3'> with a <divesites> table β€” so .ssrf files saved by any Subsurface release import. Because Subsurface has no stable dive identifier, each dive is keyed by its start date and time; uploading a newer export of the same logbook updates those dives in place and never deletes anything. Sites are matched to existing Divetracx sites by name or coordinates before new ones are created, buddies and dive guides become people, cylinders become tanks, gas-change events become the active tank on each profile sample, and boat/shore/deco tags map onto the entry type and decompression flag. A tag that matches one of your dive types assigns it. The file itself is not stored; only the parsed records and a fingerprint of the upload are kept for change detection.

bun run import:subsurface --file=backups/logbook.ssrf

πŸ“€ Getting dives out

Everything you put in, you can take out again. Settings β†’ Export offers one-click downloads, and the same files are served from /api/export/* for scripts and backups.

Format Endpoint Best for What it contains
DiveMate backup .ddb /api/export/divemate Opening your whole logbook in DiveMate again A DiveMate-compatible SQLite database rebuilt from canonical data, including Garmin-imported and hand-logged dives
Divetracx backup .json /api/export/json Complete, lossless backups Every table in the database as versioned JSON (divetracx-backup, currently version 17)
Dive spreadsheet .csv /api/export/csv Spreadsheets and data analysis One joined row per dive: site, buddies, equipment, tanks, conditions, notes, and the full profile as inline samples
Universal dive log .uddf /api/export/uddf Other logbook software UDDF 3.2.3 with diver, sites, and dives including depth profiles
Subsurface logbook .ssrf /api/export/subsurface Opening the logbook in Subsurface, or a Subsurface ⇄ Divetracx round trip Native Subsurface XML (format 3) with sites and GPS, cylinders, weights, buddies and dive team, tags, gas-change events, and full profiles with per-tank pressure and deco ceiling

The DiveMate export uses your configured .ddb only as a schema template and rebuilds the supported tables from canonical data. Besides downloading it, you can publish it back to Google Drive with Export to Drive on the Integrations page. That replaces DiveMate.ddb in your Drive folder, asks for confirmation, and is never triggered automatically; Drive keeps the previous revision. The same export is available from the CLI:

bun run export:divemate --output=backups/DiveMate.ddb

Warning

Exports contain personal data β€” locations, health-related notes, contact details, certification numbers. Treat the files accordingly.

✍️ Logging dives by hand

You do not need a dive computer or a DiveMate history to use Divetracx. The UI is a complete logbook editor, and hand-logged records live alongside imported ones in the same canonical model.

  • Dives β€” date, entry time, duration, surface interval, maximum and average depth, air and water temperature, weights, conditions (visibility, current, waves, weather), suit, boat, dive computer, shop, dive type, rating, and notes. Search for people and assign each one as Buddy, Divemaster, Instructor, or Guide; attach gear; and add one or more tanks with volume, start and end pressure, and Oβ‚‚/He mix.
  • Dive sites β€” name, water body, region, country, and notes, with a map where you click to pin the coordinates. Each site shows its dive count, deepest dive, and last visit.
  • Buddies β€” the people you dive with, including their certifications and agency memberships.
  • Gear and gear sets β€” track individual equipment items and group them into sets you can attach to a dive in one step.
  • Profile β€” your diver profile, certifications, and agency memberships, plus a generated shareable diver card.
  • Photos β€” upload pictures to dives, sites, and gear items; originals stay immutable and WebP thumbnails are generated.
  • Settings β€” maintain your own lists of dive types and agencies.

Merging a split dive

Dive computers end a dive when you surface, so a short surface interval can leave one dive logged as two. Open either entry, choose Merge dives, and pick the ones that belong with it. The earliest entry is the one that is kept β€” that is where the dive began, so it keeps its dive number β€” and the rest are appended to it at their real clock times and deleted. The tanks you kept breathing are continued rather than duplicated, buddies, gear, photos, and notes move across, and the duration, depths, and temperatures are recomputed over the whole dive.

The profile chart draws each recorded segment separately, with the surface interval between them shaded and left empty β€” no sample is invented to bridge the gap.

A merged dive is an ordinary dive and keeps syncing. It holds the provenance of every entry that went into it, so later imports update it as they always would and never recreate the entries you merged in. Merging itself is not reversible.

Because provenance is stored separately from the logbook, re-running an import never overwrites a field you edited yourself.

πŸ€– Ask your AI assistant

Divetracx exposes a scoped Model Context Protocol endpoint so your own AI tools can search and maintain dives, sites, buddies, gear, and your diver profile. Every tool can be switched on or off under Settings β†’ AI access. Each connection then receives explicit read, write, and optional delete scopes on the consent screen. Existing clients, their active scopes, immediate revocation, and the last 100 authorization and tool events are managed in the same page.

It ships its own OAuth 2.1 authorization server β€” dynamic client registration, mandatory S256 PKCE, refresh-token rotation with replay detection, hashed token storage, immediate revocation β€” and uses your existing Hodor owner session for consent, so no external identity provider is needed.

codex mcp add divetracx --url https://dives.example.com/api/mcp
codex mcp login divetracx

Tool results can contain private health, location, contact, and certification data. Write tools use the same validation and canonical mutation services as the web UI; delete tools are separately scoped and marked destructive. The endpoint is always part of Divetracx at /api/mcp and can be paused without deleting clients from Settings β†’ AI access. Routing details are in docs/deployment.md.

πŸ“Έ Screenshots

More screenshots
Dive detail page with a depth and temperature profile and a tank-switch marker Searchable dive table with type, site, date, time, depth, and water temperature
Dive detail with computer profile Searchable logbook
Statistics page with time underwater, depth, and a depth-over-time chart Diver profile with certifications and a generated share card
Statistics Profile and shareable diver card
Dive sites table with dive counts, deepest dive, last dive, and rating
Dive sites

🧱 Tech stack

A modern, boring-where-it-counts TypeScript stack with full-stack type safety from the database row to the rendered field:

Layer Choice
Runtime and package manager Bun 1.4
Full-stack framework TanStack Start with server functions
Routing TanStack Router, file-based and fully typed
UI React 19, shadcn/ui, Base UI, Tailwind CSS v4
Icons and maps Lucide, MapLibre GL
Database PostgreSQL 18 with Drizzle ORM and committed migrations; ephemeral PGlite for the demo
Media storage Local filesystem or any S3-compatible bucket
Dive computer data Garmin's official FIT JavaScript SDK
Validation Zod at every boundary, including environment variables
Tooling Vite, Biome, Knip, bun test
Deployment Docker Compose or Helm, fronted by the Hodor auth proxy

Server-only concerns β€” database access, storage providers, Google Drive and Garmin transport, and secrets β€” never cross into browser code, and configuration is validated lazily on first use, reporting the affected variable names.

πŸš€ Quick start

Docker Compose starts PostgreSQL 18, applies committed migrations, starts Divetracx, and exposes it through Hodor.

cp .env.example .env
# Replace HODOR_PASSWORD and HODOR_SECRET in .env.
# Generate a signing secret with: openssl rand -hex 32
docker compose up -d postgres
docker compose run --rm app bun run scripts/migrate.ts
docker compose up -d

Open http://localhost:3000, sign in through Hodor, and either start logging dives or head to Settings β†’ Integrations to connect DiveMate or Garmin.

πŸ”§ Local development

Divetracx uses Bun 1.4.0. With PostgreSQL available at the DATABASE_URL from .env:

bun install --frozen-lockfile
cp .env.example .env
docker compose up -d postgres
bun run db:migrate
bun run dev

The unauthenticated development server listens on http://localhost:3000. It applies pending migrations before startup and watches for newly generated migrations.

All available commands
Command Purpose
bun run dev Apply migrations and start the watched development server
bun run check Check source formatting and lint
bun run typecheck Run TypeScript without emitting files
bun run test Run tests; environment-gated integration suites are skipped
bun run test:integration:imports Run the PostgreSQL-backed import integration tests
bun run test:integration:divemate-export Run the DiveMate writeback integration tests
bun run lint:deadcode Find unused code and dependencies with Knip
bun run build Build production assets
bun run build:demo Build the database-free Vercel demo
bun run check:helm Lint and template the Helm chart
bun run verify Run the complete local quality gate
bun run db:generate Generate a Drizzle migration after a schema change
bun run db:migrate Apply committed migrations
bun run db:studio Open Drizzle Studio
bun run db:seed Seed the same fictional dataset as the public demo
bun run sync:divemate Run an incremental DiveMate import
bun run import:incremental --integration=garmin Run an incremental Garmin import
bun run import:subsurface --file=backups/logbook.ssrf Import a Subsurface logbook file
bun run export:divemate --output=backups/DiveMate.ddb Rebuild a DiveMate backup from canonical data
bun run media:refresh-thumbnails Regenerate WebP thumbnails for stored pictures

πŸ“š Documentation

Guide What is inside
Development and testing Workflow, tests, tooling, CLI import and export
Configuration reference Every environment variable
Deployment Docker, Compose, Helm, MCP routing, security boundary
Read-only demo mode How the PGlite demo is built
Design system Layout contract and UI conventions
Helm chart reference Chart values, Secrets, CronJobs

πŸ“ Project layout

src/routes/                 TanStack Router file routes and API endpoints
src/components/             Shared application UI and shadcn/ui primitives
src/modules/                Domain modules: dives, sites, gear, export, media, profile, …
src/modules/mcp/            Scoped MCP tools, owner policy, and built-in OAuth 2.1 server
src/modules/integrations/   Generic import service, runs, and provenance
src/modules/divemate/       DiveMate .ddb reader, mapper, and writer
src/modules/garmin/         Garmin Connect client, FIT mapping, and import flow
src/db/                     Drizzle schema and database connection
drizzle/                    Committed migrations and metadata
scripts/                    CLI entry points: migrate, seed, sync, and export
charts/                     Helm chart

Warning

src/routeTree.gen.ts is generated. Run bun run generate-routes instead of editing it manually.

πŸ“„ License

MIT


Open-source software for people who care about their dives. 🌊
Try the demo Β· Star it on GitHub

About

Self-hosted dive log that imports from DiveMate and Garmin, exports to UDDF, CSV, and DiveMate, and exposes a read-only MCP endpoint with a built-in OAuth server

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages