Open Source Docs

Spoonbill

Server-side SPA framework for Scala 3

Design

Architecture, render/update loop, and runtime model


Design

Spoonbill is a server-side SPA framework built on top of Avocet. The browser runs a small JS bridge. The server owns the application state and renders UI updates.

High-level architecture

Spoonbill keeps the "real app" on the server. The client is responsible for:

  • applying DOM changes sent by the server

  • sending user events back to the server

Mermaid diagram: flowchart LR

Core runtime pieces

  • ApplicationInstance[F, S, M]: one running session. It owns the render loop, state stream, message stream, and the Frontend.

  • Frontend[F]: parses incoming client messages into typed streams (domEventMessages, browserHistoryMessages) and sends DOM changes back.

  • ComponentInstance[F, ...]: applies the Avocet document to the render context, collects event handlers, and runs transitions.

  • Effect[F[_]]: Spoonbill abstracts over the effect type (Future / cats-effect / ZIO / etc).

Render/update loop

When state changes, Spoonbill re-renders and computes a diff using Avocet. The server then ships a compact "DOM patch" to the browser.

Mermaid diagram: sequenceDiagram

Deterministic ids and "outdated DOM" protection

Spoonbill relies on Avocet's deterministic ids (like 1_2_1) for:

  • mapping DOM events back to server-side handlers

  • ensuring events from an outdated DOM are ignored

The client attaches an eventCounter to each event. The server tracks counters per (targetId, eventType) and only accepts events that match the current counter. After handling a valid event, it increments the counter and tells the client the new value.

Startup paths (why dev mode is special)

ApplicationInstance.initialize() has two important startup modes:

  • pre-rendered page: the browser already shows the initial DOM, so Spoonbill registers handlers and starts streams without needing an initial diff.

  • dev mode saved render-context: when spoonbill.dev=true and a saved render-context exists, Spoonbill loads it and diffs against it to update the browser after reloads.

Mermaid diagram: flowchart TD