Skip to content

ADR 019: Public skipper profile

  • Status: Accepted
  • Date: 2026-11-01
  • Deciders: Owner (skipper profile plan and its answers, 2026-11-01; the plan was removed once built)
  • Technical Context: apps/server; behaviour in docs/specs/server/profile.md and logbook.md

Context

Skippers want a public page that shows what they have sailed, to send to charter companies, crews and friends. It is a PRO feature. It publishes personal data that was private until now: statistics over all trips, where trips started, chosen trips with their tracks, a photo. It needs its own address, rules for names in it, a way to pick trips, and images that are cheap to serve.

Decision

  1. Address. skipper.sailever.com/<handle> on its own host (SAILEVER_SKIPPER_HOST). A host constraint that matches nothing while unset keeps /<handle> from catching other paths. Every other host answers at /s/<handle>, which redirects to the skipper host in production and is the page in development and tests.
  2. Handle. Chosen by the user, 3–30 characters of a-z0-9-, reserved words refused. A changed handle stays reserved for its old owner for 30 days, and the handle can't change again in that time (table profile_handle_reservations). There are no redirects from old handles.
  3. Opt-in, per trip. The profile is off by default. Each trip appears only when its user picked it (profile_trips), for their own trips and trips they sailed on as crew (they could share those by public link anyway). ProfileTrip is the access scope for the profile trip pages (ADR 015): the trip id in the path is resolved only among a public profile's picks, else 404.
  4. Globe positions. Every trip with counted days is a dot at its exact start position, picked or not (owner). Unpicked dots carry no title, date or link: the page's data for them holds only the position and miles.
  5. Statistics. The profile shows the same blocks as the logbook from the same engine (LogbookStats), so the numbers always agree. No per-block switches: the skipper controls the page through the trips they pick and their bio.
  6. Images cropped in the browser. The skipper chooses the part of the avatar (1:1, shown round) and header image (4:1) in a Cropper.js dialog. The browser draws it at its final size and re-encodes it as WebP, which drops all photo metadata, GPS included. The server stores that image and the original (private, only for choosing another part later), and needs no image processing (libvips stays unused). Shown images go through the Active Storage proxy on an image host behind Cloudflare (SAILEVER_IMAGE_HOST). Every upload is a new blob key, so a cached image never needs purging.
  7. Indexing. noindex unless the skipper allows it, and then only the profile page. Its trips are never indexed.
  8. Night miles for the profile's numbers use nautical night (sun more than 12° below the horizon, owner), computed per point pair when a trip's days are built.

Consequences

  • New personal data, all removed with the account: handle and settings, crops, four attachments, picks and reservations. The privacy page describes the profile; no new consent is asked (the profile is opt-in, owner).
  • Owner steps before deploy: DNS for skipper (DNS only) and images (proxied, cache rule "cache everything" for /rails/active_storage/*). Both hosts go into the Kamal proxy and the environment (SAILEVER_SKIPPER_HOST, SAILEVER_IMAGE_HOST).
  • Grey dots show where every trip started, which the skipper accepts by turning the profile on; the settings say so.
  • A crew member can publish a trip owned by someone else; the page shows names only by the public trip page's rules.