simple list of pds servers with open registration
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Trust-based filtering, per-request timeouts, logging

- Servers with any trust signal (email/ToS/PP) always shown regardless
of outdated filter
- Each PDS fetch gets its own 5s timeout instead of sharing one
- Add error logging to listRepos for debugging user count failures
- Prioritize re-enriching servers missing user counts
- Footer: single row with middot separators, link to Tangled source

+19 -12
+6 -3
backend/database/queries.ts
··· 103 103 sql: ` 104 104 SELECT url FROM ${SERVERS_TABLE} 105 105 WHERE is_open = 1 106 - ORDER BY last_enriched ASC NULLS FIRST 106 + ORDER BY 107 + CASE WHEN user_count IS NULL THEN 0 ELSE 1 END, 108 + last_enriched ASC NULLS FIRST 107 109 LIMIT ? 108 110 `, 109 111 args: [limit], ··· 139 141 const args: (string | number)[] = []; 140 142 141 143 if (!showOutdated && latestVersion) { 142 - // Featured servers (contact email) always show regardless of version 143 - whereClause += " AND (version = ? OR contact_email IS NOT NULL)"; 144 + // Servers with trust signals always show regardless of version 145 + whereClause += 146 + " AND (version = ? OR contact_email IS NOT NULL OR terms_of_service IS NOT NULL OR privacy_policy IS NOT NULL)"; 144 147 args.push(latestVersion); 145 148 } 146 149
+3 -2
backend/routes/pages.ts
··· 267 267 : `<div class="empty">No servers found. Data may still be loading — check back after the first cron run.</div>` 268 268 } 269 269 <footer> 270 - <p>Built by <a href="https://bsky.app/profile/tijs.org">@tijs.org</a></p> 271 - <p><a href="/api/servers">JSON API</a></p> 270 + Built by <a href="https://bsky.app/profile/tijs.org">@tijs.org</a> 271 + &middot; <a href="/api/servers">JSON API</a> 272 + &middot; <a href="https://tangled.sh/@tijs.org/openpds">Source</a> 272 273 </footer> 273 274 </body> 274 275 </html>`;
+10 -7
backend/services/pds-enricher.ts
··· 59 59 60 60 async function enrichSinglePds(url: string): Promise<EnrichmentResult> { 61 61 const result = emptyResult(url); 62 - const signal = AbortSignal.timeout(PDS_REQUEST_TIMEOUT_MS); 63 62 64 63 // Resolve IP from hostname 65 64 const hostname = new URL(url).hostname; 66 65 result.ipAddress = await resolveIp(hostname); 67 66 68 67 // Fetch health, describeServer, and listRepos concurrently 68 + // Each gets its own timeout so DNS resolve doesn't eat into fetch time 69 69 const [health, describe, userCount] = await Promise.allSettled([ 70 - fetchHealth(url, signal), 71 - fetchDescribeServer(url, signal), 72 - fetchUserCount(url, signal), 70 + fetchHealth(url, AbortSignal.timeout(PDS_REQUEST_TIMEOUT_MS)), 71 + fetchDescribeServer(url, AbortSignal.timeout(PDS_REQUEST_TIMEOUT_MS)), 72 + fetchUserCount(url, AbortSignal.timeout(PDS_REQUEST_TIMEOUT_MS)), 73 73 ]); 74 74 75 75 if (health.status === "fulfilled" && health.value) { ··· 131 131 `${url}/xrpc/com.atproto.sync.listRepos?limit=1000`, 132 132 { signal }, 133 133 ); 134 - if (!resp.ok) return null; 134 + if (!resp.ok) { 135 + console.error(`listRepos ${url}: HTTP ${resp.status}`); 136 + return null; 137 + } 135 138 const data = await resp.json(); 136 139 const repos: unknown[] = data.repos ?? []; 137 - // If there's a cursor, there are more than 1000 138 140 return data.cursor ? 1000 : repos.length; 139 - } catch { 141 + } catch (err) { 142 + console.error(`listRepos ${url}: ${err}`); 140 143 return null; 141 144 } 142 145 }