Skip to content

SDK publishEvent() targets POST /streams/events, which the API does not expose: no ingestion path exists #514

Description

@Xhristin3

Problem

StreamingClient.publishEvent() (xstreamroll-sdk/src/client.ts) sends events to POST /streams/events, but the API has no such route. The full route inventory (api/src/streams/streams.controller.ts and every other controller) contains no POST /streams/events — the only ingestion-adjacent route is the worker's GET /streams/pending. An SDK consumer calling publishEvent() gets a 404 wrapped in ApiError every time. There is no way to get data into stream_data (the table the worker polls) over HTTP at all.

The SDK is the advertised integration surface: xstreamroll-sdk/README.md and the StreamingClient API present publishEvent as the primary method ("publishing events"), and the SDK integration tests (xstreamroll-sdk/__tests__/client.integration.test.ts) mock the route with nock (.post("/streams/events")), so the SDK's own suite passes while every real call fails.

This is the other half of the same hole as the missing POST /streams/processed route: the platform has a processing worker, a pending queue, and an event-replay API, but no HTTP surface for events to enter the system.

Meanwhile STREAM_API_KEY is mandatory (z.string().min(1, "STREAM_API_KEY is required") in api/src/config/env.ts), documented as "API key for stream authentication" (README, CONTRIBUTING.md, api/.env.example, k8s secrets), and enforced nowhere in the API — the natural enforcement point is the new ingestion endpoint.

Root cause

// xstreamroll-sdk/src/client.ts — publishEvent()
await this.requestJson<void>("/streams/events", {   // ← route does not exist
  method: "POST",
  body: { clientId: this.clientId, ...event, timestamp: new Date().toISOString() },
})

Why this is architecturally hard

  1. The ingestion endpoint defines the shape of the StreamEvent wire contract for the first time. packages/types/src/stream-event.ts already models StreamEvent/StreamEventRecord, but nothing on the server validates or stores an incoming event, so the endpoint must pick validation rules (which fields are required, size caps, schema versioning) and write them into the shared contract so SDK and API cannot drift.
  2. Authentication: the worker polls GET /streams/pending unauthenticated ("In production this route should be firewalled to the internal service network" — controller JSDoc). A public ingest endpoint cannot be anonymous; deciding to gate it with the existing-but-unused STREAM_API_KEY env var is the smallest coherent step, and the key's storage (k8s Secret, docker-compose env, .env.example) already exists.
  3. Write path: events must land in stream_data (the worker's poll source, api/src/streams/repository/streams-db.repository.ts getPendingEvents). The DB repository needs a matching insertPendingEvent method; the in-memory repository (api/src/streams/repository/streams.repository.ts) has no equivalent, so tests need one.
  4. The SDK's publishEvent currently sends clientId + a timestamp it stamps itself. The server must decide whether to trust client timestamps (latency metrics in GET /streams/:id/analytics are computed from stream_events.processing_latency_ms, which derives from the event timestamp) — trusting client clocks is a correctness decision worth making deliberately.

Proposed design

  • POST /streams/events (single event) authenticated via X-Stream-Api-Key (or Authorization: Bearer <STREAM_API_KEY>), validating against a Zod/class-validator schema derived from packages/types' StreamEvent, inserting into stream_data (stream_id, data, timestamp).
  • SDK: send the API key via StreamConfig.apiKey, drop the client-stamped timestamp if the server stamps its own (or keep it, per the decision above), and keep publishEvent semantics.
  • Add a contract test (see tests/contracts/src/streams.contract.ts for the existing pattern) covering POST /streams/events so the SDK and API sides cannot drift.

Downstream impact

  • xstreamroll-sdk: StreamConfig (xstreamroll-sdk/src/types.ts) gains an apiKey-style field; publishEvent request shape may change. This is a public SDK surface change — bump the minor version and update the SDK README example.
  • packages/types: StreamEvent may need a server-accepted shape clarification (see the shared type in packages/types/src/stream-event.ts).
  • xstreamroll-sdk/__tests__/client.integration.test.ts: the nock mocks for /streams/events must reflect the final request shape.
  • Contracts: tests/contracts/src/streams.contract.ts gains the ingest contract; provider verification runs in api/src/contract-provider.spec.ts, consumer verification in xstreamroll-sdk/__tests__/contract.consumer.test.ts.

Acceptance criteria

Contract

  • POST /streams/events exists, is Swagger-documented, and accepts a valid event, returning 2xx.
  • An event posted through the endpoint appears in GET /streams/pending and, after the worker processes it, in GET /streams/:id/events.
  • The SDK's publishEvent() against a real API instance (not nock) returns success and the event is visible downstream.

Security

  • The endpoint rejects requests without the valid STREAM_API_KEY with 401, and the key is read from the same env var that api/src/config/env.ts validates.
  • Oversized or schema-invalid payloads are rejected with 4xx and are not persisted.

Tests

  • API integration test: POST /streams/events → row in stream_data (use the harness in api/src/database.integration.spec.ts).
  • Contract test added for the endpoint; both provider and consumer suites pass (cd api && npm test, cd xstreamroll-sdk && npm test).
  • SDK integration test updated to the real request shape (no clientId-only drift).

Documentation

  • SDK README example shows publishEvent against the real endpoint, and the STREAM_API_KEY usage is documented in api/.env.example and README.

Out of scope

Batched ingestion, an authenticated GET /streams/pending for external consumers, and replay/at-least-once guarantees on the ingest side (the worker dedupe story is tracked separately in the POST /streams/processed work).

Getting started

Real files in scope: api/src/streams/streams.controller.ts, api/src/streams/repository/streams-db.repository.ts, api/src/streams/repository/streams.repository.ts (in-memory), api/src/config/env.ts, xstreamroll-sdk/src/client.ts, xstreamroll-sdk/src/types.ts, xstreamroll-sdk/__tests__/client.integration.test.ts, tests/contracts/src/streams.contract.ts.

Verify with:

cd api && npm run typecheck && npm test
cd ../xstreamroll-sdk && npm run typecheck && npm test
npm run build --workspace=tests/contracts

Good first files to read: xstreamroll-sdk/src/client.ts (publishEvent), api/src/streams/repository/streams-db.repository.ts (getPendingEvents to mirror the insert), packages/types/src/stream-event.ts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignapiREST API design and endpointsbugSomething isn't workinghigh impactsdkRelated to xstreamroll-sdk/streamingCore streaming functionality

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions