A third party ATProto appview

fix(db,firehose): Fix database migration and profile creation race condition

This commit addresses two critical issues that were preventing the application from functioning correctly:

1. **Database Migration Failure:** The database migration script, which runs inside the Docker container on startup, was failing silently. The `drizzle.config.ts` file requires the `dotenv` package, but this was not included in the production Docker image because it is a dev dependency. This resulted in an empty database, causing all subsequent data operations to fail. The fix involves adding `dotenv` to the `npm install` command in the `Dockerfile`'s production stage.

2. **Profile Creation Race Condition:** The event processor (`server/services/event-processor.ts`) was creating user profiles with the user's DID as a placeholder for their handle. A separate `identity` event would later update the handle. This created a race condition where a profile could be requested by its handle before the handle was correctly set, leading to "Profile not found" errors. The fix is to proactively resolve the user's handle using the `didResolver` at the time of user creation in both the `ensureUser` and `processProfile` functions. This ensures the handle is correct from the moment the profile is indexed.

Together, these changes ensure that the database is correctly initialized and that user data is ingested reliably, resolving the user's reported issues.

Changed files
+11 -4
server
+2 -2
Dockerfile
··· 26 26 COPY package*.json ./ 27 27 RUN npm ci --omit=dev 28 28 29 - # Install drizzle-kit and typescript for runtime migrations. 29 + # Install drizzle-kit, typescript and dotenv for runtime migrations. 30 30 # These are dev dependencies but are needed by the entrypoint script. 31 - RUN npm install drizzle-kit@0.31.4 typescript 31 + RUN npm install drizzle-kit@0.31.4 typescript dotenv 32 32 33 33 # Install PM2 globally for cluster mode 34 34 RUN npm install -g pm2
+9 -2
server/services/event-processor.ts
··· 1 1 import { storage, type IStorage } from "../storage"; 2 2 import { lexiconValidator } from "./lexicon-validator"; 3 3 import { labelService } from "./label"; 4 + import { didResolver } from "./did-resolver"; 4 5 import type { InsertUser, InsertPost, InsertLike, InsertRepost, InsertFollow, InsertBlock, InsertList, InsertListItem, InsertFeedGenerator, InsertStarterPack, InsertLabelerService } from "@shared/schema"; 5 6 6 7 function sanitizeText(text: string | undefined | null): string | undefined { ··· 299 300 try { 300 301 const user = await this.storage.getUser(did); 301 302 if (!user) { 303 + // Proactively resolve handle to avoid race conditions where a profile is indexed 304 + // before the handle is known, leading to "profile not found" errors. 305 + const handle = await didResolver.resolveHandle(did); 302 306 await this.storage.createUser({ 303 307 did, 304 - handle: did, 308 + handle: handle || did, // Fallback to DID if resolution fails 305 309 }); 306 310 } 307 311 return true; ··· 627 631 } 628 632 629 633 private async processProfile(did: string, record: any) { 634 + // Proactively resolve handle to have it available immediately on profile creation. 635 + const handle = await didResolver.resolveHandle(did); 630 636 await this.storage.createUser({ 631 637 did, 632 - handle: did, 638 + handle: handle || did, // Fallback to DID if resolution fails 633 639 displayName: sanitizeText(record.displayName), 634 640 description: sanitizeText(record.description), 635 641 avatarUrl: record.avatar?.ref?.$link, 642 + bannerUrl: record.banner?.ref?.$link, 636 643 profileRecord: record, 637 644 }); 638 645 }