@velora-dex/widget server-renders, and the @velora-dex/widget/ssr entry makes that render correct on the first paint: the tokens the request asks for, optionally their rates too, with no flash and no hydration mismatch.
All of it is optional. <Widget /> already renders on the server with no setup at all, painting its default state: the default pair for the configured chain, empty amounts, the default trade mode. The client then resolves the tokens your input names shortly after mount, which the visitor sees as a brief flash. Reach for the SSR entry when you want that first paint to be right.
What you get for what you do
Each step builds on the one above it. Stop when you have enough.Resolve the tokens
The widget resolves token addresses by looking them up in token lists, which are normally fetched in the browser after mount.resolveWidgetInitState does that lookup on the server instead, and hands you a serializable result to pass back in through ssrState.
Export the config once. resolveWidgetInitState has to run with the same config you render with, since it decides which token lists and trade modes are enabled.
widget-config.ts
server-only module
rendered on both server and client
ssrState into the page and hand the same object to the client component when it hydrates. How you carry it across is your framework’s business: props from a Server Component, an Astro island prop, a __DATA__ script tag. Because both sides seed from one initState, the first client render matches the server markup even before token lists finish loading in the browser.
resolveWidgetInitState never throws. On any failure it returns an empty init state and the widget falls back to its defaults.
For a working Next.js version of this, see the Next.js example.
Add rates and quotes
initState fixes the tokens. It does not fetch prices. resolveWidgetSSRQueries prefetches the request-independent queries (Delta prices, Market rates, bridge routes, token support) into a dehydrated query cache, so the first paint carries live numbers:
Caching token lists
Each list is fetched through three layers, cheapest first. The widget’s module-level query client holds lists withstaleTime: Infinity, so within one server process a list is fetched at most once. A persistent cache serves non-expired entries from disk, so even the first render after a restart usually skips the network. Only a miss on both reaches the network, and the result is written back to both.
The cache option is yours to choose:
new FileCache() stores under $XDG_CACHE_HOME/velora, falling back to ~/.cache/velora. Pass new FileCache({ basePath }) where $HOME is read-only but a temp directory is writable.
new Cache() is the same thing without the disk tier, for runtimes with no usable filesystem. Still worth passing: the in-process query cache garbage-collects after five minutes with nothing observing it, while these entries live out their full TTL. Importing it instead of FileCache also keeps every node:* import out of your build.
Any object implementing ICache works too, which is the escape hatch for a shared Redis- or KV-backed cache:
getEntry returns { value, expiresAt }. Without it, an entry promoted into the in-process cache is assumed to be as fresh as the read that found it, so one lifted a minute before it expires gets a second full lifetime and the effective TTL can be up to double what it says. A cache without getEntry is still correct, just coarser.
Omitting cache entirely is fine. Every server process then fetches lists from the network on first use.
Warm the cache at startup
bridgeInfo: false to skip that if your input only ever names tokens by address.
Await it rather than firing and forgetting. Many serverless runtimes freeze or kill the process once the response is sent, so anything left in flight may never finish.
Deployment
The widget’s route must render on demand: the resolved state depends on the request, so it cannot be statically prerendered. In Next.js that means a dynamic or server-rendered route; in Astro, a server adapter plusprerender = false.
“Serverless” is not one thing, and the right cache backend differs:
On Lambda-style runtimes you must set
basePath explicitly. The default resolves to $XDG_CACHE_HOME or ~/.cache/velora, and a read-only $HOME yields a path that cannot be created. Every cache operation then degrades to a miss, silently and correctly, and you get no caching at all.
Calibrate the gain there, though: /tmp lives and dies with the execution environment, exactly like the in-process query cache that is always on. Both survive warm invocations and both are empty on a cold start, so a file cache buys little on Lambda. An external cache is what helps across instances.
Workers and Deno Deploy do have node:fs now, but their filesystem is virtual and scoped to one request, so a FileCache there writes files nothing later reads. Use Cache: not because the import fails, but because the disk tier buys nothing.
Knowing what did not resolve
resolveWidgetInitState always comes back with tokens. One it could not find is replaced by the widget’s own default, which is right for rendering and invisible in the result: a URL asking for a token the lists don’t have produces a complete, plausible render of a trade nobody asked for.
So the resolution reports which sides it stands behind.
input named. A side input never named is confirmed too, since there is no request to contradict. confirmed sits beside initState rather than inside it because only one of them is state: initState goes to the client, confirmed describes the render you are about to do and stays on the server.
listsComplete is what makes confirmed meaningful. A token the lists don’t contain and a token whose list never downloaded come back the same way, and listsComplete is false when any enabled list failed to arrive. Gate on it before treating “unconfirmed” as “no such token”:
tokenFrom is worth acting on: redirect to the trade that did resolve, which is where the widget rewrites the address bar to on mount anyway. An unconfirmed tokenTo needs nothing, because the widget renders a Select Token control, which claims nothing.
Tokens named by symbol
input.tokenFrom and input.tokenTo accept { symbol } as well as { address }; an address given alongside a symbol wins. Symbols are not unique, and telling apart two tokens that share one needs bridge-info.
resolveWidgetInitState reads bridge-info from cache and never fetches it, so resolving init state never grows a blocking API call. warmTokenListsCache warms it by default, which is how you get the middle row.
Guessing would be worse than waiting. A seeded token outranks the client’s own bridge-info-aware match, so a wrong guess sticks for the whole session instead of being corrected on mount.
Deferred rendering
Everything above describes blocking SSR: the response waits for the widget’s server state, so the widget is complete in the initial HTML. That is the simple choice and the right default. The alternative is to defer it, sending the page immediately with a placeholder and rendering the widget separately. Every modern framework has a primitive for this, and the widget doesn’t care which: React Suspense with streaming, Astro server islands, Next.js streaming or partial prerendering.
Choose blocking when trade pages need to be indexable, or when the widget is essentially the whole page, since deferring it defers everything the visitor came for. Choose deferred when the widget is one element among others, or when the shell should be cacheable independently of it.
Resolving without blocking
A deferred shell wants to show something truthful and cannot wait on the network to find out what.cacheOnly resolves from the cache layers alone:
Placeholders
<WidgetSkeleton /> is a static shape with no state, shipped from the main entry:
<Widget />, seeded with cacheOnly state and never hydrated. That is usually the better placeholder, since its DOM is nearly identical to what replaces it and the swap-in shifts nothing. Seeding it with best-effort state is safe precisely because it is never hydrated: the real render hydrates against its own markup.
Three things to get right either way. Ship no JavaScript for it, using whatever your framework’s “render but don’t hydrate” mechanism is; a hydrated placeholder would run queries and defeat the purpose. Reserve the same space, since the widget’s height varies with content and a mis-sized container causes layout shift on the swap. And make it inert with the inert attribute rather than pointer-events: none, which the widget’s own styles override from inside.
Which placeholder to use follows from confirmed, because the two sides are not symmetric:
So an unresolved to-side is safe to render as-is, while an unresolved from-side is not. Fall back to
<WidgetSkeleton /> there rather than showing a token nobody asked for.
Caveats
- The
configobject must be the same forresolveWidgetInitStateand<Widget />. Export it once so the two cannot drift. initStatemust reach the client. It is serializable on purpose: resolve on the server, embed it in the page, hand the exact same object to the client widget. Recomputing different state in the browser reintroduces the mismatch SSR removes.- Don’t server-render two widgets with different
inputon one page. Each resolves its own state on the server, but they share a single store in the browser, so the second adopts the first’s trade and React reports a hydration mismatch. Two widgets showing the same trade are fine. - The persistent cache assumes a writable filesystem and fails quietly without one. Every disk operation is best-effort, so a read-only filesystem costs you caching with nothing to tell you.
- Token lists are treated as immutable and global. The cross-request query cache is safe precisely because these public lists are request-independent, so don’t route per-user data through it. At most refresh the cache/refetch the token lists on an hours-long interval.
Reference
@velora-dex/widget/ssr, server-only:
Options on
resolveWidgetInitState:
From the main
@velora-dex/widget entry: ssrState on <Widget />, and <WidgetSkeleton />.
Related pages
- Next.js example — the whole pattern in an app router project.
- Compatibility — peer dependencies, tested frameworks, known caveats.
- Configure — everything the
configobject accepts.