@lsp-indexer/react playground

@lsp-indexer/node

The foundational package — provides low-level GraphQL fetch functions, parsers, query key factories, env helpers, and the subscription client. Both @lsp-indexer/react and @lsp-indexer/next are built on top of this.

npm install @lsp-indexer/node

Environment Helpers

The package provides URL helpers that read environment variables at runtime:

import { getClientUrl, getServerUrl, getClientWsUrl, getServerWsUrl } from '@lsp-indexer/node';

// Client-side (reads NEXT_PUBLIC_INDEXER_URL)
const clientUrl = getClientUrl();

// Server-side (reads INDEXER_URL, falls back to NEXT_PUBLIC_INDEXER_URL)
const serverUrl = getServerUrl();

// WebSocket URLs (reads WS vars, falls back to HTTP URL with wss://)
const clientWs = getClientWsUrl();
const serverWs = getServerWsUrl();
FunctionEnv VarFallback
getClientUrl()NEXT_PUBLIC_INDEXER_URLthrows
getServerUrl()INDEXER_URLNEXT_PUBLIC_INDEXER_URL
getClientWsUrl()NEXT_PUBLIC_INDEXER_WS_URLderived from getClientUrl()
getServerWsUrl()INDEXER_WS_URLderived from getServerUrl()

All functions validate URLs and throw IndexerError with category CONFIGURATION on invalid input.


Fetch Functions

Every domain has a pair of fetch functions — one for single entities, one for lists:

import { fetchProfile, fetchProfiles, getClientUrl } from '@lsp-indexer/node';

// Fetch a single profile
const profile = await fetchProfile(getClientUrl(), { address: '0x...' });

// Fetch a paginated list
const { profiles, totalCount } = await fetchProfiles(getClientUrl(), {
  filter: { name: 'vitalik' },
  sort: { field: 'name', direction: 'asc' },
  limit: 10,
  offset: 0,
});

Available fetch functions

DomainSingleList
ProfilesfetchProfilefetchProfiles
Digital AssetsfetchDigitalAssetfetchDigitalAssets
NFTsfetchNftfetchNfts
Owned AssetsfetchOwnedAssetfetchOwnedAssets
Owned TokensfetchOwnedTokenfetchOwnedTokens
CreatorsfetchCreators
Issued AssetsfetchIssuedAssets
FollowsfetchFollows, fetchMutualFollows, fetchMutualFollowers, fetchFollowedByMyFollows
Encrypted AssetsfetchEncryptedAssets, fetchEncryptedAssetsBatch
Data ChangedfetchLatestDataChangedEventfetchDataChangedEvents
Token ID Data ChangedfetchLatestTokenIdDataChangedEventfetchTokenIdDataChangedEvents
Universal ReceiverfetchUniversalReceiverEvents

Additional: fetchFollowCount, fetchIsFollowing.

Additional: fetchEncryptedAssetsBatch — batch-fetches multiple encrypted assets by (address, contentId, revision) tuples.

Mutual follow queries are intersection queries across the follow graph:

  • fetchMutualFollows(url, { addressA, addressB, sort?, limit?, offset?, include? }) — profiles that both addressA and addressB follow
  • fetchMutualFollowers(url, { addressA, addressB, sort?, limit?, offset?, include? }) — profiles that follow both addressA and addressB
  • fetchFollowedByMyFollows(url, { myAddress, targetAddress, sort?, limit?, offset?, include? }) — profiles that myAddress follows and that also follow targetAddress

All three accept sort, limit, offset, and include options and return { profiles, totalCount } — the same shape as fetchProfiles.


Batch Encrypted Asset Fetch

fetchEncryptedAssetsBatch fetches multiple encrypted assets by (address, contentId, revision) tuples in a single Hasura query.

Parameters

ParameterTypeRequiredDescription
tuplesEncryptedAssetBatchTuple[]YesArray of { address: string, contentId: string, revision: number } to fetch
includeEncryptedAssetIncludeNoNarrow which related fields are returned — full TypeScript inference

Usage

import { fetchEncryptedAssetsBatch, getClientUrl } from '@lsp-indexer/node';

const { encryptedAssets } = await fetchEncryptedAssetsBatch(getClientUrl(), {
  tuples: [
    { address: '0xAssetAddress1', contentId: 'content-1', revision: 1 },
    { address: '0xAssetAddress2', contentId: 'content-2', revision: 0 },
  ],
  include: { encryption: true },
});
// encryptedAssets → EncryptedAsset[] (one per matched tuple)

