Skip to content

Houdini's Architecture

Houdini is built around a compiler-first model. We write GraphQL documents in our app, and Houdini reads them at build time, validates them against our schema, and generates the runtime code and TypeScript types our framework integrations need. The goal is to move as much work as possible out of the browser and into code generation.

The three main pieces

Houdini has three major parts:

  • Houdini Client: the runtime client used by our application. It manages network requests, normalized caching, optimistic updates, pagination, subscriptions, and document behavior.
  • Code generation: the compiler pipeline that reads GraphQL documents, validates them, and writes generated files into $houdini.
  • Framework plugins: integrations that turn generated artifacts into framework-native APIs. Each plugin implements the same codegen hooks and outputs whatever files its framework needs.

The core compiler understands GraphQL and Houdini’s runtime model. Framework plugins decide how that model should feel inside a given app.

Compile-time work

When we write a GraphQL document, Houdini can know a lot before our app ever runs:

  • the operation name
  • the result shape
  • required variables
  • fragment dependencies
  • cache selection data
  • generated TypeScript types
  • framework-specific wrappers

That lets Houdini generate strongly typed code instead of shipping a large GraphQL interpretation layer to the browser.

For example, in a Svelte component we might write:

<script lang="ts">
import { graphql } from '$houdini'
const UserList = graphql(`
query UserList {
users {
name
}
}
`)
// trigger UserList.fetch() somewhere
</script>
{$UserList.data?.users?.map((user) => user.name).join(', ')}

From that single tagged template literal, Houdini generates the UserList result and variable types, a store for loading the query, and cache metadata for every selected field. The important idea is that the document is the source of truth; everything else is derived.

How the compiler works

This is where things get interesting. The compiler has to satisfy two constraints that pull in different directions: it’s deeply integrated with Vite, which means there will always be a Node.js layer involved. But parsing and validating thousands of GraphQL documents in a hot-reload loop is the kind of work that really wants a compiled language.

The solution is a process-per-plugin model with a Node.js orchestrator. Node handles Vite integration, pipeline sequencing, and spawning. The heavy pipeline work (extraction, validation, codegen) runs in Go plugins (or any compiled binary).

Plugins as long-running processes

When the compiler starts, it spawns each plugin as a child process and keeps it alive. During houdini generate, that means the processes persist for the duration of one run. During vite dev, they persist for the entire dev session. Startup cost is paid once, and each incremental build is just the orchestrator sending hook invocations to already-running processes.

Each plugin registers itself with its name, the hooks it implements, and how it wants to be ordered relative to other plugins. After that, the orchestrator knows exactly which processes to call for each stage of the pipeline.

WebSocket communication

The orchestrator and plugins communicate over WebSockets. The persistent connection means there’s no per-call handshake overhead: we connect once and send hook invocations over the open socket for as long as the session runs. When the orchestrator goes away (Vite restarts, process killed, generate finishes), the connection close propagates to every plugin and they exit cleanly. No orphaned processes to hunt down.

SQLite as shared memory

Plugin processes need to share data (schema definitions, extracted documents, artifact metadata) across process boundaries and across language runtimes. We use a single SQLite database file for this. The path is passed to every plugin as a flag at startup, and each plugin connects directly.

SQLite in WAL mode supports concurrent reads without blocking, which matters for hooks like Validate and GenerateDocuments that are independent of each other and can run in parallel. More broadly, the database schema is the contract between the orchestrator and every plugin. A plugin written in Go, Rust, or anything else just needs to open the same file: no serialization layer, no bespoke protocol for state transfer.

Vite’s role during dev

During development, Vite drives the compiler. When source files change, Vite’s HMR pipeline calls the orchestrator, which triggers an incremental pipeline run starting from the appropriate hook. Because plugins are already running and the database already has the previous build’s state, only the work that’s actually stale gets redone. The orchestrator also serializes concurrent triggers: if a schema watcher and a file watcher fire at the same time, they queue rather than racing.

During houdini generate, the same pipeline runs end-to-end once and exits.

Why this architecture matters

The process model gives us a few things that would be difficult to get any other way:

  • Speed: the heavy pipeline work runs in compiled code, not in Node’s event loop
  • Extensibility: any language that can open a SQLite file and speak WebSocket can be a plugin
  • Resilience: liveness connections mean clean shutdown instead of orphaned processes
  • Parallelism: independent hooks read from a shared database concurrently without coordination overhead