Personal sailing voyage log: trips, legs, ships, crew and GPX tracks with honest statistics.
app.seamark/seamark (MCP Server)
The Model Context Protocol (MCP) server app.seamark/seamark provides access to βPersonal sailing voyage logβ data, covering trips, legs, ships, crew, and GPX tracks with honest statistics.
π οΈ Key Features
Personal sailing voyage log organization
Trip and leg records
Ship and crew information
GPX track data
βHonest statisticsβ associated with the logged data
π Use Cases
Reviewing a sailing voyage by trip and leg
Associating routes (GPX tracks) with specific legs
A personal voyage log for sailing trips. Seamark records trips, legs, ships, crew, and charter details, imports GPX tracks and cleans them server-side into honest statistics (distance, times, average and sustained-max speed, and night hours from a solar day/night split) with an auditable record of what was discarded and why, and shows the routes on a Leaflet map. It also derives which countries and which sea areas each voyage visited β from bundled, heavily simplified coastline and IHO sea-area polygons, entirely offline, so no position ever leaves the server for this β and shows them as flags on the trip list and named waters on the trip view and the logbook. Each leg also records the weather it was sailed in β wind speed and direction plus the Douglas sea state, typed in by hand, because no GPX file carries them; one optional click fills the wind from the free Open-Meteo service instead. That button is the only thing in Seamark that ever contacts a third party from the server, and all it sends is a position rounded to two decimals (~1 km) plus a date range β no track, no identity, nothing else. Any leg or whole trip can be downloaded back as a GPX track β by you or the crew a trip is shared with β to import into your own software. A trip can also be turned into a public, read-only share link β no login, revocable, off by default β so family and crew can follow along (a redacted view: no charter, crew shown as initials only). Its homepage is a lifetime logbook dashboard β every voyage you have ever logged on one chart, plus whole-log totals, a season-by-season and boat-by-boat breakdown, and a skipper/crew/all view that filters the map and the figures together β counting both your own voyages and the ones shared with you as crew. A visitor without a session sees a public landing page there instead β what the app does, shown with real screenshots, and a sign-in / create-account call to action. It is a hobby project β not a product β but multi-user from the start, with self-registration, an English, Czech, German, Spanish and Slovak UI, and an MCP server (OAuth 2.1) so AI agents can do anything a human can do in the UI. Running an instance is its own screen: an admin gets /admin β what the instance holds and what it occupies on disk, the self-registration switch, and a searchable, paged account list with a page per account β so administering the place never gets mixed into anybody's personal settings. The app carries its own user manual β a public /help page, in every UI language, describing every feature from the user's side, the basic workflows end to end, and how to connect an AI agent. Your data stays yours: one click in Settings downloads the complete log as a ZIP archive β plain JSON plus one GPX per leg (cleaned and raw), with the format documented inside.
For what's built and what's planned, see this project's GitLab work items β the per-story status is maintained there rather than duplicated here.
Architecture at a glance
One Bun process, one SQLite file. Small, boring, proven: TypeScript (strict) run directly by Bun (no build step for the API), Fastify for HTTP, SQLite (WAL) via bun:sqlite with Drizzle for schema/migrations. Production is a 4 GB no-swap shared VPS, so the whole app targets ~150β200 MB RSS β no headless browser, no self-hosted tile server, no second database process.
Two equal surfaces over one core. The REST API (for the SPA) and the MCP server (for AI agents) are thin adapters over the same shared operations registry β a defineOperation(ctx, input) layer that keeps REST and MCP in lockstep by construction. ctx carries the authenticated principal, the database handle, the mailer, and the outbound-mail budget.
Schema-first contracts. TypeBox schemas in @seamark/contracts emit JSON Schema and inferred TypeScript types from a single definition, feeding Fastify route validation, MCP tool schemas, and (later) the frontend β so the wire contract can't drift between surfaces.
Track processing is the core value: server-side, deterministic, auditable β the same GPX always yields the same statistics.
Auth: passwordless magic-link login, 90-day sessions, and smk_-prefixed API tokens β all secrets stored only as SHA-256 digests.
Trips are owner-private, with one deliberate exception. A crew member who accepted an email invitation is linked to the owner's address-book entry for them (people.user_id), and can read every trip of that owner they are crew on β past and future, listed under "Shared with me" β but never the charter, never a write, and never any other resource. The owner revokes the link, and with it every trip at once, from the person's form. Every other query in the app is scoped WHERE owner_user_id = β¦ with no exceptions at all. Separately, an owner can deliberately publish a trip as a public read-only link (off by default, revocable) β a redacted, anonymous view resolved by an unguessable token, never a widening of who may write.
Mail is transactional or opt-out-able, never both. Sign-in links and crew invitations always send; the new-trip announcement is a per-user preference in Settings, enforced in SQL by one shared gate. There is no unsubscribe link anywhere β control lives inside the signed-in app.
Repository layout
Path
What it is
apps/api/
The Fastify API + MCP server β the single deployed process (also serves the built SPA in production).
apps/web/
The React 19 SPA (strict TS), bundled by Bun β scripts/build.ts (Bun.build) and scripts/dev.ts (Bun.serve, proxies the API paths); the production build is served by apps/api.
packages/contracts/
@seamark/contracts: shared TypeBox schemas and types. Schemas only, no logic.
docs/
All project documentation (see below).
deploy/
Release + host artifacts: build-and-push.sh and release.sh (local build/push/deploy β there is no CI), seamark-deploy.sh (the host's forced-command deploy script the release triggers over SSH), env.template/release.env.template, seamark-backup.sh/seamark-restore.sh (nightly backup + tested restore), and apache-seamark.conf.template (the reverse-proxy vhost template).
scripts/
The policy gate's own pieces: policy.test.ts (the tree-level checks, run by the repo-root bun test) and oxlint-plugin.mjs (a custom oxlint rule). The lint rules are configured in the root .oxlintrc.json.
root
Bun workspace config (package.json#workspaces, bun.lock), the bunfig.toml test config, .oxlintrc.json, strict shared tsconfig.base.json, production docker-compose.yml, apps/api/Dockerfile, and server.json (the MCP registry listing β see docs/OPERATIONS.md).
Getting started
Requires Bun β₯ 1.4 (engines.bun) and nothing else β Bun is the runtime, the package
manager, the test runner and the SPA bundler. Install it from bun.com.
bash
bun install # install workspace deps
bun run typecheck # strict TS across all packages and repo-root scripts/
bun run check:policy # convention gate: oxlint (no any/as/!, banned imports, β¦) + tree checks
bun run test# every package's tests, the web suite last
There is no CI: typecheck, check:policy, and test are the local gate deploy/release.sh runs
before building and deploying a release (see docs/OPERATIONS.md).
Running the app locally
One command starts everything (Docker required β it runs the mail catcher):
Login is magic-link only, so mailpit is how you get in: request a link in the
SPA, open the mail in mailpit, follow it. Nothing is sent to a real inbox in
development. Without a mail catcher the login screen still reports success β
the send is fire-and-forget and its failures are only logged β so a missing
mailpit looks like a broken login rather than a missing dependency.
bun run dev starts mailpit via compose.dev.yml (dev-only; production's compose
file is separate and must stay that way β see the comment at its top), then runs
the API and the SPA dev server in parallel. The SQLite file and uploaded track
files are created under apps/api/data/ on first boot β migrations run at
startup, so there is no separate database step. Stop the app with Ctrl-C;
mailpit keeps running in the background (docker compose -f compose.dev.yml down
to stop it).
A fresh instance is an empty logbook. To have something presentable to look at β
one seven-leg voyage around Lofoten with real (date-shifted) GPX tracks, an
invented crew, weather and a charter record β seed the demo voyage:
bash
SEAMARK_DATABASE_PATH=$PWD/apps/api/data/dev.sqlite bun run --filter @seamark/api db:seed-demo
It creates the demo owner petra@example.cz (or reuses it; set
SEAMARK_SEED_EMAIL to seed into another existing account instead) β sign
in by requesting a magic link for that address and following it from mailpit. The same dataset
produced the landing page's screenshots, and its GPX fixtures live in
apps/api/test/fixtures/demo/.
The API reads configuration from the environment only β there is no .env
support, by design (apps/api/src/config.ts). bun run dev exports what it
needs; if you run the processes by hand, export SEAMARK_DATABASE_PATH yourself
or the API exits at startup. See docs/ARCHITECTURE.md β Configuration for the
full variable table β including the optional, off-by-default SEAMARK_UMAMI_*
privacy-analytics wiring (docs/ARCHITECTURE.md β Analytics).
bun run --filter @seamark/web build produces the static bundle under
apps/web/dist/ that the API serves in production.
Running the production image (Docker)
The production image is a single three-stage apps/api/Dockerfile (repo-root build context) on
oven/bun:1.4-slim: it installs the API's production dependencies, builds the SPA, and runs one
Bun process from TS source (no build step for the API) as the non-root bun user:
bash
docker build -f apps/api/Dockerfile -t seamark:dev .
cp deploy/env.template .env# fill in the values
docker compose up -d
GET http://127.0.0.1:3001/healthz should then return 200, and the SPA loads at /. See
docs/OPERATIONS.md for the full runbook: database inspection without a
sqlite3 CLI, verifying the ~150β200 MB RSS memory budget, and why the compose network is pinned
to a fixed subnet (mail delivery depends on it).
Documentation
Start here, then follow the links:
docs/ARCHITECTURE.md β the source of truth: stack, domain model, ID scheme, GPX pipeline, auth flows, database, deployment. Read before any non-trivial work.
docs/IMPLEMENTATION-GUIDE.md β the binding conventions every change follows (TypeScript, wire/database/operation/ownership/testing policies, dependency upgrades, the policy gates). Read before starting a change.
docs/MODULE-PATTERN.md β the step-by-step recipe for a domain module (contracts β table + migration β operations β routes β tests), with ships as the worked example. Read before adding a new domain module.
docs/API-ERRORS.md β the stable machine error codes and the error-envelope contract.
docs/CONNECTORS.md β setting up Seamark as a custom connector in Claude and ChatGPT, plus the manual verification checklist.
docs/OPERATIONS.md β the deployment runbook: building/running the production image, db:sql database inspection, verifying the memory budget, the pinned-network/mail-delivery coupling, the local build/push + deploy & rollback flow (local build β forced-command SSH β healthz gate), nightly backup/restore, the Apache reverse-proxy vhost, and the end-to-end host-provisioning checklist.
docs/DESIGN.md β the "Ship's Log" design system (tokens, typography, components, voice); source mockups in docs/design/.
The full implementation plan lives in this project's GitLab work items (epics #1β#9, one per epic:* label, linking the analyzed story issues). Work stories in the order given by each story's own Depends on: line.
Keeping docs current
All project knowledge lives in this repo, and the docs are part of every change. Whenever a commit changes behavior, contracts, structure, or conventions, update this README and the affected files under docs/ in the same commit β an out-of-date doc is a bug. This is a binding repo convention (see docs/IMPLEMENTATION-GUIDE.md β "Documentation policy").