Seed Protocol

Upgrading

Upgrade steps for each breaking change from @seedprotocol 0.4 to 0.8.0, with before and after code.

This page takes an app from @seedprotocol/* 0.4.x or later to 0.8.0, newest release first.

Before you start

Upgrade every @seedprotocol/* package in one step, to the same version:

npm install @seedprotocol/sdk@0.8.0 @seedprotocol/react@0.8.0 @seedprotocol/publish@0.8.0
npm install --save-dev @seedprotocol/vite@0.8.0

Add the other @seedprotocol/* packages your app uses (query, feed, mapping, eas, arweave, gateway-hyper) at the same version. A Next.js app installs @seedprotocol/next as a dev dependency in place of @seedprotocol/vite (see Next.js apps use @seedprotocol/next). @seedprotocol/react and @seedprotocol/publish declare the exact SDK version as a peer, so npm refuses a mismatched set with ERESOLVE, and no overrides block is needed. The SDK installs XState, Drizzle and ZenFS as its own dependencies, and React Query comes with @seedprotocol/react as a peer, which npm, pnpm and bun install. Add Seed to your app has the full install step.

Then read the section for each release after the one you're on. The Changelog lists the additions and fixes that need no work.

Coming fromRead
0.7.130.7.13 to 0.8.0
0.7.120.7.13 to 0.8.0, then 0.7.12 to 0.7.13
0.7.110.7.13 to 0.8.0, 0.7.12 to 0.7.13, then 0.7.11 to 0.7.12
0.7.100.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, then 0.7.10 to 0.7.11
0.7.90.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, then 0.7.9 to 0.7.10
0.7.80.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, 0.7.9 to 0.7.10, then the 0.7.9 parts of 0.7.7 to 0.7.9
0.7.70.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, 0.7.9 to 0.7.10, then 0.7.7 to 0.7.9
0.7.6Unpublished drafts leave the app's database (0.7.11 and later keep a copy of it) unless they're published first or the app starts once on 0.7.8. Then 0.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, 0.7.9 to 0.7.10, 0.7.7 to 0.7.9 and the 0.7.7 parts of 0.7.5 to 0.7.7
0.7.5Unpublished drafts leave the app's database (0.7.11 and later keep a copy of it) unless they're published first or the app starts once on 0.7.8. Then 0.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, 0.7.9 to 0.7.10, 0.7.7 to 0.7.9 and 0.7.5 to 0.7.7
0.7.4Unpublished drafts leave the app's database (0.7.11 and later keep a copy of it) unless they're published first or the app starts once on 0.7.8. Then 0.7.13 to 0.8.0, 0.7.12 to 0.7.13, 0.7.11 to 0.7.12, 0.7.10 to 0.7.11, 0.7.9 to 0.7.10, 0.7.7 to 0.7.9, 0.7.5 to 0.7.7 and 0.7.4 to 0.7.5
0.7.3The rows above, then 0.7.3 to 0.7.4
0.7.2The rows above, then 0.7.2 to 0.7.3
0.7.0 or 0.7.1The rows above, then @seedprotocol/eas and @seedprotocol/query (0.7.2) and Publishing related items (0.7.1)
0.5.x or 0.6.xThe rows above, then all of 0.6 to 0.7.2
0.4.xComing from 0.4.x, then the 0.5.x row

Upgrading from 0.7.13 to 0.8.0

Released: 2026-10-30. The contracts and session-key parts come first, then the SDK and the packages around it. The Changelog lists everything else in 0.8.0.

Publishing through a ManagedAccount needs 0.8.0

Affects: every app on 0.7.13 or earlier that publishes through a ManagedAccount on OP Sepolia. Changed in: the OP Sepolia contracts, 2026-10-30, and @seedprotocol/publish 0.8.0.

Since 2026-10-30 the OP Sepolia contracts route only the three-phase multiPublish, selector 0xa1263421. Releases from 0.6.9 to 0.7.13 send 0x2a29fadc, so they can't publish through a ManagedAccount. Upgrade before the next publish.

On 0.7.13 such a publish reverts in the Router with Router: function does not exist., reported as PUBLISH_PREFLIGHT_FAILED, or as AUTOMATION_PREFLIGHT_FAILED for a session key. The failure comes at the attestation step, after the publish's uploads have run, so a publish can spend its uploads and still fail. Direct EAS from an EOA, reads and item.unpublish() don't touch the changed contracts and work on both releases.

0.8.0 needs those contracts too. Against contracts from before them, on a chain that wasn't redeployed or an older local twin, a publish fails with MULTI_PUBLISH_NOT_ROUTED before anything is sent, naming both selectors. It goes with contracts fb7ba92; Deployments has the addresses, and assertMultiPublishRouted makes the same check in a deploy script (Other exports).

Point the config at the new executor, reinstall and grant keys again

Affects: apps that set modularAccountModuleContract, and every session key. Changed in: the OP Sepolia contracts, 2026-10-30.

The executor is now 0xa4799d6D6AF110D5d69437488aBC31A5ef8ACA5f. With the old address in the config, an executor install and enrollPublishAutomation fail with EXECUTOR_MODULE_NOT_INSTALLED, naming both addresses, and a session-key publish fails with MULTI_PUBLISH_NOT_ROUTED.

before (EXECUTOR_MODULE_NOT_INSTALLED on 0.8.0)
import { initPublish } from '@seedprotocol/publish'

initPublish({
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_SEED_UPLOAD_API_URL,
  useModularExecutor: true,
  modularAccountModuleContract: '0x80562aeEe4F16473779b1474D8D32d114D302e94',
})
after
import { initPublish } from '@seedprotocol/publish'

initPublish({
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_SEED_UPLOAD_API_URL,
  useModularExecutor: true,
  modularAccountModuleContract: '0xa4799d6D6AF110D5d69437488aBC31A5ef8ACA5f',
})

An account that installed the old executor reads as not installed, so its admin installs again, with no uninstall step. A key is approved for one executor's address, so each key is granted again. 0.8.0 has no migration code and doesn't do either for you unattended. Interactive publishing goes through neither the executor nor a key, so an app with no session keys needs only the new address; with it set, connecting or publishing installs the executor best-effort.

For each session key, connect the user and enroll the key again. That installs the executor with the admin wallet and replaces the key's target. A key can't do this itself: until the account has the new executor, a session-key publish fails with AUTOMATION_UNSUPPORTED_ACCOUNT, and once it has, a key still approved for the old one fails with MODULAR_SIGNER_ACTIVATION_FAILED.

after
import { enrollPublishAutomation } from '@seedprotocol/publish/thirdweb'

export async function grantKeyAgain(managedAddress: string, sessionKeyAddress: string) {
  const { authorization } = await enrollPublishAutomation({ managedAddress, sessionKeyAddress })
  return authorization.uid // store it in place of the old uid
}

Enrolling attests a new publishAuthorization and leaves the old one in place until it expires. An app that sends through createManagedAccountTxSender gets none of this from publish: its account has to be deployed before its first publish, and it calls ensureExecutorModuleInstalled itself, which only session keys need. See After the factory moves to a new executor.

Session-key helpers take wallets and expiresAt

Affects: code that calls ensureAutomationSessionKey or ensureExecutorModuleInstalled, or imports ensureManagedSignerSessionKey, defaultApprovedTargetsForModularPublish or toThirdwebSessionKeyPermissions. Changed in: @seedprotocol/publish 0.8.0.

ensureAutomationSessionKey requires expiresAt, with no ten-year default, and refuses a time that has passed with MODULAR_SIGNER_ACTIVATION_FAILED. From JavaScript, leaving it out fails with the same code.

before (TS2345 on 0.8.0)
import { ensureAutomationSessionKey } from '@seedprotocol/publish/thirdweb'

export async function grant(managedAddress: string, sessionKeyAddress: string) {
  await ensureAutomationSessionKey({ managedAddress, sessionKeyAddress })
}
after
import { ensureAutomationSessionKey } from '@seedprotocol/publish/thirdweb'

export async function grant(managedAddress: string, sessionKeyAddress: string) {
  const expiresAt = Math.floor(Date.now() / 1000) + 30 * 24 * 3600
  await ensureAutomationSessionKey({ managedAddress, sessionKeyAddress, expiresAt })
}

ensureExecutorModuleInstalled takes a function that returns the admin wallet, called only when an install has to be sent. The second argument was a thirdweb Account it ignored. On /thirdweb, pass undefined for the connected in-app admin.

before (TS2345 on 0.8.0)
import { ensureExecutorModuleInstalled } from '@seedprotocol/publish/thirdweb'
import type { Account } from 'thirdweb/wallets'

export function install(managedAddress: string, adminAccount: Account) {
  return ensureExecutorModuleInstalled(managedAddress, adminAccount, {
    modularAccountModuleContract: '0xa4799d6D6AF110D5d69437488aBC31A5ef8ACA5f',
  })
}
after
import { ensureExecutorModuleInstalled } from '@seedprotocol/publish/thirdweb'

export function install(managedAddress: string) {
  return ensureExecutorModuleInstalled(managedAddress, undefined, {
    modularAccountModuleContract: '0xa4799d6D6AF110D5d69437488aBC31A5ef8ACA5f',
  })
}

@seedprotocol/publish now exports these functions too, along with enrollPublishAutomation, revokePublishAutomation and removeAutomationSessionKey, and none of them needs thirdweb. There the wallets are required: userWallet sends as the account and admin is the admin EOA. Enroll and revoke refuse a userWallet that sends from another address, on both entries, with MANAGED_ACCOUNT_UNAVAILABLE. See Session keys without thirdweb.

Three exports left /thirdweb. ensureManagedSignerSessionKey and defaultApprovedTargetsForModularPublish were deprecated, since interactive publishing needs no session key. toThirdwebSessionKeyPermissions has no replacement, because grants no longer go through thirdweb's SDK; buildAutomationSessionKeyPermissions still gives the permissions.

before (TS2305 and TS2724 on 0.8.0)
import {
  defaultApprovedTargetsForModularPublish,
  ensureManagedSignerSessionKey,
  toThirdwebSessionKeyPermissions,
} from '@seedprotocol/publish/thirdweb'
after
import { buildAutomationSessionKeyPermissions } from '@seedprotocol/publish/thirdweb'

const { approvedTargets } = buildAutomationSessionKeyPermissions()
console.log(approvedTargets) // the configured executor

Grants default to 90 days

Affects: apps that enroll without expiresAt or attest a publishAuthorization without expirationTime, and code that reads PUBLISH_AUTHORIZATION_DEFAULT_EXPIRATION_SECS. Changed in: @seedprotocol/publish and @seedprotocol/eas 0.8.0.

Those defaults, and the constant, are 90 days; they were 365. Keys enrolled before the upgrade keep their end until they're enrolled again, which the executor change requires. Enroll again before the end to renew, or pass the window you want:

after
import { enrollPublishAutomation } from '@seedprotocol/publish/thirdweb'

export async function grantForAYear(managedAddress: string, sessionKeyAddress: string) {
  const expiresAt = Math.floor(Date.now() / 1000) + 365 * 24 * 3600
  return enrollPublishAutomation({ managedAddress, sessionKeyAddress, expiresAt })
}

Enroll refuses a funded account

Affects: apps that enroll session keys on accounts holding ETH. Changed in: @seedprotocol/publish 0.8.0.

Enrollment, renewal included, fails with AUTOMATION_ACCOUNT_FUNDED before anything is sent when the account's balance plus EntryPoint deposit is above maxAutomationExposureWei, 0.001 ETH by default. A failed read of those fails with MODULAR_SIGNER_ACTIVATION_FAILED. The re-enrollment above can hit this, so check before you start. Withdraw the ETH, or raise the limit for an account that holds it on purpose. An automation publish over the limit logs a warning and goes ahead.

after
import { initPublish } from '@seedprotocol/publish'

initPublish({
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_SEED_UPLOAD_API_URL,
  maxAutomationExposureWei: 50_000_000_000_000_000n, // 0.05 ETH
})

Large uploads need confirming

Affects: apps and pipelines that publish more than 25 MiB of files, or more than 100 MiB. Changed in: @seedprotocol/publish 0.8.0.

A publish adds up its uploads in checking, before it registers a schema or reads a file. Over 25 MiB it fails with UploadNeedsConfirmError unless it has confirmUpload: true, and in Chromium a smaller publish can fail the same way on a low-memory device. Over 100 MiB it fails with UploadTooLargeError, which confirmUpload doesn't lift. item.publish() rejects with these errors themselves, on Node and with Turbo too, so an unattended pipeline that used to pass stops here.

before (UploadNeedsConfirmError on 0.8.0, over 25 MiB)
import type { IItem } from '@seedprotocol/sdk'

export async function publishFromPipeline(item: IItem<any>) {
  await item.publish()
}
after
import type { IItem } from '@seedprotocol/sdk'

export async function publishFromPipeline(item: IItem<any>) {
  await item.publish({ confirmUpload: true, maxUploadBytes: 500 * 2 ** 20 })
}

An app with a user present asks and publishes again, as Confirm large uploads shows. confirmUploadAboveBytes: Infinity in the config doesn't turn off the memory check. See Upload limits.

getUnpublishedRelatedItems is removed

Affects: code that imports getUnpublishedRelatedItems. Changed in: @seedprotocol/sdk 0.8.0.

summarizePublishUploads(item, { publishMode }) returns the same list as unpublishedRelatedItems, with uploadCount, uploadBytes and uploadSizes, and reads file sizes only. summarizePublishWork still returns the list too.

before (TS2724 on 0.8.0)
import { getUnpublishedRelatedItems, type IItem } from '@seedprotocol/sdk'

export async function blockers(item: IItem<any>) {
  return getUnpublishedRelatedItems(item)
}
after
import { summarizePublishUploads, type IItem } from '@seedprotocol/sdk'

export async function blockers(item: IItem<any>) {
  const { unpublishedRelatedItems } = await summarizePublishUploads(item)
  return unpublishedRelatedItems
}

See Other read helpers.

A repeat init never changes addresses

Affects: apps that change addresses by calling client.init again. Changed in: @seedprotocol/sdk 0.8.0.

A repeat client.init leaves the stored addresses alone, whatever addresses holds. In 0.7.13 a different non-empty set replaced them. Only the first init, or the first after client.stop(), applies them, and of overlapping inits the first call's addresses win. This replaces the 0.7.13 rule.

before (keeps the old addresses on 0.8.0)
import { client, type SeedConstructorOptions } from '@seedprotocol/sdk'

export async function switchAccount(options: SeedConstructorOptions, account: string) {
  await client.init({ ...options, addresses: [account] })
}
after
import { client } from '@seedprotocol/sdk'

export async function switchAccount(account: string) {
  await client.setAddresses([account])
}

See Addresses.

SeedProvider and SeedClientGate apply address changes

Affects: apps that pass addresses to SeedProvider, or in SeedClientGate's initConfig, and hosts that pass [] while a wallet loads. Changed in: @seedprotocol/react 0.8.0.

A change to the prop goes through client.setAddresses, so a change to [] clears the stored addresses, which 0.7.13 kept. A host that passes [] for a moment while a connected wallet loads loses them. Leaving the prop out changes nothing, so leave it out until the wallet is known.

before (clears the stored addresses on 0.8.0)
import type { ReactNode } from 'react'
import { SeedProvider } from '@seedprotocol/react'
import type { SeedConstructorOptions } from '@seedprotocol/sdk'

declare const config: SeedConstructorOptions['config']

export function SeedRoot({ wallet, children }: { wallet?: string; children: ReactNode }) {
  return (
    <SeedProvider config={config} addresses={wallet ? [wallet] : []}>
      {children}
    </SeedProvider>
  )
}
after
import type { ReactNode } from 'react'
import { SeedProvider } from '@seedprotocol/react'
import type { SeedConstructorOptions } from '@seedprotocol/sdk'

declare const config: SeedConstructorOptions['config']

export function SeedRoot({ wallet, children }: { wallet?: string; children: ReactNode }) {
  return (
    <SeedProvider config={config} addresses={wallet ? [wallet] : undefined}>
      {children}
    </SeedProvider>
  )
}

A remount changes no addresses, whatever the prop holds. In 0.7.13 a remount with a different non-empty set replaced them, so an app that unmounts the provider or gate across a user switch now calls client.setAddresses with the new addresses. See Changing addresses.

relations rejects unknown values

Affects: untyped code that passes relations: 'urls' or any other value outside 'expand' and 'uids' to queryBySchema, getSeed or getSeeds. Changed in: @seedprotocol/query 0.8.0.

Such a call rejects with a TypeError (relations must be 'expand' or 'uids', got "urls") before any request. TypeScript already refused 'urls' in 0.7.13, where JavaScript got UIDs and the related seeds were still fetched.

before (TypeError on 0.8.0)
import { queryBySchema } from '@seedprotocol/query'

const posts = await queryBySchema('post', { relations: 'urls' })
after
import { queryBySchema } from '@seedprotocol/query'

const posts = await queryBySchema('post', { relations: 'uids' })

external_id keeps its name again

Affects: code written against 0.7.13's keys for a non-relation property whose EAS name ends in _id or _ids. Changed in: @seedprotocol/query 0.8.0.

Only bytes32 and bytes32[] properties are relations. A string external_id, which 0.7.13 returned as external, is back under external_id and externalId, as in 0.7.12, and its value isn't fetched as a related seed. Keys of real relations don't change: author_id is still author. The name rule now applies only to an attestation with no type.

before (undefined on 0.8.0)
import { queryBySchema } from '@seedprotocol/query'

const { items } = await queryBySchema('post', { relations: 'uids' })
const ids = items.map((post) => post.data.external)
after
import { queryBySchema } from '@seedprotocol/query'

const { items } = await queryBySchema('post', { relations: 'uids' })
const ids = items.map((post) => post.data.external_id)

See SeedRecord.

Hand-built query results need listed

Affects: code that builds a QueryBySchemaResult, such as a mock or an adapter. Changed in: @seedprotocol/query 0.8.0.

listed is required. It's the number of Seeds the listing returned for the page, before Seeds with no Version are left out. Paging code can drop its second listing: listed < limit ends the listing, and listed === limit doesn't promise more, so the next page can be empty.

before (TS2741 on 0.8.0)
import type { QueryBySchemaResult } from '@seedprotocol/query'

export const emptyPage: QueryBySchemaResult = { items: [], limit: 25, skip: 0 }
after
import type { QueryBySchemaResult } from '@seedprotocol/query'

export const emptyPage: QueryBySchemaResult = { items: [], limit: 25, skip: 0, listed: 0 }

See Page through results.

The first reads after upgrading are cold

Affects: apps that cache items with @seedprotocol/query. Changed in: @seedprotocol/query 0.8.0.

The item cache codes are e6 for 'expand' and e7 for 'uids'; they were e3 and e5. Records cached by 0.7.13, on disk included, aren't read, so the first reads after the upgrade assemble every record again. See Caching.

The document lookup table is removed

Affects: saved MappingDocuments with a top-level lookups, code that passes lookups to applyMapping, applyMappingAsync, resolveLookup, previewForEdge or FieldMapper, and code that imports MappingLookups. Changed in: @seedprotocol/mapping 0.8.0.

Lookups read lookup.entries on the edge. Nothing warns at run time: a table in a saved document, or an option passed from JavaScript, is ignored, and its values count as misses. Stored { value, ref } entries still apply. Move a table onto its edges before you apply the document; entries then match without regard to case or spacing, where the table needed the exact text.

before (TS2353 on 0.8.0)
import type { MappingDocument } from '@seedprotocol/mapping'

export const saved: MappingDocument = {
  version: 1,
  sourceKind: 'rss',
  mappings: [{ sourceId: 'creator', propertyName: 'author', resolve: 'lookup' }],
  lookups: { author: { 'Jane Doe': 'jane' } },
}
after
import { lookupEntriesFromMapping, type MappingDocument } from '@seedprotocol/mapping'

type OlderDocument = MappingDocument & {
  lookups?: Record<string, Record<string, string | string[]>>
}

export function moveLookupsOntoEdges({ lookups = {}, ...doc }: OlderDocument): MappingDocument {
  const mappings = doc.mappings.map((edge) => {
    const table = lookups[edge.propertyName]
    if (edge.resolve !== 'lookup' || !table) return edge
    const moved = Object.entries(table).map(([value, refs]) => ({ value, refs: [refs].flat() }))
    return { ...edge, lookup: { ...edge.lookup, entries: [...lookupEntriesFromMapping(edge), ...moved] } }
  })
  return { ...doc, mappings }
}

Run that once on each saved document and save the result. See Saving and loading a mapping.

FieldMapper shows its built-in lookup editor

Affects: apps that render FieldMapper with lookup edges and without renderLookup or slots.lookups: false, and apps that pass onLookupsChange. Changed in: @seedprotocol/mapping 0.8.0.

The text editor needed onLookupsChange, which is gone, so these apps now get an input they didn't have, on each lookup row by default. It writes lookup.entries through onChange. Hide it if your app sets entries elsewhere:

before (shows the editor on 0.8.0)
import { FieldMapper, type FieldMapperProps } from '@seedprotocol/mapping/react'

export function Mapper(props: FieldMapperProps) {
  return <FieldMapper {...props} />
}
after
import { FieldMapper, type FieldMapperProps } from '@seedprotocol/mapping/react'

export function Mapper(props: FieldMapperProps) {
  return <FieldMapper {...props} slots={{ ...props.slots, lookups: false }} />
}

See Built-in editor.

connectSourceToProperty returns the list

Affects: code that calls connectSourceToProperty from @seedprotocol/mapping/react. Changed in: @seedprotocol/mapping 0.8.0.

It returns FieldMapping[]. It returned { mappings, clearedLookupProps }.

before (TS2339 on 0.8.0)
import type { FieldMapping, SourceNode, TargetProperty } from '@seedprotocol/mapping'
import { connectSourceToProperty } from '@seedprotocol/mapping/react'

export function connect(edges: FieldMapping[], sources: SourceNode[], targets: TargetProperty[]) {
  return connectSourceToProperty(edges, sources, targets, 'title', 'title').mappings
}
after
import type { FieldMapping, SourceNode, TargetProperty } from '@seedprotocol/mapping'
import { connectSourceToProperty } from '@seedprotocol/mapping/react'

export function connect(edges: FieldMapping[], sources: SourceNode[], targets: TargetProperty[]) {
  return connectSourceToProperty(edges, sources, targets, 'title', 'title')
}

probeGateway rejects a bare signal

Affects: JavaScript that still calls probeGateway(url, signal). Changed in: @seedprotocol/arweave 0.8.0.

The call rejects with TypeError: probeGateway takes options: pass { signal }, not an AbortSignal before any request. The signal was ignored in 0.7.13.

before (TypeError on 0.8.0)
import { probeGateway } from '@seedprotocol/arweave'

const controller = new AbortController()
const healthy = await probeGateway('https://zigza.xyz', controller.signal)
after
import { probeGateway } from '@seedprotocol/arweave'

const controller = new AbortController()
const healthy = await probeGateway('https://zigza.xyz', { signal: controller.signal })

createTransaction is typed

Affects: TypeScript that uses the result of createTransaction, or the ArweaveTransaction type. Changed in: @seedprotocol/arweave 0.8.0.

ArweaveTransaction is the arweave package's Transaction, and createTransaction returns Promise<ArweaveTransaction>; both were any. Nothing changes at run time. Wrong use of a declared member stops compiling. Reading an undeclared property still compiles, because Transaction has an index signature.

before (TS2345 on 0.8.0)
import { BaseArweaveClient } from '@seedprotocol/arweave'

const tx = await BaseArweaveClient.createTransaction('hello')
tx.addTag('Version', 1)
after
import { BaseArweaveClient } from '@seedprotocol/arweave'

const tx = await BaseArweaveClient.createTransaction('hello')
tx.addTag('Version', '1')

See Transaction methods.

Errors and messages apps may match

Affects: code that matches error messages, and exhaustive switches on ManagedAccountPublishErrorCode. Changed in: @seedprotocol/sdk and @seedprotocol/publish 0.8.0.

getPublishModels throws UnknownModelError, with modelName and schemaName, and its message is Model "Nope" not found.; it was a plain Error reading No model named Nope.

before (doesn't match on 0.8.0)
import { getPublishModels } from '@seedprotocol/sdk'

export async function publishModels(names: string[]) {
  try {
    return await getPublishModels(names)
  } catch (error) {
    if (error instanceof Error && error.message.startsWith('No model named')) return []
    throw error
  }
}
after
import { getPublishModels, UnknownModelError } from '@seedprotocol/sdk'

export async function publishModels(names: string[]) {
  try {
    return await getPublishModels(names)
  } catch (error) {
    if (error instanceof UnknownModelError) return []
    throw error
  }
}

Model.create with a name that can't publish rejects with the reserved-word message alone, Model "Event" publishes as "event", … or Property "view" publishes as "view", …. The Model "X" could not create its properties: prefix is gone for this case, and with no properties it rejects too, where it resolved. With waitForReady: false it throws. Nothing stays in schema.models, so saveNewVersion() isn't blocked.

Renaming a property to such a name still doesn't throw, but validate() now returns an error with code reserved_name. While the bad name stands, saveNewVersion() resolves with '' if nothing else changed, and rejects with the reserved-word message if there's a valid edit to save. See Reserved Solidity words.

A direct EAS backward reference fails with Direct EAS publish from <address> can't fill a reference backwards: …, code unchanged. With no wallet, a PublishManager.createPublish run fails with PublishWalletMissingError (code: 'PUBLISH_WALLET_MISSING'), but item.publish() stops before that with a plain Error with the same text, so match the class only on direct runs. ManagedAccountPublishErrorCode gains AUTOMATION_ACCOUNT_FUNDED, PUBLISH_TOO_LARGE and MULTI_PUBLISH_NOT_ROUTED, so an exhaustive switch needs the three cases.

Upgrading from 0.7.12 to 0.7.13

Released: 2026-10-29. The SDK parts come first, then the packages around it. The Changelog lists everything else in 0.7.13.

Names that publish as a Solidity word don't load

Affects: schema files, and models and properties created in code, whose EAS name is a word the Solidity ABI reserves. Changed in: @seedprotocol/sdk 0.7.13.

The words are constructor, error, event, fallback, function, receive, struct, calldata, memory, storage, payable, indexed, tuple, returns, constant, external, internal, private, public, pure, view and override. EAS couldn't register or attest a schema under one of them, so a publish failed at its first encode. The check is on the EAS name, so a property Returns and a model Event are refused too; a List of Relation is exempt, since it publishes as <name><Model>Ids. A schema file with such a name now fails to load, so client.init rejects:

Could not load config.schema: Schema blog: Post.view: Property "view" publishes as "view", which EAS schemas can't use (a reserved Solidity ABI word). Rename it.

ModelProperty.create with such a name rejects with Failed to write property "view": …, and Model.create rejects for such a model name when you pass properties. Rename the property or model, and raise version:

seed.schema.json (before)
"view": { "type": "Number" },
"returns": { "type": "Text" }
seed.schema.json (after)
"viewCount": { "type": "Number" },
"returnPolicy": { "type": "Text" }

Don't rename a loaded property to one of these words in code either: the rename isn't saved, and the next saveNewVersion() rejects with the same message. See Reserved Solidity words.

Missing files read undefined

Affects: code that reads the value of an Image, File or Html property whose file may not be on this device, such as an item synced from another device. Changed in: @seedprotocol/sdk and @seedprotocol/react 0.7.13.

When the file isn't found, value is undefined. It was the storage seed's ID for an Html property, the file name for an Image, and 'No file found' for a File, which code couldn't tell from content. The new valueStatus says which case applies: 'missing' (with the path looked for in missingFile.path), 'fetching' while the property loads, and 'resolved' otherwise; it's always 'resolved' for other types. refResolvedValue and localStoragePath still name the file, and the property reads it again when the file is saved, by a download or another tab. useItemProperty returns valueStatus too, and re-renders when it changes.

before
if (attachment.value === 'No file found') showPlaceholder()
after
import type { IItemProperty } from '@seedprotocol/sdk'

export function attachmentUrl(attachment: IItemProperty): string | undefined {
  if (attachment.valueStatus === 'missing') {
    console.warn('Not on this device yet:', attachment.missingFile?.path)
    return undefined
  }
  if (attachment.valueStatus === 'fetching') return undefined
  return typeof attachment.value === 'string' ? attachment.value : undefined
}

See valueStatus and missingFile.

Empty addresses on a repeat init keep the stored ones

Affects: apps that sign a user out by passing empty addresses to client.init again, or by changing the addresses they pass to SeedProvider or in SeedClientGate's initConfig to []. Changed in: @seedprotocol/sdk 0.7.13.

A repeat client.init with the same config and empty addresses ([], or { owned: [], watched: [] }) keeps the stored addresses, so a remount before a wallet connects no longer drops them. It cleared them. A different non-empty set still replaces them, and the first init of a process, or the first after client.stop(), still clears them with []. To sign out, clear them yourself:

after
import { client } from '@seedprotocol/sdk'

export async function signOut() {
  await client.setAddresses([])
}

In 0.8.0 a repeat init never changes addresses. See Addresses.

ClientNotInitializedError replaces matching the message

Affects: code that tells a call made before init, or after client.stop(), from other errors by its message. Changed in: @seedprotocol/sdk 0.7.13.

Item.find rejects with ClientNotInitializedError (code: 'CLIENT_NOT_INITIALIZED', exported from both entries) once readyTimeout passes without an open database, and every client method that needs init throws it before init has finished and after stop() or unload(). Both used a plain Error. The message is still ClientManager is not initialized. Please call init() first., so code that matches it keeps working, but instanceof or code doesn't depend on the text:

after
import { ClientNotInitializedError, Item } from '@seedprotocol/sdk'

try {
  const post = await Item.find({ modelName: 'Post', seedLocalId })
  renderPost(post)
} catch (error) {
  if (error instanceof ClientNotInitializedError) showNotice('Seed is still starting.')
  else throw error
}

See Errors.

React: early creates and the status types

Affects: code that calls useCreateModelProperty().create before the client is ready, and code that compares useGlobalServiceStatus()'s status or internalStatus with a string. Changed in: @seedprotocol/react 0.7.13.

create called before the app database is open rejects with ClientNotInitializedError, where it rejected with No stored schema has the name or schema file id "<id>", which read like a misspelt schema ID. That message now means only that no stored schema matches.

status is typed ClientManagerState | 'stopped' and internalStatus ClientManagerState | 'ready' | 'stopped'; both were string. A comparison with a value outside the union no longer type-checks. 'ready' is an internalStatus value, not a status one:

before (TS2367 on 0.7.13)
const { status } = useGlobalServiceStatus()
if (status === 'ready') return null

The hook also returns stopReason: 'reset' when client.resetLocalData() stopped the client, in this tab or another, and 'stop' after client.stop() or client.unload().

after
import { useCreateModelProperty, useGlobalServiceStatus } from '@seedprotocol/react'
import { ClientNotInitializedError } from '@seedprotocol/sdk'

export function AddSubtitle() {
  const { create } = useCreateModelProperty()
  const { status, stopReason } = useGlobalServiceStatus()

  if (status === 'stopped') return <p>{stopReason === 'reset' ? 'Local data was deleted.' : 'Signed out.'}</p>

  const addSubtitle = async () => {
    try {
      await create('blog', 'Post', { name: 'subtitle', dataType: 'Text' })
    } catch (error) {
      if (error instanceof ClientNotInitializedError) showNotice('Seed is still starting. Try again in a moment.')
      else throw error
    }
  }

  return <button onClick={addSubtitle} disabled={status !== 'idle'}>Add a subtitle</button>
}

See useGlobalServiceStatus and useCreateModelProperty.

relations: 'urls' is removed

Affects: code that passes relations: 'urls' to queryBySchema, getSeed or getSeeds. Changed in: @seedprotocol/query 0.7.13.

RelationsMode is 'expand' | 'uids'. 'urls' fetched the same related seeds as 'expand', so it saved no requests. Use 'expand', or 'uids' to skip fetching related seeds:

before (TS2322 on 0.7.13)
const posts = await queryBySchema('post', { relations: 'urls' })
after
import { queryBySchema } from '@seedprotocol/query'

const posts = await queryBySchema('post', { relations: 'expand' })

Untyped code that still passes 'urls' gets no error: its relations come back as UIDs, as with 'uids', but the related seeds are still fetched. In 0.8.0 it rejects with a TypeError.

Relation keys are the same in every mode

Affects: code that reads relations from query records or changelog entries under their EAS names (author_id, author_identity_ids) or camelCase copies (authorId, authorIdentityIds), and properties whose EAS name ends in _id or _ids. Changed in: @seedprotocol/query 0.7.13.

Relations come back under their schema key with relations: 'uids', with 'expand' (also for a related seed that's missing), and in changelog entries: author_id as author, and a list's author_identity_ids as authors. A camelCase copy is added only when it differs (feature_image_id comes back as feature_image and featureImage), so authorId and authorIdentityIds are gone. In 0.7.12, 'uids' records kept the EAS names and their camelCase copies.

The key comes from the EAS name alone. A property whose EAS name ends in _id or _ids is read as a relation and loses the suffix, whatever its type, so a Text external_id comes back as external, under no other key. A list's key keeps only the first word of its EAS name, so co_author_identity_ids comes back as cos. In 0.8.0 a non-relation keeps its name, and setQueryModels gives lists their schema keys.

after
import { queryBySchema } from '@seedprotocol/query'

const { items } = await queryBySchema('post', { relations: 'uids' })

for (const post of items) {
  const authorUids = post.data.authors // was author_identity_ids and authorIdentityIds
  const coverUid = post.data.feature_image // was feature_image_id and featureImageId
  console.log(post.seedUid, authorUids, coverUid)
}

The cache keys changed with the record shape, so records cached by 0.7.12, on disk included, aren't read, and the first reads after the upgrade assemble every record again. See SeedRecord and Caching.

queryBySchema pages can be short

Affects: code that pages through queryBySchema or queryBySchemaForMonth and stops at the first page with fewer items than limit. Changed in: @seedprotocol/query 0.7.13.

queryBySchema, queryBySchemaForMonth and assembleSeeds leave out a Seed with no Version from its attester, as getSeed and getSeeds have since 0.7.12. They returned it with versionUid: '' and no properties. limit and skip still count the Seeds listed before that, so a page can hold fewer items than limit, or none, while later pages still have items. Code that read versionUid === '' to skip such records can drop the check. A page doesn't say whether more follow; to page to the end, stop when the source's own listing comes back short. In 0.8.0 listed gives that count, and Page through results uses it. See queryBySchema.

createEasNodeDispatcher is async

Affects: code that calls createEasNodeDispatcher, and subclasses of NodeEasClient that override getDispatcher. Changed in: @seedprotocol/eas 0.7.13.

createEasNodeDispatcher(options?) returns Promise<Dispatcher | undefined>. It loads undici's Agent with a dynamic import, so a bundled @seedprotocol/eas (Astro with ssr.noExternal, say) finds undici, where the first EAS request failed with Cannot find module 'undici/lib/dispatcher/agent.js'. Await it:

before (TS2322 on 0.7.13)
const dispatcher = createEasNodeDispatcher({ keepAliveTimeoutMs: 10_000 })
const eas = new NodeEasClient({ dispatcher: dispatcher ?? false })
after
import { createEasNodeDispatcher, NodeEasClient } from '@seedprotocol/eas/node'

const dispatcher = await createEasNodeDispatcher({ keepAliveTimeoutMs: 10_000 })
const eas = new NodeEasClient({ dispatcher: dispatcher ?? false })

createEasGraphQLClient's dispatcher option takes the promise as it is. NodeEasClient's protected getDispatcher() returns Promise<Dispatcher | undefined>, so an override returns a promise too. See registerNodeEasPlatform.

Automation needs ensureEasSchemasForModels

Affects: apps that publish with an automation session key, and code that matches the missing-schema message. Changed in: @seedprotocol/publish 0.7.13.

A publish now registers only the EAS schemas it attests: the models of the item and its drafts, the properties it sets or removes, and the storage transaction of a seed that uploads. An owner's publish no longer registers schemas for properties it leaves empty, so publishing once with the owner's wallet no longer prepares every schema an automation key might need. Run ensureEasSchemasForModels with the owner's wallet instead, before automation starts and again after a schema version adds a property.

An automation key that meets a missing schema throws EasSchemasNotRegisteredError (code: 'EAS_SCHEMAS_NOT_REGISTERED'), whose missing lists every missing definition, checked before anything is sent. It was a plain Error naming the first one, … Call ensureEasSchemasForItem with the owner wallet first.; the message now names ensureEasSchemasForModels, and a failed publish's errorMessage changes with it. Match the class or code:

after
import { EasSchemasNotRegisteredError, ensureEasSchemasForModels } from '@seedprotocol/publish'

// With the owner's wallet, before automation runs, and again after a schema version adds a property:
const { registered } = await ensureEasSchemasForModels(['Post'], ownerWallet)
console.log(`Registered ${registered.length} EAS schemas`)

// Where automation reports a failed publish:
export function reportPublishError(error: unknown) {
  if (error instanceof EasSchemasNotRegisteredError) {
    alertOwner(`Register these EAS schemas with the owner wallet: ${error.missing.join(', ')}`)
  } else {
    throw error
  }
}

Pass { dryRun: true } to get the missing definitions without sending anything. See Other exports and Model and property schemas.

probeGateway takes an options object

Affects: code that calls probeGateway(baseUrl, signal). Changed in: @seedprotocol/arweave 0.7.13.

The second argument is { signal?, timeoutMs?, recordCircuit? }. A probe times out after timeoutMs (default 5,000) with or without a signal; with a signal it had no timeout. A probe the caller aborts resolves false without counting as a failure, where it opened the host's circuit for 60 seconds; a timeout still counts. recordCircuit: false probes a host whose circuit is open and leaves the circuit as it is.

before (TS2559 on 0.7.13)
const healthy = await probeGateway('https://zigza.xyz', controller.signal)
after
import { probeGateway } from '@seedprotocol/arweave'

const controller = new AbortController()
const healthy = await probeGateway('https://zigza.xyz', { signal: controller.signal, timeoutMs: 3_000 })

JavaScript that still passes the signal second gets no error: the signal is ignored and the 5-second timeout applies. In 0.8.0 the call rejects. See Other read helpers.

permagate.io leaves the default gateways

Affects: apps that rely on the default read gateways, and code that reads DEFAULT_ARWEAVE_GATEWAYS. Changed in: @seedprotocol/arweave 0.7.13.

DEFAULT_ARWEAVE_GATEWAYS is ['arweave.net', 'zigza.xyz', 'ar.seedprotocol.io']. Reads, probes, status checks and the failover in nextArweaveGatewayUrl and SeedHtml no longer go to permagate.io, so the next gateway after arweave.net is zigza.xyz. Links on permagate.io still parse and get re-hosted: it's in the new KNOWN_ARWEAVE_GATEWAYS, which isKnownArweaveGatewayHostname and link parsing check. See The default gateway and its fallbacks.

Feed guids and Atom fields

Affects: apps that serve feeds with @seedprotocol/feed, and code that passes items with their own title, updated or links to createFeed. Changed in: @seedprotocol/feed 0.7.13.

An item with a storage transaction and no link of its own, such as an Image item, has the RSS guid ar://<txId>, so its Atom entry ID and JSON Feed item ID are that too. Its link is still https://<public host>/raw/<txId>. The guid was that link, so it changed whenever arweaveDomain did. Feed readers show these items once more as new, and after that a gateway change doesn't repeat them. A guid is no longer moved onto the public gateway with an item's other Arweave links. A cached document keeps the old guids until it's rebuilt: the first page when query's collection, the site config or cacheBust changes, and later pages and archives when their TTLs run out. To switch at once, delete the files in CACHE_DIR and restart.

In Atom, an item's own title, updated and links win over the defaults again, as before 0.7.12. A field that is present but null also replaces the default, so updated: null gives an entry with no <updated>. See Field mapping.

Lookup entries take a list of refs

Affects: code that builds or reads mapping lookup entries, calls lookupRefFromEntries or resolveLookupMapped, or builds FieldMapperLookupRow objects itself. Changed in: @seedprotocol/mapping 0.7.13.

LookupEntry is { value: string; refs: string[] }, so one entry can name several items, as for a joint byline. A List target gets every ref, a Relation target the first. lookupRefFromEntries is replaced by lookupRefsFromEntries, which returns the list, and resolveLookupMapped by resolveLookup, which returns { refs, unmatched, split }. Saved MappingDocuments keep working: a stored { value, ref } applies as refs: [ref], and lookupEntriesFromMapping returns it in the new shape.

before (TS2724 and TS2561 on 0.7.13)
const authorEdge: FieldMapping = {
  sourceId: 'author',
  propertyName: 'author',
  resolve: 'lookup',
  lookup: { entries: [{ value: 'Jane Doe', ref: janeId }] },
}

const ref = lookupRefFromEntries(authorEdge.lookup?.entries, 'Jane Doe')
after
import { lookupRefsFromEntries, resolveLookup, type FieldMapping } from '@seedprotocol/mapping'

const authorEdge: FieldMapping = {
  sourceId: 'author',
  propertyName: 'authors',
  resolve: 'lookup',
  lookup: {
    entries: [
      { value: 'Jane Doe', refs: [janeId] },
      { value: 'Sam Ortiz & Mira Chen', refs: [samId, miraId] },
    ],
  },
}

const refs = lookupRefsFromEntries(authorEdge.lookup?.entries, 'Jane Doe') // [janeId]
const { refs: matched, unmatched } = resolveLookup(authorEdge, 'Pat Lee') // [], ['Pat Lee']

Entries now match without regard to case or spacing ('MIRA CHEN' finds 'Mira Chen'); the deprecated document-level lookups table still needs the exact text. FieldMapperLookupRow has a required onSplitChange, so a lookup row your code builds, in a test or a custom renderer, needs one. See LookupEntry and Relation lookups.

Gateway Hyper: upgrade the operator and its clients together

Affects: Gateway Hyper operators that restrict clients with an allowlist, and their clients. Changed in: @seedprotocol/gateway-hyper 0.7.13.

A client refused by address gets the 403 for every request: the operator's 403 carries connection: close, and the client opens a new stream for its next request. That needs 0.7.13 on both sides. While the operator or the client runs 0.7.12, sequential requests alternate between the 403 and a 502 (Unexpected end of stream). See Restricting who can connect.

Node 20.18.1 for query, feed and feed-hyper

Affects: apps that run @seedprotocol/query, @seedprotocol/feed or @seedprotocol/feed-hyper on Node older than 20.18.1. Changed in: those packages, 0.7.13.

Their engines declare Node 20.18.1 or later, the range @seedprotocol/eas already declared; query and feed declared none, and feed-hyper Node 20. npm warns on an older Node, and refuses the install only with engine-strict set. Upgrade Node to 20.18.1 or later.

Upgrading from 0.7.11 to 0.7.12

Released: 2026-10-27. The SDK parts come first, then the packages around it. The Changelog lists everything else in 0.7.12.

Validation codes are names

Affects: code that reads code on ItemCreateValidationError.problems, ItemPropertySaveValidationError.validationErrors or validate() results, or that matches validation messages. Changed in: @seedprotocol/sdk 0.7.12.

Every check has a named code: type for a wrong type, pattern, min_length, max_length_exceeded, enum_violation, format for a Date that isn't one, and unknown_data_type. A property definition's own checks add required, unknown_field, minimum, maximum and rule_not_applicable. array_not_allowed, missing_type and unknown_property are unchanged. Type and pattern failures used to be numbers in strings ('41', '53', '62'), and a minLength miss was coded max_length_exceeded. An enum miss is one problem, Expected one of: "draft", "live". Received: "nope", where it was two Expected union value problems, and the other problems on a property with an enum keep their own codes instead of enum_violation. Messages changed with the codes (Text must be at least 3 characters.), so match on code:

after
import { Item, ItemCreateValidationError } from '@seedprotocol/sdk'

try {
  await Item.create({ modelName: 'Post', schemaName: 'Blog', title: form.title, status: form.status })
} catch (error) {
  if (!(error instanceof ItemCreateValidationError)) throw error
  for (const problem of error.problems) {
    switch (problem.code) {
      case 'min_length':
        showFieldError(problem.property, 'Too short.')
        break
      case 'enum_violation':
        showFieldError(problem.property, 'Pick one of the listed options.')
        break
      default:
        showFieldError(problem.property, problem.message)
    }
  }
}

See ItemPropertySaveValidationError for which rule gives which code.

Schemas with misplaced rules or unknown types don't load

Affects: schema files, and properties defined in code, that set pattern, minLength or maxLength on a type other than Text or Html, or that name a type that doesn't exist. Changed in: @seedprotocol/sdk 0.7.12.

Those rules were ignored on other types, and an unknown type name was kept (blob was read as Blob). A schema file with either now fails to load before anything is stored, so client.init rejects, for example with Could not load config.schema: Schema Blog: Post.wordCount: validation.maxLength only apply to Text and Html properties (this one is Number). Remove it or use enum. or … unknown type "Integer". Expected one of: Text, Number, …. In code, the property's validate() reports rule_not_applicable or unknown_data_type. Remove the rule and use one of the ten data types:

seed.schema.json (before)
"wordCount": { "type": "Number", "validation": { "maxLength": 6 } },
"rating": { "type": "Integer" }
seed.schema.json (after)
"wordCount": { "type": "Number" },
"rating": { "type": "Number" }

validation.custom no longer fails the property's validation: the schema loads and logs [seed] schema <name>: <Model>.<property>: Custom validators are not supported … the first time it's stored. The rule still has no effect. See Validation.

Item.getById doesn't hold

Affects: code that calls unload() after Item.getById, or keeps an item from it across long work. Changed in: @seedprotocol/sdk 0.7.12.

Item.getById(id) takes a cache hold only when called with { hold: true }, as Item.find has since 0.7.10. Without one, the item stays cached for the grace period after the lookup. Item.createById still holds. An unload() after a plain getById now releases a hold someone else took, such as a mounted useItem's, so drop it, or pass { hold: true } when you keep the item:

before
const post = Item.getById(seedLocalId)
renderPost(post)
post?.unload()
after
import { Item } from '@seedprotocol/sdk'

const post = Item.getById(seedLocalId) // no hold: don't unload() it
renderPost(post)

const kept = Item.getById(seedLocalId, { hold: true }) // one hold: unload() it when done
try {
  await exportPost(kept)
} finally {
  kept?.unload()
}

See Cache helpers.

Item.find before init rejects

Affects: code that calls Item.find before client.init or after client.stop(), such as a module that loads data when it's imported. Changed in: @seedprotocol/sdk 0.7.12.

Item.find resolved undefined then, as for an item that doesn't exist. It now waits for the database for readyTimeout (15 seconds by default) and rejects with a plain Error whose message is ClientManager is not initialized. Please call init() first. and which has no code. A find called while client.init runs waits for it and works. Start lookups once client.init has been called, or in React inside SeedClientGate; to tell this rejection from a failed load, match the message. 0.7.13 gives it a class and a code; see ClientNotInitializedError replaces matching the message. See Item.find.

Item.create checks relation targets

Affects: code that creates items with Relation or List of Relation values, such as imports that create related items in any order. Changed in: @seedprotocol/sdk 0.7.12.

The initial relation values are checked when the create's write is applied, as an assignment's are. A seedLocalId that names no item rejects the create with OutboxOpFailedError (message Relation target not found: <id>, errorName: 'RelationTargetError'), whose cause is a RelationTargetError listing the missing IDs in targets, and nothing is written. 0.7.11 stored the dangling ID. A target whose own create is still queued is waited for, and seedUids and the new item's own ID pass. Create the targets first, then:

after
import { Item, OutboxOpFailedError, RelationTargetError } from '@seedprotocol/sdk'

try {
  await Item.create({ modelName: 'Post', schemaName: 'Blog', title: 'Hello', author: authorId })
} catch (error) {
  if (error instanceof OutboxOpFailedError && error.cause instanceof RelationTargetError) {
    showError(`Missing author: ${error.cause.targets.join(', ')}`)
  } else {
    throw error
  }
}

See Validation.

removeLocalCopiesForAddresses keeps unpublished edits

Affects: code that calls client.removeLocalCopiesForAddresses, such as a sign-out flow. Changed in: @seedprotocol/sdk 0.7.12.

A copy whose publisher is still an owned address and that has an unpublished edit or a pending outbox write is kept, and its local ID is listed in the result's new keptSeedLocalIds. 0.7.11 deleted it, and its edits, with the rest. A sign-out flow that calls it before it removes the account from the owned addresses now leaves those items on the device. Pass { includeUnpublished: true } to remove them too:

after
import { client } from '@seedprotocol/sdk'

const { keptSeedLocalIds } = await client.removeLocalCopiesForAddresses([address])
if (keptSeedLocalIds.length > 0) {
  showNotice(`${keptSeedLocalIds.length} items with unpublished edits stay on this device.`)
}

// To remove those too:
await client.removeLocalCopiesForAddresses([address], { includeUnpublished: true })

See client.

arweave.net is the default gateway

Affects: apps that read or publish through Seed's gateway, ar.seedprotocol.io, without setting arweaveDomain, and code that compares Arweave URLs with a host. Changed in: @seedprotocol/arweave and @seedprotocol/sdk 0.7.12.

DEFAULT_ARWEAVE_HOST, and so the default arweaveDomain, is arweave.net. Reads try it first, and publish writes it into the links of the Html it uploads. DEFAULT_ARWEAVE_GATEWAYS is arweave.net, permagate.io, zigza.xyz, ar.seedprotocol.io (0.7.13 drops permagate.io; see permagate.io leaves the default gateways), and DEFAULT_ARWEAVE_GRAPHQL_URL is https://arweave.net/graphql. An app that sets arweaveDomain: 'arweave.net' can drop it. An app that wants Seed's gateway sets it:

after
import { client } from '@seedprotocol/sdk'
import schema from './seed.schema.json'

await client.init({
  config: { schema, arweaveDomain: 'ar.seedprotocol.io' },
})

An Html value now reads back with its Arweave links on this reader's first read gateway, while the stored file keeps them as written, so code that compares a read Html value with the markup it saved can see other hosts. See Gateway settings and Links in published content.

getArweaveUrlForTransaction returns the /raw/ URL

Affects: code that calls getArweaveUrlForTransaction (from @seedprotocol/sdk, @seedprotocol/query or @seedprotocol/arweave), or that reads query's image arweaveUrl fields and storage URLs. Changed in: @seedprotocol/arweave, @seedprotocol/sdk and @seedprotocol/query 0.7.12.

There is one getArweaveUrlForTransaction, @seedprotocol/arweave's, re-exported by the SDK and query. It returns {first read gateway}/raw/{id}, the URL this reader loads from, which can be a sidecar or proxy; it was {read host}/{id}. Query's image arweaveUrl fields and storage URLs use it. For a URL that leaves this reader, such as a share link or one in content you publish, use getPublicArweaveRawUrl (install @seedprotocol/arweave at the same version):

after
import { getPublicArweaveRawUrl } from '@seedprotocol/arweave'
import { getArweaveUrlForTransaction } from '@seedprotocol/sdk'

const shareUrl = getPublicArweaveRawUrl(txId) // https://arweave.net/raw/<txId>, for links others open
const readUrl = getArweaveUrlForTransaction(txId) // this reader's first read gateway, for loading here

See Media references.

uploadApiBaseUrl doesn't fall back to the gateway

Affects: apps with transport: 'http-gateway', or 'hybrid' falling back to HTTP, whose gateway also serves the upload API and that don't set uploadApiBaseUrl. Changed in: @seedprotocol/arweave 0.7.12.

resolveSeedGatewayEndpoints resolves an unset uploadApiBaseUrl to '', where it used the gateway's base URL. With the new default host that would be arweave.net, where no upload API runs. Set uploadApiBaseUrl to your gateway's upload API. Proxy and sidecar modes still upload through the proxy or sidecar. See resolveSeedGatewayEndpoints.

Custom query sources implement one listing method

Affects: code that registers its own QueryDataSource. Changed in: @seedprotocol/query 0.7.12.

listSeedsByUidPrefix is removed. listSeedsBySchemaName(schemaName, opts) gets the filters in opts: uidPrefix and the new attesters, both normalized, with limit and skip applying after them. A source spread from the remote one inherited its listSeedsByUidPrefix, so a uidPrefix read still filtered. Now a source that reads only limit and skip still type-checks but returns Seeds the call meant to leave out. A custom getSeedByUid gets the UID in lowercase.

before (TS2353 on 0.7.12)
const onePublisher: QueryDataSource = {
  ...remote,
  kind: 'local',
  listSeedsBySchemaName(schemaName, { limit, skip }) {
    return remote.listSeedsBySchemaName(schemaName, { limit, skip })
  },
  listSeedsByUidPrefix(schemaName, uidPrefix, { limit, skip }) {
    return remote.listSeedsByUidPrefix(schemaName, uidPrefix, { limit, skip })
  },
}
after
import { createRemoteQueryDataSource, registerLocalQuerySource, type QueryDataSource } from '@seedprotocol/query'

const publisher = '0xe870b14fcff4a408e6ecb65bd40f64d08859efb3' // lowercase, as opts.attesters is
const remote = createRemoteQueryDataSource()

const onePublisher: QueryDataSource = {
  ...remote,
  kind: 'local',
  listSeedsBySchemaName(schemaName, { limit, skip, uidPrefix, attesters }) {
    if (attesters && !attesters.includes(publisher)) return Promise.resolve([])
    return remote.listSeedsBySchemaName(schemaName, { limit, skip, uidPrefix, attesters: [publisher] })
  },
}

registerLocalQuerySource(onePublisher)

To list one publisher's Seeds in a single call, queryBySchema(name, { attesters: [address] }) needs no custom source. See QueryDataSource and One publisher's items.

normalizeBytes32Hex is strict and toSnakeCase is removed

Affects: code that imports toSnakeCase from @seedprotocol/eas, or passes values that aren't full 32-byte hex strings to normalizeBytes32Hex. Changed in: @seedprotocol/eas 0.7.12.

toSnakeCase kept the naming rule from before 0.7.4 (HTMLPage → htmlpage); use easName (html_page), which builds the names Seed writes to EAS. normalizeBytes32Hex returns '' unless the value is 64 hex digits, with or without 0x; it padded short values and turned other characters into 0. Check for '':

before (TS2305 on 0.7.12)
import { toSnakeCase } from '@seedprotocol/eas'

const easModelName = toSnakeCase(modelName)
after
import { easName, normalizeBytes32Hex } from '@seedprotocol/eas'

const easModelName = easName(modelName)

const seedUid = normalizeBytes32Hex(input)
if (!seedUid) throw new Error(`Not a Seed UID: ${input}`)

The indexer's timeout also rejects with a new message, EAS indexer request timed out after <n> ms, for code that matched the old one. See UID and address helpers.

Feed readers show items again once

Affects: apps that serve feeds with @seedprotocol/feed. Changed in: @seedprotocol/feed 0.7.12.

Atom entry IDs and JSON Feed item IDs are the item's RSS guid, where they were the Seed UID, so after the upgrade a feed reader shows each item of an existing Atom or JSON feed once more as new. An item with its own storage transaction now links to the public gateway's /raw/ URL (https://arweave.net/raw/<id> with the default host, where it was https://ar.seedprotocol.io/<id>), and the handlers use that link as its guid, so the RSS items of an Image feed show again too. Nothing in code needs to change; tell subscribers if a repeat matters. In createFeed, an item's own updated and links no longer override Atom's; 0.7.13 restores them and changes the guid of stored-transaction items (see Feed guids and Atom fields). See Field mapping.

Gateway Hyper timeouts and options

Affects: Gateway Hyper operators and clients, and code that passes storePath or gives serveRawTunnel both keyFile and keyPair. Changed in: @seedprotocol/gateway-hyper 0.7.12.

  • Timeouts. The default upstreamTimeoutMs is 130,000 (it was 30,000) and requestTimeoutMs 150,000 (it was 60,000), in the CLI too, above the 120 seconds publish waits for an upload batch. --upstream-timeout-ms and --request-timeout-ms values set for that reason can go.
  • storePath is removed from the serveTunnel, connectTunnel, createGatewayProxy and createTunnelSession options, so TypeScript rejects it (TS2353). Nothing read it; delete it.
  • https:// upstreams. serveTunnel throws for one, and seed-gateway serve --upstream https://… stops. The framed tunnel speaks plain HTTP to its upstream: point it at a plain HTTP address, or use raw TCP mode to carry TLS end to end.
  • Keys. serveRawTunnel with both keyFile and keyPair throws Pass keyFile or keyPair, not both; keyPair used to win. Pass one.

See seed-gateway serve and serveTunnel.

React: schema IDs and the stopped status

Affects: apps that pass schema IDs to useModelProperty or useCreateModelProperty, and code that reads useGlobalServiceStatus(). Changed in: @seedprotocol/react 0.7.12.

useModelProperty(schemaId, …) and useCreateModelProperty().create(schemaId, …) no longer cut a trailing -<n> off a schema ID that no stored schema has as its name or file ID, so blog-2 no longer resolves to blog. useModelProperty finds nothing for such an ID, and create rejects with No stored schema has the name or schema file id "blog-2", which it also does when called before the app database is open (0.7.13 rejects with ClientNotInitializedError then; see React: early creates and the status types). Pass the schema's name or file ID, and call create once the client is ready. See useCreateModelProperty.

After client.stop(), useGlobalServiceStatus() reports status and internalStatus as 'stopped' until the next init starts; they read 'idle', so a stopped client looked ready. status is typed string, so TypeScript doesn't flag a check that misses 'stopped' (0.7.13 narrows the type; see React: early creates and the status types):

after
import { useGlobalServiceStatus } from '@seedprotocol/react'

export function ClientStatus() {
  const { status } = useGlobalServiceStatus()
  if (status === 'stopped') return <p>Signed out.</p>
  if (status === 'initFailed') return <p>Seed didn't start.</p>
  if (status !== 'idle') return <p>Starting…</p>
  return null
}

useCreateItem now creates a new item for a second call with other values or another model, where it returned the first call's item; only a repeat of the same call shares it. See useGlobalServiceStatus and useCreateItem.

seedVitePlugin rejects unknown options

Affects: apps whose seedVitePlugin options include autoInit or a misspelt name. Changed in: @seedprotocol/vite 0.7.12.

The plugin throws when the Vite config loads: seedVitePlugin: autoInit was removed. Initialize the SDK from your app (client.init(), or <SeedProvider>)., or for any other unknown option seedVitePlugin: unknown option "fsModule". It takes fsModules, debug, includeNodePolyfills, isolationHeaders. Both were ignored. Remove or fix the option.

includeNodePolyfills: true now polyfills timers too, so an alias of timers to timers-browserify added for rssXmlToSources can go. See Options.

FieldMapper themes use --sfm-* only

Affects: apps that style FieldMapper with --fm-* or --map-* CSS variables. Changed in: @seedprotocol/mapping 0.7.12.

The --fm-* and --map-* variables are removed. Each --fm-<name> color is --sfm-color-<name>, and --fm-map-N and --map-N are --sfm-map-N:

before
.seed-field-mapper {
  --fm-ink: #1d1d1f;
  --fm-map-1: #0a7cff;
}
after
.seed-field-mapper {
  --sfm-color-ink: #1d1d1f;
  --sfm-map-1: #0a7cff;
}

applyMapping and applyMappingAsync also copy a source's value only, so an empty frontmatter field or Markdown body maps to '', not the field's name or "Full document". See Classes and CSS variables.

Publish wallets can be 'modular'

Affects: code that switches on a publish wallet's kind or checks kind === 'smart'. Changed in: @seedprotocol/publish 0.7.12.

PublishWalletKind adds 'modular', the modular executor's in-app EOA, which publishes as the user's ManagedAccount. ConnectButton with useModularExecutor and ensureSmartWalletThenPublish's modular path register that wallet as 'modular', where they said 'smart'. A 'modular' wallet signs data items, and direct EAS refuses it as it does 'smart':

after
import type { PublishWalletKind } from '@seedprotocol/publish'

export function walletLabel(kind: PublishWalletKind | undefined): string {
  switch (kind) {
    case 'eoa':
      return 'Wallet'
    case 'smart':
    case 'modular':
      return 'Smart account'
    default:
      return 'Unknown wallet'
  }
}

Two more publish changes can affect error handling and holds. A failed revocation of retracted properties rejects with the new PublishRetractionError (code: 'PUBLISH_RETRACTION_FAILED'), not the attestation-failure message; PublishManager.retryAttestations(seedLocalId) still sends it again. And every publish holds its item, PublishManager.createPublish and ensureWalletThenPublish included, so a hold you take around them, as createPublish doesn't hold the item asked for 0.7.10, is no longer needed but does no harm. See Wallets.

Upgrading from 0.7.10 to 0.7.11

Released: 2026-10-24. Deprecated aliases are removed, and a few fixes change what code sees. The Changelog lists everything else in 0.7.11.

Removed aliases

Affects: code that imports any of these. Changed in: @seedprotocol/sdk, @seedprotocol/publish, @seedprotocol/eas and @seedprotocol/arweave 0.7.11.

RemovedUse instead
Db from @seedprotocol/sdk/nodeNodeDb, the same class
FileManager from @seedprotocol/sdk/nodeNodeFileManager, the same class. The main entry's FileManager stays
propertyUids from @seedprotocol/sdk, and its property_uids table (dropped by migration 0030)Nothing: the SDK never wrote to it. A property's EAS schema UID is schemaUid
configurePublish from @seedprotocol/publishinitPublish
SEED_PROTOCOL_CONTRACT_ADDRESS_OP_SEPOLIA from @seedprotocol/publishMULTI_PUBLISH_ABI_REFERENCE_ADDRESS_OP_SEPOLIA
EasClient and QueryClient from @seedprotocol/eas/nodeNodeEasClient and NodeQueryClient. EasClient from @seedprotocol/eas and @seedprotocol/sdk stays
ArweaveClient from @seedprotocol/arweave/nodeNodeArweaveClient. ArweaveClient from @seedprotocol/arweave stays
before
import { Db, FileManager } from '@seedprotocol/sdk/node'
import { configurePublish } from '@seedprotocol/publish'
after
import { NodeDb, NodeFileManager } from '@seedprotocol/sdk/node'
import { initPublish } from '@seedprotocol/publish'

Publish authorizations report revocation and expiry

Affects: code that calls assessPublishAuthorization or assessPublishAuthorizationLive, switches on PublishAuthorizationAssessStatus, or calls enrollPublishAutomation without expiresAt. Changed in: @seedprotocol/eas and @seedprotocol/publish 0.7.11.

PublishAuthorizationAssessStatus has a new value, 'revoked', so a switch that covers every case needs one more. assessPublishAuthorizationLive returns it for a revoked grant, which it reported as 'valid' in 0.7.10, and returns 'expired' once the attestation's EAS expiration has passed; its expirationTime is now set. Calling assessPublishAuthorization yourself, pass the attestation's revoked and expirationTime, which getPublishAuthorizationFromEas now returns.

after
import type { PublishAuthorizationAssessStatus } from '@seedprotocol/eas'

function grantLabel(status: PublishAuthorizationAssessStatus): string {
  switch (status) {
    case 'valid':
      return 'Active'
    case 'revoked':
      return 'Withdrawn'
    case 'expired':
      return 'Expired'
    case 'scope_mismatch':
    case 'unknown':
      return 'Not recognized'
  }
}

enrollPublishAutomation without expiresAt now ends the session key and its grant 365 days after enrollment. In 0.7.10 the key kept thirdweb's ten-year window while the grant's attestation expired after a year. Keys enrolled before keep their window. attestPublishAuthorization throws when wallet isn't the identity account. See Tool attestations.

clear() on an unloaded property writes nothing

Affects: code that calls clear() on a property it kept past the grace period. Changed in: @seedprotocol/sdk 0.7.11.

clear() on a stopped property rejects with EntityUnloadedError, as in 0.7.10, but now before it writes. In 0.7.10 the value was cleared anyway. Code that caught the error and counted the value as cleared has to look the property up again and clear it.

Tests see init errors

Affects: test suites that run the SDK with NODE_ENV=test or IS_SEED_DEV set. Changed in: @seedprotocol/sdk 0.7.11.

With either set, client.init used to carry on when storing its config or app state, adding the schema's models or, on Node, running migrations failed. It now rejects, as it does in production. A suite that passed only because of that fails at init. NODE_ENV=test still turns off offline retries.

Upgrading from 0.7.9 to 0.7.10

Released: 2026-10-24. The first four parts cover the release's main change, how long loaded instances stay in memory. The Changelog lists everything else in 0.7.10.

Lookups don't hold

Affects: code that calls unload() after Item.find, Item.all, ItemProperty.find or ItemProperty.all, and code that keeps an item it looked up across long work. Changed in: @seedprotocol/sdk 0.7.10.

Those four lookups take a cache hold only when called with hold: true. Item.create and ItemProperty.create still take one unless you pass hold: false, and so do Item.getById (until 0.7.12, see Item.getById doesn't hold) and Item.createById. unload() still releases one hold, but after a lookup without a hold you have none to release: the call releases a hold someone else took, such as a mounted useItem's, and the item can unload under them. Drop it:

before
import { Item } from '@seedprotocol/sdk'
import type { Post } from './types'

const post = (await Item.find({ modelName: 'Post', seedLocalId })) as Post | undefined
if (post) {
  showTitle(post.title)
  post.unload()
}
after
import { Item } from '@seedprotocol/sdk'
import type { Post } from './types'

const post = (await Item.find({ modelName: 'Post', seedLocalId })) as Post | undefined
if (post) showTitle(post.title)

Code that keeps the item longer than the grace period, such as a Node import or a long upload, holds it and releases the hold when it's done. Item.retain(item) takes a hold on an instance you already have.

after
import { Item } from '@seedprotocol/sdk'
import type { Post } from './types'

const post = (await Item.find({ modelName: 'Post', seedLocalId, hold: true })) as Post | undefined
if (!post) throw new Error('Post not found')
try {
  post.body = await renderBody(post.seedLocalId)
  await post.properties.find((p) => p.propertyName === 'body')?.save()
} finally {
  post.unload()
}

Schema, Model and ModelProperty lookups and creates take no hold either unless called with hold: true, but definitions stay cached until they're destroyed or evicted or the client stops, held or not. An unload() after one of those lookups can go too. See unload.

Unheld items unload after 60 seconds

Affects: apps that kept Item or ItemProperty instances without a hold and relied on them staying loaded. Changed in: @seedprotocol/sdk 0.7.10.

An Item or ItemProperty no one holds stays cached for the grace period, entityCache.unloadGraceMs in SeedConfig (60000 ms by default), after its last unload() or its last lookup without a hold, then stops. A hold taken meanwhile keeps the same instance. In 0.7.9 every lookup held, and an item no one unloaded never stopped. Hold the items you keep, as in Lookups don't hold, or raise the grace period:

after
import { client } from '@seedprotocol/sdk'
import schema from '../seed.schema.json'

await client.init({ config: { schema, entityCache: { unloadGraceMs: 10 * 60_000 } } })

Values above 2147483647 (about 24.8 days), Infinity included, unload at once, because a timer can't wait that long. 0 stops an instance at its last unload(), and an instance that was never held stays cached until it's evicted or the client stops.

Writes on an unloaded instance throw

Affects: code that writes to or publishes an instance it kept. Changed in: @seedprotocol/sdk 0.7.10.

Assigning a property, setValue(), save(), publish() and unpublish() on an Item or ItemProperty that has stopped throw EntityUnloadedError, with a message that names the item: Cannot set title on Post <seedLocalId>: this Item instance was unloaded. …. In 0.7.9 an assignment on a stopped instance did nothing and publish() never settled. Reads still return the last values. Look the item up again:

after
import { EntityUnloadedError, Item } from '@seedprotocol/sdk'
import type { Post } from './types'

async function setTitle(post: Post, title: string) {
  try {
    post.title = title
  } catch (error) {
    if (!(error instanceof EntityUnloadedError)) throw error
    const fresh = (await Item.find({ modelName: 'Post', seedLocalId: post.seedLocalId })) as Post | undefined
    if (fresh) fresh.title = title
  }
}

React hooks hold what they show

Affects: components that keep an item from useItem, useItems or useCreateItem outside the hook. Changed in: @seedprotocol/react 0.7.10.

useItem and useItems hold the items they show while mounted and release them on unmount, so an item no mounted hook shows unloads after the grace period. In 0.7.9 they kept every item loaded for the life of the page. Code that keeps using an item after its component unmounts, such as a save that finishes later, holds it with Item.retain(item) and calls unload() when done. The schema, model and model property hooks hold what they show the same way.

useCreateItem's createItem and useCreateItemProperty create without a hold. An instance you keep in state stops after the grace period, and the next assignment throws. Keep the seedLocalId and render the item through useItem, or pass hold: true as the third argument and unload() the item yourself:

before
import { useState } from 'react'
import type { Item } from '@seedprotocol/sdk'
import { useCreateItem } from '@seedprotocol/react'

export function useDraft() {
  const { createItem } = useCreateItem()
  const [draft, setDraft] = useState<Item<any>>()
  const start = async () => setDraft(await createItem('Post', { title: 'Untitled' }))
  return { draft, start }
}
after
import { useState } from 'react'
import { useCreateItem, useItem } from '@seedprotocol/react'

export function useDraft() {
  const { createItem } = useCreateItem()
  const [draftId, setDraftId] = useState<string>()
  const { item: draft } = useItem({ modelName: 'Post', seedLocalId: draftId })
  const start = async () => setDraftId((await createItem('Post', { title: 'Untitled' }))?.seedLocalId)
  return { draft, start }
}

useCreateModel and useCreateModelProperty create without a hold too; definitions stay cached anyway. See useItem and useCreateItem.

client.stop() returns a promise

Affects: code that calls client.stop() or client.unload(), keeps instances across a stop, or has a Node liveQuery subscription open when it stops. Changed in: @seedprotocol/sdk 0.7.10.

stop() and unload() return a Promise<void> that resolves once the outbox has stopped, every cached instance is stopped and dropped, in the browser this tab has handed leadership to another tab, and the database is closed. They work in any state, before init and after a failed one. An init called while a stop is running waits for it. An instance kept from before the stop is stopped for good: look the item up again after the next init.

before
import { client } from '@seedprotocol/sdk'

client.stop()
after
import { client } from '@seedprotocol/sdk'

subscription.unsubscribe()
await client.stop()

On Node, a BaseDb.liveQuery subscription still open at the stop no longer keeps the process alive: its next poll runs on the closed database and it gets an error (Failed query: …). Unsubscribe before stopping. From 0.7.12 the stop completes the subscription instead, with no error. See Stopping the client.

Older browsers fail with UnsupportedBrowserError

Affects: browser apps whose users may have an older browser, and onInitError handlers. Changed in: @seedprotocol/sdk 0.7.10.

Seed supports Chrome and Edge 102, Firefox 114 and Safari 26 or later. In a browser without OPFS, FileSystemFileHandle.createWritable or Web Locks, client.init rejects with UnsupportedBrowserError (code === 'BROWSER_UNSUPPORTED', the absent features in missing), and SeedProvider and SeedClientGate pass it to onInitError. A retry fails the same way, so show an update message instead of a retry button. checkBrowserSupport() checks before init. See UnsupportedBrowserError and Handling init errors.

Apps that install ZenFS move to 2.7.7

Affects: apps that list @zenfs/core or @zenfs/dom in their own dependencies, for example for fsModules in @seedprotocol/vite or @seedprotocol/next. Changed in: @seedprotocol/sdk 0.7.10.

The SDK pins @zenfs/core 2.7.7 and @zenfs/dom 1.2.14 (it was 2.5.6 and 1.2.9). Pin yours to the same versions, or two copies of ZenFS break the file system:

package.json
{
  "dependencies": {
    "@zenfs/core": "2.7.7",
    "@zenfs/dom": "1.2.14"
  }
}

createPublish doesn't hold the item

Affects: code that publishes with PublishManager.createPublish or ensureWalletThenPublish. Changed in: @seedprotocol/sdk and @seedprotocol/publish 0.7.10.

item.publish(), publishItem and usePublishItem hold the item until the publish settles. createPublish and ensureWalletThenPublish don't, and an item from Item.find now has no hold, so it can unload partway through a publish that outlasts the grace period. Hold it until the publish settles (from 0.7.12 createPublish holds it too, and your own hold does no harm):

after
import { Item } from '@seedprotocol/sdk'
import { PublishManager, getPublishWallet, waitForPublishActor } from '@seedprotocol/publish'

const post = (await Item.find({ modelName: 'Post', seedLocalId, hold: true })) as Item<any> | undefined
if (!post) throw new Error('Post not found')
try {
  const wallet = getPublishWallet()
  if (!wallet) throw new Error('Connect a wallet first.')
  const address = wallet.publisherAddress ?? wallet.signer.address
  const run = await PublishManager.createPublish(post, address, wallet, {
    dataItemSigner: wallet.dataItemSigner,
  })
  if (run) await waitForPublishActor(run, post.seedLocalId)
} finally {
  post.unload()
}

See PublishManager.createPublish.

seed-gateway checks --port

Affects: scripts that run seed-gateway connect with --port 0 or a port that isn't a number. Changed in: @seedprotocol/gateway-hyper 0.7.10.

--port 0 picks a free port, and the command prints the URL it listens on ([Seed Protocol] Local sidecar: http://127.0.0.1:<port>). A value that isn't a whole number from 0 to 65535 stops the command with --port must be 0–65535. In 0.7.9, 0 and a value that isn't a number fell back to 1984. See seed-gateway CLI.

Upgrading from 0.7.7 to 0.7.9

Released: 0.7.8 on 2026-10-20 and 0.7.9 on 2026-10-22. The parts are grouped by package, and each says which release made the change; within a group the 0.7.9 parts come first. 0.7.9 reverses some of 0.7.8's behavior, so each part describes what 0.7.9 does. From 0.7.8, read only the parts changed in 0.7.9. The Changelog lists everything else in 0.7.9 and 0.7.8.

Databases older than 0.7.7 are recreated

Affects: apps whose users last opened the app with SDK 0.7.6 or older. Changed in: @seedprotocol/sdk 0.7.9 and 0.7.11.

At the first start on 0.7.9 or later, a local database last opened by an SDK older than 0.7.7 is dropped and created again, with this console warning: [seed-protocol] This local database was written by an SDK older than 0.7.7. It is being recreated: unpublished items are dropped, and published items come back through EAS sync. From 0.7.11 the SDK first copies the database to seed.before-recreate-<ms>.db beside seed.db, and the warning ends A copy of the old database is at <path>.; when the copy fails, the database is opened as it is. Files under filesDir stay. The repairs that 0.7.1 to 0.7.6 databases got at start-up are gone with it.

The copy holds the drafts, but the SDK has no way to load them back. To keep your users' drafts in the app, have them publish before the upgrade, or ship 0.7.8 first and let the app start once on it before you move to 0.7.9: a database 0.7.8 has opened isn't recreated. A database last opened by 0.7.7 is recreated only if 0.7.7's background clean-up never finished in it; the same two steps keep its drafts, and 0.7.11 copies it first too. See Storage.

Rejected values stay shown

Affects: apps that show a value after validation rejected it, apps that use useDebouncedItemProperty, and apps that copied the useDraftProperty helper. Changed in: @seedprotocol/sdk and @seedprotocol/react 0.7.9.

This reverses Rejected values roll back. A value that breaks a rule stays on the property, post.title and the cached instance, with the reasons in saveValidationErrors, until the next assignment. It isn't written, so a fresh load or another process reads the stored value. useDebouncedItemProperty keeps the rejected text in the input with error set. Code that read property.value after a rejection as the stored value has to check saveValidationErrors first.

A write that's refused (an OwnershipError) or fails shows the property's current value again: its newest pending value from any tab, else the stored one. The ownership check now runs when the write is applied, after the setter returns.

Copies of the 0.7.7 useDraftProperty (the one with rolledBack and a setValue branch) show a false "Changed in another tab" after a refused write. Replace yours with the one in Hold unsaved text in the input; its signature and return value are the same. See Writing.

Writes go through the outbox

Affects: code that waits on save(), handles its errors, or reads the database right after an assignment. Changed in: @seedprotocol/sdk 0.7.8, 0.7.9 (errors from file writes, no time limit).

An assignment queues a write, and the leader tab applies it (The outbox). save() resolves once the write is applied. It no longer rejects after 10 seconds; from 0.7.12, save({ timeoutMs }) sets a limit. A write that fails when it's applied rejects with an error whose name is 'OutboxOpFailedError', code 'OUTBOX_OP_FAILED', and errorName the original error's class. That includes failed Image, File and Html writes ('TypeError' for a URL that can't be fetched, 'Error' for No file data found) and a Relation to a local item that doesn't exist ('RelationTargetError'). An ownership refusal is still an OwnershipError. Match code and errorName (OutboxOpFailedError is exported from 0.7.11):

before
try {
  await coverProp.save()
} catch (error) {
  if (error instanceof TypeError) showError("The image couldn't be downloaded.")
  else throw error
}
after
import { OutboxSaveTimeoutError, OwnershipError } from '@seedprotocol/sdk'

const failedAs = (error: unknown, errorName: string) =>
  (error as { code?: string }).code === 'OUTBOX_OP_FAILED' &&
  (error as { errorName?: string }).errorName === errorName

try {
  await coverProp.save({ timeoutMs: 30_000 })
} catch (error) {
  if (error instanceof OwnershipError) showError("You can't edit this post.")
  else if (error instanceof OutboxSaveTimeoutError) showError('Saving the cover is taking a while. It will finish in the background.')
  else if (failedAs(error, 'TypeError')) showError("The image couldn't be downloaded.")
  else throw error
}

timeoutMs is on the ItemProperty class; the IItemProperty type that item.properties holds doesn't declare it until 0.7.13. ItemCreateError's cause is usually such an error. See save.

setValue takes one argument

Affects: code that calls setValue(value, { before }). Changed in: @seedprotocol/sdk 0.7.9.

The before option and the PropertyValueSnapshot type are gone. To show a value without saving it, call showValue (new in 0.7.8); it no longer remembers a value to go back to.

before
titleProp.setValue(draft, { before: savedTitle })
after
titleProp.showValue(draft) // shown, not saved
titleProp.setValue(draft) // saved

status means loaded

Affects: code that compares an entity's status with 'idle', or calls waitForEntityIdle. Changed in: @seedprotocol/sdk 0.7.9.

status on a Schema, Model, ModelProperty, Item or ItemProperty is 'loading' | 'ready' | 'error'. Their machines start in idle, so 'idle' no longer means loaded. waitForEntityIdle is removed; each entity has waitForReady({ timeout }). useGlobalServiceStatus().status is the client manager's state and still reaches 'idle'.

before
import { waitForEntityIdle } from '@seedprotocol/sdk'

await waitForEntityIdle(post)
if (model.status === 'idle') showFields(model)
after
await post.waitForReady()
if (model.status === 'ready') showFields(model)

See Identity and metadata fields.

Request syncs with client.syncFromEas()

Affects: code that emits syncDbWithEas. Changed in: @seedprotocol/sdk 0.7.9.

The SDK no longer listens for any event, so eventEmitter.emit('syncDbWithEas') does nothing. Until 0.7.12, which logs a warning outside production, nothing warned. The same goes for fs.downloadAll.request; the download runs after each sync anyway. requestEasSyncFromEventBus and the 'event_bus' sync source are removed.

before
import { eventEmitter } from '@seedprotocol/sdk'

eventEmitter.emit('syncDbWithEas')
after
import { client } from '@seedprotocol/sdk'

await client.syncFromEas()

Synced values don't replace unpublished edits

Affects: apps that sync items the user also edits on another device, and apps that copied syncAndFindReplacedEdits from these docs. Changed in: @seedprotocol/sdk 0.7.8.

An unpublished edit now stays the value when sync brings a newer published one. If the synced value differs from both the edit and the value the edit replaced, the SDK records a conflict on the property (property.conflict, item.conflicts(), resolveConflict('local' | 'remote')). The docs' syncAndFindReplacedEdits helper, which found edits sync had replaced, finds nothing; use src/syncConflicts.ts from What wins when both devices edit. No React hook re-renders on a conflict, so read them after client.syncFromEas(). See Conflicts.

Item holds and blob: URLs

Affects: code that calls property.unload(), and code that keeps a blob: URL from an Image or File value. Changed in: @seedprotocol/sdk 0.7.8.

An item holds its properties, and its last unload() or destroy() stops the ones nobody else holds. Each ItemProperty.find() or create() is one hold, released by one property.unload(), so call item.unload() once rather than unloading each property. The SDK revokes a blob: URL it made for an Image or File value when the value changes and when the property stops; read property.value again after a change instead of keeping the URL. See unload and File, Image and Html properties.

Clearing a published value publishes the removal

Affects: apps that set a published property to '' or null, and publish code that reads getPublishPendingDiff or calls getPublishUploadData. Changed in: @seedprotocol/sdk 0.7.9.

An empty value used to be skipped at publish, so the old value stayed on chain. Now the next publish attests the type's empty value, and other apps read the property as absent. property.clear() does the same, and clear({ retract: true }) revokes the property's attestations instead (clear). Boolean and Date have no empty value: clear() rejects with ClearNotSupportedError, and a published Boolean or Date whose draft is empty fails publish validation with publish_clear_unsupported. On Image, File and Html, an empty value ('', null) clears the property. See Remove a published value.

For code that builds publishes itself: getPublishPendingDiff entries gain change ('set' | 'removed' | 'retracted'), and list a property under its storage name (authorId). getPublishUploadData(item, options) takes the options second. Publishing a property with no stored definition throws; it used to attest it as string <name>.

Schemas: ids from names, same-version edits replace the file

Affects: apps with config.schema files, apps that import schemas with importJsonSchema, and apps that define rules in code. Changed in: @seedprotocol/sdk 0.7.9.

  • On a fresh database, ids the SDK assigns come from the names (the first 10 hex characters of SHA3-256 of blog, blog/Post, blog/Post/title), so adding a property no longer changes every id. A database that already has the schema keeps its ids, and an id in the file is used with or without $schema.
  • An edit at the same version no longer duplicates models: the SDK warns and replaces the stored schema file, so the previous version's file is gone. Keep raising version with every edit.
  • A schema is applied at start only when it changed; the first start after the upgrade applies it once more.
  • importJsonSchema(input, version) uses version only when the JSON has none. Minimal <name>-v<n>.json files in the working directory are no longer imported at init.
  • Validation rules on properties defined in code are stored with the property and apply after a restart.

See The schema file format.

Raw SQL reads

Affects: apps that query metadata themselves. Changed in: @seedprotocol/sdk 0.7.8 (outbox), 0.7.9 (names).

An edit reaches metadata when its write is applied, so await property.save() before reading it. New rows store a single Relation, Image, File or Html property under its storage name (authorId), and rows are matched to their property by propertyId: group or filter on propertyId (rows without one by their name), as pickLatestMetadataRowPerProperty does. See Tables.

Removed exports

Affects: code that imports any of these. Changed in: @seedprotocol/sdk 0.7.9.

RemovedUse instead
withSeed from @seedprotocol/sdk/node, so the "before" in Next.js apps use @seedprotocol/next no longer compileswithSeed from @seedprotocol/next
./node's CLI exports: PathResolver, BasePathResolver, commandExists, ensureNodeDbSchema, runSeedInit, findSeedBinary, INIT_SCRIPT_SUCCESS_MESSAGE, SCHEMA_TS, getRefValueTypeNothing
The Markdown import: parseMarkdownFrontmatter, processSeedConfig, saveModelsFromMarkdownA JSON schema file
The schema file API: createSchema, readSchema, deleteModelFromSchema, deletePropertyFromModelNothing: the SDK never loaded the files createSchema wrote
waitForEntityIdleentity.waitForReady()
requestEasSyncFromEventBus, EasSyncRequestSourceclient.syncFromEas()
getEasSchemaUidBySchemaName, getEasSchemaForItemPropertygetEasSchemaUidForExactDefinition
getPublishDraftGraph, PublishDraftGraph, PublishDraftGraphNode, GetPublishDraftGraphOptionsgetPublishGraph (Other read helpers)
getSegmentedItemProperties, getRelatedItemsForPublish, shouldAttestProperty, restoreDataItemIdsOverwrittenByL1, applyArweaveL1TransactionIdLocalNothing
Model.updateNameIndex, Model.trackPendingWrite, Model.getPendingModelIds, ModelProperty.adoptForModelNothing
The types PropertyValueSnapshot, SaveValueToDbEvent, GetItem, GetItemParams, GetPropertyDataOptions, SeedInitBrowserProps, SeedInitBrowser, DbQueryResult, ResultObject, SqliteWasmResult, SqliteWasmCallback, ReturnObj, PublishRequestData, PropertyToUpdateWithSeed, GlobalMachineContext, PublishMachineContext, GetSchemaForModelEvent, HydrateExistingItemEvent, WebpackConfigContext, Environment, ClientCallbackNothing

EasClient from the main entry is now BaseEasClient from @seedprotocol/eas, and on Node the SDK registers NodeEasClient, which keeps its own connection pool. To send EAS requests through fetch's defaults, after client.init:

after
import { EasClient } from '@seedprotocol/sdk'
import { NodeEasClient } from '@seedprotocol/eas/node'

EasClient.configure(new NodeEasClient({ dispatcher: false }))

See Using Seed in Node and Electron.

Smaller SDK changes

  • addresses.persisted fires only when the owned or watched addresses differ from the ones this tab last announced (0.7.8). A setAddresses() call that changes nothing no longer fires it.
  • Item.all waits for its items only with waitForReady: true (0.7.8); the default returns them while they load. It also takes a signal.
  • watchEntityRows has no since option (0.7.9).

React hooks

Affects: apps that use @seedprotocol/react. Changed in: @seedprotocol/react 0.7.8, 0.7.9 where marked.

useCreateModelProperty().create returns a Promise of the ModelProperty:

before
const property = create('blog', 'Post', { name: 'summary', dataType: 'Text' })
after
const property = await create('blog', 'Post', { name: 'summary', dataType: 'Text' })
  • useCreateItem's createItem takes ItemCreateOptions third, so { validate: false } works there. A call made while another is in flight returns the first call's promise and item, whatever model and values it was given, and creates nothing; it used to return undefined. Await one create before starting the next. From 0.7.12 only a repeat of the same call shares the first one's item. See useCreateItem.
  • After client.stop(), the status hooks and SeedClientGate follow the next client.init(), so a key that remounted the gate can go. See After client.stop().
  • Hooks outside SeedProvider. useItems and useItemProperties still throw without it. The others fall back to a shared client and log a development warning, and from 0.8 they will throw too. See Which hooks need SeedProvider.
  • react-dom 18 or 19 is a peer dependency.
  • Live query errors from useLiveQuery and BaseDb.liveQuery go to debug (seedSdk:browser:db:Db), not console.error; the hook still returns them.
  • SeedProvider no longer refetches useItemProperties on itemProperty.saved (0.7.9); it refetches once per save, when the change feed (on Node, its live query) reports the write.
  • isClientReadySnapshot comes from @seedprotocol/sdk (0.7.9). @seedprotocol/react re-exports it from 0.7.12.

Publishing uploads only signed data items

Affects: apps that set useArweaveBundler, arweaveJwk or signArweaveTransactions, apps that left useArweaveBundler unset, and code that compares a cost estimate's path with 'l1'. Changed in: @seedprotocol/publish 0.7.9.

The L1 path is gone: every publish uploads signed data items through the upload API, or to Turbo with payment: { storage: 'user' }. useArweaveBundler: true changes nothing, useArweaveBundler: false makes initPublish throw PublishPaymentConfigError, and arweaveJwk and signArweaveTransactions are removed. An app that left useArweaveBundler unset used the L1 path and now needs a data item signer for items with Image, File or Html values: ConnectButton registers one, or pass dataItemSigner or signDataItems.

before
initPublish({ ...config, useArweaveBundler: false, arweaveJwk })
after
import { initPublish } from '@seedprotocol/publish'

initPublish({ ...config, uploadApiBaseUrl: '/seed-api' })

A publish saved during an L1 step comes back interrupted; publish the item again. An estimate's arweave.path is always 'bundler'. See initPublish.

Publishing also revokes retracted properties after the attestations. A failure there waits in the new retractionFailureRecoverable state, which PublishManager.retryAttestations retries, so add it wherever your code lists recoverable states. item.unpublish() revokes the Seed first, then the properties, then the Versions. See Handle failures and resume.

query, eas and feed

Affects: apps that use @seedprotocol/query, @seedprotocol/eas or @seedprotocol/feed. Changed in: 0.7.9, except where marked.

expandRelations is replaced by relations: 'expand' | 'urls' | 'uids' (0.7.13 removes 'urls'; see relations: 'urls' is removed). An expandRelations option passed from untyped code is ignored, so relations come back expanded.

before
const posts = await queryBySchema('post', { expandRelations: false })
after
import { queryBySchema } from '@seedprotocol/query'

const posts = await queryBySchema('post', { relations: 'urls' })
  • query: the …Feed… aliases are removed (use the names without Feed), and so are QueryCacheConfig.backgroundRefresh and refreshInterval. queryBySchemaForMonth bounds the month in UTC. A property whose attestation is the empty value is left out of the record. See queryBySchema.
  • eas: getEasSchemaUidBySchemaName and fetchEasSchemaUidBySchemaName are removed. Look a property schema up by its full definition with getEasSchemaUidForExactDefinition('bytes32 post'), a model's with the model schema lookup. An IEasClient of your own gets the 30-second request limit (0.7.8) only if it builds its client with createEasGraphQLClient.
  • feed: FEED_SITE_URL and FEED_EXPAND_RELATIONS are removed (feeds always expand relations), as are initializeSeedClient, getClient and teardownSeedClient (use initializeFeedPlatform and teardownFeedPlatform), and GraphQLItem is now FeedItem. TransformOptions requires itemUrlBase and itemUrlPath. initializeFeedPlatform keeps EAS and Arweave clients already registered. The first page is checked on every request, and its Cache-Control max-age is CACHE_MAX_AGE (default 0, was 3600). Archive months are UTC. See Caching.

Upgrading from 0.7.5 to 0.7.7

Released: 0.7.6 and 0.7.7, both on 2026-10-18. Each part says which release made the change. From 0.7.6, read only the parts changed in 0.7.7, which come first. The Changelog lists everything else in 0.7.7 and 0.7.6.

Client calls after client.stop() throw

Affects: code that uses the client after client.stop() or client.unload(), or stops it twice. Changed in: @seedprotocol/sdk 0.7.7.

After stop() or unload(), client methods such as getAddresses, setAddresses and syncFromEas throw ClientManager is not initialized. Please call init() first. until client.init runs again. They used to keep working on the open database. A second stop() or unload() logs a console.warn and returns.

before
client.stop()
const { owned } = await client.getAddresses() // read the stopped client's database
after
const { owned } = await client.getAddresses()
client.stop()

See client.

Errors keep their class

Affects: code that matches the message of an error from save(), item.publish() or usePublishItem. Changed in: @seedprotocol/sdk 0.7.7.

ItemProperty.save() and item.publish() reject with the error that was thrown. They used to reject with a new Error that copied its message, so instanceof, code and cause were lost. usePublishItem's error is the same error. save()'s message no longer starts with Failed to save property <name>: :

before
try {
  await coverProp.save()
} catch (error) {
  if (error instanceof Error && error.message === 'Failed to save property cover: No file data found') {
    showError('Choose an image file.')
  }
}
after
import { OwnershipError } from '@seedprotocol/sdk'

try {
  await coverProp.save()
} catch (error) {
  if (error instanceof OwnershipError) showError("You can't edit this post.")
  else if (error instanceof Error && error.message === 'No file data found') showError('Choose an image file.')
  else throw error
}

A publish that fails after it started rejects with Publish of <seedLocalId> failed at <step>: …, and its cause is now the step's own error, such as a ManagedAccountPublishError. See save and publish.

Rejected values roll back

Affects: apps that show a value after validation rejected it, and apps that copied the useDraftProperty helper. Changed in: @seedprotocol/sdk 0.7.7.

A value that fails its property's rules doesn't stay in memory any more: property.value, post.title, Item.find and Item.all return the value from before the assignment, as they already did after an ownership refusal. The reasons are still in saveValidationErrors. To keep the user's invalid text on screen so they can fix it, hold it in component state.

The useDraftProperty helper from earlier versions of these docs breaks on this: after a rejected value it shows a false "Changed in another tab" and drops the text on blur. 0.7.9 reverses the rollback and removes setValue's before option, so take the current helper as Rejected values stay shown says.

Item.create throws ItemCreateError

Affects: code that handles Item.create failures for File, Image, Html or ItemStorage values. Changed in: @seedprotocol/sdk 0.7.7.

When one of those values fails to save for a reason other than its property's rules, Item.create throws ItemCreateError instead of a plain Error. The message is the same, so code that matched it keeps working. Its code ('save_failed' or 'property_not_loaded'), property and cause say what failed:

after
import { Item, ItemCreateError } from '@seedprotocol/sdk'

const values = { modelName: 'Post', schemaName: 'Blog', title, attachment: file }

try {
  await Item.create(values)
} catch (error) {
  if (error instanceof ItemCreateError) showFieldError(error.property, "This file couldn't be saved.")
  else throw error
}

ItemCreateError is exported from @seedprotocol/sdk, not from @seedprotocol/sdk/node. See Item.create.

File values work after a reload

Affects: apps that worked around a File value going stale after a page reload. Changed in: @seedprotocol/sdk 0.7.7.

Each time an item loads, the SDK makes a new URL for its File and Image values, so property.value works as a link in a later session. A hook that rebuilt the URL with FileManager.getContentUrlFromPath, like the useFileUrl these docs used to show, can go:

before
const { property } = useItemProperty({ seedLocalId, propertyName: 'attachment' })
const url = useFileUrl(property) // a fresh blob: URL from property.localStoragePath
after
const { property } = useItemProperty({ seedLocalId, propertyName: 'attachment' })
const url = property?.value

The first init after the upgrade clears the URLs that 0.7.5 and 0.7.6 stored with File and Image values. See Offer a file for download.

Relations hold Seed UIDs after a publish

Affects: code that compares a Relation or List value with a related item's seedLocalId. Changed in: @seedprotocol/sdk and @seedprotocol/publish 0.7.7.

Publishing an item rewrites its stored Relation values to the related items' seedUids, and a List of relations to a JSON array of them, as an EAS sync of the same attestations stores them. They used to keep the drafts' seedLocalIds. Compare against both IDs:

before
const theirPosts = posts.filter((post) => post.author === author.seedLocalId)
after
const ids = [author.seedLocalId, author.seedUid].filter(Boolean)
const theirPosts = posts.filter((post) => post.author && ids.includes(post.author))

See Relations between items and Find the items that point at an item.

Direct EAS refuses updates to earlier requests

Affects: apps that build or reorder publish batches themselves and attest straight to EAS. Changed in: @seedprotocol/publish 0.7.7.

On the direct-EAS route, a propertiesToUpdate entry that targets a request attested earlier in the batch is refused before anything is sent, with ManagedAccountPublishError code PUBLISH_PREFLIGHT_FAILED. Before, the update was applied to a request already sent and never reached the chain. The SDK's own batches don't contain such entries, so item.publish() isn't affected. See Rules and errors.

Production hosting must send the isolation headers

Affects: browser apps whose host doesn't send Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy. Changed in: @seedprotocol/sdk 0.7.6.

Without cross-origin isolation, SQLite can't keep the database in the browser's private file system (OPFS). The client used to fall back to an in-memory database, which lost everything on reload. Browser client.init now rejects with DatabaseNotPersistentError (code: 'DB_NOT_PERSISTENT'), and the error reaches SeedClientGate's and SeedProvider's onInitError. The dev server gets the headers from the Vite plugin; check that your production host sends them too. See Headers in production, Production hosting for Next.js, and DatabaseNotPersistentError.

Install @seedprotocol/vite to import @seedprotocol/sdk/vite

Affects: apps that import the plugin from @seedprotocol/sdk/vite. Changed in: @seedprotocol/sdk 0.7.6.

@seedprotocol/vite is an optional peer of the SDK, no longer a dependency, so installing the SDK doesn't bring Vite with it. @seedprotocol/sdk/vite still re-exports the plugin, but only when your app installs @seedprotocol/vite; otherwise the import fails with ERR_MODULE_NOT_FOUND: Cannot find package '@seedprotocol/vite'. Install it and import from it:

npm install --save-dev @seedprotocol/vite
vite.config.ts (before)
import { seedVitePlugin } from '@seedprotocol/sdk/vite'
vite.config.ts (after)
import { seedVitePlugin } from '@seedprotocol/vite'

The Vite plugin's defaults changed

Affects: apps that use seedVitePlugin. Changed in: @seedprotocol/vite 0.7.6.

The plugin now does less by default: it sends the isolation headers, builds workers as ES modules, keeps the dev pre-bundle in order and dedupes the packages your app has. The Node polyfills and fs aliases are opt-in, and it sets no optimizeDeps.include or exclude lists. What the plugin configures lists everything. To upgrade:

  1. Delete node_modules/.vite and restart the dev server.
  2. Remove autoInit from the plugin's options. It's no longer in the options type, so TypeScript rejects it, and from 0.7.12 the plugin throws on it.
  3. Remove workarounds copied from older plugin versions or app configs: optimizeDeps.exclude entries for Seed packages, sqlocal, drizzle-orm or SQLite WASM, optimizeDeps.include entries for the SDK's dependencies, and aliases for nanoid-dictionary, debug or the ZenFS entry files. Under pnpm, an exclude entry for the SDK breaks vite dev.
  4. Drop node-linker=hoisted from .npmrc if you added it for Seed. pnpm's default layout works.
  5. Set includeNodePolyfills: true only if another dependency of your app needs Node built-ins or the Buffer, process or global globals in the browser. The SDK, @seedprotocol/react and @seedprotocol/publish don't.
  6. Set fsModules: ['fs', 'fs/promises', 'node:fs', 'node:fs/promises'] only if your own code or a dependency imports fs in the browser.
vite.config.ts (before)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { seedVitePlugin } from '@seedprotocol/vite'

export default defineConfig({
  plugins: [react(), ...seedVitePlugin({ autoInit: true })],
  optimizeDeps: { exclude: ['@seedprotocol/sdk', 'sqlocal'] },
})
vite.config.ts (after)
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { seedVitePlugin } from '@seedprotocol/vite'

export default defineConfig({
  plugins: [react(), ...seedVitePlugin()],
})

Before 0.7.12, includeNodePolyfills didn't cover timers, so a page that called rssXmlToSources from @seedprotocol/mapping failed after vite build with Module "timers" has been externalized for browser compatibility. 0.7.12 polyfills it; see In a browser. Troubleshooting lists the errors a leftover workaround produces.

Import Drizzle operators from the SDK

Affects: apps that install drizzle-orm to write their own queries. Optional. Changed in: @seedprotocol/sdk 0.7.6.

@seedprotocol/sdk/drizzle re-exports drizzle-orm from the SDK's own copy, so your app doesn't have to install Drizzle or keep it on 0.44:

before
import { eq } from 'drizzle-orm'
after
import { eq } from '@seedprotocol/sdk/drizzle'

Then remove drizzle-orm from package.json if nothing else imports it. See Entry points.

Next.js apps use @seedprotocol/next

Affects: Next.js apps that use withSeed from @seedprotocol/sdk/node. Changed in: @seedprotocol/sdk 0.7.6 (deprecated), @seedprotocol/next 0.7.7 (first release).

The SDK's withSeed is deprecated. withSeed from @seedprotocol/next configures Turbopack and webpack and adds the cross-origin isolation headers, which the old helper didn't. It needs Next.js 15.5 or later:

npm install --save-dev @seedprotocol/next
next.config.mjs (before)
import { withSeed } from '@seedprotocol/sdk/node'

export default {
  webpack: (config, { webpack, isServer }) => withSeed(config, webpack, isServer),
}
next.config.ts (after)
import type { NextConfig } from 'next'
import { withSeed } from '@seedprotocol/next'

const nextConfig: NextConfig = {}

export default withSeed(nextConfig)

Your own aliases, webpack function and headers are kept. Use the SDK only in client components. See @seedprotocol/next, and Next.js 15 for what doesn't work under Turbopack there.

Gateway Hyper rewrites Host and times out

Affects: operators who run seed-gateway serve or serveTunnel, and apps that upload through Gateway Hyper. Changed in: @seedprotocol/gateway-hyper 0.7.6.

The operator now passes the client's Host on as X-Forwarded-Host, sends the upstream its own host, and drops hop-by-hop headers. An upstream rule that matched on the forwarded Host, such as a host-based routing rule on the upstream, stops matching: route by path, or read X-Forwarded-Host.

Requests also time out now. The operator answers 504 once the upstream has been silent for 30 seconds, and a client does after 60 seconds with no bytes either way. Publish waits up to 120 seconds for an upload batch, so for the upload API, raise --upstream-timeout-ms on the operator above 120000, and the clients' request timeout above that. 0.7.12's defaults, 130000 and 150000, already are. seed-gateway CLI has the flags.

Upgrading from 0.7.4 to 0.7.5

Released: 2026-10-17. Each part below says which apps it affects; the Changelog lists the additions and fixes that need no work.

Remove the overrides block

Affects: npm apps that followed the 0.7.3 or 0.7.4 install steps.

@seedprotocol/react and @seedprotocol/publish now declare the exact SDK version as a peer. React 0.7.4 declared ^0.6.0, which npm read as excluding 0.7.x, so the docs had you add an overrides block. Remove it: npm installs one SDK at the matching version, even when your app lists only @seedprotocol/publish. The direct entries that earlier install steps added for XState, Drizzle, ZenFS and React Query can go too, since the SDK and the React bindings bring them, unless your own code imports them:

package.json (before)
{
  "dependencies": {
    "@seedprotocol/react": "0.7.4",
    "@seedprotocol/sdk": "0.7.4",
    "@tanstack/react-query": "^5.104.1",
    "@zenfs/core": "2.5.6",
    "@zenfs/dom": "1.2.9",
    "drizzle-orm": "^0.44.7",
    "xstate": "^5.28.0"
  },
  "overrides": {
    "@seedprotocol/react": {
      "@seedprotocol/sdk": "$@seedprotocol/sdk"
    }
  }
}
package.json (after)
{
  "dependencies": {
    "@seedprotocol/react": "0.7.9",
    "@seedprotocol/sdk": "0.7.9"
  }
}

If your code imports drizzle-orm, see Import Drizzle operators from the SDK. Keep @zenfs/* only if you set fsModules or import ZenFS yourself, at the exact versions in the SDK's package.json. Under pnpm, an app that imports @tanstack/react-query itself keeps it listed.

Number properties publish as strings

Affects: apps that publish Number properties, and other apps, indexers or scripts that read them from EAS.

A Number property publishes as string <name>, where it used to be uint8 <name>, so negative numbers, decimals and values above 255 publish (they failed with Failed to encode property). The value is its decimal string. A stored value that isn't a finite number fails publishing with publish_invalid_number.

The first publish after the upgrade registers the string schema, so new Number values land under a different EAS schema UID. Items published before keep their uint8 attestations until that property changes. The SDK's EAS sync and @seedprotocol/query read both. A reader that looks up uint8 <name> by schema UID or name sees no new values, so update it to read string <name> too. See What's published where.

Sync skips attestations whose type doesn't fit

Affects: apps that sync properties another app published under the same name with a different type.

EAS sync used to store any property attestation whose name matched. It now skips one whose EAS type doesn't fit the local property, with a console warning (EAS property "<name>" is <type>, which doesn't fit local property …; skipping it). A Number accepts any uint* or int* type and a numeric string, so its old uint8 values still sync; every other type accepts only the type the SDK publishes for it. Synced Number values, and Dates attested as integers, are stored decoded, as a decimal string and an ISO 8601 string. Rows synced before stay as they are.

Publishing also attests 0 and false now. They used to be skipped as empty, so readers see values that weren't on EAS before.

A File value is its content URL

Affects: code that reads a File property's value as the file's text.

A File property's value is a URL to the stored file after a save and after a reload: a blob: URL in the browser, a file:// URL on Node. Before, it was the URL right after a save, and the file's text a moment later. Read the contents through the URL:

before
const notes = post.notes // the file's text, once the property had loaded
after (browser)
const notes = await fetch(post.notes).then((response) => response.text())
after (Node)
import { readFile } from 'node:fs/promises'

const notes = await readFile(new URL(post.notes), 'utf8')

Html and ItemStorage values still read as their text. This replaces the Node note in File properties: raw base64 strings are stored as text. See File, Image and Html properties.

Drafts without a publisher are owned

Affects: Node scripts that pass owned addresses, and browser apps that set owned addresses before a wallet connects.

A draft that has never had a publisher is owned whatever the owned addresses are: it's editable, addressFilter: 'owned' lists it, and the next wallet to connect claims it. A draft with a publisher is owned only while that address is one of the owned addresses. When nothing supplies a publisher and exactly one owned address is set, a new draft gets that address. This reverses Drafts without a publisher are read-only once owned addresses are set from 0.7.3. With one owned address, a script doesn't need to set the publisher any more (Post is the type from Typing items in TypeScript):

before
import { client, Item, setGetPublisherForNewSeeds } from '@seedprotocol/sdk'

setGetPublisherForNewSeeds(async () => address)
await client.init({ config, addresses: { owned: [address], watched: [] } })
const values = { modelName: 'Post', schemaName: 'Blog', title: 'Draft' }
const post = (await Item.create(values)) as Post
post.title = 'Edited'
after
import { client, Item } from '@seedprotocol/sdk'
import type { Post } from './types' // from Typing items in TypeScript

await client.init({ config, addresses: { owned: [address], watched: [] } })
const values = { modelName: 'Post', schemaName: 'Blog', title: 'Draft' }
const post = (await Item.create(values)) as Post
post.title = 'Edited'

With several owned addresses, keep setGetPublisherForNewSeeds, or claim the drafts with client.claimUnpublishedDrafts, so drafts get the right publisher. In the browser, drafts created before a wallet connects are editable and are claimed when it connects, so code that kept users from editing them until then can go. See How ownership is decided.

Publish wallets say what kind they are

Affects: apps that build a PublishWallet without fromEip1193Provider or fromEthersWallet, and publish from an address with no contract code without useModularExecutor or useDirectEas.

On that route, publish attests straight to EAS only when the wallet has kind: 'eoa' and signs as the publisher. It used to guess from the factory whether the address was an undeployed smart account, which let some through. A wallet with no kind is refused with "…the publish wallet doesn't say whether it is an EOA or an undeployed smart account…". fromEip1193Provider, fromEthersWallet and ConnectButton set kind; for fromThirdwebAccount, pass it:

before
import { fromThirdwebAccount } from '@seedprotocol/publish/thirdweb'

const wallet = fromThirdwebAccount(account)
after
import { fromThirdwebAccount } from '@seedprotocol/publish/thirdweb'

const wallet = fromThirdwebAccount(account, { kind: 'eoa' }) // 'smart' for a smart wallet

PublishWalletKind is exported for typing a wallet you assemble yourself. See Wallets.

ConnectButton deploys the account and signs data items

Affects: apps that use ConnectButton, and code that worked around the two 0.7.4 problems below.

Without useModularExecutor, ConnectButton registers a deploy function for a smart wallet, so a new user's first publish deploys the account and goes through multiPublish. In 0.7.4 it failed with MANAGED_ACCOUNT_NOT_DEPLOYED (First publish from an undeployed smart account is refused). ConnectButton also registers an EOA as the data item signer (a smart wallet's admin key, or the connected EOA with useModularExecutor), and item.publish() uses it by default, so you don't pass dataItemSigner on the bundler path any more (item.publish() publishes):

before
import { deploySmartWalletContract, isSmartWalletDeployed } from '@seedprotocol/publish/thirdweb'
import { getDataItemSigner } from './dataItemSigner'

if (account && !(await isSmartWalletDeployed(account.address))) {
  await deploySmartWalletContract(account)
}
await post.publish({ dataItemSigner: getDataItemSigner() })
after
await post.publish()

A wallet you register yourself without a deploy function is still refused with MANAGED_ACCOUNT_NOT_DEPLOYED. PublishManager.createPublish still has no default data item signer, so pass dataItemSigner there. See ConnectButton.

initPublish checks uploadApiBaseUrl

Affects: apps that call initPublish (or render PublishProvider) without uploadApiBaseUrl.

initPublish throws initPublish: uploadApiBaseUrl is required (the upload API that stores files on Arweave), unless payment.storage is "user". A missing value used to fail only at the first upload. A value that's set has to be an absolute http(s) URL or a path on the page's origin, such as /seed-api. See initPublish.

ensureWalletThenPublish results

Affects: code that calls ensureWalletThenPublish or ensureSmartWalletThenPublish.

Both wait for the publish to be spawned and return its actor as publish. An item that's already publishing gets { outcome: 'already_publishing' }, where started used to resolve to undefined. started is deprecated and already resolved.

before
const result = await ensureWalletThenPublish(post)
if (result.outcome === 'started') {
  const actor = await result.started
  if (!actor) showMessage('This post is already publishing.')
}
after
import { ensureWalletThenPublish } from '@seedprotocol/publish'

const result = await ensureWalletThenPublish(post)
if (result.outcome === 'already_publishing') showMessage('This post is already publishing.')
else if (result.outcome === 'started') watchPublish(result.publish)

See Other exports.

Other changes in 0.7.5

  • Node scripts exit on their own after client.stop(), so a process.exit() that was only there to end the script can go. client.init no longer creates <cwd>/.seed, so with an absolute filesDir the working directory doesn't have to be writable, and an Electron app doesn't need to chdir. See The working directory.
  • unload() releases one holder. item.unload() and modelProperty.unload() stop the shared instance only when no other holder has it. Code that avoided unload() because it stopped the item for everyone can call it.
  • A bundler or Turbo publish caught mid-upload by a reload is marked interrupted instead of resuming, because its signed data items weren't saved. Offer to publish again. See Handle failures and resume.
  • PublishManager.query(seedLocalId) only reads the publish's current snapshot. It used to stop the publish; call PublishManager.stopPublish for that.
  • Cost estimates. evm.usd is 0 on a testnet, and the new evm.testnet is true. The new arweave.chargedBytes is what the upload path charges for (an L1 upload at least 256 KiB, a bundle once, Turbo uploads up to 100 KiB free); arweave.bytes stays the raw size. See estimatePublishCost.
  • getCanonicalItemPropertiesFromEas from @seedprotocol/eas counts a Version only if its Seed's attester made it, and a property only if its Version's attester did. Pass checkAttesters: false for the old behavior. See getCanonicalItemPropertiesFromEas.
  • @seedprotocol/query returns integers as decimal strings. Code that decoded the indexer's {"type":"BigNumber","hex":…} JSON can drop that. See SeedRecord.

Upgrading from 0.7.3 to 0.7.4

Released: 2026-10-16. Each part below says which apps it affects; the Changelog lists the additions and fixes that need no work.

Upgrade every package together

Affects: apps that use @seedprotocol/react or @seedprotocol/publish.

React 0.7.4 imports easName, which first shipped in SDK 0.7.4, so with SDK 0.7.3 it fails at import with SyntaxError: The requested module '@seedprotocol/sdk' does not provide an export named 'easName'. Since 0.7.5 npm refuses a mismatched set at install instead; install every package as in Before you start.

A second copy of eas or arweave makes query throw

Affects: apps that use @seedprotocol/query (directly, or through @seedprotocol/feed) and have mixed versions installed, or a bundler setup that loads @seedprotocol/eas twice.

Every @seedprotocol/query read checks that the EAS and Arweave clients were registered on the same copy of @seedprotocol/eas and @seedprotocol/arweave that query uses. If the SeedClient registered them on another copy, the read throws DuplicateEasModuleError (or DuplicateArweaveModuleError) instead of reading through other clients, possibly on another chain:

DuplicateEasModuleError: Two copies of @seedprotocol/eas are loaded, and EAS clients were registered on the other copy (e.g. by SeedClient). …

Check that each package is installed once:

npm ls @seedprotocol/eas @seedprotocol/arweave @seedprotocol/query

Each should appear once, deduped, at the version you installed. With Vite, keep @seedprotocol/* packages out of optimizeDeps.exclude; seedVitePlugin dedupes them.

Item.create validates its values

Affects: code that calls Item.create, Model#create or React's useCreateItem, especially imports and tests that pass extra keys or loose values.

Item.create used to write whatever it was given: an unknown model still created an item, unknown keys appeared on the instance, and too-long or wrongly typed values were stored. It now checks everything before writing anything:

  • An unknown modelName (in schemaName, if you pass one) throws UnknownModelError, with modelName and schemaName.
  • Unknown keys, and values that fail their property's type or validation rules, throw ItemCreateValidationError. Its problems array has one { property, message, code } per problem, and its message lists them all (Cannot create Author: twitter: Author has no property "twitter").
  • required isn't checked, so an item with a required property missing is still created. File, Image, Html and ItemStorage values are checked by their own save, as before.
before
// Created the Author: the 300-character name was stored, and `twitter` appeared on the instance.
const author = await Item.create({ modelName: 'Author', name: 'x'.repeat(300), twitter: '@ada' })
after
import { Item, ItemCreateValidationError, UnknownModelError } from '@seedprotocol/sdk'

const values = { modelName: 'Author', schemaName: 'Blog', name: form.name }

try {
  const author = await Item.create(values)
} catch (error) {
  if (error instanceof ItemCreateValidationError) {
    for (const problem of error.problems) showFieldError(problem.property, problem.message)
  } else if (error instanceof UnknownModelError) {
    throw new Error(`No model ${error.modelName}; is the schema loaded?`)
  } else {
    throw error
  }
}

Use problem.property and problem.message. problem.code is a name for some checks (unknown_property, max_length_exceeded, enum_violation) and a number in a string for type and pattern failures ('41', '53'), so don't match on it in 0.7.4 to 0.7.11. 0.7.12 names every code; see Validation codes are names.

When you import data that's already stored or attested and must be written as it is, pass validate: false in the second argument. It skips the value and key checks, not the model lookup:

await Item.create(
  { modelName: 'Post', schemaName: 'Blog', ...importedValues },
  { validate: false },
)

Model#create(values) calls Item.create without options, so it always validates and can't opt out; to import through a model, call Item.create with modelName and schemaName yourself. Since 0.7.8, useCreateItem's createItem takes the options third. A create rejected by these checks writes nothing, so it's safe to retry once the values are fixed. Since 0.7.5, a create that fails later, in a File, Image, Html or ItemStorage save, leaves nothing behind either.

Validation rules that were skipped are enforced

Affects: apps whose schema uses pattern on Text, pattern with a length rule on Html, enum together with other rules, or validation on properties defined in code. Also Date properties.

0.7.3 dropped some rules without saying so. In 0.7.4 they apply, on Item.create and on assignment:

Rule0.7.30.7.4
pattern on TextIgnored (the default length limit replaced it)Enforced
pattern on Html with minLength or maxLengthDroppedEnforced with the length rule
enum with other rulesReplaced every other ruleCombined with them; enum no longer adds Text's default 255-character limit
validation on properties passed to Model.create(…, { properties })IgnoredEnforced
Date valuesAssignment failed with Unknown format 'date-time'Validated and saved

So a value that saved under 0.7.3 may now be rejected. Item.create throws ItemCreateValidationError. An assignment doesn't throw: the stored value stays, save() rejects with ItemPropertySaveValidationError, and the property's saveValidationErrors say why, as for any other validation failure.

seed.schema.json
"slug": { "type": "Text", "validation": { "pattern": "^[a-z0-9-]+$" } }
before (0.7.3)
post.slug = 'Not A Slug!!' // saved: the pattern was never checked
after
post.slug = 'Not A Slug!!' // not saved: the reason is in saveValidationErrors
post.slug = toSlug(input) // make the value fit the rule before assigning it

Before upgrading, check that your forms produce values that pass these rules, and loosen any rule that was written for 0.7.3 and never actually ran. Values already stored aren't re-checked; a stored value that breaks a rule only fails when it's assigned again or passed to Item.create. That includes validate: false imports you later edit.

Rules on properties defined in code lived only on the loaded Model until 0.7.9, which stores them with the property, so they apply after a restart. saveNewVersion() writes them into the version file. Other devices don't get them from the local database, so put rules you rely on in your schema file. validation.custom still has no effect.

File properties: raw base64 strings are stored as text

Affects: code that assigns a string to a File property.

A string given to a File property used to be decoded as base64, and a data: URL was stored as literal text. In 0.7.4:

  • a data: URL is decoded to bytes;
  • an http(s): or blob: URL is fetched;
  • the name of a file already in the files folder links that file;
  • anything else, including a raw base64 string, is stored as the file's text.
before
post.attachment = btoa('hello') // decoded to "hello"
after
post.attachment = `data:text/plain;base64,${btoa('hello')}` // decoded to "hello"
post.attachment = new File(['hello'], 'hello.txt', { type: 'text/plain' }) // or pass a File

Since 0.7.5 the value reads back as the file's URL, not its text; see A File value is its content URL.

EAS names follow easName

Affects: apps that publish models or properties whose names have an acronym followed by more words (HTMLPage), a digit before a capital (ID3Tag), spaces or hyphens; apps with single Relation, Image, File or Html properties that were published as <name>_id; and anything that reads EAS by schema name or filters seeds.type in SQL.

0.7.4 builds every name it writes to EAS with easName (exported from @seedprotocol/sdk and @seedprotocol/eas; the decision is ADR 0008 in the SDK repository). It splits words at a change from lowercase or a digit to uppercase and before the last capital of an acronym, turns other characters into _, and lowercases. Most names come out as before. These change:

Name in your schemaPublished by 0.7.3Published by 0.7.4
Model HTMLPagebytes32 htmlpagebytes32 html_page
Model ID3Tagbytes32 id3tagbytes32 id3_tag
Model New modelbytes32 new modelbytes32 new_model
Single relation authorbytes32 author or bytes32 author_id, depending on how the property was loadedAlways bytes32 author
Post, BlogPost, publishedAt, coverURLpost, blog_post, published_at, cover_urlUnchanged

To see whether your schema is affected, compare each model and property name's new form with the old one:

scripts/checkEasNames.ts
import { easName } from '@seedprotocol/sdk'

// What 0.7.3 and earlier published: underscores only between a lowercase letter and a capital.
const oldEasName = (name: string) => name.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase()

for (const name of ['Post', 'HTMLPage', 'ID3Tag', 'coverURL', 'publishedAt']) {
  if (easName(name) !== oldEasName(name)) console.log(`${name}: ${oldEasName(name)} → ${easName(name)}`)
}
// HTMLPage: htmlpage → html_page
// ID3Tag: id3tag → id3_tag

What happens to data published under the old names. It stays on chain where it is. The next publish of an affected model or property registers the new EAS schema and attests there, so that model's Seeds end up under two schema UIDs: the old ones under htmlpage, new ones under html_page. A new version of an old item keeps its Seed under the old schema; its new property attestations use the new property names. Nothing is migrated or re-attested.

The SDK's own EAS sync finds both. It fetches each model's schemas under every name it could have been published as, and matches EAS names to your local names: exact first, then ignoring case and separators, then also a trailing plural s and, for relations, a trailing id or ids. So bytes32 htmlpage Seeds sync into HTMLPage, an old author_id attestation lands on the relation author, and synced values are stored under your property names (coverURL, not coverUrl). An EAS name that matches two of your models or properties equally well is skipped with a console warning. Since 0.7.5, nameMatch in the schema file turns the looser levels off for a schema, model or property, and an attestation whose type doesn't fit the local property is skipped (Sync skips attestations whose type doesn't fit). Rows that an earlier SDK synced under a converted name (publishedAt for a property named published_at) stay until that Seed syncs again.

Readers that look EAS up by one name don't get this matching. @seedprotocol/query's queryBySchema(name) (and @seedprotocol/feed, which reads through it), getSeedsBySchemaName, getEasSchemaUidBySchemaName (removed in 0.7.9), and your own EAS GraphQL queries match the exact name, so for an affected model they now see only the old Seeds or only the new ones. Query both names and merge:

import { queryBySchema } from '@seedprotocol/query'

const [newer, older] = await Promise.all([queryBySchema('html_page'), queryBySchema('htmlpage')])
const pages = [...newer.items, ...older.items]

Do the same in indexers, explorers or anything else outside the SDK that filters on a schema UID or name. If no app has published an affected model yet, nothing needs to change: it starts out under the new name.

Model types in the local database. After init, the leader tab rewrites the model type stored with each item (seeds.type, and the matching columns of versions and metadata) to the easName form, once per type: image_v_2 becomes image_v2, a synced htmlpage becomes html_page. A type that matches none of your models, or several, is left as it is. New rows already use the new form. Raw SQL with a hard-coded type stops matching, so build the type with easName:

import { easName, seeds } from '@seedprotocol/sdk'
import { eq } from '@seedprotocol/sdk/drizzle'

db.select().from(seeds).where(eq(seeds.type, easName('ImageV2')))

Renaming a property in a schema file needs its id

Affects: apps that rename properties by editing the schema file passed as config.schema.

When a property name in the file matched no stored property, 0.7.3 guessed a rename: it took the first unmatched stored property of the same type, even if it was unrelated. 0.7.4 treats a property as renamed only when its id matches a stored property's, and then moves its stored values to the new name. Without a matching id, a new name is an added property plus a removed one, and the values stay under the old name.

seed.schema.json (before: 0.7.3 guessed the rename)
"Author": {
  "properties": {
    "about": { "type": "Text" }
  }
}
seed.schema.json (after: keep the property's id)
"Author": {
  "properties": {
    "about": { "id": "<id of bio>", "type": "Text" }
  }
}

Copy the id from the latest version file the SDK wrote in the files directory ({id}_<schema>_v<n>.json), or read it from property.id on the loaded ModelProperty. renameModelProperty still renames in code and moves the values. See Rename a property.

First publish from an undeployed smart account is refused

Affects: apps that publish from a smart account with no contract code yet, without useModularExecutor.

In 0.7.3 such a publish attested straight to EAS, one transaction per Seed. 0.7.4 refuses it in checking with a ManagedAccountPublishError whose code is MANAGED_ACCOUNT_NOT_DEPLOYED:

The publishing smart account 0x… is not deployed on <chain> yet. Deploy it first (your wallet's setup flow, or deploySmartWalletContract from @seedprotocol/publish/thirdweb), then publish again.

Since 0.7.5 ConnectButton deploys the account on its first publish, so the default setup needs no change (ConnectButton deploys the account and signs data items). A wallet you register yourself without a deploy function is still refused, and needs kind (Publish wallets say what kind they are); from 0.7.7 the refusal is the cause of item.publish()'s rejection (Errors keep their class). A wallet that sends transactions from another address than the publisher is refused too, because EAS would record the wrong attester.

Who pays: the payment option

Affects: apps that set thirdweb.sponsorGas, paymasterUrl or useArweaveBundler, apps that read estimatePublishCost().evm.userPays or arweave.userPays, and apps that want users to pay for storage.

Nothing changes unless you set payment. It states who pays gas ('sponsored' or 'user') and storage ('operator' or 'user'), fills in the defaults of the lower-level settings, and initPublish throws PublishPaymentConfigError when one of them contradicts it, for example gas: 'sponsored' with a plain wallet, or storage: 'user' with useArweaveBundler: false.

before
initPublish({
  ...config,
  useModularExecutor: true,
  thirdweb: { sponsorGas: false }, // the account pays its own gas
})
after
import { initPublish } from '@seedprotocol/publish'

initPublish({
  ...config,
  useModularExecutor: true,
  payment: { gas: 'user' }, // sponsorGas now defaults to false
})

With an EIP-7702 sender, payment: { gas: 'user' } sends without a paymaster (remove paymasterUrl). payment: { storage: 'user' } uploads data items straight to Turbo, charged to the signer's Turbo credits (or to userStorage.paidBy first), and turns on useArweaveBundler. An unfunded signer gets InsufficientStorageCreditsError. The signer is the data item signer, which ConnectButton registers since 0.7.5.

estimatePublishCost now reports who pays from the sender the publish will use. If your UI showed costs from userPays, check it: evm.userPays is false when Thirdweb or a paymaster sponsors, and arweave.userPays is false unless payment.storage is 'user' (it used to be always true). The prices themselves are unchanged.

authorizePublish and the authorizing state

Affects: apps that show publish states, and operators who want to approve or meter publishes.

A publish now starts in a new authorizing state, before checking. Without authorizePublish it passes straight through, but code that lists states, or assumes a fresh run starts in checking, should allow for it:

const busy = ['authorizing', 'checking', 'creatingArweaveTransactions' /* … */].includes(status)

