Context
This is the front door of the whole platform and the endpoint every integration touches first. Its latency is the first thing a developer measures, and it sits inline in someone else’s checkout, so the budget is not negotiable. ADR-013 resolves the tension between “acknowledge fast” and “do a lot of work” with fast-ack: authenticate, validate, rate-limit, enqueue, answer 202. The heavy path — identity resolution, segment evaluation, rules matching — happens behind the queue. The cost is that reads are eventually consistent, and the contract says so explicitly by answering 202 rather than 200. Retries are guaranteed, because networks fail. Idempotency is therefore not an optimisation but the correctness condition.Scope (normative)
POST /v1/core/track,/identifyand/batchwith fast-ack.core.tracked_events, append-only, monthly RANGE partitioned.- Idempotency key per tenant, deduplicating at the storage fence.
- Rate limiting in Upstash, per tenant and per key.
- The processor: identity resolution, append, and hand-off to segment evaluation and rules.
- Retention by plan with export to Storage before dropping a partition.
core.event.trackedon the outbox.
Non-scope (normative)
- Segment evaluation itself — FS-CORE-0008.
- Rules matching — FS-LOY-0004.
- Public browser write keys — F1b, with the widget (DEC-D6). F1a is server-side only.
- Segment-style aliases. The canonical path is module-scoped (DEC-D2); a compatibility layer only if a real integration demands it.
Behaviour (normative)
- The endpoint does five things and nothing else: authenticate, Zod-validate, rate-limit, enqueue, answer 202 with the event id. p95 <100 ms. FORBIDDEN: any business logic on this path.
- The response is 202, never 200. The status code is the contract that processing has not happened yet, and it is what stops an integrator from writing a read-after-write assumption.
idempotency_keyis unique per tenant. A duplicate returns 202 with the original event id and creates no second row and no second downstream effect.occurred_atis supplied by the caller with its timezone;received_atis ours. Both are stored — a point of sale that was offline for an hour must not have its events reordered.- The processor is idempotent through
core.processed_jobsand the event’s idempotency key. Two fences, because this is the path where a duplicate costs points. - Identity resolution runs first: an unknown identifier creates an anonymous contact rather than dropping the event.
tracked_eventsis append-only and monthly partitioned. Every query carries the partition key — a query bycontact_idalone is rejected in review and by the CI lint.- Event properties are JSONB, validated against the installed taxonomy where one applies (FS-CORE-0012) and accepted permissively where none does.
- Retention by plan: 13 / 25 / 37+ months. A partition is exported to Storage as compressed NDJSON before being dropped, never simply dropped.
- Rate limits are published per plan (DEC-D4) and returned in
RateLimit-*headers.
Data (normative)
API (normative)
Events (normative)
core.event.tracked on the outbox after the processor appends. Available as an outgoing webhook,
which is how a tenant mirrors its own event stream back into its warehouse.
Acceptance criteria (normative)
trackp95 <100 ms under the k6 profile at the documented target throughput.- The same
idempotency_keysent 100 times produces one row and one downstream effect. - Events arriving with
occurred_atan hour in the past land in the correct partition and are ordered byoccurred_at, not by arrival. - A batch of 500 is accepted; 501 is rejected with a typed error.
- End-to-end effect visibility <5 s p95: event in, points awarded, notification queued.
- A month-boundary synthetic test leaves the DEFAULT partition empty.
- Retention export produces a readable NDJSON file with a verified checksum before the partition is dropped.
- Negative: a query against
tracked_eventswithout a time predicate fails the CI lint.
Execution
Asynchronous event pipeline — the canonical archetype. Endpoint inbackend/api, processor in
backend/workers, retention and partition jobs in backend/scheduler. This is TS-003, and
trackEvent (FS-LOY-0004 + TS-004) completes it.