Client and configuration
The client singleton, client.init, the readiness helpers and every SeedConfig option, plus storage, gateway and EAS settings, environment variables and the multi-tab lock.
The client is a single object per page or process. You start it once with client.init, passing a SeedConfig that says where to keep data, which schema to load and which gateways to use. After that, the Schema, Model and Item classes work against the local database it opened. In React, SeedClientGate calls client.init for you.
import { client, getClient } from '@seedprotocol/sdk'
import type { SeedConfig, SeedConstructorOptions } from '@seedprotocol/sdk'client
client is the SDK's singleton. getClient() returns the same object, so use whichever reads better.
const client: {
init(options: SeedConstructorOptions): Promise<void>
isInitialized(): boolean
onReady(callback: () => void): () => void
setAddresses(addresses: AddressConfiguration): Promise<void>
getAddresses(): Promise<{ owned: string[]; watched: string[] }>
getOwnedAddresses(): Promise<string[]>
getWatchedAddresses(): Promise<string[]>
claimUnpublishedDrafts(publisher: string): Promise<{ seeds: number; versions: number; metadata: number; pendingCreates: number; seedLocalIds: string[] }>
removeLocalCopiesForAddresses(addresses: string[], options?: { includeUnpublished?: boolean }): Promise<{ removedSeedLocalIds: string[]; removedSeedUids: string[]; keptSeedLocalIds: string[] }>
syncFromEas(options?: { addresses?: string[] }): Promise<void>
stop(): Promise<void>
unload(): Promise<void>
resetLocalData(options?: ResetLocalDataOptions): Promise<ResetLocalDataResult>
getService(): Actor // the client's XState actor
subscribeService(listener: (service: Actor) => void): () => void
}This is simplified from the published types.
| Method | What it does |
|---|---|
init(options) | Starts the client. See client.init |
isInitialized() | true once init has finished, and false again after stop() or unload() |
onReady(callback) | Calls callback once the client is ready, or on the next microtask if it already is. Returns a function that cancels the callback |
setAddresses(addresses) | Saves the user's owned and watched wallet addresses. See Addresses |
getAddresses(), getOwnedAddresses(), getWatchedAddresses() | Read the saved addresses. Owned addresses are stored lowercased |
claimUnpublishedDrafts(publisher) | Makes publisher the publisher of every draft that has none. See Item: claimUnpublishedDrafts |
removeLocalCopiesForAddresses(addresses, options?) | Permanently deletes the local copies of published items from those publishers. Drafts are kept, and nothing changes on-chain. A copy whose publisher is still an owned address and that has an unpublished edit or a pending outbox write is kept too, and its local id is listed in keptSeedLocalIds; pass { includeUnpublished: true } to remove it with the rest |
syncFromEas(options?) | Pulls attestations from EAS into the local database, for the given addresses or all saved owned and watched addresses. Throws if the sync fails, and isn't retried |
getService() | The client's XState actor. init after stop() replaces it with a new one |
subscribeService(listener) | Calls listener with the actor when stop() ends it and when init after stop() replaces it. Returns a function that removes the listener. Subscribe to the actor itself for its state changes |
stop(), unload() | Stop the client and release what it holds. See Stopping the client |
resetLocalData(options?) | Browser only. Stops the client and deletes its database and files. See Deleting local data |
Every method in the table except init, isInitialized, onReady, getService, subscribeService, stop, unload and resetLocalData throws ClientNotInitializedError before init has finished and after stop() or unload(). Check error instanceof ClientNotInitializedError or error.code === 'CLIENT_NOT_INITIALIZED'; the message is still ClientManager is not initialized. Please call init() first.
Stopping the client
stop() and unload() work in any state: before init, while it runs, after it fails, and once the client is ready. They cancel pending retries, unregister the client's @seedprotocol/query source, and then release, in this order:
- The outbox stops applying writes in this tab. Pending writes stay in the database, and the leader tab applies them once the next
initfinishes. - Every cached
Item,ItemProperty,Schema,ModelandModelPropertyis stopped and dropped from the cache, whoever holds it. - In the browser, this tab hands tab leadership to another tab and closes its channel.
- The database closes (
BaseDb.close()): in the browser the SQLite worker ends and the change feed stops; on Node the libSQL client closes.
The promise resolves once all of that is done. unload() also ends the client's internal subscription to its own state; otherwise the two are the same. An instance you kept from before the stop is stopped for good: reads return its last values, and writes throw EntityUnloadedError. Look the item up again after the next init, which gets a new instance.
Once the client has stopped, client.getService().getSnapshot() has status: 'stopped' and context.stopReason says why: 'stop' after stop() or unload(), 'reset' after resetLocalData() in this tab or another. It's undefined before init and while the client runs.
init can run again afterwards, with any config. With another filesDir, it opens that directory's database and schema files, not the previous one's. An init called while a stop is still releasing waits for it. Called again on a client that's already stopped, stop() and unload() log a console.warn (client.stop() or client.unload() was called after the client had already stopped; this call does nothing…) and resolve.
import { client } from '@seedprotocol/sdk'
import schema from '../seed.schema.json'
await client.stop()
await client.init({ config: { schema, filesDir: 'other-account' } })Deleting local data
In the browser, client.resetLocalData() deletes the client's database and files, so the next init starts empty. Use it to wipe a shared device or offer a "reset this app" action.
import { client } from '@seedprotocol/sdk'
const { deleted, failed } = await client.resetLocalData()
if (failed.length > 0) console.warn('Could not delete', failed)It stops this tab's client from any state, asks every other tab that has the same database open to stop its client, and waits up to otherTabsTimeoutMs (default 10000) for them to close it. It then deletes filesDir from OPFS entry by entry: the database, the schema files and stored files. filesDir defaults to the running or last init's, else '.seed'; pass { filesDir } to delete another one.
It resolves with deleted, the paths it removed, and failed, each entry it couldn't remove as { path, error }, where error is <error name>: <message>. A file another tab still holds open after the timeout ends up in failed; the rest are removed anyway. Unpublished items are gone for good. The saved addresses go with the database, so pass them to the next init; its EAS sync brings their published items back.
The other tabs stop with stopReason: 'reset' and don't reload; your app decides what they show next. An init started during a reset, in this tab or another, waits for it to finish and then opens an empty database. A tab running with multiTab: 'off' takes no part: it doesn't hear the reset, the reset doesn't wait for it, and its init doesn't wait for the reset.
localStorage isn't touched. @seedprotocol/publish keeps its in-app wallet session there; to clear it as well, call clearInAppPublishSession from Components and hooks.
In Node, resetLocalData() throws ResetLocalDataBrowserOnlyError (code: 'RESET_LOCAL_DATA_BROWSER_ONLY'), with the message client.resetLocalData() runs in the browser only; in Node, stop the client and delete filesDir. Do that instead: await client.stop(), then remove the directory.
client also has an addModel(def) method. It's left over from an older API and only inserts a model name into the database. Define models through config.schema or Model.create instead.
client.init
Opens the local database and file store, applies your schema, and saves your config and addresses. It resolves when the client is ready and rejects with the error that stopped it.
init(options: SeedConstructorOptions): Promise<void>Parameters
SeedConstructorOptions:
| Name | Type | Default | Description |
|---|---|---|---|
config | SeedConfig | (required) | Storage, schema, gateway and EAS settings. See SeedConfig |
addresses | { owned: string[]; watched?: string[] } or string[] | none | The user's wallet addresses. A plain array means owned addresses only. Applied by the first init, and the first after stop(). See Addresses |
syncFromEasOnAddressChange | boolean | true | When setAddresses saves new addresses, run an EAS sync straight away. Set false if you call syncFromEas yourself |
multiTab | 'coordinate' | 'off' | 'coordinate' | How browser tabs of the same app share background work. See Multiple tabs |
Example
import { client } from '@seedprotocol/sdk'
import type { SeedConstructorOptions } from '@seedprotocol/sdk'
import schema from '../seed.schema.json'
const options: SeedConstructorOptions = {
config: { schema },
}
await client.init(options)Behavior
- Init runs once per config. Calls made while init is in progress share the same promise, and the first call's
addressesare the ones applied. Once the client is ready, a call with the same config (compared deeply) resolves immediately and leaves the stored addresses alone, whatever itsaddresseshold. To change them, callsetAddresses. A call with a different config throwsINIT_CONFIG_CONFLICT_MESSAGE(exported), whether the first init has finished or not. To change the config,await client.stop()first. If init fails, you can call it again with any config. - It waits up to 120 seconds, then rejects.
- Steps, in order: pick the browser or Node platform and resolve the gateway, mount the file system, open and migrate the database, save the config and addresses, load schemas, then register models. The
ClientManagerStateenum names these steps. - On Node, a relative
filesDir, the default included, needs a writable working directory. It resolves under<cwd>/.seed(see filesDir and endpoints), and init rejects withEACCES: permission denied, mkdir '<cwd>/.seed/…'if it can't create it. An absolutefilesDirworks from any working directory. - Schema errors fail init. If
schemaorschemaFilecan't be read or applied, init rejects withCould not load config.schema from "<path>": <reason>(orconfig.schemaFile; there's nofrompart for an inline schema object). The original error is thecause. WithSeedClientGate, it reachesonInitError. See Your first schema.
isClientReady and whenDbReady
Two functions tell code outside React whether the client can be used. Import them from @seedprotocol/sdk.
function isClientReady(): boolean
function isClientReadySnapshot(snapshot: { status: string; value: unknown }): boolean
function whenDbReady(timeoutMs?: number): Promise<any | undefined>isClientReady() is true once client.init has finished and the database is open, and false before init, while it runs and after client.stop(). isClientReadySnapshot(snapshot) answers the same for a snapshot of the client's actor (client.getService().getSnapshot()): it's true when the actor is running and in the idle state.
whenDbReady(timeoutMs = 30000) resolves with the Drizzle database (what BaseDb.getAppDb() returns) as soon as init has opened it, before schemas load. It resolves with undefined if that doesn't happen within timeoutMs, such as when init isn't called or fails. After client.stop() it resolves with undefined at once, since the database is closed.
import { whenDbReady } from '@seedprotocol/sdk'
const db = await whenDbReady(10_000)
if (!db) throw new Error('The Seed database did not open within 10 seconds')SeedConfig
The config object you pass to client.init. Every field is optional, and most apps set only schema.
| Name | Type | Default | Description |
|---|---|---|---|
filesDir | string | endpoints.files, then '.seed' | Where the SDK keeps its database and files. See filesDir and endpoints |
endpoints | { filePaths: string; files: string } | both filesDir | Path labels saved with the app's config. Nothing else reads them, so most apps leave them out |
schema | string | SchemaFileInput | none | Your app's schema, as an object or a path to a JSON file. An object needs only metadata.name and models (see Schema). Applied at start when it changed since the last apply, adding and updating models and properties; skipped otherwise. Takes precedence over schemaFile |
schemaFile | string | none | Older way to pass a schema file path. Ignored when schema is set |
arweaveDomain | string | 'arweave.net' | Arweave gateway host. https:// is assumed; prefix http:// for a plain HTTP gateway. Before uploading an Html value, publishing moves its Arweave links onto this host, keeping each link's path form (or writes them as ar://<id> with htmlArweaveRefs: 'ar'), and those links are permanent; see Links in published content, and Arweave: Links in Html for the helpers that re-host links |
uploadApiBaseUrl | string | none ('') | Upload API origin used when publishing. Unset, it stays empty rather than falling back to the arweaveDomain gateway, which doesn't serve the upload API. While a proxy or sidecar is in use, its base URL is used instead of this value |
gateway | SeedGatewayConfig | { transport: 'http-gateway' } | Gateway transport, proxy and read-gateway settings. See Gateway settings |
eas | { chainId?: number; indexerUrl?: string } | OP Sepolia | Which EAS chain and indexer the SDK reads from. See EAS settings |
models | Record<string, Model> | {} | Model instances to register at init. Most apps use schema instead |
entityCache | { unloadGraceMs?: number } | { unloadGraceMs: 60000 } | unloadGraceMs is the grace period: how long, in milliseconds, an Item or ItemProperty no one holds stays cached after its last unload() or its last lookup without a hold. A hold taken meanwhile keeps the same instance. With 0, the last unload() stops an instance at once, but one that was never held (created with hold: false, or only looked up without a hold) stays cached until it's evicted or the client stops. Negative values count as 0. Schemas, models and model properties aren't affected; they stay cached until destroyed, evicted or client.stop(). See Item: unload |
dbConfig | { dbUrl?: string; schemaDir?: string; outDir?: string } | none | Node only; the browser ignores it. dbUrl is the database file path or libSQL URL (default <outDir>/seed.db); a relative path resolves against the working directory, and a missing folder is created. outDir is where the SDK copies its migrations and runs them from (default <filesDir>/db), and is created if needed; without dbUrl the database goes there too. schemaDir is unused |
filesDir and endpoints
In the browser, filesDir is a directory in the browser's private file system (OPFS), and the database lives at <filesDir>/db/seed.db inside it.
On Node, a relative filesDir resolves under a .seed directory in the current working directory, which the SDK creates when it opens the database:
filesDir | Database ends up at |
|---|---|
A relative path, such as 'data' or '.seed-test' | <cwd>/.seed/data/db/seed.db, <cwd>/.seed/.seed-test/db/seed.db |
A relative path whose first segment is exactly .seed, such as '.seed', '.seed/app' or './.seed/app' | relative to the working directory: <cwd>/.seed/db/seed.db, <cwd>/.seed/app/db/seed.db |
A single-segment absolute path that doesn't exist, such as '/app-files' | <cwd>/.seed/app-files/db/seed.db (treated as a browser-style path) |
Any other absolute path, such as '/var/lib/my-app' | /var/lib/my-app/db/seed.db. Nothing is written to the working directory |
The SDK also writes a copy of your schema ({id}_{name}_v{version}.json) into filesDir. dbConfig.dbUrl moves only the database file. dbConfig.outDir moves the migrations folder, and the database too (to <outDir>/seed.db) when dbUrl isn't set. The schema copies and stored files stay under filesDir.
With no filesDir and no endpoints.files, filesDir is '.seed': the OPFS directory .seed in the browser, <cwd>/.seed on Node. Using Seed in Node and Electron covers running the client on Node.
Storage
| Browser | Node | |
|---|---|---|
| Database | SQLite (WASM) through SQLocal, in OPFS | libSQL (@libsql/client) |
| Files | ZenFS, mounted over the OPFS root | Node's fs |
| Database file | <filesDir>/db/seed.db | <filesDir>/db/seed.db, dbConfig.dbUrl, or <outDir>/seed.db when only dbConfig.outDir is set |
| Live queries | Reactive (SQLocal) | Polls every second |
Both use Drizzle ORM and the same migrations, which the SDK runs at init. A database last opened by an SDK older than 0.7.7 is recreated instead. Init first copies it to seed.before-recreate-<ms>.db beside seed.db (in OPFS in the browser), then drops its tables before the migrations run, and logs a console warning that names the copy. Unpublished items are then gone from the database but stay in the copy, which the SDK never deletes; published ones come back through EAS sync, and files under filesDir stay. If the copy can't be made, or dbConfig.dbUrl is a remote libsql URL, init logs a warning and opens the database as it is. Migration says how to keep drafts. There are no other storage adapters. In the browser, Seed supports Chrome and Edge 102, Firefox 114 and Safari 26 or later; in an older browser init rejects with UnsupportedBrowserError. The browser database also needs a cross-origin isolated page; without one, init rejects with DatabaseNotPersistentError. Add Seed to your app has the headers to send.
The SDK decides which platform it's on when init runs. It's a browser when window and document exist and the page isn't running in Electron. Otherwise it's Node when process.versions.node exists. Electron renderers that can see Node's process count as Node.
UnsupportedBrowserError
In the browser, init first checks that the page has the file system and locking features Seed needs. If any is missing, it rejects with UnsupportedBrowserError before it loads or stores anything. Its code is 'BROWSER_UNSUPPORTED', and missing lists the absent features as BrowserFeature values:
BrowserFeature | Missing when |
|---|---|
'opfs' | The page has no OPFS (navigator.storage.getDirectory) |
'opfs-writable' | FileSystemFileHandle.createWritable is absent. Every file write uses it |
'web-locks' | The Web Locks API (navigator.locks), which coordinates tabs, is absent |
'cross-origin-isolation' | The page isn't cross-origin isolated. This is a hosting setting, not a browser version, so init reports it with DatabaseNotPersistentError instead |
The message names the missing features and the supported browsers:
This browser lacks features Seed needs (opfs-writable). Seed supports Chrome and Edge 102, Firefox 114 and Safari 26 or later.Retrying init in the same browser fails the same way; ask the user to update it. With SeedClientGate, the error reaches onInitError.
To check before init and show your own message, call checkBrowserSupport(). It returns { supported, missing }, where missing is a BrowserFeature[] that includes 'cross-origin-isolation' when the page isn't isolated. Outside a browser, as on Node, the file system features are reported missing.
import { checkBrowserSupport, client } from '@seedprotocol/sdk'
import schema from '../seed.schema.json'
const { missing } = checkBrowserSupport()
if (missing.some((feature) => feature !== 'cross-origin-isolation')) {
document.body.textContent = 'Update your browser to use this app.'
} else {
await client.init({ config: { schema } })
}checkBrowserSupport() can't see features that exist only inside workers: OPFS sync access handles, which set the Chrome and Edge 102 floor, and module workers, which set Firefox 114. Chrome 86 to 101 and Firefox 111 to 113 pass the check and fail later in init.
DatabaseNotPersistentError
In the browser, SQLite keeps the database in OPFS only when the page can use SharedArrayBuffer, which needs the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless) response headers. Init checks for SharedArrayBuffer and Atomics before it starts the database worker, and checks that SQLite opened an OPFS database afterwards. If either check fails, init rejects with DatabaseNotPersistentError instead of running on an in-memory database that a reload would empty. Its code is 'DB_NOT_PERSISTENT'. On a page served without the headers, the message reads:
SQLite can't store the Seed database in the browser's private file system: the page is not cross-origin isolated, so SharedArrayBuffer is unavailable. Serve the app with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp (or credentialless). @seedprotocol/vite sets these on the Vite dev and preview servers; production hosting has to set them too.Retrying init on the same page fails the same way; the fix is in how the page is served. With SeedClientGate, the error reaches onInitError. @seedprotocol/sdk/node doesn't export this class; import it from @seedprotocol/sdk.
Gateway settings
config.gateway controls how the SDK reaches Arweave. The defaults read from arweave.net, with other public gateways and then Seed's gateway (ar.seedprotocol.io) as fallbacks, and need no other configuration. An app that wants to read from Seed's gateway first sets arweaveDomain: 'ar.seedprotocol.io'. Arweave and gateways gives the fallback order.
| Name | Type | Default | Description |
|---|---|---|---|
transport | 'http-gateway' | 'hyper' | 'hybrid' | 'http-gateway' | http-gateway uses an HTTP gateway (or your proxy). hyper uses a local Gateway Hyper sidecar. hybrid tries your proxy, then the sidecar, then the public HTTP gateway. In a browser, hybrid tries the sidecar only from a loopback page origin (localhost, 127.0.0.1, [::1], *.localhost) or with hyper.probeSidecarFromBrowser: true |
proxyBaseUrl | string | none | Your app server's gateway proxy, absolute or relative to the page origin (for example /api/seed-gateway). With http-gateway, init throws if the proxy doesn't respond |
preferredReadBaseUrls | string[] | none | Gateways tried first when reading files, such as the one you upload through |
arweaveDomain, uploadApiBaseUrl | string | Same as the top-level options. The top-level values win | |
hyper | { gatewayHyperKey?, localSidecarHost?, localSidecarPort?, probeSidecar?, probeProxy?, probeSidecarFromBrowser? } | host 127.0.0.1, port 1984, probeSidecar: true, probeProxy the same as probeSidecar, probeSidecarFromBrowser: false | Sidecar and proxy settings. The first four also have a top-level shorthand on gateway (gatewayHyperKey, localSidecarHost, …); probeProxy and probeSidecarFromBrowser don't |
With transport: 'hyper', init throws if the sidecar isn't reachable. Set probeSidecar: false to skip the reachability checks for the sidecar, and for the proxy too unless probeProxy is set; probeProxy turns the proxy's check on or off by itself. Arweave and gateways and Gateway Hyper explain the transports.
const config: SeedConfig = {
schema,
gateway: { proxyBaseUrl: '/api/seed-gateway' },
}EAS settings
config.eas sets the chain the SDK reads attestations from when it syncs and looks up schemas. The default is OP Sepolia, the testnet Seed Protocol is deployed on.
| Name | Type | Default | Description |
|---|---|---|---|
chainId | number | OP Sepolia (11155420), or the chain @seedprotocol/publish is configured with | Known chains find their EAS indexer automatically |
indexerUrl | string | the chain's known indexer | EAS GraphQL endpoint. Overrides the chain default and the EAS_ENDPOINT environment variable |
The read chain has to match the publish chain. If config.eas.chainId and @seedprotocol/publish disagree, or the local database already holds data from another chain, the SDK throws rather than mixing chains.
Addresses
The SDK keeps two lists of wallet addresses. Owned addresses are wallets the user controls, and decide which items they can edit, publish and delete. Watched addresses are read-only: their published items can't be edited. The SDK syncs published items from EAS for both lists.
await client.setAddresses({ owned: [account], watched: [] })
const { owned, watched } = await client.getAddresses()You can pass the same value as addresses to the first client.init. Addresses are stored lowercased in the local database. Both setAddresses and init emit addresses.persisted once they've saved them, but only when the addresses differ, ignoring case, order and duplicates, from the ones this tab last announced. client.stop() forgets the last announcement. Only setAddresses starts an EAS sync for the new addresses; for init, the start-up sync covers them.
The first init, and the first after stop(), saves the addresses you pass, replacing what was stored. Leaving addresses out keeps the stored ones, and logs a one-time console warning when there are any. Passing empty lists (addresses: [] or { owned: [], watched: [] }) clears them in Node and in the first browser tab to open the app; a tab opened while another is running keeps them. A repeat init never changes the stored addresses, whether its addresses are empty, different or left out, so remounting SeedClientGate before a wallet connects doesn't drop them. Once the client is running, change them with client.setAddresses, and clear them with client.setAddresses([]).
A draft that has never had a publisher is owned whatever the owned addresses are: it's editable, and the next wallet to connect becomes its publisher (@seedprotocol/publish calls claimUnpublishedDrafts when a wallet connects).
A new draft takes its publisher from the getter registered with setGetPublisherForNewSeeds, if it answers within 3 seconds; @seedprotocol/publish registers one for the connected wallet. Without an answer, the draft gets the owned address when exactly one is set, and no publisher otherwise. With several owned addresses and no @seedprotocol/publish, as in a Node script, register a getter to give new drafts a publisher:
import { setGetPublisherForNewSeeds } from '@seedprotocol/sdk'
setGetPublisherForNewSeeds(async () => account)Item: Ownership has the ownership checks, and Ownership and drafts explains the rules.
Offline retries
Two kinds of background work are retried when they fail: the EAS schema lookup at start-up, and the EAS syncs the SDK starts itself, such as the start-up sync and the one setAddresses starts. In a browser that reports itself offline, the retry waits for the online event. Otherwise, including on Node, it waits 15 seconds, then 30 seconds, 1, 2 and 5 minutes, and keeps retrying every 5 minutes. A success resets the delay. The timers don't keep a Node process running, and client.stop() cancels them.
A client.syncFromEas() call you make yourself isn't retried: it rejects, and retrying is up to you. Automatic syncs that were merged into its run are retried. Nothing syncs on reconnect after a sync that succeeded either. Syncing across devices shows how to sync on a schedule or on online.
Retries are off when NODE_ENV (or Vite's import.meta.env.MODE) is test. setRetriesWhenOnlineEnabled(enabled) overrides that: false turns them off, true on, and undefined restores the default. It returns the previous setting.
import { setRetriesWhenOnlineEnabled } from '@seedprotocol/sdk'
setRetriesWhenOnlineEnabled(false) // e.g. in tests that stub the networkMultiple tabs
In the browser, every tab of your app opens the same database. With multiTab: 'coordinate' (the default), the tabs elect one leader using the Web Locks API. Only the leader applies the outbox of item edits (other tabs' edits wait for it), prunes the outbox and the change feed's log, and runs automatic EAS syncs, bulk file downloads and the Arweave finalize worker. Other tabs forward requests to it. Some events reach every tab; Events: Across tabs lists them. With multiTab: 'off', every tab runs that work itself. On Node the client always acts as leader. A browser without Web Locks can't run the client: init rejects with UnsupportedBrowserError.
isLeaderTab() and whenLeaderTab() tell you whether this tab leads. client.syncFromEas() isn't limited to the leader; it runs in whichever tab calls it. Multi-tab covers this in depth.
import { isLeaderTab, whenLeaderTab } from '@seedprotocol/sdk'
if (!isLeaderTab()) await whenLeaderTab()FileSystemLockedError
If another tab or worker is holding the app's OPFS files when init mounts the file system, the SDK retries for about two seconds and then rejects with FileSystemLockedError. Its code is 'OPFS_LOCKED', and its cause is the original browser error.
import { client, FileSystemLockedError } from '@seedprotocol/sdk'
try {
await client.init(options)
} catch (error) {
if (error instanceof FileSystemLockedError) {
// Ask the user to close other tabs of the app, then retry
}
throw error
}With SeedClientGate, the same error reaches the gate's onInitError prop.
Environment variables
The SDK reads a few environment variables from process.env. In a browser build they only take effect if your bundler defines them on process.env; the SDK doesn't read import.meta.env.
| Variable | Effect |
|---|---|
EAS_ENDPOINT, NEXT_PUBLIC_EAS_ENDPOINT | EAS GraphQL indexer URL. config.eas.indexerUrl takes precedence. Also read from window.env in the browser. A URL that belongs to another chain's indexer throws |
EAS_CHAIN_ID, NEXT_PUBLIC_EAS_CHAIN_ID | EAS read chain, used only when neither config.eas nor @seedprotocol/publish sets one |
ARWEAVE_HOST, NEXT_PUBLIC_ARWEAVE_HOST, VITE_ARWEAVE_HOST | Primary Arweave gateway host for code that runs before client.init. Init always sets the host from arweaveDomain (default arweave.net), so after init these have no effect. Use arweaveDomain instead |
ARWEAVE_READ_GATEWAYS, NEXT_PUBLIC_ARWEAVE_READ_GATEWAYS | Comma-separated list of gateway hosts to fall back to for reads, replacing the default list |
DEBUG | Turns on the SDK's debug logs, for example DEBUG=seedSdk:*. seedSdk:timing logs how long item creates, Item.find, item and property loads and property saves took, with database statement and live-query counts. In the browser, set localStorage.debug = 'seedSdk:*' instead |
NODE_ENV=test turns off offline retries. Init and its errors are the same under it as in production.
Related
- Add Seed to your app: the browser setup this config runs in
- Types:
SeedConfig,SeedConstructorOptionsand the gateway types - Using Seed in Node and Electron: running the client on Node and Electron
- Node entry point: what
@seedprotocol/sdk/nodeexports - React overview:
SeedProviderandSeedClientGate - Multi-tab and Local-first and offline
- Syncing across devices
Overview and install
What @seedprotocol/sdk contains, how to install it, which entry point to import from, and where each part of its API is documented.
Schema
The Schema class and the schema file format: load schemas by name, list them, inspect their models, and the field reference for the schema file.