···
1
-
# Wisp.place - Codebase Overview
1
+
The project is wisp.place. It is a static site hoster built on top of the AT Protocol. The overall basis of the project is that users upload site assets to their PDS as blobs, and creates a manifest record listing every blob as well as site name. The hosting service then catches events relating to the site (create, read, upload, delete) and handles them appropriately.
3
-
**Project URL**: https://wisp.place
3
+
The lexicons look like this:
7
+
$type: 'place.wisp.fs'
5
-
A decentralized static site hosting service built on the AT Protocol (Bluesky). Users can host static websites directly in their AT Protocol accounts, keeping full control and ownership while benefiting from fast CDN distribution.
15
+
$type?: 'place.wisp.fs#file'
23
+
interface Directory {
24
+
$type?: 'place.wisp.fs#directory'
9
-
## ๐๏ธ Architecture Overview
30
+
$type?: 'place.wisp.fs#entry'
32
+
node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
11
-
### Multi-Part System
12
-
1. **Main Backend** (`/src`) - OAuth, site management, custom domains
13
-
2. **Hosting Service** (`/hosting-service`) - Microservice that serves cached sites
14
-
3. **CLI Tool** (`/cli`) - Rust CLI for direct site uploads to PDS
15
-
4. **Frontend** (`/public`) - React UI for onboarding, editor, admin
18
-
- **Backend**: Elysia (Bun) + TypeScript + PostgreSQL
19
-
- **Frontend**: React 19 + Tailwind CSS 4 + Radix UI
20
-
- **CLI**: Rust with Jacquard (AT Protocol library)
21
-
- **Database**: PostgreSQL for session/domain/site caching
22
-
- **AT Protocol**: OAuth 2.0 + custom lexicons for storage
36
+
$type?: 'place.wisp.fs#subfs'
38
+
subject: string // AT-URI pointing to a place.wisp.subfs record
44
+
$type: 'place.wisp.subfs'
26
-
## ๐ Directory Structure
51
+
$type?: 'place.wisp.subfs#file'
28
-
### `/src` - Main Backend Server
29
-
**Purpose**: Core server handling OAuth, site management, custom domains, admin features
59
+
interface Directory {
60
+
$type?: 'place.wisp.subfs#directory'
32
-
- `/api/auth/*` - OAuth signin/callback/logout/status
33
-
- `/api/domain/*` - Custom domain management (BYOD)
34
-
- `/wisp/*` - Site upload and management
35
-
- `/api/user/*` - User info and site listing
36
-
- `/api/admin/*` - Admin console (logs, metrics, DNS verification)
66
+
$type?: 'place.wisp.subfs#entry'
68
+
node: $Typed<File> | $Typed<Directory> | $Typed<Subfs> | { $type: string }
39
-
- `index.ts` - Express-like Elysia app setup with middleware (CORS, CSP, security headers)
40
-
- `lib/oauth-client.ts` - OAuth client setup with session/state persistence
41
-
- `lib/db.ts` - PostgreSQL schema and queries for all tables
42
-
- `lib/wisp-auth.ts` - Cookie-based authentication middleware
43
-
- `lib/wisp-utils.ts` - File compression (gzip), manifest creation, blob handling
44
-
- `lib/sync-sites.ts` - Syncs user's place.wisp.fs records from PDS to database cache
45
-
- `lib/dns-verify.ts` - DNS verification for custom domains (TXT + CNAME)
46
-
- `lib/dns-verification-worker.ts` - Background worker that checks domain verification every 10 minutes
47
-
- `lib/admin-auth.ts` - Simple username/password admin authentication
48
-
- `lib/observability.ts` - Logging, error tracking, metrics collection
49
-
- `routes/auth.ts` - OAuth flow handlers
50
-
- `routes/wisp.ts` - File upload and site creation (/wisp/upload-files)
51
-
- `routes/domain.ts` - Domain claiming/verification API
52
-
- `routes/user.ts` - User status/info/sites listing
53
-
- `routes/site.ts` - Site metadata and file retrieval
54
-
- `routes/admin.ts` - Admin dashboard API (logs, system health, manual DNS trigger)
72
+
$type?: 'place.wisp.subfs#subfs'
74
+
subject: string // AT-URI pointing to another place.wisp.subfs record
56
-
### `/lexicons` & `src/lexicons/`
57
-
**Purpose**: AT Protocol Lexicon definitions for custom data types
77
+
//place.wisp.settings
79
+
$type: 'place.wisp.settings'
80
+
directoryListing: boolean
83
+
indexFiles?: string[]
85
+
headers?: CustomHeader[]
59
-
**Key File**: `fs.json` - Defines `place.wisp.fs` record format
60
-
- **structure**: Virtual filesystem manifest with tree structure
61
-
- **site**: string identifier
62
-
- **root**: directory object containing entries
63
-
- **file**: blob reference + metadata (encoding, mimeType, base64 flag)
64
-
- **directory**: array of entries (recursive)
65
-
- **entry**: name + node (file or directory)
67
-
**Important**: Files are gzip-compressed and base64-encoded before upload to bypass PDS content sniffing
69
-
### `/hosting-service`
70
-
**Purpose**: Lightweight microservice that serves cached sites from disk
73
-
- Routes by domain lookup in PostgreSQL
74
-
- Caches site content locally on first access or firehose event
75
-
- Listens to AT Protocol firehose for new site records
76
-
- Automatically downloads and caches files from PDS
77
-
- SSRF-protected fetch (timeout, size limits, private IP blocking)
80
-
1. Custom domains (`/*`) โ lookup custom_domains table
81
-
2. Wisp subdomains (`/*.wisp.place/*`) โ lookup domains table
82
-
3. DNS hash routing (`/hash.dns.wisp.place/*`) โ lookup custom_domains by hash
83
-
4. Direct serving (`/s.wisp.place/:identifier/:site/*`) โ fetch from PDS if not cached
85
-
**HTML Path Rewriting**: Absolute paths in HTML (`/style.css`) automatically rewritten to relative (`/:identifier/:site/style.css`)
88
-
**Purpose**: Rust CLI tool for direct site uploads using app password or OAuth
91
-
1. Authenticate with handle + app password or OAuth
92
-
2. Walk directory tree, compress files
93
-
3. Upload blobs to PDS via agent
94
-
4. Create place.wisp.fs record with manifest
95
-
5. Store site in database cache
98
-
- `--password` flag for app password auth
99
-
- OAuth loopback server for browser-based auth
100
-
- Supports both (password preferred if provided)
104
-
## ๐ Key Concepts
106
-
### Custom Domains (BYOD - Bring Your Own Domain)
108
-
1. User claims custom domain via API
109
-
2. System generates hash (SHA256(domain + secret))
110
-
3. User adds DNS records:
111
-
- TXT at `_wisp.example.com` = their DID
112
-
- CNAME at `example.com` = `{hash}.dns.wisp.place`
113
-
4. Background worker checks verification every 10 minutes
114
-
5. Once verified, custom domain routes to their hosted sites
116
-
**Tables**: `custom_domains` (id, domain, did, rkey, verified, last_verified_at)
118
-
### Wisp Subdomains
120
-
1. Handle claimed on first signup (e.g., alice โ alice.wisp.place)
121
-
2. Stored in `domains` table mapping domain โ DID
122
-
3. Served by hosting service
126
-
- **Authoritative**: PDS (AT Protocol repo) as `place.wisp.fs` record
127
-
- **Cache**: PostgreSQL `sites` table (rkey, did, site_name, created_at)
128
-
- **File Cache**: Hosting service caches downloaded files on disk
131
-
- MAX_SITE_SIZE: 300MB total
132
-
- MAX_FILE_SIZE: 100MB per file
133
-
- MAX_FILE_COUNT: 2000 files
135
-
### File Compression Strategy
136
-
**Why**: Bypass PDS content sniffing issues (was treating HTML as images)
139
-
1. All files gzip-compressed (level 9)
140
-
2. Compressed content base64-encoded
141
-
3. Uploaded as `application/octet-stream` MIME type
142
-
4. Blob metadata stores original MIME type + encoding flag
143
-
5. Hosting service decompresses on serve
149
-
### User Registration โ Site Upload
151
-
1. OAuth signin โ state/session stored in DB
152
-
2. Cookie set with DID
153
-
3. Sync sites from PDS to cache DB
154
-
4. If no sites/domain โ redirect to onboarding
155
-
5. User creates site โ POST /wisp/upload-files
156
-
6. Files compressed, uploaded as blobs
157
-
7. place.wisp.fs record created
158
-
8. Site cached in DB
159
-
9. Hosting service notified via firehose
88
+
interface CustomHeader {
89
+
$type?: 'place.wisp.settings#customHeader'
92
+
path?: string // Optional glob pattern
162
-
### Custom Domain Setup
164
-
1. User claims domain (DB check + allocation)
165
-
2. System generates hash
166
-
3. User adds DNS records (_wisp.domain TXT + CNAME)
167
-
4. Background worker verifies every 10 min
168
-
5. Hosting service routes based on verification status
96
+
The main differences between place.wisp.fs and place.wisp.subfs:
97
+
- place.wisp.fs has a required site field
98
+
- place.wisp.fs#subfs has an optional flat field that place.wisp.subfs#subfs doesn't have
174
-
1. Request arrives at custom domain or *.wisp.place
175
-
2. Domain lookup in PostgreSQL
176
-
3. Check cache for site files
178
-
- Fetch from PDS using DID + rkey
180
-
- Save to disk cache
181
-
5. Serve files (with HTML path rewriting)
100
+
The project is a monorepo. The package handler it uses for the typescript side is Bun. For the Rust cli, it is cargo.
102
+
### Typescript Bun Workspace Layout
186
-
## ๐ ๏ธ Important Implementation Details
104
+
Bun workspaces: `packages/@wisp/*`, `apps/main-app`, `apps/hosting-service`
188
-
### OAuth Implementation
189
-
- **State & Session Storage**: PostgreSQL (with expiration)
190
-
- **Key Rotation**: Periodic rotation + expiration cleanup (hourly)
191
-
- **OAuth Flow**: Redirects to PDS, returns to /api/auth/callback
192
-
- **Session Timeout**: 30 days
193
-
- **State Timeout**: 1 hour
106
+
There are two typescript apps
107
+
**`apps/main-app`** - Main backend (Bun + Elysia)
195
-
### Security Headers
196
-
- X-Frame-Options: DENY
197
-
- X-Content-Type-Options: nosniff
198
-
- Strict-Transport-Security: max-age=31536000
199
-
- Content-Security-Policy (configured for Elysia + React)
200
-
- X-XSS-Protection: 1; mode=block
201
-
- Referrer-Policy: strict-origin-when-cross-origin
203
-
### Admin Authentication
204
-
- Simple username/password (hashed with bcrypt)
205
-
- Session-based cookie auth (24hr expiration)
206
-
- Separate `admin_session` cookie
207
-
- Initial setup prompted on startup
210
-
- **Logging**: Structured logging with service tags + event types
211
-
- **Error Tracking**: Captures error context (message, stack, etc.)
212
-
- **Metrics**: Request counts, latencies, error rates
213
-
- **Log Levels**: debug, info, warn, error
214
-
- **Collection**: Centralized log collector with in-memory buffer
218
-
## ๐ Database Schema
221
-
- key (primary key)
223
-
- created_at, expires_at (timestamps)
226
-
- sub (primary key - subject/DID)
227
-
- data (JSON with OAuth session)
228
-
- updated_at, expires_at
231
-
- kid (primary key - key ID)
232
-
- jwk (JSON Web Key)
236
-
- domain (primary key - e.g., alice.wisp.place)
237
-
- did (unique - user's DID)
238
-
- rkey (optional - record key)
242
-
- id (primary key - UUID)
243
-
- domain (unique - e.g., example.com)
246
-
- verified (boolean)
247
-
- last_verified_at (timestamp)
251
-
- id, did, rkey, site_name
252
-
- created_at, updated_at
253
-
- Indexes on (did), (did, rkey), (rkey)
256
-
- username (primary key)
257
-
- password_hash (bcrypt)
262
-
## ๐ Key Workflows
265
-
1. POST /api/auth/signin with handle
266
-
2. System generates state token
267
-
3. Redirects to PDS OAuth endpoint
268
-
4. PDS redirects back to /api/auth/callback?code=X&state=Y
269
-
5. Validate state (CSRF protection)
270
-
6. Exchange code for session
271
-
7. Store session in DB, set DID cookie
272
-
8. Sync sites from PDS
273
-
9. Redirect to /editor or /onboarding
275
-
### File Upload Flow
276
-
1. POST /wisp/upload-files with siteName + files
277
-
2. Validate site name (rkey format rules)
279
-
- Check size limits
280
-
- Read as ArrayBuffer
283
-
4. Upload all blobs in parallel via agent.com.atproto.repo.uploadBlob()
284
-
5. Create manifest with all blob refs
285
-
6. putRecord() for place.wisp.fs with manifest
286
-
7. Upsert to sites table
287
-
8. Return URI + CID
289
-
### Domain Verification Flow
290
-
1. POST /api/custom-domains/claim
291
-
2. Generate hash = SHA256(domain + secret)
292
-
3. Store in custom_domains with verified=false
293
-
4. Return hash for user to configure DNS
294
-
5. Background worker periodically:
295
-
- Query custom_domains where verified=false
296
-
- Verify TXT record at _wisp.domain
297
-
- Verify CNAME points to hash.dns.wisp.place
298
-
- Update verified flag + last_verified_at
299
-
6. Hosting service routes when verified=true
303
-
## ๐จ Frontend Structure
306
-
- **index.tsx** - Landing page with sign-in form
307
-
- **editor/editor.tsx** - Site editor/management UI
308
-
- **admin/admin.tsx** - Admin dashboard
309
-
- **components/ui/** - Reusable components (Button, Card, Dialog, etc.)
310
-
- **styles/global.css** - Tailwind + custom styles
313
-
1. `/` - Landing page (sign in / get started)
314
-
2. `/editor` - Main app (requires auth)
315
-
3. `/admin` - Admin console (requires admin auth)
316
-
4. `/onboarding` - First-time user setup
109
+
- OAuth authentication and session management
110
+
- Site CRUD operations via PDS
111
+
- Custom domain management
112
+
- Admin database view in /admin
113
+
- React frontend in public/
320
-
## ๐ Notable Implementation Patterns
115
+
**`apps/hosting-service`** - CDN static file server (Node + Hono)
323
-
- Files stored as base64-encoded gzip in PDS blobs
324
-
- Metadata preserves original MIME type
325
-
- Hosting service decompresses on serve
326
-
- Workaround for PDS image pipeline issues with HTML
117
+
- Watches AT Protocol firehose for `place.wisp.fs` record changes
118
+
- Downloads and caches site files to disk
119
+
- Serves sites at `https://sites.wisp.place/{did}/{site-name}` and custom domains
120
+
- Handles redirects (`_redirects` file support) and routing logic
121
+
- Backfill mode for syncing existing sites
329
-
- Comprehensive logging with context
330
-
- Graceful degradation (e.g., site sync failure doesn't break auth)
331
-
- Structured error responses with details
123
+
### Shared Packages (`packages/@wisp/*`)
334
-
- Site sync: Batch fetch up to 100 records per request
335
-
- Blob upload: Parallel promises for all files
336
-
- DNS verification: Batched background worker (10 min intervals)
337
-
- Caching: Two-tier (DB + disk in hosting service)
125
+
- **`lexicons`** - AT Protocol lexicons (`place.wisp.fs`, `place.wisp.subfs`, `place.wisp.settings`) with
126
+
generated TypeScript types
127
+
- **`fs-utils`** - Filesystem tree building, manifest creation, subfs splitting logic
128
+
- **`atproto-utils`** - AT Protocol helpers (blob upload, record operations, CID handling)
129
+
- **`database`** - PostgreSQL schema and queries
130
+
- **`constants`** - Shared constants (limits, file patterns, default settings)
131
+
- **`observability`** - OpenTelemetry instrumentation
132
+
- **`safe-fetch`** - Wrapped fetch with timeout/retry logic
340
-
- Lexicon validation on manifest creation
341
-
- Record type checking
342
-
- Domain format validation
343
-
- Site name format validation (AT Protocol rkey rules)
344
-
- File size limits enforced before upload
348
-
## ๐ Known Quirks & Workarounds
350
-
1. **PDS Content Sniffing**: Files must be uploaded as `application/octet-stream` (even HTML) and base64-encoded to prevent PDS from misinterpreting content
352
-
2. **Max URL Query Size**: DNS verification worker queries in batch; may need pagination for users with many custom domains
354
-
3. **File Count Limits**: Max 500 entries per directory (Lexicon constraint); large sites split across multiple directories
356
-
4. **Blob Size Limits**: Individual blobs limited to 100MB by Lexicon; large files handled differently if needed
358
-
5. **HTML Path Rewriting**: Only in hosting service for `/s.wisp.place/:identifier/:site/*` routes; custom domains handled differently
136
+
**`cli/`** - Rust CLI using Jacquard (AT Protocol library)
137
+
- Direct PDS uploads without interacting with main-app
138
+
- Can also do the same firehose watching, caching, and serving hosting-service does, just without domain management
362
-
## ๐ Environment Variables
140
+
### Other Directories
364
-
- `DOMAIN` - Base domain with protocol (default: `https://wisp.place`)
365
-
- `CLIENT_NAME` - OAuth client name (default: `PDS-View`)
366
-
- `DATABASE_URL` - PostgreSQL connection (default: `postgres://postgres:postgres@localhost:5432/wisp`)
367
-
- `NODE_ENV` - production/development
368
-
- `HOSTING_PORT` - Hosting service port (default: 3001)
369
-
- `BASE_DOMAIN` - Domain for URLs (default: wisp.place)
373
-
## ๐งโ๐ป Development Notes
375
-
### Adding New Features
376
-
1. **New routes**: Add to `/src/routes/*.ts`, import in index.ts
377
-
2. **DB changes**: Add migration in db.ts
378
-
3. **New lexicons**: Update `/lexicons/*.json`, regenerate types
379
-
4. **Admin features**: Add to /api/admin endpoints
382
-
- Run with `bun test`
383
-
- CSRF tests in lib/csrf.test.ts
384
-
- Utility tests in lib/wisp-utils.test.ts
387
-
- Check logs via `/api/admin/logs` (requires admin auth)
388
-
- DNS verification manual trigger: POST /api/admin/verify-dns
389
-
- Health check: GET /api/health (includes DNS verifier status)
393
-
## ๐ Deployment Considerations
395
-
1. **Secrets**: Admin password, OAuth keys, database credentials
396
-
2. **HTTPS**: Required (HSTS header enforces it)
397
-
3. **CDN**: Custom domains require DNS configuration
399
-
- Main server: Horizontal scaling with session DB
400
-
- Hosting service: Independent scaling, disk cache per instance
401
-
5. **Backups**: PostgreSQL database critical; firehose provides recovery
405
-
## ๐ Related Technologies
407
-
- **AT Protocol**: Decentralized identity, OAuth 2.0
408
-
- **Jacquard**: Rust library for AT Protocol interactions
409
-
- **Elysia**: Bun web framework (similar to Express/Hono)
410
-
- **Lexicon**: AT Protocol's schema definition language
411
-
- **Firehose**: Real-time event stream of repo changes
412
-
- **PDS**: Personal Data Server (where users' data stored)
416
-
## ๐ฏ Project Goals
418
-
โ
Decentralized site hosting (data owned by users)
419
-
โ
Custom domain support with DNS verification
420
-
โ
Fast CDN distribution via hosting service
421
-
โ
Developer tools (CLI + API)
422
-
โ
Admin dashboard for monitoring
423
-
โ
Zero user data retention (sites in PDS, sessions in DB only)
427
-
**Last Updated**: November 2025
428
-
**Status**: Active development
142
+
- **`docs/`** - Astro documentation site
143
+
- **`binaries/`** - Compiled CLI binaries for distribution