Instruction file imported from OnSocial-Labs/onsocial-protocol (
.cursor/rules/app-indexer-first-paint.mdc). Copyright stays with the author.
App indexer-first paint
For @onsocial/app (and SDK query helpers it consumes), browse/list/detail first paint must be indexer-first. Do not reintroduce chain RPC fan-out on critical paths.
Source of truth by concern
| Concern | Prefer | Keep on RPC |
|---|---|---|
| Feeds, lists, shells, counts, catalog, owned inventory browse | Hasura / os.query.* / Substreams views |
— |
| Buy / bid / cancel / offer verify | — | Contract views at action time |
| Wallet balances, spend configs, wallet mint/allowlist remaining | — | Contract / BFF |
| Privileged guild ACL at action time | Indexer hint for paint | Chain roles before mutate |
| Writes / wallet submit | — | Always chain |
Required patterns
- SSR critical path — batch indexer reads only (
createServerOnSocialClient). NoviewNearContract,get_collection,nft_tokens_for_owner,social.getOne, or N×getConfigbefore first paint on the happy path. - Soft-fill after paint — structure, brand, ACL, sheet detail may reconcile client-side; never blank a painted shell for it.
- No duplicate keyed refetch — if SSR seed matches the active lens/sort/filter, skip the immediate client re-query (home hot/global and guild default feed are the model).
- No N× RPC under the API key — lists use batch/indexed helpers (
activeListings,memberCountsFor,ownedBy,enrichIndexedGuildSummaryCards). Never N×get_lazy_listing,isAdmin/canModerate, orgetConfigper row for browse. - Owned vault —
os.query.scarces.ownedBy+ catalog enrich; RPCnft_tokens_for_owneronly on Hasura failure. - Feed scarce CTAs —
activeListings/ post path match;get_lazy_listingonly in buy/bid verify sheets. - Drop counters — soft refresh via
collectionCurrent/fetchCollectionPreferIndexer; RPC only if catalog thin/missing. - Browser OnAPI — all client graph traffic still burns
ONSOCIAL_API_KEYvia/api/onapi. Treat extra soft-fetches as load. Prefer fewer, batched queries.
Explicitly OK soft paths (not first paint)
- Guild
getConfigafter SSR for structure/ACL - Series brand
social.getOneafter drops paint - DAO roles via
/api/profile/dao-rolesafter portfolio paint - Sheet-open verify (
get_lazy_listing, auction/offer views,nft_tokentraits)
When adding a feature
- Can first paint come from an existing Substreams view / SDK query? Use it.
- If not, add/extend the indexer + SDK query — do not paper over with browse RPC.
- Soft-fill or action-time RPC only for money, permissions to mutate, or true lag catch-up.
- Skip client refetch when SSR already matches.
- Add a unit test that mocks the indexer client and asserts RPC is not called on the happy path (see
market-listings-indexer.test.ts).
Closed hardening (do not regress)
- Portfolio SSR trusts indexer only; empty
page/mainsoft-fills client-side - Owned vault catalog enrich uses
collectionsCurrentByIds(one round-trip) - Series catalog headings prefetch brands with one
social.getper creator - Market default browse + sales skip duplicate SSR refetch; collection skips first soft refresh when SSR solid
- Guild writers sheet / counts paint from
permissions.forPathfold;permissions.hasstay mutate-time - DAO
get_policysoft-fill uses short process TTL cache (never SSR) - Home + personal + guild post SSR seed engagement counts and lazy scarce CTAs (
feed-paint-hydrate) — no empty→fill for first page - Feed engagement row always mounts (
EMPTY_POST_ENGAGEMENTfallback); scarce seeds viaseedScarceEmbedsFromSsr