Context
GET /streams filters by status, visibility, and ownerOnly (api/src/streams/dto/list-streams.query.dto.ts, ListStreamsQueryDto), but a user cannot find a stream by name or by tag. The platform's discovery story is "list everything, then page through it": with the tags feature live (GET /tags, per-stream tag chips), the natural next query — "streams tagged live" or "streams matching football" — requires the client to fetch pages of every stream and filter locally. That is untenable at any real scale (pagination caps at 100 per page, PaginationQueryDto), and the SDK's paginateAll helper (xstreamroll-sdk/src/pagination.ts) would walk the entire table to answer a single search.
The maintenance trap a naive fix would create is filtering in the app layer (streams-list-live.tsx post-processing fetched pages), which breaks pagination totals and duplicates logic that belongs in one SQL predicate.
Goal
Add server-side search and tag filtering to GET /streams: a q parameter matching stream name/description, and a tag parameter (slug or id) restricting results to streams with that tag — applied inside the existing visibility-aware query so totals and hasMore stay correct.
Scope
1. Query parameters
q (case-insensitive substring on name, and optionally description) and tag (tag slug or id, resolved via TagsService/TagsDbRepository.findBySlug). Extend ListStreamsQueryDto with validation (length caps, format), keeping the existing visibility/status/ownerOnly filters orthogonal.
2. Repository predicate
- Extend
StreamsDbRepository.listPaginated (api/src/streams/repository/streams-db.repository.ts) with the new predicates in the shared WHERE fragment (the COUNT and SELECT already share it, so totals stay correct by construction). The tag filter joins stream_tags/tags; the q filter uses ILIKE with a bounded pattern (escape %/_ so user input cannot become a wildcard). Keep the in-memory StreamsRepository behaviorally equivalent for unit tests.
3. SDK and app surface
- Add
q/tag params to the SDK's stream-listing types/helpers (xstreamroll-sdk/src/types.ts, client.ts if it grows a list method) and to app/lib/api/streams.ts listStreams, so the dashboard search box can pass them through. No UI is required by this issue beyond plumbing the params.
Downstream impact
api/src/streams/dto/list-streams.query.dto.ts, api/src/streams/repository/streams-db.repository.ts, api/src/streams/repository/streams.repository.ts (in-memory), api/src/streams/streams.controller.ts (Swagger docs).
app/lib/api/streams.ts, app/hooks/useStreams.ts (query keys must include the new params or cache collisions occur).
xstreamroll-sdk/src/types.ts (pagination params), README/docs for the list endpoint.
- Contracts: extend
tests/contracts/src/streams.contract.ts with a search query case so provider/consumer suites pin the new params.
Acceptance criteria
Contract
Service
Tests
Documentation
Out of scope
Full-text search ranking, fuzzy matching, and the dashboard search UI itself.
Getting started
Real files in scope: api/src/streams/dto/list-streams.query.dto.ts, api/src/streams/repository/streams-db.repository.ts (listPaginated), api/src/streams/repository/streams.repository.ts, api/src/streams/streams.controller.ts, app/lib/api/streams.ts, app/hooks/useStreams.ts, xstreamroll-sdk/src/types.ts, tests/contracts/src/streams.contract.ts.
Verify with:
cd api && npm run typecheck && npm test
cd ../app && npm run typecheck && npm test
cd ../xstreamroll-sdk && npm run typecheck && npm test
Good first files to read: api/src/streams/repository/streams-db.repository.ts (listPaginated WHERE fragment), api/src/streams/dto/list-streams.query.dto.ts, api/src/tags/repository/tags-db.repository.ts (findBySlug).
Context
GET /streamsfilters bystatus,visibility, andownerOnly(api/src/streams/dto/list-streams.query.dto.ts,ListStreamsQueryDto), but a user cannot find a stream by name or by tag. The platform's discovery story is "list everything, then page through it": with the tags feature live (GET /tags, per-stream tag chips), the natural next query — "streams taggedlive" or "streams matchingfootball" — requires the client to fetch pages of every stream and filter locally. That is untenable at any real scale (pagination caps at 100 per page,PaginationQueryDto), and the SDK'spaginateAllhelper (xstreamroll-sdk/src/pagination.ts) would walk the entire table to answer a single search.The maintenance trap a naive fix would create is filtering in the app layer (
streams-list-live.tsxpost-processing fetched pages), which breaks pagination totals and duplicates logic that belongs in one SQL predicate.Goal
Add server-side search and tag filtering to
GET /streams: aqparameter matching stream name/description, and atagparameter (slug or id) restricting results to streams with that tag — applied inside the existing visibility-aware query so totals andhasMorestay correct.Scope
1. Query parameters
q(case-insensitive substring onname, and optionallydescription) andtag(tag slug or id, resolved viaTagsService/TagsDbRepository.findBySlug). ExtendListStreamsQueryDtowith validation (length caps, format), keeping the existing visibility/status/ownerOnly filters orthogonal.2. Repository predicate
StreamsDbRepository.listPaginated(api/src/streams/repository/streams-db.repository.ts) with the new predicates in the shared WHERE fragment (the COUNT and SELECT already share it, so totals stay correct by construction). The tag filter joinsstream_tags/tags; theqfilter usesILIKEwith a bounded pattern (escape%/_so user input cannot become a wildcard). Keep the in-memoryStreamsRepositorybehaviorally equivalent for unit tests.3. SDK and app surface
q/tagparams to the SDK's stream-listing types/helpers (xstreamroll-sdk/src/types.ts,client.tsif it grows a list method) and toapp/lib/api/streams.tslistStreams, so the dashboard search box can pass them through. No UI is required by this issue beyond plumbing the params.Downstream impact
api/src/streams/dto/list-streams.query.dto.ts,api/src/streams/repository/streams-db.repository.ts,api/src/streams/repository/streams.repository.ts(in-memory),api/src/streams/streams.controller.ts(Swagger docs).app/lib/api/streams.ts,app/hooks/useStreams.ts(query keys must include the new params or cache collisions occur).xstreamroll-sdk/src/types.ts(pagination params), README/docs for the list endpoint.tests/contracts/src/streams.contract.tswith a search query case so provider/consumer suites pin the new params.Acceptance criteria
Contract
GET /streams?q=fooreturns only streams whose name (and description, if chosen) containsfoo(case-insensitive), respecting visibility andownerOnly.GET /streams?tag=<slug>returns only streams carrying that tag, respecting visibility; an unknown tag returns an empty page (not an error) or 404 per the documented choice.totalandhasMorein the response reflect the filtered set, not the unfiltered table.Service
qinput with%/_is treated literally (escaped), not as a SQL wildcard.TagsDbRepository.findBySlug) and does not duplicate slugification logic.Tests
qmatching name and description, case-insensitivity,q+status/visibilitycombinations,tagfiltering, and the escaped-wildcard case.qlength andtagformat.Documentation
qandtagwith examples; SDK/README list-endpoint docs mention them.Out of scope
Full-text search ranking, fuzzy matching, and the dashboard search UI itself.
Getting started
Real files in scope:
api/src/streams/dto/list-streams.query.dto.ts,api/src/streams/repository/streams-db.repository.ts(listPaginated),api/src/streams/repository/streams.repository.ts,api/src/streams/streams.controller.ts,app/lib/api/streams.ts,app/hooks/useStreams.ts,xstreamroll-sdk/src/types.ts,tests/contracts/src/streams.contract.ts.Verify with:
Good first files to read:
api/src/streams/repository/streams-db.repository.ts(listPaginatedWHERE fragment),api/src/streams/dto/list-streams.query.dto.ts,api/src/tags/repository/tags-db.repository.ts(findBySlug).