Skip to content

ADR 010: Trips are private, shared by expiring links; crew join read-only (amended 2026-10-26: trip roles)

Context

Trips had a visibility column (private, group, public) that only drove a badge and a card icon: there was no public page, no crew and no link, while the public site promised "share the link or QR code". The owner wanted one simple model: every trip private, a public link anyone can open in a browser (no account, no ids in the URL), and crew who get the trip into their own SailEver account with its miles counting for them. With accounts created through crew links, accepting terms (with the safety disclaimer and future charges) became part of creating an account.

Decision

  1. No visibility levels. Every trip is private. The column is ignored now and dropped in the next migration (strong_migrations: running code must not read a dropped column).
  2. One link table, two kinds (trip_links.kind = public / crew), a 22-character random token as the only thing in the URL, never a record id. Separate routes (/t/, /crew/) and each page looks up only its kind, so one kind of token can't be used as the other.
  3. Every link expires after 30 days, for everybody, the owner included. An owner who opens an old link sees what their friends see and knows to make a new one. Links are created only by a click.
  4. Public links add up; a new crew link replaces the old one (enforced by a partial unique index). Crew stay when their link ends; joining is permanent until they leave or are removed.
  5. Crew are read-only. Only the owner edits, exports and shares. A crew member's totals count the crewed trip, but never a date twice: own days win, then the longest crewed day of a date.
  6. Names on the public page are opt-in (users.show_first_name_publicly, off by default) and first names only. Signed-in owner and crew see first name + last initial of each other.
  7. The public page shows the live position (the newest point of a track still being sent) and refreshes itself every minute. The position moves at the watch's upload rhythm (about hourly); faster live tracking would need a watch app change, which is not part of this decision.
  8. A crew link counts as an approved beta invite: during the beta, an unknown email that comes from a working crew link may create a full account.
  9. Terms accepted like the privacy consent: versioned (config/terms.yml), a required checkbox at sign-up and on the beta form, and the consent page for everyone else. The privacy policy went to version 2 (sharing and crew data added), so existing users see one page for both.
  10. Considered and rejected: a permanent share slug on the trip (can't be withdrawn without a column change and never ends on its own); per-viewer visibility levels (the old model, unused); counting crewed miles in full (the same day sailed and recorded twice would double the miles); a crew member export (only the owner exports, by the owner's choice).

Consequences

  • Privacy: a public link exposes positions and the boat to anyone who has it, for 30 days; the privacy page says so, and nothing is public without the owner's click. Links and memberships cascade with the trip and with either account (account_erasure_test.rb).
  • The mobile specs and click dummies still show visibility levels; the specs were updated, the dummies follow.
  • Legal text (terms, privacy version 2) was written by the developer, not a lawyer; the owner reviews it before launch.

Amendment 2026-10-26: trip roles, the skipper edits

  • Deciders: Owner (plan docs/plans/trip_replay_skipper_menus_plan.md, agreed 2026-10-26)

Owners often sail on a charter boat whose skipper is a friend in the crew; the skipper should be able to keep the trip's logbook right. Decision 5 ("Crew are read-only. Only the owner edits, exports and shares.") is replaced by:

  1. Everyone on a trip has one role: owner, skipper, crew, or public on a public link. What each may do is one permission table (TripRole::PERMISSIONS, sharing.md § Roles). Code asks for permissions, never roles, so a further role (co-skipper, watch leader) is one more row.
  2. Crew stay read-only.
  3. The skipper (a crew member the owner or skipper marks, at most one per trip) has the owner's rights, except: deleting the trip, deleting tracks they didn't upload, and moving tracks to other trips. "I was the skipper" (the owner) and a crew skipper exclude each other.
  4. The owner can always do everything, including editing and deleting the tracks the skipper uploaded.
  5. Tracks stay the trip owner's. A track the skipper uploads becomes the owner's track (a trip only holds its owner's tracks, ADR 015); tracks.uploaded_by_id records the uploader so the skipper can delete their own uploads. When the skipper leaves or deletes their account, the track stays and the uploader is cleared.
  6. Considered and rejected: the skipper as a second owner (deleting the trip would delete the owner's recordings); tracks uploaded by the skipper staying the skipper's (a trip would hold tracks of two accounts, and the owner couldn't keep their logbook complete).