Skip to main content

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, /identify and /batch with 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.tracked on 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)

  1. 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.
  2. 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.
  3. idempotency_key is unique per tenant. A duplicate returns 202 with the original event id and creates no second row and no second downstream effect.
  4. occurred_at is supplied by the caller with its timezone; received_at is ours. Both are stored — a point of sale that was offline for an hour must not have its events reordered.
  5. The processor is idempotent through core.processed_jobs and the event’s idempotency key. Two fences, because this is the path where a duplicate costs points.
  6. Identity resolution runs first: an unknown identifier creates an anonymous contact rather than dropping the event.
  7. tracked_events is append-only and monthly partitioned. Every query carries the partition key — a query by contact_id alone is rejected in review and by the CI lint.
  8. Event properties are JSONB, validated against the installed taxonomy where one applies (FS-CORE-0012) and accepted permissively where none does.
  9. Retention by plan: 13 / 25 / 37+ months. A partition is exported to Storage as compressed NDJSON before being dropped, never simply dropped.
  10. 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)

  1. track p95 <100 ms under the k6 profile at the documented target throughput.
  2. The same idempotency_key sent 100 times produces one row and one downstream effect.
  3. Events arriving with occurred_at an hour in the past land in the correct partition and are ordered by occurred_at, not by arrival.
  4. A batch of 500 is accepted; 501 is rejected with a typed error.
  5. End-to-end effect visibility <5 s p95: event in, points awarded, notification queued.
  6. A month-boundary synthetic test leaves the DEFAULT partition empty.
  7. Retention export produces a readable NDJSON file with a verified checksum before the partition is dropped.
  8. Negative: a query against tracked_events without a time predicate fails the CI lint.

Execution

Asynchronous event pipeline — the canonical archetype. Endpoint in backend/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.

Open questions

Changelog

Delivery record

Not implemented yet.