Architecture

How Rabbithole dynamically generates and serves entire websites on demand.

Overview

Rabbithole is a Rust web server that sits between a browser and an LLM. When a visitor requests any URL, Rabbithole checks a local SQLite database for a cached page. If none exists, it calls the LLM with a prepared prompt, receives complete HTML in response, stores it in the cache, and serves it to the visitor — all in a single request cycle. The LLM also returns a structured mappings block that tells Rabbithole what prompts to use for future pages linked from the generated HTML.

No templates, no CMS, no static files. The entire site is a lazily-evaluated graph of prompts and cached HTML responses.

System Diagram

Browser Rabbithole Server SQLite DB LLM (e.g. Claude) | | | | | GET /some-path.html | | | |--------------------------->| | | | | lookup(url) | | | |----------------------------->| | | | | | | | [CACHE HIT] html | | | |<-----------------------------| | | 200 OK (cached HTML) | | | |<---------------------------| | | | | | | | : [CACHE MISS] : | | | get_prompt(url) | | | |----------------------------->| | | | prompt string | | | |<-----------------------------| | | | | | | | generate(system + prompt) | | |---------------------------------------------------->| | | | | | | <!DOCTYPE html>...</html> | | | ---MAPPINGS--- | | | [{url, prompt}, ...] | | |<----------------------------------------------------| | | | | | | store_html(url, html) | | | |----------------------------->| | | | store_mappings(pairs) | | | |----------------------------->| | | | | | | 200 OK (fresh HTML) | | | |<---------------------------| | |

Core Components

1. The Seed Prompt

Every Rabbithole site starts with a single seed prompt: a string describing what the homepage (/) should be. This is the only piece of human-authored content required to bootstrap an entire multi-page website. The seed prompt is stored in the mappings table under the key / before the server starts accepting requests.

seed_prompt = "A documentation site for a Rust library called 'widgets'.
Use a clean minimal style. The homepage should introduce the library,
show a quick install snippet, and link to the API reference,
getting started guide, and changelog."

2. The SQLite Cache

Rabbithole maintains two tables in a local SQLite database:

Table Key Value Purpose
pages url (TEXT, PRIMARY KEY) html (TEXT) Stores the fully-generated HTML for each URL. Served directly on cache hit.
mappings url (TEXT, PRIMARY KEY) prompt (TEXT) Stores the prompt to use when a URL is requested but not yet generated.

On every incoming request, Rabbithole first queries pages. A cache hit means an immediate response with no LLM call — sub-millisecond latency after first generation. A cache miss triggers the generation pipeline.

Pages are cached permanently. There is no TTL or invalidation by default. To regenerate a page, delete its row from the pages table. See Configuration for options controlling cache behavior.

3. The Generation Pipeline

When a URL is not cached, Rabbithole runs the following steps:

1 Prompt Lookup: Query the mappings table for the URL. If no mapping exists, serve a 404. If a mapping exists, retrieve the prompt string.
2 LLM Call: Construct the full prompt (system instructions + page-specific prompt) and send it to the configured LLM provider (e.g. Claude via Anthropic API, or a local model).
3 Response Parsing: Split the LLM output on the ---MAPPINGS--- sentinel. Everything before it is the HTML document; everything after is a JSON array of {"url": "...", "prompt": "..."} objects.
4 Storage: Store the HTML in pages under the current URL. Store each mapping pair into the mappings table, without overwriting any URL that already has a cached page or an existing mapping.
5 Response: Serve the generated HTML to the browser with Content-Type: text/html.

4. The Mappings Block

The most important design constraint in Rabbithole is that the LLM must emit a ---MAPPINGS--- separator followed by a valid JSON array at the end of every response. Each entry describes a page that the generated HTML links to:

<!DOCTYPE html>
<html>
  ... full page HTML ...
</html>
---MAPPINGS---
[
  {"url": "/api-reference.html", "prompt": "API reference for the widgets library..."},
  {"url": "/changelog.html",     "prompt": "Changelog page for the widgets library..."},
  {"url": "/examples.html",      "prompt": "Examples page showing usage of widgets..."}
]

Rabbithole parses this block with a simple string split on ---MAPPINGS---. The JSON is then deserialized and each pair upserted into the mappings table. This means pages are registered lazily: a URL only enters the mappings table when some other page links to it and declares its prompt.

5. Page Isolation

Each page is generated in complete isolation. The only information available to the LLM when generating /api-reference.html is the prompt string stored for that URL. There is no shared session, no memory of previously generated pages, and no access to the HTML of the homepage or any other page.

Implication: The Prompt IS the Context

For a site to feel visually and tonally consistent, every prompt must be self-contained. It must describe the color scheme, font choices, navigation structure, overall theme, any recurring characters or terminology, and the specific content for that page — as if briefing a designer who has never seen the rest of the site.