To approve each publish before anything is spent, set authorizePublish in the config (or per publish). Return false or throw to refuse, and the publish fails with PublishNotAuthorizedError; return { token } to send that token as X-Seed-Publish-Authorization on the upload API requests:

import { initPublish } from '@seedprotocol/publish'

initPublish({
  ...config,
  authorizePublish: async ({ seedLocalId }) => {
    const res = await fetch('/api/approve-publish', { method: 'POST', body: JSON.stringify({ seedLocalId }) })
    return res.ok ? { token: await res.text() } : false
  },
})

Connecting and disconnecting a wallet

Affects: apps that set watched addresses, apps that call clearPublishWallet(), and apps that relied on owned addresses staying after a disconnect.

Connecting (setPublishWallet, which ConnectButton calls) replaces the owned list and keeps the watched list. In 0.7.3 it cleared watched addresses, so apps set them again after every connect; you can drop that. Disconnecting (ConnectButton, useSeedWallet().disconnect, or clearPublishWallet()) clears the owned list and keeps watched. Drafts stamped with the old wallet are read-only until it reconnects; drafts created while signed out are editable and are claimed by the next connect.

clearPublishWallet() now returns a Promise. Await it before you create drafts or read addresses, or they may still see the old owned list:

before
clearPublishWallet()
await client.setAddresses({ owned: [], watched }) // cleared owned addresses by hand
after
import { clearPublishWallet } from '@seedprotocol/publish'

