feat(api): rate-limit /api/v1 and report when a trip last changed

Two gaps from the first pass, both about a client that polls.

Rate limiting is keyed by user rather than by IP. A self-hosted integration and
its owner's browser routinely leave the same address, and an IP bucket would let
one starve the other; 120 requests a minute is generous for a sync surface and
still bounded, so a runaway poll degrades its own integration instead of the
instance.

updated_at now ships with every trip, and its docblock says plainly what it is
not: a content timestamp. It moves when the trip's own fields change — title,
dates, currency, cover — and stays put when a place, a day or a note is edited,
because the child tables carry no updated_at of their own. That also rules out
the ?since= filter this was meant to enable: it would silently hide every trip
whose itinerary changed, which is worse than no filter at all. Making it honest
needs a schema change, not an API field, so the field is documented for what it
is and the filter is left out.
This commit is contained in:
Maurice
2026-08-28 01:12:44 +02:00
committed by Maurice
parent fd6381c814
commit 9cddf683a5
6 changed files with 69 additions and 8 deletions
@@ -101,6 +101,15 @@ export const publicApiTripSummarySchema = z.object({
end_date: z.string().nullable(),
currency: z.string().nullable(),
archived: z.boolean(),
/**
* When the trip's own fields last changed — title, dates, currency, cover.
*
* Explicitly NOT a content timestamp: editing a place, a day or a note does not
* touch it, because the child tables carry no `updated_at` of their own. A client
* that polls must compare the payload it received, not this value; using it to
* skip a fetch would silently miss every itinerary edit.
*/
updated_at: z.string().nullable(),
});
export type PublicApiTripSummary = z.infer<typeof publicApiTripSummarySchema>;