Federated Hosting is a production-grade, decentralised static site hosting network.
This is not a toy. This is not a proof of concept. This is infrastructure intended to serve over 1.5 billion people — with a specific focus on the Global South, Southeast Asia, and markets currently underserved by centralised cloud providers. Every line of code written here is load-bearing.
Design and build accordingly.
Every feature ships with:
- Proper error handling (no unhandled promise rejections, no silent failures)
- Input validation at the boundary (Zod schemas on every route)
- Database transactions where atomicity matters
- Structured logging with context (
logger.info({ siteId, userId }, "message")) - Graceful degradation — never let a failed peer sync crash a local deploy
- All node-to-node communication is Ed25519-signed
- No trusting
X-Forwarded-*headers withouttrust proxyset - No raw SQL string interpolation — Drizzle ORM only
- File paths sanitised before storage (directory traversal prevention)
- Tokens hashed before storage (SHA-256 for API tokens, scrypt for passwords)
- Rate limiting on every write endpoint, not just federation ones
- Never log private keys, passwords, or tokens
The network only works if nodes can trust each other and reliably replicate content. Federation logic must be:
- Cryptographically verified (Ed25519 signatures on all inter-node messages)
- Resilient to partial failure (
Promise.allSettled, neverPromise.allfor peer ops) - Logged to
federation_eventsfor auditability - Documented in
FEDERATION.md(language-agnostic spec for third-party implementors)
- Schema lives in
lib/db/src/schema/ - Changes go through Drizzle migrations (
pnpm --filter @workspace/db run migrate) — neverdb pushin production - Every table has created/updated timestamps
- Every hot query path has an index
- Foreign keys use
onDelete: "cascade"where appropriate
- OpenAPI spec in
lib/api-spec/openapi.yamlmust stay in sync with actual routes - Breaking changes require a version bump
- All list endpoints return
{ data: [...], meta: { total, page, limit } } - All error responses return
{ message: string, code: string, status: number }
Browser / CLI
│
▼
artifacts/federated-hosting ← React + Vite frontend (port 25231 in dev)
│ fetch()
▼
artifacts/api-server ← Express 5 API (port 8080 in dev)
│
├── lib/db ← Drizzle ORM + PostgreSQL
├── lib/objectStorage ← deprecated shim → use storageProvider.ts
├── federation peers ← Other nodes (Ed25519-verified)
└── background jobs ← healthMonitor, analyticsFlush, gossipPusher
lib/
db/ ← Schema, migrations, DB connection
api-spec/ ← OpenAPI 3.1 spec + Orval codegen config
api-client-react/ ← Generated React Query hooks (from OpenAPI)
api-zod/ ← Generated Zod schemas (from OpenAPI)
auth-web/ ← useAuth() hook
object-storage-web/ ← Upload component + useUpload hook
artifacts/
api-server/ ← Express API server
federated-hosting/ ← React + Vite frontend
cli/ ← fh CLI tool (deploy, sites, tokens)
docs/
API.md ← REST API reference
ARCHITECTURE.md ← System design
FEDERATION_PROTOCOL.md ← Inter-node protocol spec
SELF_HOSTING.md ← Docker + manual deployment guide
CHANGELOG.md ← Version history
FEDERATION.md ← Language-agnostic federation spec (root)
ROADMAP.md ← Living feature tracker
CLAUDE.md ← This file
pnpm install
cp .env.example .env # fill in DATABASE_URL, object storage config
pnpm --filter @workspace/db run migrate # apply migrations
pnpm run dev # starts API (:8080) + frontend (:25231) concurrently# 1. Edit lib/db/src/schema/*.ts
# 2. Generate a migration
pnpm --filter @workspace/db run generate
# 3. Review the generated SQL in lib/db/migrations/
# 4. Apply it
pnpm --filter @workspace/db run migrate
# 5. Commit BOTH the schema change AND the migration file- Create
artifacts/api-server/src/routes/myfeature.ts - Use
asyncHandler+AppError— no raw try/catch in routes - Validate all inputs with Zod at the top of the handler
- Register the router in
artifacts/api-server/src/routes/index.ts - Add the endpoint to
lib/api-spec/openapi.yaml - Regenerate client:
pnpm --filter @workspace/api-spec run generate - Update
docs/API.md
# Always check for stale lock files first
ls .git/*.lock .git/refs/remotes/origin/*.lock 2>/dev/null && rm -f .git/*.lock .git/refs/remotes/origin/*.lock
git status
git add -A
git commit -m "feat|fix|chore|docs: description"
git push origin mainAt 1.5B+ users, the following matter from day one:
- Connection pooling — the DB pool is shared across all requests; never open ad-hoc connections
- Indexes on every foreign key and hot filter — already in schema; don't add columns without thinking about query patterns
- Pagination everywhere — no endpoint returns unbounded lists
- Analytics are buffered —
analytics_bufferabsorbs per-request writes;analyticsFlushrolls up once per minute. Never write tosite_analyticsdirectly from a request handler.
- Gossip propagates peer lists — nodes don't need manual registration; the gossip pusher runs every 5 minutes
- Sync is eventually consistent — a deploy pushes
site_syncnotifications; peers pull files asynchronously. A peer being down doesn't fail the deploy. - Signatures prevent spoofing — every inter-node message includes an Ed25519 signature. Verify before trusting.
- Files are immutable — once uploaded to object storage, a file at a given
objectPathnever changes. Deployments are versioned by creating new file records. - Presigned URLs — clients upload/download directly from object storage; the API server never proxies file bytes (except for serving via
hostRouter)
- The
X-Served-By: federated-hostingandCache-Control: public, max-age=3600headers are already set on all served files - The architecture is designed for a CDN layer to sit in front of nodes
- TypeScript strict mode — no
anyunless genuinely unavoidable; add a comment explaining why - No
console.log— use the pino logger (import logger from "../lib/logger") - Async handlers — wrap every async route in
asyncHandler(); never.catch(next)manually - Zod for all external input — request bodies, query params, URL params
- Drizzle for all DB access — no raw SQL except in migrations
- Named exports — avoid default exports in lib files; use them only in React pages/components
date-fnsfor date formatting — nottoLocaleString()or manual formatting
A full Playwright E2E suite is a high-priority item on the roadmap. Until it exists:
- Test the critical path manually before every push: sign in → register site → upload files → deploy → verify site serves
- Backend routes should be testable with
curlor the built-in API reference in the Federation Protocol page - The
scripts/src/seed.tsscript creates realistic test data
- Don't break the federation protocol —
FEDERATION.mdis a public spec; changes must be backwards-compatible or versioned - Don't remove Drizzle transactions from the deploy endpoint — partial deployments corrupt site state
- Don't add legacy dependencies without an abstraction layer — the project must remain self-hostable
- Don't return stack traces in production — the global error handler already strips them; don't bypass it
- Don't log private keys — the pino logger has
privateKeyandpasswordin its redaction list, but don't work around it - Don't use
Promise.allfor peer operations — alwaysPromise.allSettled; a dead peer must never crash a user-facing request
The No Hands Company — https://github.com/The-No-Hands-company
This codebase is MIT licensed. Contributions welcome — see CONTRIBUTING.md.