···11+---
22+title: Architecture Guide
33+description: How the hosting service, firehose service, and tiered storage work together
44+---
55+66+Wisp.place's serving infrastructure is split into two microservices: the **firehose service** (write path) and the **hosting service** (read path). They communicate through S3-compatible storage and Redis pub/sub.
77+88+## Service Overview
99+1010+### Firehose Service
1111+1212+The firehose service watches the AT Protocol firehose (Jetstream WebSocket) for `place.wisp.fs` and `place.wisp.settings` record changes. When a site is created, updated, or deleted, it:
1313+1414+1. Downloads all blobs from the user's PDS
1515+2. Decompresses gzipped content
1616+3. Rewrites HTML for subdirectory serving (absolute paths become relative)
1717+4. Writes the processed files to S3 (or disk)
1818+5. Publishes a cache invalidation event to Redis
1919+2020+The firehose service is **write-only** — it never serves requests to end users.
2121+2222+**Key configuration:**
2323+2424+```bash
2525+# Firehose connection
2626+FIREHOSE_URL="wss://jetstream2.us-east.bsky.network/subscribe"
2727+2828+# S3 storage (recommended for production)
2929+S3_BUCKET="wisp-sites"
3030+S3_REGION="auto"
3131+S3_ENDPOINT="https://your-account.r2.cloudflarestorage.com"
3232+S3_ACCESS_KEY_ID="..."
3333+S3_SECRET_ACCESS_KEY="..."
3434+3535+# Redis for cache invalidation
3636+REDIS_URL="redis://localhost:6379"
3737+3838+# Concurrency control
3939+FIREHOSE_CONCURRENCY=5 # Max parallel event processing
4040+```
4141+4242+**Backfill mode:** Start with `--backfill` to do a one-time bulk sync of all existing sites from the database into the cache.
4343+4444+### Hosting Service
4545+4646+The hosting service is a **read-only** CDN built with Node.js and Hono. It serves static files from a three-tier cache and handles routing for custom domains, wisp subdomains, and direct URLs.
4747+4848+On each request, the hosting service:
4949+5050+1. Resolves the site from the request hostname/path
5151+2. Looks up the file in tiered storage (hot → warm → cold)
5252+3. On a cache miss, fetches from the PDS on-demand and populates the cache
5353+4. Applies HTML path rewriting if serving from a subdirectory
5454+5. Processes `_redirects` rules
5555+6. Serves the file with appropriate headers
5656+5757+The hosting service subscribes to Redis pub/sub for cache invalidation messages from the firehose service. When it receives an invalidation, it evicts the affected entries from its hot and warm tiers so the next request fetches fresh content.
5858+5959+## Tiered Storage
6060+6161+The `@wispplace/tiered-storage` package implements a three-tier cascading cache. Data flows **down** on writes and is looked up **upward** on reads.
6262+6363+```
6464+Read path: Hot (memory) → Warm (disk) → Cold (S3/disk)
6565+Write path: Hot ← Warm ← Cold (writes cascade down through all tiers)
6666+```
6767+6868+### Hot Tier (Memory)
6969+7070+- **Implementation:** In-memory LRU cache
7171+- **Eviction:** Size-based (bytes) and count-based (max items)
7272+- **Use case:** Frequently accessed files (index.html, CSS, JS)
7373+- **Lost on restart** — repopulated from warm/cold tiers on access
7474+7575+```bash
7676+HOT_CACHE_SIZE=104857600 # 100 MB (default)
7777+HOT_CACHE_COUNT=500 # Max items
7878+```
7979+8080+### Warm Tier (Disk)
8181+8282+- **Implementation:** Filesystem with human-readable paths
8383+- **Eviction:** Configurable — `lru` (default), `fifo`, or `size`
8484+- **Structure:** `cache/sites/{did}/{sitename}/path/to/file`
8585+- **Survives restarts** — provides fast local reads without network calls
8686+8787+```bash
8888+WARM_CACHE_SIZE=10737418240 # 10 GB (default)
8989+WARM_EVICTION_POLICY=lru # lru, fifo, or size
9090+CACHE_DIR=./cache/sites
9191+```
9292+9393+The warm tier is optional when S3 is configured. Without S3, disk acts as the cold (source of truth) tier.
9494+9595+### Cold Tier (S3 or Disk)
9696+9797+- **With S3:** The firehose service writes here; the hosting service reads (read-only wrapper)
9898+- **Without S3:** A disk-based tier serves as both warm and cold
9999+- **Compatible with:** Cloudflare R2, MinIO, AWS S3, or any S3-compatible endpoint
100100+101101+```bash
102102+S3_BUCKET="wisp-sites"
103103+S3_REGION="auto"
104104+S3_ENDPOINT="https://your-account.r2.cloudflarestorage.com"
105105+S3_ACCESS_KEY_ID="..."
106106+S3_SECRET_ACCESS_KEY="..."
107107+S3_METADATA_BUCKET="wisp-metadata" # Optional, recommended for production
108108+```
109109+110110+### Tier Placement Rules
111111+112112+Not all files are placed on every tier. The hosting service uses placement rules to keep the hot tier efficient:
113113+114114+| File Pattern | Tiers | Rationale |
115115+|---|---|---|
116116+| `index.html`, `*.css`, `*.js` | Hot, Warm, Cold | Critical for page loads |
117117+| Rewritten HTML (`.rewritten/`) | Hot, Warm, Cold | Pre-processed for fast serving |
118118+| Images, fonts, media (`*.jpg`, `*.woff2`, etc.) | Warm, Cold | Already compressed, large — skip memory |
119119+| Everything else | Warm, Cold | Default placement |
120120+121121+### Promotion and Bootstrap
122122+123123+When a file is found in a lower tier but not a higher one, it's **eagerly promoted** upward. For example, a cache miss on hot that hits warm will copy the file into hot for future requests.
124124+125125+On startup, the hosting service can **bootstrap** tiers:
126126+- Hot bootstraps from warm by loading the most-accessed items
127127+- Warm bootstraps from cold by loading recently written items
128128+129129+## Cache Invalidation
130130+131131+The firehose service and hosting service communicate through Redis pub/sub:
132132+133133+```
134134+Firehose Service Hosting Service
135135+ │ │
136136+ │ ── Redis pub/sub ──────────────→ │
137137+ │ (wisp:revalidate) │
138138+ │ │
139139+ │ Site updated/deleted: │ Receives invalidation:
140140+ │ 1. Write new files to S3 │ 1. Evict from hot tier
141141+ │ 2. Publish invalidation │ 2. Evict from warm tier
142142+ │ │ 3. Next request fetches fresh
143143+```
144144+145145+If Redis is not configured, the hosting service still works — it just won't receive real-time invalidation and will rely on TTL-based expiry (default 14 days) and on-demand fetching.
146146+147147+## On-Demand Cache Population
148148+149149+When the hosting service receives a request for a site that isn't in any cache tier, it fetches directly from the user's PDS:
150150+151151+1. Resolves the user's DID to their PDS endpoint
152152+2. Downloads the `place.wisp.fs` record
153153+3. Fetches the requested blob
154154+4. Decompresses and processes the file
155155+5. Stores it in the appropriate tiers based on placement rules
156156+6. Serves the response
157157+158158+This means the hosting service works even without the firehose service running — it just won't have pre-populated caches.
159159+160160+## Deployment Scenarios
161161+162162+### Minimal (Disk Only)
163163+164164+No S3 or Redis required. The hosting service uses disk as both warm and cold tier. Best for small deployments or development.
165165+166166+```bash
167167+# Hosting service only
168168+CACHE_DIR=./cache/sites
169169+HOT_CACHE_SIZE=104857600
170170+```
171171+172172+### Production (S3 + Redis)
173173+174174+The firehose service pre-populates S3 and notifies the hosting service of changes via Redis. Multiple hosting service instances can share the same S3 backend.
175175+176176+```bash
177177+# Both services
178178+S3_BUCKET=wisp-sites
179179+S3_ENDPOINT=https://account.r2.cloudflarestorage.com
180180+REDIS_URL=redis://localhost:6379
181181+182182+# Hosting service
183183+HOT_CACHE_SIZE=104857600
184184+WARM_CACHE_SIZE=10737418240
185185+```
186186+187187+### Scaled (Multiple Hosting Instances)
188188+189189+Run multiple hosting service instances behind a load balancer. Each has its own hot and warm tiers, but they share the S3 cold tier and receive the same Redis invalidation events.
190190+191191+```
192192+ Load Balancer
193193+ / | \
194194+ Hosting-1 Hosting-2 Hosting-3
195195+ (hot+warm) (hot+warm) (hot+warm)
196196+ \ | /
197197+ S3 (cold tier)
198198+ |
199199+ Firehose Service
200200+```
201201+202202+## Observability
203203+204204+Both services expose internal observability endpoints:
205205+206206+- `/__internal__/observability/logs` — Recent log entries
207207+- `/__internal__/observability/errors` — Error log entries
208208+- `/__internal__/observability/metrics` — Prometheus-format metrics
209209+- `/__internal__/observability/cache` — Cache tier statistics (hosting service only)
210210+211211+See [Monitoring & Metrics](/monitoring) for Grafana integration details.
+110-38
docs/src/content/docs/deployment.md
···33description: Deploy your own Wisp.place instance
44---
5566-This guide covers deploying your own Wisp.place instance. Wisp.place consists of two services: the main backend (handles OAuth, uploads, domains) and the hosting service (serves cached sites).
66+This guide covers deploying your own Wisp.place instance. Wisp.place consists of three services: the main backend (handles OAuth, uploads, domains), the firehose service (watches the AT Protocol firehose and populates the cache), and the hosting service (serves cached sites). See the [Architecture Guide](/architecture) for a detailed breakdown of how these services work together.
7788## Prerequisites
991010- **PostgreSQL** database (14 or newer)
1111-- **Bun** runtime for the main backend
1111+- **Bun** runtime for the main backend and firehose service
1212- **Node.js** (18+) for the hosting service
1313- **Caddy** (optional, for custom domain TLS)
1414- **Domain name** for your instance
1515+- **S3-compatible storage** (optional, recommended for production — Cloudflare R2, MinIO, etc.)
1616+- **Redis** (optional, for real-time cache invalidation between services)
15171618## Architecture Overview
17191820```
1919-┌─────────────────────────────────────────┐ ┌─────────────────────────────────────────┐
2020-│ Main Backend (port 8000) │ │ Hosting Service (port 3001) │
2121-│ - OAuth authentication │ │ - Firehose listener │
2222-│ - Site upload/management │ │ - Site caching │
2323-│ - Domain registration │ │ - Content serving │
2424-│ - Admin panel │ │ - Redirect handling │
2525-└─────────────────────────────────────────┘ └─────────────────────────────────────────┘
2626- │ │
2727- └─────────────────┬───────────────────────────┘
2828- ▼
2929- ┌─────────────────────────────────────────┐
3030- │ PostgreSQL Database │
3131- │ - User sessions │
3232- │ - Domain mappings │
3333- │ - Site metadata │
3434- └─────────────────────────────────────────┘
2121+┌──────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
2222+│ Main Backend (:8000) │ │ Firehose Service │ │ Hosting Service (:3001) │
2323+│ - OAuth authentication │ │ - Watches AT firehose │ │ - Tiered cache (mem/ │
2424+│ - Site upload/manage │ │ - Downloads blobs │ │ disk/S3) │
2525+│ - Domain registration │ │ - Writes to S3/disk │ │ - Content serving │
2626+│ - Admin panel │ │ - Publishes invalidation │ │ - Redirect handling │
2727+└──────────────────────────┘ └──────────────────────────┘ └──────────────────────────┘
2828+ │ │ │ │
2929+ │ │ S3/Disk │ Redis pub/sub │
3030+ └────────┬───────────────┘ └─────────────────────┘
3131+ ▼
3232+┌─────────────────────────────────────────┐
3333+│ PostgreSQL Database │
3434+│ - User sessions │
3535+│ - Domain mappings │
3636+│ - Site metadata │
3737+└─────────────────────────────────────────┘
3538```
36393740## Database Setup
···106109107110Admin panel is available at `https://yourdomain.com/admin`
108111112112+## Firehose Service Setup
113113+114114+The firehose service watches the AT Protocol firehose for site changes and pre-populates the cache. It is **write-only** — it never serves requests to users.
115115+116116+### Environment Variables
117117+118118+```bash
119119+# Required
120120+DATABASE_URL="postgres://user:password@localhost:5432/wisp"
121121+122122+# S3 storage (recommended for production)
123123+S3_BUCKET="wisp-sites"
124124+S3_REGION="auto"
125125+S3_ENDPOINT="https://your-account.r2.cloudflarestorage.com"
126126+S3_ACCESS_KEY_ID="..."
127127+S3_SECRET_ACCESS_KEY="..."
128128+S3_METADATA_BUCKET="wisp-metadata" # Optional, recommended
129129+130130+# Redis (for notifying hosting service of changes)
131131+REDIS_URL="redis://localhost:6379"
132132+133133+# Firehose
134134+FIREHOSE_URL="wss://jetstream2.us-east.bsky.network/subscribe"
135135+FIREHOSE_CONCURRENCY=5 # Max parallel event processing
136136+137137+# Optional
138138+CACHE_DIR="./cache/sites" # Fallback if S3 not configured
139139+```
140140+141141+### Installation
142142+143143+```bash
144144+cd firehose-service
145145+146146+# Install dependencies
147147+bun install
148148+149149+# Production mode
150150+bun run start
151151+152152+# With backfill (one-time bulk sync of all existing sites)
153153+bun run start -- --backfill
154154+```
155155+156156+The firehose service will:
157157+1. Connect to the AT Protocol firehose (Jetstream)
158158+2. Filter for `place.wisp.fs` and `place.wisp.settings` events
159159+3. Download blobs, decompress, and rewrite HTML paths
160160+4. Write files to S3 (or disk)
161161+5. Publish cache invalidation events to Redis
162162+109163## Hosting Service Setup
110164111111-The hosting service is a separate microservice that serves cached sites.
165165+The hosting service is a **read-only** CDN that serves cached sites through a three-tier storage system (memory, disk, S3).
112166113167### Environment Variables
114168115169```bash
116170# Required
117171DATABASE_URL="postgres://user:password@localhost:5432/wisp"
118118-BASE_HOST="wisp.place" # Same as main backend
172172+BASE_HOST="wisp.place" # Same as main backend
173173+174174+# Tiered storage
175175+HOT_CACHE_SIZE=104857600 # Hot tier: 100 MB (memory, LRU)
176176+HOT_CACHE_COUNT=500 # Max items in hot tier
177177+178178+WARM_CACHE_SIZE=10737418240 # Warm tier: 10 GB (disk, LRU)
179179+WARM_EVICTION_POLICY="lru" # lru, fifo, or size
180180+CACHE_DIR="./cache/sites" # Warm tier directory
181181+182182+# S3 cold tier (same bucket as firehose service, read-only)
183183+S3_BUCKET="wisp-sites"
184184+S3_REGION="auto"
185185+S3_ENDPOINT="https://your-account.r2.cloudflarestorage.com"
186186+S3_ACCESS_KEY_ID="..."
187187+S3_SECRET_ACCESS_KEY="..."
188188+S3_METADATA_BUCKET="wisp-metadata"
189189+190190+# Redis (receive cache invalidation from firehose service)
191191+REDIS_URL="redis://localhost:6379"
119192120193# Optional
121121-PORT="3001" # Default: 3001
122122-CACHE_DIR="./cache/sites" # Site cache directory
123123-CACHE_ONLY_MODE="false" # Set true to disable DB writes
194194+PORT="3001" # Default: 3001
124195```
125196126197### Installation
···136207137208# Production mode
138209npm run start
139139-140140-# With backfill (downloads all sites from DB on startup)
141141-npm run start -- --backfill
142210```
143211144212The hosting service will:
145145-1. Connect to PostgreSQL
146146-2. Start firehose listener (watches for new sites)
147147-3. Create cache directory
148148-4. Serve sites on port 3001
213213+1. Initialize tiered storage (hot → warm → cold)
214214+2. Subscribe to Redis for cache invalidation events
215215+3. Serve sites on port 3001
149216150150-### Cache Management
217217+### Cache Behavior
218218+219219+Files are cached across three tiers with automatic promotion:
220220+221221+- **Hot (memory):** Fastest, limited by `HOT_CACHE_SIZE`. Evicted on restart.
222222+- **Warm (disk):** Fast local reads at `CACHE_DIR`. Survives restarts.
223223+- **Cold (S3):** Shared source of truth, populated by firehose service.
151224152152-Sites are cached to disk at `./cache/sites/{did}/{sitename}/`. The cache is automatically populated:
153153-- **On first request**: Downloads from PDS and caches
154154-- **Via firehose**: Updates when sites are deployed
155155-- **Backfill mode**: Downloads all sites from database on startup
225225+On a cache miss at all tiers, the hosting service fetches directly from the user's PDS and promotes the file into the appropriate tiers.
226226+227227+**Without S3:** Disk acts as both warm and cold tier. The hosting service still works — it just relies on on-demand fetching instead of pre-populated S3 cache.
156228157229## Reverse Proxy Setup
158230···318390319391## Scaling Considerations
320392321321-- **Multiple hosting instances**: Run multiple hosting services behind a load balancer
393393+- **Multiple hosting instances**: Run multiple hosting services behind a load balancer — each has its own hot/warm tiers but shares the S3 cold tier and Redis invalidation
322394- **Separate databases**: Split read/write with replicas
323395- **CDN**: Put Cloudflare or Bunny in front for global caching
324324-- **Cache storage**: Use NFS/S3 for shared cache across instances
325325-- **Redis**: Add Redis for session storage at scale
396396+- **S3 cold tier**: Shared storage across all hosting instances (Cloudflare R2, MinIO, AWS S3)
397397+- **Redis**: Required for real-time cache invalidation between firehose and hosting services at scale
326398327399## Security Notes
328400
+29-12
docs/src/content/docs/index.mdx
···57575858The deployment process starts when you upload your files. Each file is compressed with gzip, base64-encoded, and uploaded as a blob to your PDS. A `place.wisp.fs` record then stores the complete site structure with references to these blobs, creating a verifiable manifest of your site.
59596060-Hosting services continuously watch the AT Protocol firehose for new and updated sites. When your site is first accessed or updated, the hosting service downloads the manifest and blobs, caching them locally for optimized delivery. Custom domains work through DNS verification, allowing your site to be served from your own domain while maintaining the cryptographic guarantees of the AT Protocol.
6060+The **firehose service** continuously watches the AT Protocol firehose for new and updated sites. When a site is created or updated, it downloads the manifest and blobs from the PDS, writes them to S3 (or disk), and publishes a cache invalidation event via Redis. The **hosting service** is a read-only CDN that serves files from a three-tier cache (memory, disk, S3). When a file isn't in cache, the hosting service fetches it on-demand from the PDS and promotes it through the tiers.
6161+6262+Custom domains work through DNS verification, allowing your site to be served from your own domain while maintaining the cryptographic guarantees of the AT Protocol.
61636264## Architecture Overview
6365···6769 │ (Rust Binary)│ │ Website │
6870 │ │ │ (React UI) │
6971 └──────────────┘ └──────────────┘
7070- │ │
7172 │ │
7273 ▼ ▼
7374┌─────────────────────────────────────────────────────────┐
···8384│ │
8485│ ┌──────────────────────────────────────────────┐ │
8586│ │ Blobs (gzipped + base64 encoded) │ │
8686-│ │ - index.html │ │
8787-│ │ - styles.css │ │
8888-│ │ - assets/* │ │
8787+│ │ - index.html, styles.css, assets/* │ │
8988│ └──────────────────────────────────────────────┘ │
9089└─────────────────────────────────────────────────────────┘
9190 │
···9796 │
9897 ▼
9998┌─────────────────────────────────────────────────────────┐
100100-│ Wisp Hosting Service │
9999+│ Firehose Service (Write Path) │
100100+│ │
101101+│ - Watches firehose for place.wisp.fs changes │
102102+│ - Downloads blobs from PDS │
103103+│ - Writes cached files to S3 / disk │
104104+│ - Publishes cache invalidation via Redis │
105105+└─────────────────────────────────────────────────────────┘
106106+ │ │
107107+ │ (S3 / Disk) │ (Redis pub/sub)
108108+ ▼ ▼
109109+┌─────────────────────────────────────────────────────────┐
110110+│ Hosting Service (Read Path) │
101111│ │
102112│ ┌──────────────────────────────────────────────┐ │
103103-│ │ Cache (Disk + In-Memory) │ │
104104-│ │ - Downloads sites on first access │ │
105105-│ │ - Auto-updates on firehose events │ │
106106-│ │ - LRU eviction for memory limits │ │
113113+│ │ Tiered Storage │ │
114114+│ │ ┌──────┐ ┌──────┐ ┌──────────────┐ │ │
115115+│ │ │ Hot │ → │ Warm │ → │ Cold │ │ │
116116+│ │ │(Mem) │ │(Disk)│ │(S3/Disk) │ │ │
117117+│ │ └──────┘ └──────┘ └──────────────┘ │ │
118118+│ │ On miss: fetch from PDS and promote up │ │
107119│ └──────────────────────────────────────────────┘ │
108120│ │
109121│ ┌──────────────────────────────────────────────┐ │
···120132 └─────────────┘
121133```
122134135135+For a detailed breakdown of the services and storage system, see the [Architecture Guide](/architecture).
136136+123137## Tech Stack
124138125139- **Backend**: Bun + Elysia + PostgreSQL
126140- **Frontend**: React 19 + Tailwind 4 + Radix UI
127127-- **Hosting**: Node.js + Hono
141141+- **Hosting Service**: Node.js + Hono
142142+- **Firehose Service**: Bun
128143- **CLI**: Rust + Jacquard (AT Protocol library)
129144- **Protocol**: AT Protocol OAuth + custom lexicons
145145+- **Storage**: S3-compatible (Cloudflare R2, MinIO, etc.) + Redis for cache invalidation
130146131147## Limits
132148···139155## Getting Started
140156141157- [CLI Documentation](/cli) - Deploy sites from the command line
142142-- [Deployment Guide](/deployment) - Configure domains, redirects, and hosting
158158+- [Architecture Guide](/architecture) - How hosting, firehose, and tiered storage work
159159+- [Self-Hosting Guide](/deployment) - Deploy your own instance
143160- [Lexicons](/lexicons) - AT Protocol record schemas and data structures
144161145162## Links