Empty tuples short-circuits — no network call is made and encryptedAssets returns []. If no tuples match, encryptedAssets returns [] — no error is thrown. Address matching is case-insensitive. Duplicate tuples are not deduplicated — pass unique tuples. The return shape is { encryptedAssets: P[] } (no totalCount).


Include Fields (Partial Selects)

All fetch functions accept an include parameter to control which related data is returned. This reduces payload size and improves performance:

import { fetchProfile, getClientUrl } from '@lsp-indexer/node';

// Only fetch the profile name and owned assets
const profile = await fetchProfile(getClientUrl(), {
  address: '0x...',
  include: {
    ownedAssets: true,
    issuedAssets: false,
    creators: false,
  },
});
// profile.ownedAssets → OwnedAsset[]
// profile.issuedAssets → undefined (not included)

The return type narrows automatically based on which fields you include — full TypeScript inference.


Query Key Factories

For React Query cache management, every domain exports a key factory:

import { profileKeys, digitalAssetKeys, encryptedAssetKeys } from '@lsp-indexer/node';

profileKeys.all; // ['profiles']
profileKeys.lists(); // ['profiles', 'list']
profileKeys.list(filter, sort, limit, offset, include); // ['profiles', 'list', { filter, sort, ... }]
profileKeys.details(); // ['profiles', 'detail']
profileKeys.detail(address, include); // ['profiles', 'detail', { address, include }]

// Batch key factories
encryptedAssetKeys.batch(tuples, include); // ['encryptedAssets', 'batch', tuples, include]

These are used internally by the React and Next.js hooks, but you can also use them directly for manual cache invalidation:

import { useQueryClient } from '@tanstack/react-query';
import { profileKeys } from '@lsp-indexer/node';

const queryClient = useQueryClient();
queryClient.invalidateQueries({ queryKey: profileKeys.lists() });

Parsers

Raw Hasura responses are parsed into typed domain objects. Parsers handle:

  • Nested relationship mapping
  • Null safety for optional fields
  • Include-aware partial selects (omits fields not in include)
import { parseProfile, parseProfiles } from '@lsp-indexer/node';

// Usually called internally by fetch functions, but available for custom use
const profile = parseProfile(rawHasuraResponse);

Subscription Client

Low-level WebSocket subscription client for real-time data. Manages connection state, reconnection detection, and multiple independent subscriptions via useSyncExternalStore.

import { SubscriptionClient } from '@lsp-indexer/node';

// Connection is lazy — no explicit connect() needed
const client = new SubscriptionClient('ws://localhost:8080/v1/graphql');

// Create a subscription instance (used internally by hooks)
const instance = client.createSubscription(config, options);

// Access reactive state
instance.data; // TParsed[] | null — null until first data received
instance.error; // unknown
instance.isSubscribed; // boolean

// Subscribe to state changes (useSyncExternalStore pattern)
const unsubscribe = instance.subscribe(() => {
  console.log('State changed:', instance.data);
});

In practice, you'll use the higher-level subscription hooks (useProfileSubscription, etc.) from @lsp-indexer/react instead of the raw client.


Error Handling

All errors are wrapped in IndexerError with structured metadata:

import { IndexerError } from '@lsp-indexer/node';

try {
  const url = getClientUrl();
} catch (err) {
  if (err instanceof IndexerError) {
    console.log(err.category); // 'CONFIGURATION'
    console.log(err.code); // 'MISSING_ENV_VAR'
    console.log(err.message); // 'NEXT_PUBLIC_INDEXER_URL is not set...'
  }
}
CategoryCodes
CONFIGURATIONMISSING_ENV_VAR, INVALID_URL
NETWORKNETWORK_TIMEOUT, NETWORK_UNREACHABLE, NETWORK_ABORTED, NETWORK_UNKNOWN
HTTPHTTP_UNAUTHORIZED, HTTP_FORBIDDEN, HTTP_NOT_FOUND, HTTP_TOO_MANY_REQUESTS, HTTP_SERVER_ERROR, HTTP_UNKNOWN
GRAPHQLGRAPHQL_VALIDATION, GRAPHQL_EXECUTION, PERMISSION_DENIED, GRAPHQL_UNKNOWN
PARSERESPONSE_NOT_JSON, EMPTY_RESPONSE, PARSE_FAILED
VALIDATIONVALIDATION_FAILED

Next Steps