#3064·clawhub

perf: bound publisher profile, membership, and catalog reads

Author: vyctorbrzezowskiCreated Jul 13, 2026Updated Sep 18, 2026
LabelsenhancementP2clawsweeper:source-reproclawsweeper:no-new-fix-prclawsweeper:needs-maintainer-reviewclawsweeper:needs-product-decisionissue-rating: 🦞 diamond lobsterimpact:other

Summary

Several publisher-profile and authenticated publisher paths read or serialize complete resource catalogs even when the caller only needs identity, role, counters, or one page. These paths are currently bounded only at the response layer, not at the database-read layer.

Related umbrella: #2925.

Confirmed source evidence

  • publishers.getProfileByHandle calls toPublisherListItem with forceComputedStats, includePublishedItems, and includeStarredCount. This can collect all active skills/packages and all stars. src/routes/user/$handle.tsx does not consume publisher.publishedItems, and it invokes the profile query once in the loader and again as a subscription.
  • publishers.listMine collects the user's memberships, then requests all published items for every publisher. There are eight runtime subscribers; only the destructive summary in Settings consumes publishedItems.
  • publishers.listPublishedPage collects and sorts every active skill and package before applying its offset cursor with slice.
  • publishers.getPublishedDisplayManifest is a sibling profile path that also collects the complete published catalog for GitHub-backed publishers.

These are source-proven unbounded read shapes. This issue does not claim production p50/p95 or billing numbers because Convex production Insights were unavailable during the audit.

Proposed outcome

  1. Make the profile summary use complete denormalized counters when available, with the existing computed fallback for legacy incomplete publishers. Do not include publishedItems.
  2. Add a lightweight membership summary for the eight listMine consumers.
  3. Load deletion inventory only while the relevant destructive dialog is open. Keep authorization in the deletion mutation.
  4. Paginate the published catalog directly through the existing publisher-scoped index for the requested kind.
  5. Explicitly bound, redesign, or document the remaining getPublishedDisplayManifest behavior before claiming the profile path is fully bounded.

Non-goals

  • Changing profile reactivity solely for performance.
  • Adding a migrated starredCount counter in the first change.
  • Solving alternate sorting of starred skills. That requires workload data and a product/consistency decision.
  • Building a mixed skill/plugin cursor: the only runtime catalog caller currently supplies kind.

Acceptance criteria

  • With complete publisher counters, profile and membership summary tests fail if skills or packages are queried.
  • Missing-counter fixtures preserve the existing computed fallback and visibility rules.
  • The membership summary contains no publishedItems; deletion inventory is skipped until its dialog opens.
  • Published skill/plugin pages use indexed pagination without .collect() and have no duplicate or missing rows across cursors.
  • Soft-deleted, blocked, official, legacy-personal, deleted-user, and deactivated-publisher cases retain current behavior.
  • The destructive mutation's authorization and deletion semantics remain unchanged.

Validation

  • Focused Convex tests that enforce table-read bounds, not only returned shape.
  • Profile and Settings component tests for query skip behavior.
  • Seeded large-publisher A/B for documents read and serialized bytes when deployment Insights are available.