···11+# Database
22+DATABASE_URL=postgres://postgres:postgres@localhost:5432/wisp
33+44+# Firehose
55+FIREHOSE_SERVICE=wss://bsky.network
66+FIREHOSE_MAX_CONCURRENCY=5
77+88+# Redis (cache invalidation + revalidation queue)
99+REDIS_URL=redis://localhost:6379
1010+1111+# S3 Storage (leave empty for local disk fallback)
1212+S3_BUCKET=
1313+S3_METADATA_BUCKET=
1414+S3_REGION=auto
1515+S3_ENDPOINT=
1616+S3_PREFIX=sites/
1717+S3_FORCE_PATH_STYLE=true
1818+1919+# AWS Credentials (required if using S3)
2020+AWS_ACCESS_KEY_ID=
2121+AWS_SECRET_ACCESS_KEY=
2222+2323+# Health check server
2424+HEALTH_PORT=3001
2525+2626+# For local disk fallback (when S3_BUCKET is empty)
2727+CACHE_DIR=./cache/sites
···44# Server
55PORT=3001
66BASE_HOST=wisp.place
77+88+# Redis (cache invalidation + revalidation queue)
99+REDIS_URL=redis://localhost:6379
1010+1111+# S3 Storage (leave empty for local disk fallback)
1212+S3_BUCKET=
1313+S3_METADATA_BUCKET=
1414+S3_REGION=auto
1515+S3_ENDPOINT=
1616+S3_PREFIX=sites/
1717+S3_FORCE_PATH_STYLE=true
1818+1919+# AWS Credentials (required if using S3)
2020+AWS_ACCESS_KEY_ID=
2121+AWS_SECRET_ACCESS_KEY=
2222+2323+# For local disk fallback (when S3_BUCKET is empty)
2424+CACHE_DIR=./cache/sites
···1818import { enqueueRevalidate } from './revalidate-queue';
1919import { recordStorageMiss } from './revalidate-metrics';
2020import { normalizeFileCids } from '@wispplace/fs-utils';
2121+import { fetchAndCacheSite } from './on-demand-cache';
2222+import type { StorageResult } from '@wispplace/tiered-storage';
2323+2424+type FileStorageResult = StorageResult<Uint8Array>;
21252226/**
2327 * Helper to retrieve a file with metadata from tiered storage
···9195 rkey: string,
9296 filePath: string,
9397 preferRewrittenHtml: boolean
9494-): Promise<{ result: Awaited<ReturnType<typeof storage.getWithMetadata>>; filePath: string } | null> {
9898+): Promise<{ result: FileStorageResult; filePath: string } | null> {
9599 const mimeTypeGuess = lookup(filePath) || 'application/octet-stream';
96100 if (preferRewrittenHtml && isHtmlContent(filePath, mimeTypeGuess)) {
97101 const rewrittenPath = `.rewritten/${filePath}`;
···107111}
108112109113function buildResponseFromStorageResult(
110110- result: Awaited<ReturnType<typeof storage.getWithMetadata>>,
114114+ result: FileStorageResult,
111115 filePath: string,
112116 settings: WispSettings | null,
113117 requestHeaders?: Record<string, string>
···149153}
150154151155/**
156156+ * Ensure a site is cached locally. If the site has no DB entry (completely unknown),
157157+ * attempt to fetch and cache it on-demand from the PDS.
158158+ */
159159+async function ensureSiteCached(did: string, rkey: string): Promise<void> {
160160+ const existing = await getSiteCache(did, rkey);
161161+ if (existing) return; // Site is known, proceed normally
162162+163163+ // Site is completely unknown — try on-demand fetch
164164+ console.log(`[FileServing] Site ${did}/${rkey} not in DB, attempting on-demand cache`);
165165+ await fetchAndCacheSite(did, rkey);
166166+}
167167+168168+/**
152169 * Helper to serve files from cache (for custom domains and subdomains)
153170 */
154171export async function serveFromCache(
···158175 fullUrl?: string,
159176 headers?: Record<string, string>
160177): Promise<Response> {
178178+ // Check if this site is completely unknown (not in DB, no files in storage)
179179+ // If so, attempt to fetch and cache it on-demand from the PDS
180180+ await ensureSiteCached(did, rkey);
181181+161182 // Load settings for this site
162183 const settings = await getCachedSettings(did, rkey);
163184 const indexFiles = getIndexFiles(settings);
···445466 fullUrl?: string,
446467 headers?: Record<string, string>
447468): Promise<Response> {
469469+ // Check if this site is completely unknown (not in DB, no files in storage)
470470+ // If so, attempt to fetch and cache it on-demand from the PDS
471471+ await ensureSiteCached(did, rkey);
472472+448473 // Load settings for this site
449474 const settings = await getCachedSettings(did, rkey);
450475 const indexFiles = getIndexFiles(settings);