···8181 )
8282`
83838484-// Sites table - cache of place.wisp.fs records from PDS
8484+// Legacy sites table. Main-app now uses site_cache as the authoritative runtime projection.
8585await db`
8686 CREATE TABLE IF NOT EXISTS sites (
8787 did TEXT NOT NULL,
···339339}
340340341341export const getSitesByDid = async (did: string) => {
342342- const rows = await db`SELECT * FROM sites WHERE did = ${did} ORDER BY created_at DESC`
342342+ const rows = await db`
343343+ SELECT
344344+ did,
345345+ rkey,
346346+ rkey AS display_name,
347347+ cached_at AS created_at,
348348+ updated_at
349349+ FROM site_cache
350350+ WHERE did = ${did}
351351+ ORDER BY cached_at DESC
352352+ `
343353 return rows
344344-}
345345-346346-export const upsertSite = async (did: string, rkey: string, displayName?: string) => {
347347- try {
348348- // Only set display_name if provided (not undefined/null/empty)
349349- const cleanDisplayName = displayName?.trim() ? displayName.trim() : null
350350-351351- await db`
352352- INSERT INTO sites (did, rkey, display_name, created_at, updated_at)
353353- VALUES (${did}, ${rkey}, ${cleanDisplayName}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW()))
354354- ON CONFLICT (did, rkey)
355355- DO UPDATE SET
356356- display_name = CASE
357357- WHEN EXCLUDED.display_name IS NOT NULL THEN EXCLUDED.display_name
358358- ELSE sites.display_name
359359- END,
360360- updated_at = EXTRACT(EPOCH FROM NOW())
361361- `
362362- return { success: true }
363363- } catch (err) {
364364- console.error('Failed to upsert site', err)
365365- return { success: false, error: err }
366366- }
367367-}
368368-369369-export const deleteSite = async (did: string, rkey: string) => {
370370- try {
371371- await db`DELETE FROM sites WHERE did = ${did} AND rkey = ${rkey}`
372372- return { success: true }
373373- } catch (err) {
374374- console.error('Failed to delete site', err)
375375- return { success: false, error: err }
376376- }
377377-}
378378-379379-export const upsertSiteCache = async (
380380- did: string,
381381- rkey: string,
382382- recordCid: string,
383383- fileCids: Record<string, string> = {},
384384-) => {
385385- try {
386386- await db`
387387- INSERT INTO site_cache (did, rkey, record_cid, file_cids, cached_at, updated_at)
388388- VALUES (${did}, ${rkey}, ${recordCid}, ${JSON.stringify(fileCids)}, EXTRACT(EPOCH FROM NOW()), EXTRACT(EPOCH FROM NOW()))
389389- ON CONFLICT (did, rkey)
390390- DO UPDATE SET
391391- record_cid = EXCLUDED.record_cid,
392392- file_cids = EXCLUDED.file_cids,
393393- updated_at = EXTRACT(EPOCH FROM NOW())
394394- `
395395- return { success: true }
396396- } catch (err) {
397397- console.error('Failed to upsert site cache', err)
398398- return { success: false, error: err }
399399- }
400354}
401355402356// Get all domains (wisp + custom) mapped to a specific site
+22-6
apps/main-app/src/lib/migrations.ts
···9898 { silent: true },
9999 )
100100101101- // Ensure existing domain mappings only point to owned sites before adding FK constraints.
101101+ // Ensure existing domain mappings only point to owned cached sites before adding FK constraints.
102102 await runMigration('normalize invalid domains.rkey mappings', async () => {
103103 await db`
104104 UPDATE domains d
···106106 WHERE rkey IS NOT NULL
107107 AND NOT EXISTS (
108108 SELECT 1
109109- FROM sites s
109109+ FROM site_cache s
110110 WHERE s.did = d.did
111111 AND s.rkey = d.rkey
112112 )
···120120 WHERE rkey IS NOT NULL
121121 AND NOT EXISTS (
122122 SELECT 1
123123- FROM sites s
123123+ FROM site_cache s
124124 WHERE s.did = d.did
125125 AND s.rkey = d.rkey
126126 )
···135135 { silent: true },
136136 )
137137138138- // Enforce mapped site rkeys belong to same DID as mapped domain.
138138+ // Mapped site rkeys must refer to an existing cached site owned by the same DID.
139139+ await runMigration(
140140+ 'drop legacy fk_domains_site_owner',
141141+ async () => {
142142+ await db`ALTER TABLE domains DROP CONSTRAINT IF EXISTS fk_domains_site_owner`
143143+ },
144144+ { silent: true },
145145+ )
146146+147147+ await runMigration(
148148+ 'drop legacy fk_custom_domains_site_owner',
149149+ async () => {
150150+ await db`ALTER TABLE custom_domains DROP CONSTRAINT IF EXISTS fk_custom_domains_site_owner`
151151+ },
152152+ { silent: true },
153153+ )
154154+139155 await runMigration(
140156 'add fk_domains_site_owner',
141157 async () => {
···143159 ALTER TABLE domains
144160 ADD CONSTRAINT fk_domains_site_owner
145161 FOREIGN KEY (did, rkey)
146146- REFERENCES sites(did, rkey)
162162+ REFERENCES site_cache(did, rkey)
147163 ON UPDATE CASCADE
148164 ON DELETE SET NULL
149165 `
···158174 ALTER TABLE custom_domains
159175 ADD CONSTRAINT fk_custom_domains_site_owner
160176 FOREIGN KEY (did, rkey)
161161- REFERENCES sites(did, rkey)
177177+ REFERENCES site_cache(did, rkey)
162178 ON UPDATE CASCADE
163179 ON DELETE SET NULL
164180 `
-84
apps/main-app/src/lib/sync-sites.ts
···11-import { Agent } from '@atproto/api'
22-import type { OAuthSession } from '@atproto/oauth-client-node'
33-import { upsertSite } from './db'
44-55-/**
66- * Sync sites from user's PDS into the database cache
77- * - Fetches all place.wisp.fs records from AT Protocol repo
88- * - Validates record structure
99- * - Backfills into sites table
1010- */
1111-export async function syncSitesFromPDS(
1212- did: string,
1313- session: OAuthSession,
1414-): Promise<{ synced: number; errors: string[] }> {
1515- console.log(`[Sync] Starting site sync for ${did}`)
1616-1717- const agent = new Agent((url, init) => session.fetchHandler(url, init))
1818- const errors: string[] = []
1919- let synced = 0
2020-2121- try {
2222- // List all records in the place.wisp.fs collection
2323- console.log('[Sync] Fetching place.wisp.fs records from PDS')
2424- const records = await agent.com.atproto.repo.listRecords({
2525- repo: did,
2626- collection: 'place.wisp.fs',
2727- limit: 100, // Adjust if users might have more sites
2828- })
2929-3030- console.log(`[Sync] Found ${records.data.records.length} records`)
3131-3232- // Process each record
3333- for (const record of records.data.records) {
3434- try {
3535- const { uri, value } = record
3636-3737- // Extract rkey from URI (at://did/collection/rkey)
3838- const rkey = uri.split('/').pop()
3939- if (!rkey) {
4040- errors.push(`Invalid URI format: ${uri}`)
4141- continue
4242- }
4343-4444- // Validate record structure
4545- if (!value || typeof value !== 'object') {
4646- errors.push(`Invalid record value for ${rkey}`)
4747- continue
4848- }
4949-5050- const siteValue = value as any
5151-5252- // Check for required fields
5353- if (siteValue.$type !== 'place.wisp.fs') {
5454- errors.push(`Invalid $type for ${rkey}: ${siteValue.$type}`)
5555- continue
5656- }
5757-5858- if (!siteValue.site || typeof siteValue.site !== 'string') {
5959- errors.push(`Missing or invalid site name for ${rkey}`)
6060- continue
6161- }
6262-6363- // Upsert into database
6464- const displayName = siteValue.site
6565- await upsertSite(did, rkey, displayName)
6666-6767- console.log(`[Sync] ✓ Synced site: ${displayName} (${rkey})`)
6868- synced++
6969- } catch (err) {
7070- const errorMsg = `Error processing record: ${err instanceof Error ? err.message : 'Unknown error'}`
7171- console.error(`[Sync] ${errorMsg}`)
7272- errors.push(errorMsg)
7373- }
7474- }
7575-7676- console.log(`[Sync] Complete: ${synced} synced, ${errors.length} errors`)
7777- return { synced, errors }
7878- } catch (err) {
7979- const errorMsg = `Failed to fetch records from PDS: ${err instanceof Error ? err.message : 'Unknown error'}`
8080- console.error(`[Sync] ${errorMsg}`)
8181- errors.push(errorMsg)
8282- return { synced, errors }
8383- }
8484-}
+9-9
apps/main-app/src/routes/admin.ts
···139139140140 try {
141141 // Get total counts
142142- const allSitesResult = await db`SELECT COUNT(*) as count FROM sites`
142142+ const allSitesResult = await db`SELECT COUNT(*) as count FROM site_cache`
143143 const wispSubdomainsResult = await db`SELECT COUNT(*) as count FROM domains WHERE domain LIKE '%.wisp.place'`
144144 const customDomainsResult = await db`SELECT COUNT(*) as count FROM custom_domains WHERE verified = true`
145145 const siteCacheResult = await db`SELECT COUNT(*) as count FROM site_cache`
···150150 SELECT
151151 s.did,
152152 s.rkey,
153153- s.display_name,
154154- s.created_at,
153153+ s.rkey as display_name,
154154+ s.cached_at as created_at,
155155 d.domain as subdomain,
156156 cd.domain as custom_domain
157157- FROM sites s
157157+ FROM site_cache s
158158 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place'
159159 LEFT JOIN custom_domains cd ON s.did = cd.did AND s.rkey = cd.rkey AND cd.verified = true
160160- ORDER BY s.created_at DESC
160160+ ORDER BY s.cached_at DESC
161161 LIMIT 10
162162 `
163163···217217 SELECT
218218 s.did,
219219 s.rkey,
220220- s.display_name,
221221- s.created_at,
220220+ s.rkey as display_name,
221221+ s.cached_at as created_at,
222222 d.domain as subdomain,
223223 cd.domain as custom_domain
224224- FROM sites s
224224+ FROM site_cache s
225225 LEFT JOIN domains d ON s.did = d.did AND s.rkey = d.rkey AND d.domain LIKE '%.wisp.place'
226226 LEFT JOIN custom_domains cd ON s.did = cd.did AND s.rkey = cd.rkey AND cd.verified = true
227227- ORDER BY s.created_at DESC
227227+ ORDER BY s.cached_at DESC
228228 LIMIT ${limit} OFFSET ${offset}
229229 `
230230
+1-15
apps/main-app/src/routes/auth.ts
···22import { createLogger } from '@wispplace/observability'
33import { Elysia, t } from 'elysia'
44import { getDomainByDid, getSitesByDid } from '../lib/db'
55-import { syncSitesFromPDS } from '../lib/sync-sites'
65import { authenticateRequest } from '../lib/wisp-auth'
7687const logger = createLogger('main-app')
···9897 maxAge: 30 * 24 * 60 * 60, // 30 days
9998 })
10099101101- // Sync sites from PDS to database cache
102102- logger.debug('[Auth] Syncing sites from PDS for', session.did as any)
103103- try {
104104- const syncResult = await syncSitesFromPDS(session.did, session)
105105- logger.debug(`[Auth] Sync complete: ${syncResult.synced} sites synced`)
106106- if (syncResult.errors.length > 0) {
107107- logger.debug('[Auth] Sync errors:', syncResult.errors)
108108- }
109109- } catch (err) {
110110- logger.error('[Auth] Failed to sync sites', err)
111111- // Don't fail auth if sync fails, just log it
112112- }
113113-114114- // Check if user has any sites or domain
100100+ // Check if user has any cached sites or a claimed domain
115101 const sites = await getSitesByDid(session.did)
116102 const domain = await getDomainByDid(session.did)
117103
-7
apps/main-app/src/routes/site.ts
···33import { extractSubfsUris } from '@wispplace/atproto-utils'
44import { createLogger } from '@wispplace/observability'
55import { Elysia } from 'elysia'
66-import { deleteSite } from '../lib/db'
76import { requireAuth } from '../lib/wisp-auth'
8798const logger = createLogger('main-app')
···108107 )
109108110109 logger.info(`[Site] Deleted ${subfsUris.length} subfs records for ${rkey}`)
111111- }
112112-113113- // Delete from database
114114- const result = await deleteSite(auth.did, rkey)
115115- if (!result.success) {
116116- throw new Error('Failed to delete site from database')
117110 }
118111119112 logger.info(`[Site] Successfully deleted site ${rkey} for ${auth.did}`)