await clearPublishWallet() // clears owned, keeps watched

Restored publishes don't have your Arweave key

Affects: apps that pass arweaveJwk or dataItemSigner to createPublish or item.publish() only per publish.

0.7.4 no longer saves the per-publish arweaveJwk or dataItemSigner in publish_processes. A publish resumed after a reload signs with PublishConfig.arweaveJwk or dataItemSigner, and fails at creatingArweaveTransactions without one. That includes the data item signer ConnectButton registers: item.publish() uses it for a new publish, but a restored publish doesn't. If your users reload during publishes, set the key or signer in the config too. Rows written by 0.7.3 and earlier still hold the key they were given; clear finished publish history after upgrading if that matters to you.

Smaller changes to check

  • unload() on a Schema, Model or other entity releases one reference and stops the instance only when none remain. Item.unload() does the same since 0.7.5.
  • Schema.destroy() also deletes the schema's version files in the files directory. If the schema is still in config.schema, the next start adds it back.
  • saveNewVersion() now writes a file that loads. Pass that file as config.schema from then on: if the app keeps passing the older file, the next start reports the older version and saveNewVersion() throws ConflictError.
  • Background draft saves (after a rename) that fail now log a console.warn and keep the error on schema.lastSaveError or model.lastSaveError.
  • Offline retries. Failed automatic syncs and the start-up EAS lookup are retried with a backoff. Tests that stub the network outside NODE_ENV=test can turn this off with setRetriesWhenOnlineEnabled(false).
  • config.dbConfig now takes effect in Node. If you set it while it did nothing, check that dbUrl and outDir point where you want: the SDK opens (or creates) the database there.

Upgrading from 0.7.2 to 0.7.3

Released: 2026-10-14. Each part below says which apps it affects; the Changelog lists the additions and fixes that need no work.

Upgrade React and the SDK together

Affects: apps that use @seedprotocol/react or @seedprotocol/publish.

React 0.7.3 imports exports that first shipped in SDK 0.7.3, so with an older SDK it fails at import with SyntaxError: The requested module '@seedprotocol/sdk' does not provide an export named 'placeholderUidSql'. An app that installed @seedprotocol/publish without listing the SDK got SDK 0.6.10 at the top level and the same kind of failure (does not provide an export named 'pickLatestMetadataRowPerProperty'). Since 0.7.5 npm refuses a mismatched set at install; install every package as in Before you start.

Node 20.19 or 22.12 or later

Affects: Node scripts, servers and Electron apps on older Node releases, and CI images.

The SDK's engines field is now ^20.19.0 || >=22.12.0 <=24. It was >= 20 <= 24. Node 20.0–20.18, Node 21 and Node 22.0–22.11 are outside it: npm warns on install, and fails with engine-strict. Those are the releases that can require() an ES module, which is how require('@seedprotocol/sdk') works now.

package.json (before)
{ "engines": { "node": ">=20" } }
package.json (after)
{ "engines": { "node": "^20.19.0 || >=22.12.0 <=24" } }

The SDK no longer needs --import @seedprotocol/sdk/node-eas-lodash, and it finds its database migrations from any working directory. Keep the --import flag in processes that also load @seedprotocol/publish, which still needs it. See Using Seed in Node and Electron.

The property decorators are removed

Affects: code that imports Text, Number, Boolean, Date, Json, Image, List or Relation from @seedprotocol/sdk/node.

They only recorded metadata that nothing read, so a decorated class never created a model. Define the model with the Property builders (or in your schema file) instead:

before
import { Text, Relation } from '@seedprotocol/sdk/node'

class Post {
  @Text() title!: string
  @Relation('Author') author!: string
}
after
import { Model, Property } from '@seedprotocol/sdk/node'
import { Schema } from '@seedprotocol/sdk'

const blog = await Schema.create('Blog')
await Model.create('Post', blog, {
  properties: {
    title: Property.Text(),
    author: Property.Relation('Author'),
  },
})

You can also drop experimentalDecorators and reflect-metadata from your project if nothing else uses them. See Node entry.

A second init with a different config throws

Affects: apps that call client.init more than once, including dev setups where hot reload re-runs it after a config edit.

A second client.init with a config that isn't deeply equal to the first now throws INIT_CONFIG_CONFLICT_MESSAGE. It used to resolve straight away and keep the first config. A repeat call with the same config still resolves, and applies new addresses (since 0.7.13, except empty ones; see Empty addresses on a repeat init keep the stored ones; since 0.8.0 none).

before
await client.init({ config })
// Resolved, but the client kept using the first config.
await client.init({ config: { ...config, filesDir: '.seed/other' } })
after
import { client } from '@seedprotocol/sdk'

await client.init({ config })

// To switch config, stop the client first.
client.stop()
await client.init({ config: { ...config, filesDir: '.seed/other' } })

// To change only the addresses, keep the config.
await client.setAddresses({ owned: [account], watched: [] })

In development, reload the page after editing the config. Build the config once at module level rather than inside a component, so re-renders pass the same object.

Leaving addresses out of init now keeps the stored addresses (with a one-time console warning). It used to clear them in Node and in the first tab. Pass addresses: { owned: [], watched: [] } if you meant to clear them.

A schema that doesn't load rejects init

Affects: apps whose config.schema or config.schemaFile has errors, or points at a file that isn't there.

client.init used to succeed and only log the problem. It now rejects with Could not load config.schema from "<path>": <reason> (or config.schemaFile), and with React the error reaches SeedClientGate's onInitError. A later init isn't affected by an earlier failure. Don't keep using the client after this rejection: the database is open, so some calls still work, but the schema isn't loaded.

after
try {
  await client.init({ config })
} catch (error) {
  // e.g. Could not load config.schema from "./seed.schema.json": …
  showStartupError(error)
}

Assigning to an item you don't own is refused

Affects: apps that let users edit items they don't own, for example watched addresses' items.

Assignment now checks ownership, like save(), destroy(), publish() and unpublish() already did. On an item the client's owned addresses don't own, post.title = 'x' writes nothing and the value rolls back to the stored one. Nothing is thrown; the property records an OwnershipError as its save error. Before, the assignment wrote to the local database.

before
<input value={post.title} onChange={(e) => (post.title = e.target.value)} />
after
import { isItemOwned } from '@seedprotocol/sdk'

const [owned, setOwned] = useState(false)
useEffect(() => { isItemOwned(post).then(setOwned) }, [post])

return owned
  ? <input value={post.title} onChange={(e) => (post.title = e.target.value)} />
  : <p>{post.title}</p>

Check ownership before showing inputs, as above; otherwise the user's typing flickers back. ItemProperty#destroy() also checks ownership now, and throws OwnershipError. See Ownership and drafts.

Drafts without a publisher are read-only once owned addresses are set

In 0.7.3 and 0.7.4, once owned addresses were set, a draft with no publisher wasn't owned, so assignments to it, and initial ItemStorage values passed to Item.create, were refused. 0.7.5 reversed this; see Drafts without a publisher are owned.

destroy() rejects when the delete fails

Affects: code that calls destroy() on an Item, ItemProperty, Model, ModelProperty or Schema.

destroy() used to resolve even when the delete failed and the rows stayed. It now rejects. Destroying a ModelProperty, Model or Schema that items have values for now works: the items' values are kept and unlinked from the property.

before
await post.destroy()
removeFromList(post) // ran even if nothing was deleted
after
import { OwnershipError } from '@seedprotocol/sdk'

try {
  await post.destroy()
  removeFromList(post)
} catch (error) {
  showError(error instanceof OwnershipError ? "You can't delete this post." : 'Delete failed.')
}

readyTimeout defaults to 15 seconds

Affects: code that relies on create, find or all failing fast.

The default readyTimeout for create, find and all on Schema, Model, ModelProperty, Item and ItemProperty is 15 000 ms. Most were 5000 ms. Loads that used to time out may now succeed, and real failures take longer to show. Pass the old value where you need it:

after
const post = await Item.find({ seedLocalId, readyTimeout: 5000 })

item.publish() publishes

Affects: code that calls item.publish() or Item.publish(), and React apps that use usePublishItem.

In 0.7.2 item.publish() only built a payload, and without @seedprotocol/publish it stayed pending for five minutes. In 0.7.3 it hands the item to @seedprotocol/publish, which runs a full publish: uploads to Arweave and attestations on EAS, signed by the wallet from ConnectButton or setPublishWallet. It resolves when the publish has finished and rejects if it fails. It also rejects when the item is already publishing, and throws "Publishing is not configured…" at once when initPublish() (or <PublishProvider config>) hasn't run. usePublishItem calls it, so it publishes too, and its error reports these rejections.

If your code called item.publish() to check an item or to build its payload, remove that call: it now publishes on-chain. To publish, call it in place of your own PublishManager.createPublish wrapper:

before
await publishPost(post) // PublishManager.createPublish(...), from the publishing guide
after
await post.publish()

PublishManager.createPublish and the ensure*ThenPublish helpers still work; the helpers' results changed again in 0.7.5 (ensureWalletThenPublish results). See Publishing and revoking.

Upgrading from 0.6 to 0.7.2

From 0.5.x or 0.6.x, read every part below; each says which release made the change. From 0.7.0 or 0.7.1, read only the first two.

@seedprotocol/eas and @seedprotocol/query

Affects: code that uses these packages directly. Changed in: 0.7.2.

  • Version and property attestations from @seedprotocol/eas's queries (GET_VERSIONS, GET_PROPERTIES, GET_ALL_PROPERTIES_FOR_ALL_VERSIONS, GET_FILES_METADATA, GET_IMAGE_VERSIONS) no longer include schema { schemaNames }. Read schemaId, and look names up with getSchemaNamesBySchemaUids if you need them.
  • Records from @seedprotocol/query are deep-frozen outside production. Copy before you change one:
const { items } = await queryBySchema('Post')

// before: changed the cached record for every later caller
items[0].data.title = items[0].data.title.trim()

// after: throws a TypeError unless NODE_ENV is 'production'; copy instead
const post = { ...items[0].data, title: String(items[0].data.title).trim() }

See Frozen records.

Affects: apps that unpublish items other items refer to. Changed in: @seedprotocol/sdk and @seedprotocol/publish 0.7.0 and 0.7.1.

  • Publishing an item that refers to an unpublished (revoked) item now fails before anything is sent, with RelatedItemUnpublishedError (code related_item_unpublished). It names the property and the related item. Republish the related item first, or remove the reference.
  • Republishing an unpublished item creates a new seed, with a new UID, instead of adding a version under the revoked seed. Other items that stored the old UID aren't rewritten.
  • Since 0.7.1 a publish includes every draft reachable through relation and list properties, with all their properties. A publish can now send more than before; Estimate the cost shows what it will include.

Model names are scoped to their schema

Affects: apps that load more than one schema with the same model name. Changed in: @seedprotocol/sdk and @seedprotocol/react 0.7.0.

Model names only need to be unique within a schema. When two loaded schemas both define Post, lookups by name alone, such as Item.create, Model.getByName and Model.find, throw AmbiguousModelError. Listing without a schema still works and returns both schemas' items. Pass the schema name:

before
const post = await Item.create({ modelName: 'Post', title: 'Hello' })
const { items } = useItems({ modelName: 'Post' })
after
const values = { modelName: 'Post', schemaName: 'Blog', title: 'Hello' }
const post = await Item.create(values)
const { items } = useItems({ modelName: 'Post', schemaName: 'Blog' })

modelFileId (the model's Model.id) works in place of schemaName. Apps with one schema need no change. See Model.

The first time 0.7.x opens a database from an earlier version, it runs a one-time check. If items saved before then have no recorded model and their model name now exists in more than one schema, the SDK can't tell which model they belong to, and it clears the local item data. Schemas and models stay, and published items come back on the next EAS sync, but unpublished drafts of those models are lost. If your app loads several schemas that share a model name, publish or export drafts before upgrading.

Multi-tab coordination is on by default

Affects: browser apps. Changed in: @seedprotocol/sdk 0.7.0.

Tabs of the same app now elect a leader that runs automatic EAS sync, bulk downloads and the upload finalize worker, and other tabs get its results. client.syncFromEas() still runs in the tab that calls it. Most apps need no change. If something in your app relied on every tab syncing on its own, opt out:

await client.init({ config, multiTab: 'off' })

Handle FileSystemLockedError (code OPFS_LOCKED) and TabLockTimeoutError at init; Multi-tab behavior shows how. With React, SeedClientGate reports these through onInitError.

OPFSFilesManager

Affects: apps that render OPFSFilesManager. Changed in: @seedprotocol/react 0.7.0.

The component was rebuilt with its own stylesheet. Two defaults changed:

  • theme defaults to 'system'. It was 'dark'.
  • onBeforeDelete runs after the user confirms the delete. It used to run before, so a confirm dialog of your own inside it now asks twice.
before
<OPFSFilesManager
  onBeforeDelete={(file) => window.confirm(`Delete ${file.name}?`)}
/>
after
<OPFSFilesManager
  theme="dark" // keep the old look, or drop it to follow the OS
  onBeforeDelete={(file) => !file.path.endsWith('db/seed.db')} // the component already asked
/>

The old component used Tailwind classes that the package never shipped. Restyle the new one with the --seed-* variables; see Files and OPFS.

Reverted transactions throw

Affects: code that calls waitForPublishReceipt, the sidecar attest and revoke helpers, or ensureExecutorModuleInstalled. Changed in: @seedprotocol/publish 0.6.10.

waitForPublishReceipt used to return a reverted receipt, so a revert looked like success. It now throws PublishTransactionRevertedError. Functions that return its receipt, such as attestPublishAuthorization, revokePublishAuthorization and the DomainOwnership and PublishedBy attest and revoke functions, throw too.

before
const receipt = await waitForPublishReceipt(hash)
if (receipt.status === 'reverted') showError('Transaction reverted')
after
import { waitForPublishReceipt, PublishTransactionRevertedError } from '@seedprotocol/publish'

try {
  const receipt = await waitForPublishReceipt(hash)
} catch (error) {
  if (error instanceof PublishTransactionRevertedError) showError('Transaction reverted')
  else throw error
}

ensureExecutorModuleInstalled no longer tries installModule on thirdweb ModularCore accounts, which could never work. It returns { status: 'skipped' }, { status: 'already-installed' } or { status: 'installed', eas, transactionHash }. PublishConfig.modularAccountModuleData is ignored; remove it.

The 2026-10-03 contracts need publish 0.6.9 or later

Affects: every app that publishes. Changed in: @seedprotocol/publish 0.6.9.

On 2026-10-03 the ManagedAccount factory on OP Sepolia switched every account to a new SeedProtocolExtension and a new executor. The account's multiPublish now takes a publishIndex (selector 0x2a29fadc), the old string-publishLocalId function is no longer routed, and the executor moved to a new address with a different field order. With @seedprotocol/publish 0.6.8 or earlier, interactive publishes revert and automation publishes go to the old executor. See Deployments for the addresses and how they were checked.

Upgrading to 0.7.7 brings the new encoding. Then:

  1. Point modularAccountModuleContract at the new executor, everywhere your app and any server that publishes for it are built:
src/publish.ts (before)
initPublish({
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_SEED_UPLOAD_API_URL,
  modularAccountModuleContract: '0x043462304114da543add6B693c686B7d98865F3E', // old executor
})
src/publish.ts (after)
initPublish({
  thirdwebClientId: import.meta.env.VITE_THIRDWEB_CLIENT_ID,
  uploadApiBaseUrl: import.meta.env.VITE_SEED_UPLOAD_API_URL,
  modularAccountModuleContract: '0x80562aeEe4F16473779b1474D8D32d114D302e94',
})

To catch a stale address at startup instead of at the first publish, compare it with what the factory pins (readFactorySeedExecutor, added in 0.6.10):

import { readFactorySeedExecutor, getPublishConfig } from '@seedprotocol/publish'

const pinned = await readFactorySeedExecutor()
const configured = getPublishConfig().modularAccountModuleContract
if (pinned && configured?.toLowerCase() !== pinned.executor.toLowerCase()) {
  console.warn(`modularAccountModuleContract is ${configured}, but the factory pins ${pinned.executor}`)
}
  1. If your app uses publish automation, users have to turn it on again. Enrolling again installs the new executor on their account and grants a session key whose only allowed target is the new executor. Grants made for the old executor 0x0434…5F3E no longer work, and their UserOps revert.
  2. Remove any use of the exports 0.6.9 dropped: encodeMultiPublishInteger, transformPayloadToIntegerIds, the RequestWithStringIds and RequestWithIntegerIds types, and the useIntegerLocalIds option. No deployed contract accepts them.

If you call the encoders yourself: encodeMultiPublish now writes publishIndex references, encodeExecutorMultiPublish uses the extension's field order (seedUid, seedSchemaUid, versionUid, versionSchemaUid), and both throw on a duplicate local ID or a missing or unknown publishLocalId. Cross-references explains the format.

Revocation and schema registration are owner-only

Affects: apps with publish automation. Changed in: @seedprotocol/publish 0.6.8.

The new contracts don't let a delegate revoke, so the SDK revokes only through EAS, signed by the owner. An automation session key can't unpublish items, and items attested by the executor itself can't be revoked by anyone. Session keys also can't register missing EAS schemas: publish fails first with schema "…" is not registered; automation keys can't register schemas.

Register schemas once from the owner's wallet before handing publishing to a key. ensureEasSchemasForItem(item, wallet) does it, and so does an ordinary publish by the owner. See Revocation and Security and session keys.

The local database remembers its chain

Affects: apps that change chains or read EAS before initPublish runs. Changed in: @seedprotocol/sdk and @seedprotocol/publish 0.6.8.

The local database records which chain's EAS it holds, and the SDK refuses to sync, publish or revoke against a different chain. A database created before 0.6.8 that already holds attestations is treated as OP Sepolia. Pointing the app at another chain then throws, so give each chain its own database (its own filesDir). Deploying to a new chain covers the config.

EAS sync also waits up to 30 seconds for initPublish to set the chain. If your app syncs without the publish package, set the chain yourself with SeedConfig.eas.chainId or the EAS_CHAIN_ID environment variable.

VERSION_SCHEMA_UID_OPTIMISM_SEPOLIA was renamed VERSION_SCHEMA_UID. The old name still works and is deprecated:

// before
import { VERSION_SCHEMA_UID_OPTIMISM_SEPOLIA } from '@seedprotocol/sdk'
// after
import { VERSION_SCHEMA_UID } from '@seedprotocol/sdk'

FieldMapper moved to its own entry

Affects: apps that use @seedprotocol/mapping's UI. Changed in: @seedprotocol/mapping 0.6.0.

The main entry is headless, so it loads in Node without React. The UI moved:

// before
import { FieldMapper } from '@seedprotocol/mapping'
// after
import { FieldMapper } from '@seedprotocol/mapping/react'

layout now defaults to 'rows'. Pass layout="wires" to keep the two-pane connector view; it still works and is deprecated, as are theme="unstyled" (use 'none'), showPreview and connectionColors. Since 0.6.1, relation lookups are stored on each edge as lookup.entries; MappingDocument.lookups still reads and writes as a fallback. See FieldMapper UI.

Coming from 0.4.x

0.5.0 was never published; 0.5.1 (2026-08-29) was the first 0.5 release. From 0.4.x, also make these changes:

  • React hooks live in @seedprotocol/react. They moved out of the SDK in 0.4.15 (2026-03). Import useItems, useItem, SeedProvider and the rest from @seedprotocol/react.
  • The Vite plugin is its own package. Import seedVitePlugin from @seedprotocol/vite, as in Install @seedprotocol/vite to import @seedprotocol/sdk/vite.
  • thirdweb is optional in @seedprotocol/publish. ConnectButton, the thirdweb PublishProvider and the thirdweb helpers moved to @seedprotocol/publish/thirdweb. thirdwebClientId is optional, and rpcUrl is required without it.
// before
import { ConnectButton, PublishProvider } from '@seedprotocol/publish'
// after
import { ConnectButton, PublishProvider } from '@seedprotocol/publish/thirdweb'
  • SeedSigner no longer sends transactions. Pass a PublishWallet ({ signer, txSender }): build one with fromEthersWallet, fromEip1193Provider, or fromThirdwebAccount from the /thirdweb entry.

On this page

Before you startUpgrading from 0.7.13 to 0.8.0Publishing through a ManagedAccount needs 0.8.0Point the config at the new executor, reinstall and grant keys againSession-key helpers take wallets and expiresAtGrants default to 90 daysEnroll refuses a funded accountLarge uploads need confirminggetUnpublishedRelatedItems is removedA repeat init never changes addressesSeedProvider and SeedClientGate apply address changesrelations rejects unknown valuesexternal_id keeps its name againHand-built query results need listedThe first reads after upgrading are coldThe document lookup table is removedFieldMapper shows its built-in lookup editorconnectSourceToProperty returns the listprobeGateway rejects a bare signalcreateTransaction is typedErrors and messages apps may matchUpgrading from 0.7.12 to 0.7.13Names that publish as a Solidity word don't loadMissing files read undefinedEmpty addresses on a repeat init keep the stored onesClientNotInitializedError replaces matching the messageReact: early creates and the status typesrelations: 'urls' is removedRelation keys are the same in every modequeryBySchema pages can be shortcreateEasNodeDispatcher is asyncAutomation needs ensureEasSchemasForModelsprobeGateway takes an options objectpermagate.io leaves the default gatewaysFeed guids and Atom fieldsLookup entries take a list of refsGateway Hyper: upgrade the operator and its clients togetherNode 20.18.1 for query, feed and feed-hyperUpgrading from 0.7.11 to 0.7.12Validation codes are namesSchemas with misplaced rules or unknown types don't loadItem.getById doesn't holdItem.find before init rejectsItem.create checks relation targetsremoveLocalCopiesForAddresses keeps unpublished editsarweave.net is the default gatewaygetArweaveUrlForTransaction returns the /raw/ URLuploadApiBaseUrl doesn't fall back to the gatewayCustom query sources implement one listing methodnormalizeBytes32Hex is strict and toSnakeCase is removedFeed readers show items again onceGateway Hyper timeouts and optionsReact: schema IDs and the stopped statusseedVitePlugin rejects unknown optionsFieldMapper themes use --sfm-* onlyPublish wallets can be 'modular'Upgrading from 0.7.10 to 0.7.11Removed aliasesPublish authorizations report revocation and expiryclear() on an unloaded property writes nothingTests see init errorsUpgrading from 0.7.9 to 0.7.10Lookups don't holdUnheld items unload after 60 secondsWrites on an unloaded instance throwReact hooks hold what they showclient.stop() returns a promiseOlder browsers fail with UnsupportedBrowserErrorApps that install ZenFS move to 2.7.7createPublish doesn't hold the itemseed-gateway checks --portUpgrading from 0.7.7 to 0.7.9Databases older than 0.7.7 are recreatedRejected values stay shownWrites go through the outboxsetValue takes one argumentstatus means loadedRequest syncs with client.syncFromEas()Synced values don't replace unpublished editsItem holds and blob: URLsClearing a published value publishes the removalSchemas: ids from names, same-version edits replace the fileRaw SQL readsRemoved exportsSmaller SDK changesReact hooksPublishing uploads only signed data itemsquery, eas and feedUpgrading from 0.7.5 to 0.7.7Client calls after client.stop() throwErrors keep their classRejected values roll backItem.create throws ItemCreateErrorFile values work after a reloadRelations hold Seed UIDs after a publishDirect EAS refuses updates to earlier requestsProduction hosting must send the isolation headersInstall @seedprotocol/vite to import @seedprotocol/sdk/viteThe Vite plugin's defaults changedImport Drizzle operators from the SDKNext.js apps use @seedprotocol/nextGateway Hyper rewrites Host and times outUpgrading from 0.7.4 to 0.7.5Remove the overrides blockNumber properties publish as stringsSync skips attestations whose type doesn't fitA File value is its content URLDrafts without a publisher are ownedPublish wallets say what kind they areConnectButton deploys the account and signs data itemsinitPublish checks uploadApiBaseUrlensureWalletThenPublish resultsOther changes in 0.7.5Upgrading from 0.7.3 to 0.7.4Upgrade every package togetherA second copy of eas or arweave makes query throwItem.create validates its valuesValidation rules that were skipped are enforcedFile properties: raw base64 strings are stored as textEAS names follow easNameRenaming a property in a schema file needs its idFirst publish from an undeployed smart account is refusedWho pays: the payment optionauthorizePublish and the authorizing stateConnecting and disconnecting a walletRestored publishes don't have your Arweave keySmaller changes to checkUpgrading from 0.7.2 to 0.7.3Upgrade React and the SDK togetherNode 20.19 or 22.12 or laterThe property decorators are removedA second init with a different config throwsA schema that doesn't load rejects initAssigning to an item you don't own is refusedDrafts without a publisher are read-only once owned addresses are setdestroy() rejects when the delete failsreadyTimeout defaults to 15 secondsitem.publish() publishesUpgrading from 0.6 to 0.7.2@seedprotocol/eas and @seedprotocol/queryPublishing related itemsModel names are scoped to their schemaMulti-tab coordination is on by defaultOPFSFilesManagerReverted transactions throwThe 2026-10-03 contracts need publish 0.6.9 or laterRevocation and schema registration are owner-onlyThe local database remembers its chainFieldMapper moved to its own entryComing from 0.4.xRelated