Skip to content

Contributing

First off, thanks for the interest in contributing to Houdini.

This document should provide some guidance for working on the project, including tips for local development and an introduction to the internal architecture and relevant files.

Note: this document contains links to files that could easily be invalidated by future work. If you run into a broken link, please open a PR to fix it. Keeping documentation up to date is as important as any bug fix or new feature.

Before diving in, the architecture guide is worth a read. It explains how the compiler’s process model works, how plugins communicate, and how the shared database fits together. That context makes the rest of this document a lot easier to follow.

General Introduction

At a high level, Houdini is broken up into a few parts. The core compiler pipeline lives in packages/houdini-core and handles document extraction, validation, and artifact generation. The shared plugin runtime library lives in packages/houdini and provides the cache, Vite plugin, and utilities for building extensions. Apart from that, Houdini has framework-specific packages that take the generated artifacts and deliver an experience tailored to each framework. The Svelte bindings live in packages/houdini-svelte and the React bindings in packages/houdini-react.

Local Development

The quickest way to test and develop new features is by using the end-to-end tests. Starting with pnpm i && pnpm build at the root of the repository will handle linking everything up. Once that’s done, run pnpm dev inside the e2e/kit directory to start both the web app and API development servers. After all of this, visiting localhost:5173 should show the end-to-end test suite. We recommend creating a route in that application to work against. Don’t worry about where it “belongs”; we’ll sort that out when the PR is open.

Make sure you’re using Node.js and pnpm versions compatible with the engines config in the repo’s package.json, otherwise behavior between local, CI, and deploy environments may diverge. Some good options for managing multiple Node versions:

Code Generation

Houdini’s code generation pipeline is written in Go. The core pipeline logic lives in packages/houdini-core, with framework-specific codegen in the corresponding package (e.g. packages/houdini-react). The shared library for building plugins lives in plugins/. Each plugin runs as a long-running process and communicates with the Node.js orchestrator over WebSocket, sharing state through a common SQLite database. See the architecture guide for the full picture.

The pipeline tasks fall into three categories:

  • Validators ensure that assumptions made by the rest of the pipeline hold. For example, one validator makes sure every document has a unique name so the preprocessor can reliably import the correct artifact.
  • Transforms modify the actual documents the user provides. For example, the collectDefinitions transform adds any fragment definitions used by an operation so they can be included in the network request.
  • Generators write things to disk. For example, the TypeScript generator creates type definitions for every document in a project.

Document Artifacts

Artifacts are the central output of code generation. They encode everything the runtime needs: the raw query string, the selection shape, list operation metadata, and more. Rather than outlining every field (which would go stale quickly), the best place to get a feel for them is the artifact snapshot tests.

The Vite Plugin

The base Vite plugin lives in packages/houdini/src/vite and handles tasks like polling the API for schema changes. The framework-specific plugins extend this to transform every graphql-tagged string into something the runtime can use.

The Runtime

The shared runtime lives in packages/houdini/src/runtime and contains the cache and utilities shared across all frameworks.

The Cache

The cache is built on two core operations: writing data and subscribing to a selection.

When writing, the cache walks the result and stores every field value in a normalized map keyed by entity ID. References to other entities are stored as references rather than inline values, so updates only need to happen in one place regardless of how many times an entity appears across queries.

Subscriptions keep stores up to date as values change. The cache walks a given selection and embeds a reference to the store’s set function alongside the field values for each entity. When data is written, the cache captures every relevant set function and calls it with the updated selection. For a good conceptual introduction to normalized caching, the urql docs on Normalized Caching are worth a read; the concepts carry over even if some implementation details differ.

End-to-End Tests

The best way to verify a feature is to write a test that simulates the user’s experience. These live in the e2e/ directory at the root of the repository. The general pattern is to create a route in one of the test applications that showcases a specific behavior, then use Playwright to simulate a user interacting with the UI.

Test Scripts

There are three layers of testing, each with its own set of scripts.

TypeScript unit tests (root)

Run from the repo root using Vitest:

Terminal window
pnpm tests # run all TypeScript unit tests
pnpm tests:ui # run with the Vitest browser UI + coverage report

These cover the runtime cache, client plugins, and other TypeScript-only logic that doesn’t require a full pipeline run.

Go pipeline tests (per package)

The Go pipeline tests live in plugin/ subdirectories of each package (e.g. packages/houdini-core/plugin/, packages/houdini-react/plugin/). Run them with the standard Go toolchain from the package root:

Terminal window
go test ./...

Each test uses tests.RunTable from plugins/tests/. It spins up an in-memory SQLite database, runs the pipeline steps (extract → validate → generate), and asserts on the resulting artifacts. These are the right tests to write when you change Go plugin logic.

Playwright e2e tests (e2e apps)

The e2e/kit/ (SvelteKit) and e2e/react/ apps each have a family of scripts for running the full Playwright suite. The key ones:

ScriptWhat it does
build:Rebuilds all packages from the repo root (pnpm run build)
build:testbuild: then runs Playwright (pnpm test): use this to verify changes against a fresh build
build:buildbuild: then builds the e2e app itself (produces a static output)
build:testsbuild:build then runs Playwright: use this when you also need the app compiled
build:devbuild: then starts the dev server
build:generatebuild: then runs houdini generate
tests / testPlaywright only, no rebuild: use when packages are already built

For faster iteration when only one package changed, the compile:* variants recompile a single package without doing a full root build:

Terminal window
pnpm compile:core # recompile houdini-core only
pnpm compile:houdini # recompile houdini only
pnpm compile:svelte # recompile houdini-svelte only (kit app)
pnpm compile:react # recompile houdini-react only (react app)

Each of these has :dev, :generate, and :dev suffixes that chain into the next step after compiling.

Cache benchmarks

The cache has a benchmark suite in packages/houdini/src/runtime/cache/benchmarks/. Benchmarks are grouped into categories (core, subscriptions, lists, multi-doc, optimistic, gc, ssr) and can be run selectively.

For active work on the cache, watch-bench re-runs a category in fast mode (minimal iterations, just enough to catch regressions) every time a file changes:

Terminal window
pnpm watch-bench gc # re-run gc suite on every save
pnpm watch-bench core # re-run core suite on every save

To run the full suite at full statistical precision:

Terminal window
pnpm bench # all categories, full run
BENCH=core pnpm bench # single category, full run

A CI job runs automatically on any PR that touches packages/houdini/src/runtime/cache/. It benchmarks the base branch and the PR branch on the same runner and flags any benchmark that regressed beyond its noise band.

Piecing It All Together

When thinking about adding a feature, a few questions help frame the work:

  1. Does the feature appear in GraphQL documents? If so, figure out how to persist what the user writes in the generated artifacts. The runtime walks the selection field when writing values to the cache and can look for special keys to perform additional logic when processing a response.
  2. Are there validation steps needed? Validators protect users but also provide guarantees to the runtime; they can save a lot of conditional checks when processing server responses.
  3. Can the framework layer help the runtime? Because the generated code lives in the user’s project, things like reactive statements and lifecycle functions work out of the box.

An end-to-end feature will typically touch the artifact generator and the runtime at minimum. It’s easy to get lost in how the pieces fit together, so the implementation of list operations in the codebase is a good worked example to trace through when getting oriented.