Short, vague prompts like "API reference page" will produce pages that look completely disconnected from each other. Rich, detailed prompts produce cohesive sites. See Web Tools for a discussion of how the system prompt helps encode cross-page conventions.

6. Depth Limiting

Because each generated page can register new mappings for further pages, and those pages can in turn register yet more mappings, the site graph could grow without bound. Rabbithole enforces a configurable depth limit to prevent runaway generation chains.

Depth is tracked per URL: the root / is depth 0, pages it links to are depth 1, their links are depth 2, and so on. Mappings registered at or beyond the depth limit are stored in the mappings table (so their URLs resolve correctly if visited manually) but the server will not proactively pre-generate them.

# In rabbithole.toml
max_depth = 3   # Links beyond depth 3 are stored but not eagerly generated

Request Lifecycle (Detailed)

Incoming request: GET /foo/bar.html | +-- [1] Query pages WHERE url = '/foo/bar.html' | | | +-- ROW FOUND --> serve html, done. | | | +-- NOT FOUND --> continue | +-- [2] Query mappings WHERE url = '/foo/bar.html' | | | +-- NOT FOUND --> 404 Not Found, done. | | | +-- ROW FOUND --> prompt = mappings.prompt | +-- [3] Build full LLM message: | system = <rabbithole system instructions> | user = prompt | +-- [4] POST to LLM API, await streamed response | +-- [5] Split response on "---MAPPINGS---" | html_part = text before sentinel | mappings_part = text after sentinel (JSON) | +-- [6] INSERT INTO pages (url, html) VALUES (...) | +-- [7] For each {url, prompt} in mappings_part: | INSERT OR IGNORE INTO mappings (url, prompt) VALUES (...) | (existing mappings are never overwritten) | +-- [8] Return html_part as HTTP 200 text/html

The System Prompt

Every LLM call includes a fixed system prompt that instructs the model on output format requirements. This includes:

The system prompt is the backbone of format correctness. If the LLM fails to include the mappings block, Rabbithole will serve the HTML anyway but no new URL mappings will be registered. Parsing is lenient: if ---MAPPINGS--- is absent, the entire LLM output is treated as HTML and an empty mappings list is assumed.

See Configuration for how to customize the system prompt, and Web Tools for how tools like web search integrate into the generation step.

Concurrency Model

Rabbithole is built on async Rust. Multiple requests can be in-flight simultaneously. The SQLite layer uses a connection pool with write serialization so that concurrent cache writes do not collide. Two simultaneous requests for the same uncached URL will both trigger LLM generation independently — the second write to the pages table will simply overwrite the first with an equivalent result. This is safe but wasteful; a future version may add per-URL in-flight deduplication.

Web Tool Integration

When web tools are enabled (see Web Tools), the system prompt instructs the LLM that it may call external search and fetch functions before generating the HTML. This allows pages to contain real, up-to-date information rather than relying entirely on the LLM's training data. Tool calls happen during the generation step and add latency but significantly improve content quality for factual pages.

Data Flow Summary

Event Read from DB Write to DB LLM Called?
Cache hit pages.html No
Cache miss, mapping exists mappings.prompt pages.html, new mappings rows Yes
Cache miss, no mapping No (404)
Server startup mappings row for / (seed prompt) No

Design Tradeoffs

Prompt Context vs. Prompt Brevity

The biggest practical tension in Rabbithole is between context richness and prompt size. For a site to look coherent, each prompt needs to re-describe the full design system, navigation, color scheme, and tone of the site. This means prompts can become very long, consuming tokens on every generation call. The alternative — short prompts that assume the LLM "knows" the site — produces inconsistent, disconnected pages.

A practical strategy is to define a prompt template or style preamble string once in the configuration and have the LLM prepend it to every child prompt it emits. See Configuration for the prompt_preamble option.

Permanent Caching vs. Staleness

By default, pages never expire. This is ideal for static-feeling content and avoids runaway LLM costs. For sites where content should update (e.g. a "current news" page), you can delete specific rows from the pages table, or enable the cache_ttl option to expire pages after a configurable duration.

Laziness vs. Eagerness

Pages are only generated on first request. A freshly seeded site with no visitors is an empty SQLite database — just one row in the mappings table. Pages come into existence the moment someone clicks a link to them. This makes cold starts fast but means the first visitor to a new page experiences LLM latency. The Deployment guide covers strategies for pre-warming the cache.

Source Code

The full source is available at github.com/ajbt200128/rabbithole. Key source files:

File Responsibility
src/main.rs Server startup, config loading, route registration
src/handler.rs HTTP request handler; cache lookup and generation dispatch
src/db.rs SQLite abstraction: pages and mappings CRUD
src/llm.rs LLM client; prompt construction; response parsing and mappings extraction
src/tools.rs Web search and fetch tool implementations
rabbithole.toml Configuration file (seed prompt, LLM settings, depth limit, etc.)

See Also