Signal K server plugin that accumulates vessel positions into tracks and implements the track API.
Positions are recorded to a SQLite file in the plugin's data directory, at a configured time resolution, so tracks survive a restart with no other plugin required. The plugin is enabled on install and needs no configuration to start recording.
| Setting | Behaviour |
|---|---|
| Track resolution | Minimum spacing between recorded positions. |
| Days of the own vessel's track to keep | Trims only the own vessel. 0 keeps everything; it is never dropped for idling. |
| Days to keep another vessel after its last fix | Drops a vessel that has gone quiet. 0 keeps every vessel indefinitely. |
The plugin's configuration page shows the current defaults, and is the authority on them.
The package also exports a client side TrackAccumulator class that manages the track for a single
vessel, exposing the result as Observable<LatLngTuple[]>.
With a history provider installed — signalk-questdb, signalk-to-influxdb2 — a track is answered from both it and the plugin's own store: the provider is the finer record for as long as its retention reaches, and the store is what remains of everything older or of any period the provider missed.
Nothing needs configuring for this, and the plugin works fully with no provider installed. Its own store is a SQLite file in the plugin's data directory, so tracks survive a restart on their own. See docs/history-and-storage.md for what that means in practice.
The own vessel is kept indefinitely by default. Other vessels are kept for 30 days after their last fix — a harbour puts hundreds of AIS targets past a receiver in a day, and keeping every one of them forever is rarely what anybody wants. Both are settings.
A receiver occasionally reports a position far from the vessel — a bad almanac, a multipath reflection, a unit resetting. On a live map it flickers past. In a stored track it is permanent: one spike stretches the bounding box across an ocean and draws a line over the chart every time the track is rendered.
Positions implying a speed above Discard positions implying a speed above this (knots) since the previous accepted fix are discarded, defaulting to 100 knots. That is well above any real vessel, and glitches usually miss it by orders of magnitude rather than by a little. The test is speed rather than distance, so a track resuming after a long gap — a passage with the plugin stopped, or an AIS target reappearing — is not filtered. Set it to 0 to record everything.
Positions a history provider supplies are not filtered here — they are that provider's record, and it is the one that decides what to keep.
A boat on a mooring for the winter emits a position every second and travels nowhere. Setting Pause recording while navigation.state is one of stops those months costing any rows.
On by default for moored, not-under-way and aground. It needs navigation.state to be set —
by signalk-autostate or by hand — and a
vessel that reports no state is always recorded, so an install with no state source behaves as
though this were off. Clear the list to record regardless of state.
Two limits are deliberate. anchored is offered but rarely wanted: an anchor alarm watches
exactly the track a vessel makes while swinging on its rode, so pausing there would break it.
And AIS targets are never paused — their navigational status comes from the transponder and
is often stale, so a vessel under way still reporting moored would otherwise vanish from the
track.
While paused, the plugin says so in its status on the server dashboard.
navigation.position often arrives from several sources at once — an internal GPS, an AIS
transponder, a plotter echoing its own fix. Signal K decides which one wins through source
priority, and the stream this plugin records from is already filtered by it, so normally only
the winning source is stored.
When no priority rule matches the path, though, every source comes through. Their fixes are
metres apart, so the track zigzags between receivers instead of following the boat. If that is
happening, the plugin says so in its status on the server dashboard, naming the sources it has
seen. The fix is to set a source priority for navigation.position in the server settings.
Retrieve track for an individual vessel:
/signalk/v1/api/vessels/<vesselId>/track
<vesselId> may be self or a fully qualified context such as urn:mrn:imo:mmsi:123456789.
Retrieve the own vessel's track:
/signalk/v1/api/self/track
Narrow a track to a time window:
/signalk/v1/api/self/track?from=2026-08-09T06:00:00Z&to=2026-08-09T12:00:00Z
/signalk/v1/api/self/track?duration=6h
from and to are ISO-8601 timestamps; to defaults to now. duration is a window
ending now. A window ending at now includes its most recent point; one ending earlier is
half-open, so consecutive windows tile without returning the shared point twice.
timespan and timespanOffset are also accepted for Freeboard-SK compatibility, where
timespan=23h×panOffset=1 means "23 hours ending an hour ago". They are not part of
the proposed track API and are expected to be superseded by from/to.
Reduce the number of points returned:
/signalk/v1/api/self/track?duration=24h&resolution=5m
resolution is the minimum spacing between returned points. Durations accept a bare
number of seconds or an s/m/h/d suffix. The first and last points are always kept,
so thinning never shortens the track.
Retrieve the time each position was recorded:
/signalk/v1/api/self/track?duration=6h×
{
"type": "MultiLineString",
"coordinates": [
[
[24.9, 60.1],
[25.0, 60.2]
]
],
"times": [["2026-08-14T09:00:00.000Z", "2026-08-14T09:01:00.000Z"]],
"context": "vessels.urn:mrn:imo:mmsi:123456789",
"isSelf": true,
"name": "Own Ship"
}times adds a times array positionally aligned with coordinates: times[i][j] is when
coordinates[i][j] was recorded, as ISO-8601 UTC. It is opt-in because the response grows by
roughly a third and clients that only draw the geometry have no use for it. Accepts
true/1/yes and false/0/no; a valueless ?times reads as true.
context is the fully qualified context the track belongs to, and isSelf says whether it is
the own vessel. Asking for self resolves the alias, so the response tells you which vessel
self actually is.
name is a display label, the way a chart plotter shows one: Own Ship, or AIS <shipname>
falling back to AIS <mmsi> and finally the raw context. It is for putting in a list — the v2
Track API's contextName carries the undecorated vessel name instead, and is absent for a
vessel that has not sent one.
Retrieve tracks for all vessels:
/signalk/v1/api/tracks
Every vessel the plugin holds a track for. Add ?radius= to narrow it to vessels near your own.
Each entry carries isSelf, so the own vessel can be told from an AIS target without
string-matching the context against the server's self-identity, and name for display.
?times works here too, adding a times array to every vessel's entry. Note that asking for
times also segments each track on the gap threshold, so coordinates and times line up;
without times each vessel keeps its single unsegmented line.
Retrieve tracks for all vessels within a given radius (in meters) from your vessel position:
/signalk/v1/api/tracks?radius=50000
Distance from the own vessel's current position, matched against each track's last position — "which vessels are near me now". The v2 Track API asks a different question and matches any position in the window.
Retrieve tracks for all vessels within a bounded area:
/signalk/v1/api/tracks?bbox=130,-35,139,-33
Bounded area is defined as west, south, east, north — GeoJSON coordinate order, the
same as the coordinates this endpoint returns, the Resources API, and the v2 Track API.
A box crossing the antimeridian is expressed with west greater than east, for
example bbox=175,-10,-175,10.
Changed: this parameter was
lat1, lon1, lat2, lon2— latitude first — up to and including 2.0.2. A box in the old order is still four valid numbers, so it will not be rejected; it will simply describe a different area. Freeboard-SK is unaffected, as it filters byradiusrather thanbbox.
npm ci
npm run build # vite library build -> dist/
npm test # vitest
npm run test:e2e # against a real signalk-server; see below
npm run typecheck # tsc --noEmit
npm run lint # eslint
npm run format # prettier --writenpm run test:e2e packs the plugin, installs it into a throwaway config directory, boots a real
Signal K server against it and feeds positions as deltas — so it covers plugin loading, route
mounting and the delta path, none of which the unit suite can. It needs a built server checkout;
set SIGNALK_SERVER_DIR if yours is not at ~/dev/xxx_signalk-server. A second tier installs a
real history provider (signalk-questdb) into that server and exercises the reconciliation through
it; it skips itself if no QuestDB is reachable at QUESTDB_URL. Neither tier runs in CI.
The package is ESM only and targets Node >= 22.5.0, the release that added node:sqlite. ESM alone
would only need 20.19, the first release in which the Signal K server's require()-based plugin
loader can load an ES module, but recording to SQLite raises the floor. The server itself
requires Node >= 22, so this rules out nothing that could have run the plugin anyway.