I’m building a local-first Sveltekit application that I plan on bundling with Electron or Tauri. Since my app is an SPA, I fell back to the same architecture I’ve used over and over again in my Svelte projects: a writable store (or stores) utilizing Immer to update the state, passed down to layout components via contexts, which pass down bits of state to other components via props. Pretty standard stuff.
I began prototyping on the client-side, but given that I was eventually going to turn it into a desktop app anyway, I naturally moved the Svelte store to operate on the backend ~ in my case, a simple websocket server.
- the user does some kind of action that requires a state update,
- the action executes an API call that emits some kind of message to the backend,
- the Svelte store on the backend receives the API call & updates the state,
- the entire app state gets pushed back to the frontend;
- the frontend store updates itself entirely. It’s just a plain Svelte writable whose main job is to listen for an
update-statemessage from the backend and replace its entire current state with whatever the backend sends it.
Here’s a diagram outlining these steps for an example, a desktop app with editor cell components (e.g. a Jupyter notebook):
This is probably not an uncommon pattern for certain types of applications, but here are a few times I’ve used this sort of approach:
- a web extension w/ a Svelte frontend. You keep the storage and state changes entirely in a background script and send the entire state back to the content script or options page.
- a frontend app powered by Firestore. In this case, you let Firestore handle the server-side store, and the client-side store simply reactively updates when Firestore sends a message w/ the new state. You execute API calls to Firestore from the frontend.
- a localhost-first desktop app. My app might be hitting different databases or microservices, or performing heavier computations on the backend of the app.
This approach probably doesn’t scale well when you’re sending a lot of data back to the frontend, or if you eventually need to move the frontend source of truth to another machine entirely (e.g. your typical server approach).
Regardless, this pattern enables a bunch of benfits:
- It preserves optionality. If I end up changing assumptions about where the source of truth should be, I can move the backend store back to the frontend.
- It enables out-of-loop updates. If a long-running or asynchronous update happens without the user’s input, we can easily update the backend store, and the frontend reactively updates. This part depends on what you choose for your communication layer. If you’re using websockets, this is straightforward. One could also utilize server-sent events instead of web sockets. When I’ve implemented this pattern in web extensions, typically one just utilizes the message passing APIs available to web extensions.
It’s a testament to Svelte’s thoughtful design: from the frontend’s point of view, as long as there’s a Svelte store being passed to components, it’ll do its job. It doesn’t care what kind of store the components get, as long as the store has a
Some other time, I’ll write about the details of how I build rich application stores using Immer & Svelte.