@seedprotocol/vite
The Vite plugin for browser apps that use @seedprotocol/sdk: cross-origin isolation headers, ES module workers, in-order dev pre-bundles, and opt-in Node polyfills and fs aliases.
@seedprotocol/vite is a Vite plugin that configures a browser build for @seedprotocol/sdk. The SDK keeps its SQLite database and files in the browser's private file system (OPFS), which needs a cross-origin isolated page. The plugin sends the cross-origin isolation headers on the dev and preview servers, builds workers as ES modules, keeps the dev pre-bundle in order, and makes Seed packages linked into your app share one copy of React and of each Seed package. Node polyfills and fs aliases are opt-in, because the SDK doesn't need them. The plugin runs at build time only and adds no runtime API.
import { seedVitePlugin } from '@seedprotocol/vite'Install
npm install --save-dev @seedprotocol/viteThe plugin needs Vite 8 or later (vite >= 8.0.0 is a peer dependency). @zenfs/core (^2.7.7) and @zenfs/dom (^1.2.14) are optional peers; the SDK depends on both, so an app with the SDK has them. Use @vitejs/plugin-react 6 alongside it for React apps. It depends on vite-plugin-node-polyfills, which it uses only when you turn on includeNodePolyfills, so you don't install a polyfill package yourself.
Add Seed to your app walks through the whole setup for a Vite + React app, including the SDK install.
seedVitePlugin
Returns an array of Vite plugins that configures a project for the SDK. Spread it into plugins after your framework plugin.
function seedVitePlugin(options?: SeedVitePluginOptions): Plugin[]import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { seedVitePlugin } from '@seedprotocol/vite'
export default defineConfig({
plugins: [react(), ...seedVitePlugin()],
})Options
All options are optional. An option the plugin doesn't know throws when the config loads, for example seedVitePlugin: unknown option "fsModule". It takes fsModules, debug, includeNodePolyfills, isolationHeaders. The removed autoInit option throws seedVitePlugin: autoInit was removed. Initialize the SDK from your app (client.init(), or <SeedProvider>).
| Name | Type | Default | Description |
|---|---|---|---|
isolationHeaders | boolean | { coop?: string; coep?: string; corp?: string } | true | Sets cross-origin isolation headers on vite and vite preview. true sends Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: credentialless and Cross-Origin-Resource-Policy: same-origin. Pass an object to change individual values (the rest keep their defaults), or false to send none. One of these headers set in server.headers or preview.headers, in any letter case, wins over the plugin's value for that server; vite preview uses server.headers when you set no preview.headers |
includeNodePolyfills | boolean | false | Adds Node polyfills, the global and process defines, a stream stub and inline shims in index.html (see With includeNodePolyfills). Turn it on when another dependency of your app uses Node built-ins or the Buffer, process or global globals in the browser |
fsModules | string[] | [] | Module names aliased to ZenFS, for app code or a dependency that imports Node's fs in the browser. A name containing promises maps to @zenfs/core/promises, any other name to @zenfs/core. To alias all four forms, pass ['fs', 'fs/promises', 'node:fs', 'node:fs/promises'] |
debug | boolean | false | Logs what the plugin adds to the console with a [seed-vite-plugin] prefix, and after a production build warns about Node globals left in the output (see Warnings). It isn't related to the debug npm package |
The type is exported too:
import type { SeedVitePluginOptions } from '@seedprotocol/vite'Example
Switch the embedder policy to require-corp and turn on logging:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { seedVitePlugin } from '@seedprotocol/vite'
export default defineConfig({
plugins: [
react(),
...seedVitePlugin({
isolationHeaders: { coep: 'require-corp' },
debug: true,
}),
],
})What the plugin configures
With the default options, the plugin returns one Vite plugin that makes these changes. Only the headers can be turned off.
- Sends the cross-origin isolation headers (see
isolationHeaders) on every response fromviteandvite preview, including responses written directly withwriteHead, which framework HTML handlers use and Vite'sserver.headersdoesn't reach. - Sets
worker.formatto'es'. Vite's default'iife'can't build the database worker, which uses code splitting, so the database never opens in a production build. - Sets
optimizeDeps.rolldownOptions.output.strictExecutionOrdertotrue.drizzle-orm's modules import each other in a cycle, and when your app imports both@seedprotocol/sdkand@seedprotocol/sdk/drizzle, the dev pre-bundle can otherwise run them out of order. Production builds aren't affected. - Adds
react,react-dom,thirdweb,xstate,@xstate/react,@zenfs/core,@zenfs/domand the Seed packages (sdk,react,publish,feed,eas,arweave,query) toresolve.dedupe, each only when anode_modulesfolder at or above your app root has it. Under pnpm's default layout that means the packages your app lists itself. Without this, a Seed package linked into your app (file:,link:or a workspace) loads its own copies: a second React makes hooks fail, and a second copy of a Seed package splits its module state, such as the registered EAS and Arweave clients. - In
vite dev, extendsserver.fs.allowwith the SDK's directory and thesqlocaland@sqlite.org/sqlite-wasmdirectories when they sit outside the directories Vite already allows, for example when the SDK is linked. Without this, the dev server answers the database worker and WASM requests with 403.
The plugin doesn't set optimizeDeps.include or exclude, so Vite pre-bundles the SDK and its dependencies like any other package. That's what lets vite dev work under pnpm's default isolated layout.
The plugin only adds to your config. A worker.format or strictExecutionOrder you set is kept, resolve.dedupe entries you already listed aren't repeated, and your resolve.alias entries match before its own.
With includeNodePolyfills
includeNodePolyfills: true adds four more Vite plugins, which make these changes:
- Adds
vite-plugin-node-polyfillsforpath,buffer,events,string_decoder,utilandtimers(and theirnode:forms), plusBuffer,globalandprocessglobals.streamandcryptoaren't polyfilled, because those pull instream-browserifyand break server-side rendering and prerendering.readlineis excluded. - Resolves the polyfill package's shim imports (
buffer,global,process) to absolute file paths inside the plugin's install, so they work when the polyfill package isn't visible from your app. - Aliases
streamandnode:streamto a minimal stub, so packages such asrss-parserload without the full stream polyfill. - Defines
globalasglobalThis(in dev pre-bundling too),process.browserastrueandprocess.platformas"browser", except for a key you define yourself. It leavesprocess.env.NODE_ENVto Vite, so React Fast Refresh keeps working in dev. - Injects a small inline
<script>at the top of<head>inindex.htmlthat createsglobalThis.globaland a minimalglobalThis.process(withenv.NODE_ENVset from Vite's mode) before any module runs. It's added through Vite'stransformIndexHtmlhook, so it only appears in HTML that Vite transforms.
Warnings
With debug: true, the plugin parses each output chunk after a production build and prints free references to global, process, Buffer, __dirname and __filename under [seed-vite-plugin] Bare Node.js globals in browser chunks. It skips typeof checks, property names, names the chunk declares itself, and uses behind a typeof guard or inside a try block. A chunk it can't parse reports nothing.
It also skips chunks made only of Seed packages' modules (the SDK, @seedprotocol/react, query, eas and arweave, or anything under node_modules/@seedprotocol/). A chunk that mixes Seed modules with other code is still scanned, so an app with only the SDK still lists a few hits, such as Buffer in drizzle-orm's blob columns and process.cwd() in Node path handling, and runs without errors. A hit throws a ReferenceError only if that code runs in the browser, so check the named chunk for a dependency that expects Node, and turn on includeNodePolyfills if it does.
@seedprotocol/sdk/vite
The SDK re-exports the plugin and its options type, so older projects that import it from the SDK keep working:
import { seedVitePlugin, type SeedVitePluginOptions } from '@seedprotocol/sdk/vite'It's the same function. @seedprotocol/vite is an optional peer of the SDK, so this import works only when your app installs @seedprotocol/vite; without it, the import fails with ERR_MODULE_NOT_FOUND: Cannot find package '@seedprotocol/vite'. New projects should import from @seedprotocol/vite.
Headers in production
The plugin sets headers on the Vite dev and preview servers only. Your production host must send the same headers on the HTML page, or the page isn't cross-origin isolated and client.init rejects with DatabaseNotPersistentError (code: 'DB_NOT_PERSISTENT'). SeedClientGate then stays on its loading state; Handling init errors shows how to report it.
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
Cross-Origin-Resource-Policy: same-originUse Cross-Origin-Embedder-Policy: require-corp instead if your stack needs it or a browser you target doesn't support credentialless. With require-corp, every cross-origin resource the page loads (images, fonts, scripts from a CDN) must opt in with CORS or a Cross-Origin-Resource-Policy header. Sending the headers on every path, as the examples below do, also covers the worker scripts.
These host snippets follow each host's documented header syntax, but we haven't tested them against a deployed Seed app. Check the response headers in your browser's network panel after deploying, and confirm self.crossOriginIsolated is true in the console.
/*
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless
Cross-Origin-Resource-Policy: same-originFor Netlify and Cloudflare Pages, a _headers file in Vite's public/ folder is copied to the root of the build output, which is where both hosts look for it. In nginx, add_header directives in a location block replace any inherited from the server block, so keep all three together.
You can check a production build locally with vite build followed by vite preview, which gets the same headers as the dev server.
React Router framework mode and prerendering
React Router framework mode, and other setups that render or prerender pages with Vite's SSR build, take the plugin with no options. By default it adds no polyfills, defines or aliases, and it applies the isolation headers to the HTML that framework mode writes with writeHead. If you turn on includeNodePolyfills, it still leaves out the stream and crypto polyfills, which break prerendering. Don't alias crypto to crypto-browserify yourself for the same reason.
Start the Seed client in client-side code (with SeedClientGate or client.init), not during prerender: the browser client needs OPFS and a cross-origin isolated page.
We haven't run a framework-mode app with the current plugin. The paragraph above describes what the plugin's code does.
Electron
You can use the plugin in a Vite-built Electron renderer config. The renderer needs a cross-origin isolated page like any other browser build, and Node-only work, such as a better-sqlite3 database, stays in the main process. Using Seed in Node and Electron covers the setup.
Troubleshooting
Buffer is not definedorprocess is not definedat runtime, or Vite's "has been externalized for browser compatibility" error in the browser console. A dependency of your app, not the SDK, expects Node.includeNodePolyfills: truecoverspath,buffer,events,string_decoder,utilandtimers, theBuffer,globalandprocessglobals, and astreamstub. For any other built-in, such ashttp,https,urlorcrypto, install a browser version and alias it inresolve.alias. @seedprotocol/mapping in a browser shows the case forrss-parser. To find the dependency, runvite buildwithdebug: trueand read the Warnings.- The client never gets past loading, and the console shows
DatabaseNotPersistentError. The page isn't cross-origin isolated. In dev, check you haven't setisolationHeaders: falseand that your ownserver.headersdon't replace the plugin's with other values. In production, check your host's headers (see Headers in production). Failed to resolve import "sqlocal/drizzle", ordoes not provide an export named 'default', under pnpm. Your config lists the SDK,sqlocalordrizzle-orminoptimizeDeps.exclude, often a workaround copied from an older setup. Remove those entries, deletenode_modules/.viteand restart the dev server.Cannot read properties of undefined (reading 'Symbol')at startup invite dev.drizzle-orm's pre-bundled chunks ran out of order. Check that...seedVitePlugin()is inpluginsand that your config doesn't setoptimizeDeps.rolldownOptions.output.strictExecutionOrdertofalse.Failed queryat init in a production build only. The database worker was built as'iife'. Removeworker.format: 'iife'from your config, or set it to'es'.- File reads fail or come back empty. You probably have two copies of ZenFS. Run
npm ls @zenfs/core @zenfs/dom(orpnpm why @zenfs/core): each package should appear at one version, the one the SDK lists. If not, remove your own@zenfs/*entries frompackage.json, or pin them to the SDK's versions, and install again. - 403 for a worker or
.wasmfile under/@fs/in dev, orsqlocal worker failed to startwith a linked SDK. The file is outside Vite's allowed directories. The plugin adds the SDK,sqlocaland@sqlite.org/sqlite-wasmdirectories automatically; turn ondebug: trueto see what it added, and add any other directory toserver.fs.allowyourself. Module "node:dns" has been externalized for browser compatibility(and the same fornet) duringvite buildin an app that uses@seedprotocol/publish. They come from publish's domain ownership checks, which a browser app doesn't run, and are expected. An app with only the SDK and@seedprotocol/reactbuilds without them.DuplicateEasModuleErrororDuplicateArweaveModuleErrorin the browser. Two copies of@seedprotocol/easor@seedprotocol/arweavewere loaded. Runnpm ls @seedprotocol/eas @seedprotocol/arweave: each should be one deduped copy, at the SDK's version. Keep Seed packages out ofoptimizeDeps.excludeso Vite pre-bundles them together, then deletenode_modules/.viteand restart the dev server. See @seedprotocol/query.
Related
- Add Seed to your app: the full Vite + React setup
- @seedprotocol/next: the Next.js config helper
- Migration: upgrade steps from earlier plugin versions
- @seedprotocol/sdk overview
- The Seed toolkit: where each package fits
Files and OPFS
Hooks and a ready-made component for listing, browsing and managing the files a Seed app stores in the browser's private file system (OPFS).
@seedprotocol/next
The Next.js config helper for apps that use @seedprotocol/sdk in the browser: browser builds of the SDK in every graph and cross-origin isolation headers, under Turbopack or webpack.