···2020 "Bash(npm:*)",
2121 "Bash(docker-compose restart:*)",
2222 "Bash(echo \"# Bluesky Branding Removal Checklist\n\n## 1. App Icons & Images\n- assets/app-icons/*.png - App icons (replace with Aurora Prism branding)\n- assets/favicon.png - Browser favicon\n- assets/icon-android-*.png - Android icons\n- assets/default-avatar.png - Default avatar image\n\n## 2. App Metadata\n- app.json - App name, slug, description\n- package.json - App name and description\n\n## 3. Text References (276 occurrences)\n- Onboarding screens (src/screens/Onboarding/)\n- Signup screens (src/screens/Signup/)\n- Settings/About screens\n- Terms of Service / Privacy Policy references\n- Help text and tooltips\n- Error messages mentioning Bluesky\n\n## 4. URLs\n- bsky.app references (feed URLs, profile URLs)\n- bsky.social references\n- Links to Bluesky support/help\n\n## 5. Service Names\n- Bluesky Moderation Service references\n- Default feed generator names\n\nTotal: 276 text references found\")",
2323- "Bash(psql \"$DATABASE_URL\" -c \"SELECT \n (SELECT COUNT(*) FROM users) as users,\n (SELECT COUNT(*) FROM posts) as posts,\n (SELECT COUNT(*) FROM likes) as likes,\n (SELECT COUNT(*) FROM reposts) as reposts,\n (SELECT COUNT(*) FROM follows) as follows,\n (SELECT COUNT(*) FROM blocks) as blocks;\")"
2323+ "Bash(psql \"$DATABASE_URL\" -c \"SELECT \n (SELECT COUNT(*) FROM users) as users,\n (SELECT COUNT(*) FROM posts) as posts,\n (SELECT COUNT(*) FROM likes) as likes,\n (SELECT COUNT(*) FROM reposts) as reposts,\n (SELECT COUNT(*) FROM follows) as follows,\n (SELECT COUNT(*) FROM blocks) as blocks;\")",
2424+ "Bash(dig +short TXT _atproto.spacelawshitpost.me)",
2525+ "Bash(python3 -m json.tool)"
2426 ],
2527 "deny": [],
2628 "ask": []
+9
.env.example
···11# AppView Configuration
22# Copy this to .env and customize as needed
3344+# Relay/Firehose Configuration
55+# Primary relay source (usually Bluesky's main relay)
66+# RELAY_URL=wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos
77+88+# Additional relay sources (comma-separated)
99+# Use this to crawl independent PDS instances that aren't in Bluesky's relay
1010+# Example: atproto.africa hosts blacksky.app and other independent instances
1111+# ADDITIONAL_RELAY_URLS=wss://atproto.africa/xrpc/com.atproto.sync.subscribeRepos
1212+413# Constellation Integration (enhanced stats from microcosm.blue)
514# Set to false to disable and avoid timeout errors
615CONSTELLATION_ENABLED=false
+84
ON_DEMAND_PDS_BACKFILL.md
···11+# On-Demand PDS Backfill
22+33+This feature allows your AppView to automatically fetch users from independent PDS instances that aren't federated to Bluesky's relay.
44+55+## How It Works
66+77+### Automatic Backfill
88+When a user tries to view a profile that doesn't exist in your AppView:
99+1. The system detects the 404
1010+2. Resolves the user's DID to find their PDS (via plc.directory)
1111+3. Fetches their profile and recent content directly from their PDS
1212+4. Indexes it into your AppView
1313+5. Future requests will return the cached data
1414+1515+### Manual Backfill (Admin Panel)
1616+You can also manually trigger backfills via the admin API:
1717+1818+```bash
1919+# Trigger backfill for a specific DID
2020+curl -X POST https://your-appview.com/api/admin/backfill/pds \
2121+ -H "Content-Type: application/json" \
2222+ -d '{"did": "did:plc:63hvnyjvqi2nzzcsjgnry5we"}'
2323+2424+# Check backfill status
2525+curl https://your-appview.com/api/admin/backfill/pds/status
2626+```
2727+2828+## Example: Backfilling spacelawshitpost.me
2929+3030+The user `spacelawshitpost.me` is on blacksky.app PDS and isn't in Bluesky's relay.
3131+3232+### Automatic Method
3333+Just try to view their profile in your AppView:
3434+```
3535+GET /xrpc/app.bsky.actor.getProfile?actor=did:plc:63hvnyjvqi2nzzcsjgnry5we
3636+```
3737+3838+First request: Returns 404 with message "Attempting to fetch from their PDS"
3939+Wait 5-10 seconds, then retry
4040+Second request: Returns full profile!
4141+4242+### Manual Method
4343+```bash
4444+curl -X POST https://appview.dollspace.gay/api/admin/backfill/pds \
4545+ -H "Content-Type: application/json" \
4646+ -d '{"did": "did:plc:63hvnyjvqi2nzzcsjgnry5we"}'
4747+```
4848+4949+## Rate Limiting
5050+5151+- **Cooldown**: 5 minutes per DID (won't re-backfill the same user more frequently)
5252+- **Record Limit**: Maximum 1000 records per collection (prevents abuse)
5353+5454+## Collections Backfilled
5555+5656+When a user is backfilled, the system fetches:
5757+- `app.bsky.actor.profile` - Profile info
5858+- `app.bsky.feed.post` - Posts
5959+- `app.bsky.feed.like` - Likes
6060+- `app.bsky.feed.repost` - Reposts
6161+- `app.bsky.graph.follow` - Follows
6262+- `app.bsky.graph.block` - Blocks
6363+- And any other collections the PDS returns
6464+6565+## Monitoring
6666+6767+Check the server logs for backfill progress:
6868+```
6969+[ON_DEMAND_BACKFILL] Starting backfill for did:plc:...
7070+[ON_DEMAND_BACKFILL] did:plc:... is on PDS: blacksky.app
7171+[ON_DEMAND_BACKFILL] Backfilling spacelawshitpost.me from blacksky.app
7272+[ON_DEMAND_BACKFILL] Collections: app.bsky.actor.profile, app.bsky.feed.post, ...
7373+[ON_DEMAND_BACKFILL] Backfilled 42 records from app.bsky.feed.post
7474+[ON_DEMAND_BACKFILL] Completed backfill for did:plc:...
7575+```
7676+7777+## Why Is This Needed?
7878+7979+Some users are on independent PDS instances that:
8080+1. Aren't federated to Bluesky's main relay
8181+2. Require authentication for firehose access
8282+3. Are on relays that are currently offline (like atproto.africa)
8383+8484+This on-demand system ensures your AppView can still serve these users when requested, without continuously polling or maintaining permanent connections to every independent PDS.
···710710}
711711712712export const firehoseClient = new FirehoseClient();
713713+714714+// Additional relay sources for multi-relay support
715715+export const additionalRelays: FirehoseClient[] = [];
716716+717717+// Initialize additional relay clients from environment variable
718718+export function initializeAdditionalRelays() {
719719+ const additionalRelayUrls = process.env.ADDITIONAL_RELAY_URLS?.split(',').map(url => url.trim()).filter(Boolean) || [];
720720+721721+ if (additionalRelayUrls.length > 0) {
722722+ console.log(`[FIREHOSE] Initializing ${additionalRelayUrls.length} additional relay sources:`, additionalRelayUrls);
723723+724724+ for (const relayUrl of additionalRelayUrls) {
725725+ const client = new FirehoseClient(relayUrl);
726726+ additionalRelays.push(client);
727727+ console.log(`[FIREHOSE] Additional relay registered: ${relayUrl}`);
728728+ }
729729+ }
730730+731731+ return additionalRelays;
732732+}
+232
server/services/on-demand-backfill.ts
···11+import { logCollector } from './log-collector';
22+import { redisQueue } from './redis-queue';
33+import { metricsService } from './metrics';
44+55+interface BackfillJob {
66+ did: string;
77+ pdsUrl: string;
88+ timestamp: number;
99+}
1010+1111+export class OnDemandBackfill {
1212+ private activeJobs: Map<string, BackfillJob> = new Map();
1313+ private recentlyBackfilled: Set<string> = new Set();
1414+ private readonly BACKFILL_COOLDOWN = 5 * 60 * 1000; // Don't re-backfill same DID for 5 minutes
1515+1616+ /**
1717+ * Backfill a user from their PDS when they're not found in our AppView
1818+ */
1919+ async backfillUser(did: string): Promise<boolean> {
2020+ // Check if we're already backfilling this DID
2121+ if (this.activeJobs.has(did)) {
2222+ console.log(`[ON_DEMAND_BACKFILL] Already backfilling ${did}`);
2323+ return false;
2424+ }
2525+2626+ // Check cooldown - don't spam backfill the same user
2727+ if (this.recentlyBackfilled.has(did)) {
2828+ console.log(`[ON_DEMAND_BACKFILL] ${did} recently backfilled, skipping`);
2929+ return false;
3030+ }
3131+3232+ try {
3333+ console.log(`[ON_DEMAND_BACKFILL] Starting backfill for ${did}`);
3434+ logCollector.info(`On-demand backfill started for ${did}`);
3535+3636+ // First, resolve the DID to find their PDS
3737+ const pdsUrl = await this.resolvePDS(did);
3838+3939+ if (!pdsUrl) {
4040+ console.warn(`[ON_DEMAND_BACKFILL] Could not resolve PDS for ${did}`);
4141+ return false;
4242+ }
4343+4444+ console.log(`[ON_DEMAND_BACKFILL] ${did} is on PDS: ${pdsUrl}`);
4545+4646+ // Mark as active
4747+ this.activeJobs.set(did, {
4848+ did,
4949+ pdsUrl,
5050+ timestamp: Date.now(),
5151+ });
5252+5353+ // Perform the backfill
5454+ await this.performBackfill(did, pdsUrl);
5555+5656+ // Mark as recently backfilled (with cooldown)
5757+ this.recentlyBackfilled.add(did);
5858+ setTimeout(() => {
5959+ this.recentlyBackfilled.delete(did);
6060+ }, this.BACKFILL_COOLDOWN);
6161+6262+ // Remove from active jobs
6363+ this.activeJobs.delete(did);
6464+6565+ console.log(`[ON_DEMAND_BACKFILL] Completed backfill for ${did}`);
6666+ logCollector.success(`On-demand backfill completed for ${did}`);
6767+6868+ return true;
6969+ } catch (error) {
7070+ console.error(`[ON_DEMAND_BACKFILL] Error backfilling ${did}:`, error);
7171+ logCollector.error(`On-demand backfill failed for ${did}`, { error });
7272+ this.activeJobs.delete(did);
7373+ metricsService.incrementError();
7474+ return false;
7575+ }
7676+ }
7777+7878+ private async resolvePDS(did: string): Promise<string | null> {
7979+ try {
8080+ // Fetch DID document from PLC directory
8181+ const plcUrl = `https://plc.directory/${did}`;
8282+ const response = await fetch(plcUrl);
8383+8484+ if (!response.ok) {
8585+ console.warn(`[ON_DEMAND_BACKFILL] Failed to resolve DID ${did}: ${response.status}`);
8686+ return null;
8787+ }
8888+8989+ const didDoc = await response.json();
9090+9191+ // Extract PDS service endpoint
9292+ const pdsService = didDoc.service?.find(
9393+ (s: any) => s.type === 'AtprotoPersonalDataServer'
9494+ );
9595+9696+ if (!pdsService?.serviceEndpoint) {
9797+ console.warn(`[ON_DEMAND_BACKFILL] No PDS service found in DID document for ${did}`);
9898+ return null;
9999+ }
100100+101101+ // Remove https:// prefix to get just the hostname
102102+ const pdsUrl = pdsService.serviceEndpoint.replace(/^https?:\/\//, '');
103103+104104+ return pdsUrl;
105105+ } catch (error) {
106106+ console.error(`[ON_DEMAND_BACKFILL] Error resolving PDS for ${did}:`, error);
107107+ return null;
108108+ }
109109+ }
110110+111111+ private async performBackfill(did: string, pdsUrl: string) {
112112+ try {
113113+ // First, get repo description
114114+ const describeUrl = `https://${pdsUrl}/xrpc/com.atproto.repo.describeRepo?repo=${did}`;
115115+ const describeResponse = await fetch(describeUrl);
116116+117117+ if (!describeResponse.ok) {
118118+ throw new Error(`Failed to describe repo: ${describeResponse.status}`);
119119+ }
120120+121121+ const describeData = await describeResponse.json();
122122+ const handle = describeData.handle;
123123+ const collections = describeData.collections || [];
124124+125125+ console.log(`[ON_DEMAND_BACKFILL] Backfilling ${handle} (${did}) from ${pdsUrl}`);
126126+ console.log(`[ON_DEMAND_BACKFILL] Collections: ${collections.join(', ')}`);
127127+128128+ // Process handle/identity first
129129+ await redisQueue.push({
130130+ type: 'identity',
131131+ data: {
132132+ did: did,
133133+ handle: handle,
134134+ },
135135+ });
136136+137137+ metricsService.incrementEvent('#identity');
138138+139139+ // Backfill each collection
140140+ for (const collection of collections) {
141141+ await this.backfillCollection(did, pdsUrl, collection);
142142+ }
143143+144144+ console.log(`[ON_DEMAND_BACKFILL] Backfilled all collections for ${did}`);
145145+ } catch (error) {
146146+ console.error(`[ON_DEMAND_BACKFILL] Error during backfill:`, error);
147147+ throw error;
148148+ }
149149+ }
150150+151151+ private async backfillCollection(did: string, pdsUrl: string, collection: string) {
152152+ try {
153153+ let cursor: string | undefined = undefined;
154154+ let totalRecords = 0;
155155+156156+ do {
157157+ // Fetch records from this collection
158158+ const listUrl = `https://${pdsUrl}/xrpc/com.atproto.repo.listRecords?repo=${did}&collection=${collection}&limit=100${cursor ? `&cursor=${cursor}` : ''}`;
159159+ const response = await fetch(listUrl);
160160+161161+ if (!response.ok) {
162162+ if (response.status === 400) {
163163+ // Collection doesn't exist, skip it
164164+ return;
165165+ }
166166+ throw new Error(`Failed to list ${collection}: ${response.status}`);
167167+ }
168168+169169+ const data = await response.json();
170170+ const records = data.records || [];
171171+172172+ if (records.length === 0) {
173173+ break;
174174+ }
175175+176176+ // Process each record
177177+ for (const record of records) {
178178+ const path = record.uri.split('/').slice(-2).join('/'); // collection/rkey
179179+180180+ const commit = {
181181+ repo: did,
182182+ ops: [
183183+ {
184184+ action: 'create',
185185+ path: path,
186186+ cid: record.cid,
187187+ record: record.value,
188188+ },
189189+ ],
190190+ };
191191+192192+ // Push to Redis queue for processing
193193+ await redisQueue.push({
194194+ type: 'commit',
195195+ data: commit,
196196+ seq: undefined,
197197+ });
198198+199199+ metricsService.incrementEvent('#commit');
200200+ totalRecords++;
201201+ }
202202+203203+ cursor = data.cursor;
204204+205205+ // Don't backfill more than 1000 records per collection (prevent abuse)
206206+ if (totalRecords >= 1000) {
207207+ console.log(`[ON_DEMAND_BACKFILL] Reached limit of 1000 records for ${collection}, stopping`);
208208+ break;
209209+ }
210210+211211+ } while (cursor);
212212+213213+ if (totalRecords > 0) {
214214+ console.log(`[ON_DEMAND_BACKFILL] Backfilled ${totalRecords} records from ${collection}`);
215215+ }
216216+217217+ } catch (error) {
218218+ console.error(`[ON_DEMAND_BACKFILL] Error backfilling collection ${collection}:`, error);
219219+ throw error;
220220+ }
221221+ }
222222+223223+ getStatus() {
224224+ return {
225225+ activeJobs: Array.from(this.activeJobs.values()),
226226+ recentlyBackfilled: this.recentlyBackfilled.size,
227227+ cooldownMs: this.BACKFILL_COOLDOWN,
228228+ };
229229+ }
230230+}
231231+232232+export const onDemandBackfill = new OnDemandBackfill();
+22
server/services/xrpc/services/actor-service.ts
···1717 suggestedUsersUnspeccedSchema,
1818} from '../schemas';
1919import { xrpcApi } from '../../xrpc-api';
2020+import { onDemandBackfill } from '../../on-demand-backfill';
20212122/**
2223 * Get a single actor profile
···3031 const profiles = await (xrpcApi as any)._getProfiles([params.actor], req);
31323233 if (profiles.length === 0) {
3434+ // Profile not found - trigger on-demand backfill from their PDS
3535+ const actor = params.actor;
3636+3737+ // Check if it's a DID (not a handle)
3838+ if (actor.startsWith('did:')) {
3939+ console.log(`[ON_DEMAND] Profile ${actor} not found, triggering backfill...`);
4040+4141+ // Trigger backfill (non-blocking - don't wait for it)
4242+ onDemandBackfill.backfillUser(actor).catch(error => {
4343+ console.error(`[ON_DEMAND] Backfill failed for ${actor}:`, error);
4444+ });
4545+4646+ // Return 404 with a helpful message
4747+ res.status(404).json({
4848+ error: 'ProfileNotFound',
4949+ message: 'Profile not found. Attempting to fetch from their PDS - try again in a few seconds.',
5050+ });
5151+ return;
5252+ }
5353+5454+ // For handles, we'd need to resolve to DID first
3355 res.status(404).json({ error: 'Profile not found' });
3456 return;
3557 }