Adapters
Where @seedprotocol/query reads from: the remote EAS source, the SDK's local SQLite source and auto mode, the exact fallback rules, how sources are registered, and how to write your own.
Every read in @seedprotocol/query goes through a data source: an object that lists Seeds and fetches their Versions and property attestations. There are two slots, remote (EAS, always available) and local (empty until something registers a source), and the source option on each call picks between them.
import { registerLocalQuerySource, type QueryDataSource } from '@seedprotocol/query'Choosing a source
Pass source to queryBySchema, getSeed, getSeeds or queryBySchemaForMonth:
source | Reads from | Cache | Use it when |
|---|---|---|---|
'remote' (default) | The EAS indexer, plus Arweave gateways for content | Yes | You don't run the SDK, or you need everything that's on-chain |
'local' | The registered local source. With the SDK, the published items its database has synced. Never drafts | No | Your app runs the SDK and you want to read without network requests |
'auto' | The local source when one is registered, falling back to remote as described below | Only after falling back | You want local reads with a network fallback |
import { queryBySchema, getSeed } from '@seedprotocol/query'
const local = await queryBySchema('post', { source: 'local' })
const post = await getSeed('0xfd8c50ca131b725c288be7f46e4abe0c7b06f1226e98f552642edde8280ad900', { source: 'auto' })Fallback rules
'local' never falls back. With no local source registered it throws:
[query] source: "local" requires registerLocalQuerySource(...) (e.g. SDK DB ready)'auto':
- No local source registered: behaves exactly like
'remote', cache andetagincluded. queryBySchemaandqueryBySchemaForMonth: reads the local source. If that returns no items at all, the whole call is repeated against EAS with the samelimit,skip,uidPrefixandattesters. One local item is enough to stay local, so a partly synced collection isn't topped up from EAS.getSeed: reads the local source. If it doesn't have the Seed, or has it with no Version from its attester, reads EAS (using the item cache). If it has the Seed with a Version, EAS isn't asked: a Seed whose Versions were all revoked returnsnull.getSeeds: reads every Seed from the local source, then reads the ones it doesn't have, or has with no Version, from EAS in one batch (using the item cache). The others are never read from EAS, as withgetSeed.- Results read locally have no
etag.
Paging with 'auto' can switch sources mid-list. If the local source has 30 items and you ask for skip: 40, the local page is empty, so you get EAS's items 41 to 60, which may not continue the local list. Use one source explicitly when you page.
The local source from the SDK
The SDK registers its SQLite database as the local source when client.init finishes, and clears it on client.stop() and client.unload(). From then on source: 'local' and 'auto' read the published items its EAS sync brought in: those of the client's owned and watched addresses. To read another publisher's items locally, add their address as a watched address and let a sync run; see Sync a user's data to another device.
@seedprotocol/sdk also exports helpers around it:
| Export | Description |
|---|---|
getPublishedSeedRecord(seedUid, options?) | getSeed with source: 'local' by default. Pass { source: 'auto' } to fall back to EAS. Registers the source first if it isn't yet |
registerSeedQueryLocalSource(options?: { force?: boolean }) | Registers a fresh SDK source. Returns false without registering while the database isn't ready, unless force |
unregisterSeedQueryLocalSource() | Clears the local slot |
isSeedQueryLocalSourceRegistered() | Whether the SDK registered its source |
createLocalQueryDataSource() | The SDK's QueryDataSource, if you want to wrap it |
The SDK's source takes revocation from the database's revoked_at columns, which unpublishing sets before EAS's indexer catches up, so a local read can leave out an item that a remote read still returns. It also leaves out Seeds marked for deletion and Seeds whose Versions were all revoked. See Read published data for a full example, and Querying for reading the local database directly.
registerLocalQuerySource
Puts a source in the local slot, replacing whatever was there.
function registerLocalQuerySource(source: QueryDataSource): void
function clearLocalQuerySource(): void
function getRegisteredLocalQuerySource(): QueryDataSource | null
function hasLocalQuerySource(): booleanThe source must have kind: 'local'; anything else throws [query] registerLocalQuerySource expects a source with kind: "local". There's one slot per process, and the remote slot can't be replaced. Reads with source: 'local' don't check for a second copy of @seedprotocol/eas or @seedprotocol/arweave; 'auto' reads do, and throw if there is one (see Troubleshooting). To read from a different indexer, set EAS_ENDPOINT instead (see environment variables).
client.init registers the SDK's source with force, replacing any source you registered before it. In an app that runs the SDK, register your own source after client.init resolves, and remember that getPublishedSeedRecord will then read from yours.
QueryDataSource
The interface a source implements. Every method returns attestations in the shape EAS's GraphQL API uses.
type QueryDataSource = {
readonly kind: 'remote' | 'local'
getSeedByUid(seedUid: string): Promise<AttestationLike | null>
listSeedsBySchemaName(schemaName: string, opts: { limit: number; skip: number } & SeedListFilter): Promise<AttestationLike[]>
listSeedsBySchemaNameForMonth(schemaName: string, year: number, month: number): Promise<AttestationLike[]>
getVersionsForSeed(seedUid: string): Promise<AttestationLike[]>
getVersionsForSeeds(seedUids: string[], opts?: { includeRevoked?: boolean }): Promise<AttestationLike[]>
getPropertiesForVersionUids(versionUids: string[]): Promise<AttestationLike[]>
getSeedsByUids(uids: string[]): Promise<AttestationLike[]>
listChangesSince?(opts: { refUIDs: string[]; ids: string[]; since: number }): Promise<AttestationChange[]>
readStorageBody?(ref: { propertyName: string; value: unknown; localPathHint?: string }): Promise<string | null>
}
type SeedListFilter = {
uidPrefix?: string // '0x' plus lowercase hex
attesters?: readonly string[] // lowercase addresses, at least one
}
type AttestationLike = {
id: string
decodedDataJson: string
refUID: string
schemaId: string
timeCreated: number
attester?: string
schema?: { schemaNames?: Array<{ name: string }> }
revoked?: boolean
}| Method | Used by | Contract |
|---|---|---|
getSeedByUid | getSeed | The Seed attestation, with schema.schemaNames[0].name set to its schema name (getSeed returns null without it). null if unknown. Called with the UID lowercased |
listSeedsBySchemaName | queryBySchema | Seeds of the schema, newest first, revoked ones left out. When opts has uidPrefix, only UIDs starting with it; when it has attesters, only Seeds attested by one of them. Both are already normalized, and limit and skip apply after them. A source that ignores them lists Seeds the call meant to leave out |
listSeedsBySchemaNameForMonth | queryBySchemaForMonth | Seeds created in that month, in UTC like the built-in sources: from 00:00 UTC on the 1st, up to but not including 00:00 UTC on the next month's 1st |
getVersionsForSeed, getVersionsForSeeds | getSeed, getSeeds, assembly and changelogs | Version attestations whose refUID is the Seed. With includeRevoked, include revoked ones marked revoked: true, so a Seed whose Versions were all revoked can be told apart from one that never had any |
getPropertiesForVersionUids | Assembly and changelogs | Property attestations whose refUID is one of the Versions |
getSeedsByUids | getSeeds, and reading related items (not called for them with relations: 'uids') | The Seeds with these UIDs, with schema.schemaNames set as for getSeedByUid. Revoked ones left out |
listChangesSince (optional) | The query cache | Attestations created or revoked after since that reference refUIDs or are ids. Only consulted for remote sources |
readStorageBody (optional) | Arweave hydration | Return the body of an Html or File value from local storage, or null to fetch it from a gateway. Called with the field's key as propertyName and its current value, usually the gateway URL; query doesn't set localPathHint. A throw or an empty string counts as null |
A source doesn't have to filter Versions and properties by attester: the query layer keeps only Versions attested by their Seed's attester, and properties attested by their Version's attester, so return each attestation's attester when you have it. Attestations without one are kept.
createRemoteQueryDataSource() returns a new instance of the built-in EAS source and getRemoteQueryDataSource() the shared one. Both are useful to wrap, as below.
Example: one publisher's items
To list one account's Seeds in one call, pass attesters to queryBySchema. To keep every listing to one account whatever the call passes, register a source that adds the account to the EAS request and reuses the remote source for everything else. It reads uidPrefix and attesters from opts, so those options still filter:
import { EasClient, GET_SEEDS_LEAN, withExcludeRevokedFilter } from '@seedprotocol/eas'
import {
createRemoteQueryDataSource,
registerLocalQuerySource,
queryBySchema,
type AttestationLike,
type QueryDataSource,
} from '@seedprotocol/query'
const publisher = '0xE870B14fCfF4A408e6eCb65Bd40f64d08859Efb3' // as EAS stores it: checksummed
const remote = createRemoteQueryDataSource()
const onePublisher: QueryDataSource = {
...remote,
kind: 'local',
async listSeedsBySchemaName(schemaName, { limit, skip, uidPrefix, attesters }) {
if (attesters && !attesters.includes(publisher.toLowerCase())) return []
const where = withExcludeRevokedFilter({
attester: { equals: publisher },
...(uidPrefix ? { id: { startsWith: uidPrefix } } : {}),
schema: { is: { schemaNames: { some: { name: { equals: schemaName } } } } },
})
const { itemSeeds } = await EasClient.getEasClient().request(GET_SEEDS_LEAN, { where, take: limit, skip })
return itemSeeds.map(
(seed): AttestationLike => ({ ...seed, schema: { schemaNames: [{ name: schemaName }] } }),
)
},
}
registerLocalQuerySource(onePublisher)
const { items } = await queryBySchema('post', { source: 'local', limit: 20 })The query layer registers the EAS client before it calls the source, so EasClient.getEasClient() is ready inside the method. Reads through this source aren't cached, because it's in the local slot. Month listings (queryBySchemaForMonth) still return every publisher's Seeds unless you override listSeedsBySchemaNameForMonth too.
resolveQuerySource
Returns the source a call would use, without reading anything.
function resolveQuerySource(mode?: QuerySourceMode): {
mode: QuerySourceMode
dataSource: QueryDataSource
useQueryCache: boolean
}
function normalizeSourceMode(source?: QuerySourceMode): QuerySourceMode // undefined → 'remote'mode defaults to 'remote'. For 'local' without a registered source it throws, as above. For 'auto' it returns the local source when one is registered, otherwise the remote one; the per-call fallback described above happens later.
Browser and Node
The data sources are the same in both builds. The differences are around them:
| Node and Bun | Browser bundle | |
|---|---|---|
| Remote source | EAS through fetch | The same |
| Setup on the first read | Registers fetch-based EAS clients and the Node Arweave client, where none are registered | Registers fetch-based EAS clients where none are registered, and sets the Arweave read gateway without loading the Node clients |
| Query cache | Memory and disk under CACHE_DIR | Memory only |
CACHE_* and NODE_ENV | Read from process.env | Usually not available, so the cache is on and records are frozen |
| Local source | The SDK's libsql database, after client.init | The SDK's SQLite WASM database in OPFS, after client.init |
In a browser app that runs the SDK, prefer the local source (or getPublishedSeedRecord from @seedprotocol/sdk): it reads what the app has already synced and needs no network. Use 'remote' for items no one in the app watches.
Related
- @seedprotocol/query overview:
queryBySchema,getSeed,getSeeds, the record shape and caching - Read published data: remote and local reads in practice
- Sync a user's data to another device: what ends up in the local database
- @seedprotocol/eas: the attestation queries behind the remote source
Overview
Reference for @seedprotocol/query: queryBySchema, getSeed, getSeeds, getSeedAttestations, pinned reads and the other exports, the SeedRecord shape, paging and UID prefixes, schema names, caching and frozen records, and the environment variables it reads.
@seedprotocol/eas
Reference for @seedprotocol/eas, the low-level read client for the EAS GraphQL indexer: client registration, chain and endpoint configuration, schema lookups, Seed, Version and property queries, and the tool attestation helpers.