···77## Current Implementation
8899### Cards
1010+1011- **Create**: `ATProtoCardPublisher.publishCardToLibrary()` creates/updates records, stores `PublishedRecordId` in card's library membership
1112- **Update**: Uses `putRecord()` with existing rkey when card already has a published record
1213- **Delete**: `unpublishCardFromLibrary()` deletes the AT Protocol record
13141415### Collections
1616+1517- **Create**: `ATProtoCollectionPublisher.publish()` creates/updates collection records
1618- **Update**: Uses `putRecord()` with existing rkey when collection has `publishedRecordId`
1719- **Delete**: `unpublish()` deletes the collection record
18201921### Collection Links
2222+2023- **Create**: `publishCardAddedToCollection()` creates link records
2124- **Delete**: `unpublishCardAddedToCollection()` deletes link records
2225- **No Update**: Collection links don't have update operations
23262427### Storage Pattern
2828+2529All use the `publishedRecords` table with:
3030+2631```sql
2732id: uuid (primary key)
2833uri: text (AT URI)
···3439## Duplicate Detection Strategy
35403641### For CREATE Events
4242+3743✅ **Works as designed**: Check if `(uri, cid)` exists in `publishedRecords` table
4444+3845- If exists → already processed, skip
3946- If not exists → process the create
40474141-### For UPDATE Events
4848+### For UPDATE Events
4949+4250⚠️ **Needs verification**: Check if `(uri, cid)` exists in `publishedRecords` table
5151+4352- **Key question**: When we update a collection/card, does the new CID get stored?
4453- Looking at the code: Yes, `putRecord()` operations should generate new CIDs, and these should be stored
45544655### For DELETE Events
5656+4757⚠️ **Needs implementation**: More complex logic required
5858+48591. Find all `publishedRecords` with matching `uri` (ignore CID)
49602. Use `ATUri.create(uri)` to determine entity type from collection field
50613. Query appropriate table to see if entity still exists
···5465## Changes Needed
55665667### 1. Verify UPDATE CID Storage
6868+5769Need to confirm that when we call `putRecord()`, the new CID is captured and stored. Looking at the publishers:
58705971```typescript
···7082**Required change**: Ensure `putRecord()` responses update the `publishedRecords` table with new CID.
71837284### 2. Enhance AT URI Resolution for DELETE Detection
8585+7386Expand `IAtUriResolutionService` to handle all entity types:
74877588```typescript
···7891 resolveCardId(atUri: string): Promise<Result<CardId | null>>;
7992 resolveCollectionId(atUri: string): Promise<Result<CollectionId | null>>;
8093 // Add collection link resolution
8181- resolveCollectionLinkId(atUri: string): Promise<Result<{collectionId: CollectionId, cardId: CardId} | null>>;
9494+ resolveCollectionLinkId(
9595+ atUri: string,
9696+ ): Promise<Result<{ collectionId: CollectionId; cardId: CardId } | null>>;
8297}
83988499enum AtUriResourceType {
···89104```
9010591106### 3. Add Collection Type Detection
107107+92108Enhance `ATUri` or create a helper to determine entity type from collection field:
9310994110```typescript
···103119```
104120105121### 4. Implement DELETE Detection Service
122122+106123```typescript
107124interface IDeleteDetectionService {
108125 hasBeenDeleted(atUri: string): Promise<Result<boolean>>;
···113130 // 1. Find all publishedRecords with matching URI
114131 const records = await this.findPublishedRecordsByUri(atUri);
115132 if (records.length === 0) return ok(true); // No records = was deleted
116116-133133+117134 // 2. Determine entity type from AT URI
118135 const atUriResult = ATUri.create(atUri);
119136 if (atUriResult.isErr()) return err(atUriResult.error);
120120-137137+121138 const entityType = atUriResult.value.getEntityType(this.configService);
122139123140 // 3. Check if entity still exists
124141 switch (entityType) {
125142 case AtUriResourceType.COLLECTION:
126126- const collectionId = await this.atUriResolver.resolveCollectionId(atUri);
143143+ const collectionId =
144144+ await this.atUriResolver.resolveCollectionId(atUri);
127145 return ok(collectionId === null);
128146 case AtUriResourceType.CARD:
129147 const cardId = await this.atUriResolver.resolveCardId(atUri);
130148 return ok(cardId === null);
131149 case AtUriResourceType.COLLECTION_LINK:
132132- const linkInfo = await this.atUriResolver.resolveCollectionLinkId(atUri);
150150+ const linkInfo =
151151+ await this.atUriResolver.resolveCollectionLinkId(atUri);
133152 return ok(linkInfo === null);
134153 }
135154 }
···137156```
138157139158### 5. Clean Up Published Records on DELETE
159159+140160Modify delete operations to remove `publishedRecords` entries:
141161142162```typescript
143163// In delete use cases and services
144164async deleteCollection(collectionId: CollectionId) {
145165 const collection = await this.repository.findById(collectionId);
146146-166166+147167 // Delete the collection
148168 await this.repository.delete(collectionId);
149149-169169+150170 // Clean up published records
151171 if (collection.publishedRecordId) {
152172 await this.cleanupPublishedRecord(collection.publishedRecordId);
153173 }
154154-174174+155175 // Clean up collection link published records
156176 for (const link of collection.cardLinks) {
157177 if (link.publishedRecordId) {
···164184## Implementation Priority
1651851661861. **High Priority**: Verify and fix UPDATE CID storage
167167-2. **High Priority**: Implement card and collection link AT URI resolution
187187+2. **High Priority**: Implement card and collection link AT URI resolution
1681883. **Medium Priority**: Add DELETE detection service
1691894. **Low Priority**: Clean up published records on delete (nice to have, but detection works without it)
170190
···1616### Core Pattern
17171818Each firehose processor will:
1919+19201. Parse the AT URI and extract relevant data from the record
20212. Use `IAtUriResolutionService` to resolve AT URIs to domain entities
21223. Call existing use cases with an optional `publishedRecordId` parameter
···2425### Use Case Modifications
25262627Add optional `publishedRecordId?: PublishedRecordId` parameter to these use cases:
2828+2729- `AddUrlToLibraryUseCase`
2828-- `CreateCollectionUseCase`
3030+- `CreateCollectionUseCase`
2931- `UpdateCollectionUseCase`
3032- `DeleteCollectionUseCase`
3133- `RemoveCardFromLibraryUseCase`
···3739### URL Card Events
38403941#### Create
4242+4043- Extract URL and curator DID from record
4144- Call `AddUrlToLibraryUseCase` with `publishedRecordId` from event
42454343-#### Update
4646+#### Update
4747+4448- Skip for now (URL cards don't typically update)
45494650#### Delete
5151+4752- Resolve AT URI to card ID
4853- Call `RemoveCardFromLibraryUseCase` with `publishedRecordId` to skip unpublishing
49545055### Note Card Events
51565257#### Create/Update
5858+5359- Extract parent card AT URI, note text, and curator DID
5460- Resolve parent card AT URI to card ID
5561- Call `UpdateUrlCardAssociationsUseCase` with note and `publishedRecordId`
56625763#### Delete
6464+5865- Resolve AT URI to card ID
5966- Verify it's a note card
6067- Call `RemoveCardFromLibraryUseCase` with `publishedRecordId`
···6269### Collection Events
63706471#### Create
7272+6573- Extract name, description, curator DID from record
6674- Call `CreateCollectionUseCase` with `publishedRecordId` from event
67756876#### Update
7777+6978- Resolve AT URI to collection ID
7079- Extract updated fields from record
7180- Call `UpdateCollectionUseCase` with `publishedRecordId`
72817382#### Delete
8383+7484- Resolve AT URI to collection ID
7585- Call `DeleteCollectionUseCase` with `publishedRecordId` to skip unpublishing
76867787### Collection Link Events
78887989#### Create
9090+8091- Extract collection and card strong refs from record
8192- Resolve AT URIs to collection ID and card ID
8293- Call `UpdateUrlCardAssociationsUseCase` with `addToCollections` and link `publishedRecordId`
83948495#### Delete
9696+8597- Extract collection and card strong refs from record
8686-- Resolve AT URIs to collection ID and card ID
9898+- Resolve AT URIs to collection ID and card ID
8799- Call `UpdateUrlCardAssociationsUseCase` with `removeFromCollections` and link `publishedRecordId`
8810089101## Key Benefits
···96108## Files to Modify
9710998110### Use Cases (add optional publishedRecordId parameter):
111111+99112- `AddUrlToLibraryUseCase.ts`
100113- `CreateCollectionUseCase.ts`
101101-- `UpdateCollectionUseCase.ts`
114114+- `UpdateCollectionUseCase.ts`
102115- `DeleteCollectionUseCase.ts`
103116- `RemoveCardFromLibraryUseCase.ts`
104117- `UpdateUrlCardAssociationsUseCase.ts` (more complex - add collection link published record IDs)
105118106119### Firehose Processors (simplify to call existing use cases):
120120+107121- `ProcessCardFirehoseEventUseCase.ts`
108122- `ProcessCollectionFirehoseEventUseCase.ts`
109123- `ProcessCollectionLinkFirehoseEventUseCase.ts`
110124111125### Services (add mapping storage methods):
126126+112127- `IAtUriResolutionService.ts` (already has the methods we need)
113128114129This approach maintains clean separation of concerns while maximizing code reuse and minimizing the complexity of firehose event handling.
···99**Approach**: Extend the DTO with optional published record IDs for different operations.
10101111**Pros**:
1212+1213- Single interface, maintains existing API
1314- Minimal code changes
1415- Handles all scenarios in one place
15161617**Cons**:
1818+1719- Makes the DTO more complex
1820- Mixed concerns (normal operations vs firehose events)
1921- Hard to understand which parameters apply to which operations
20222123**Implementation**:
2424+2225```typescript
2326export interface UpdateUrlCardAssociationsDTO {
2427 cardId: string;
···3841**Approach**: Create dedicated use cases for firehose event processing that handle the publishing logic differently.
39424043**Pros**:
4444+4145- Clear separation of concerns
4246- Easier to understand and maintain
4347- Can optimize for firehose-specific requirements
4448- No impact on existing use cases
45494650**Cons**:
5151+4752- Code duplication
4853- More files to maintain
4954- Need to keep business logic in sync
50555156**Implementation**:
5757+5258```typescript
5359// New use case specifically for firehose events
5460export class ProcessNoteCardFirehoseEventUseCase {
5555- async execute(request: ProcessNoteCardFirehoseEventDTO): Promise<Result<void>> {
6161+ async execute(
6262+ request: ProcessNoteCardFirehoseEventDTO,
6363+ ): Promise<Result<void>> {
5664 // Handle note card creation/update with pre-existing published record ID
5765 // Skip all publishing operations
5866 // Reuse domain services but with different flow
···6068}
61696270export class ProcessCollectionLinkFirehoseEventUseCase {
6363- async execute(request: ProcessCollectionLinkFirehoseEventDTO): Promise<Result<void>> {
7171+ async execute(
7272+ request: ProcessCollectionLinkFirehoseEventDTO,
7373+ ): Promise<Result<void>> {
6474 // Handle collection link operations with pre-existing published record ID
6575 // Skip publishing operations
6676 }
···7282**Approach**: Add a context parameter that controls publishing behavior throughout the operation chain.
73837484**Pros**:
8585+7586- Clean separation of concerns
7687- Reuses existing logic
7788- Easy to extend for other contexts
7889- Minimal API changes
79908091**Cons**:
9292+8193- Requires threading context through multiple layers
8294- Could be forgotten in new code paths
83958496**Implementation**:
9797+8598```typescript
8699export enum OperationContext {
87100 USER_INITIATED = 'user_initiated',
88101 FIREHOSE_EVENT = 'firehose_event',
8989- SYSTEM_MIGRATION = 'system_migration'
102102+ SYSTEM_MIGRATION = 'system_migration',
90103}
9110492105export interface UpdateUrlCardAssociationsDTO {
···109122**Approach**: Modify domain services to accept publishing control parameters.
110123111124**Pros**:
125125+112126- Centralized publishing control
113127- Reuses existing use case logic
114128- Clean separation at service level
115129116130**Cons**:
131131+117132- Changes to multiple service interfaces
118133- Could make services more complex
119134120135**Implementation**:
136136+121137```typescript
122138// Modify CardLibraryService
123139async addCardToLibrary(
···129145 }
130146): Promise<Result<Card, ...>>
131147132132-// Modify CardCollectionService
148148+// Modify CardCollectionService
133149async addCardToCollections(
134150 card: Card,
135151 collectionIds: CollectionId[],
···146162**Approach**: Create different variants of the use case based on the execution context.
147163148164**Pros**:
165165+149166- Clean separation without code duplication
150167- Easy to test different behaviors
151168- Flexible for future contexts
152169153170**Cons**:
171171+154172- More complex setup
155173- Factory pattern overhead
156174157175**Implementation**:
176176+158177```typescript
159178export class UpdateUrlCardAssociationsUseCaseFactory {
160160- static createForUserOperation(dependencies): UpdateUrlCardAssociationsUseCase {
161161- return new UpdateUrlCardAssociationsUseCase(dependencies, { publishingEnabled: true });
179179+ static createForUserOperation(
180180+ dependencies,
181181+ ): UpdateUrlCardAssociationsUseCase {
182182+ return new UpdateUrlCardAssociationsUseCase(dependencies, {
183183+ publishingEnabled: true,
184184+ });
162185 }
163163-164164- static createForFirehoseEvent(dependencies): UpdateUrlCardAssociationsUseCase {
165165- return new UpdateUrlCardAssociationsUseCase(dependencies, { publishingEnabled: false });
186186+187187+ static createForFirehoseEvent(
188188+ dependencies,
189189+ ): UpdateUrlCardAssociationsUseCase {
190190+ return new UpdateUrlCardAssociationsUseCase(dependencies, {
191191+ publishingEnabled: false,
192192+ });
166193 }
167194}
168195```
···170197## Recommended Approach: Option 3 (Context-Based Publishing Control)
171198172199**Rationale**:
200200+173201- Provides clean separation without major architectural changes
174202- Reuses existing business logic
175203- Easy to understand and maintain
···177205- Minimal impact on existing code
178206179207**Implementation Strategy**:
208208+1802091. Add `OperationContext` enum and optional context parameter to DTOs
1812102. Thread context through service calls
1822113. Modify services to check context before publishing
···119119 if (request.publishedRecordId) {
120120 // Update published record ID with provided value
121121 collection.markAsPublished(request.publishedRecordId);
122122-122122+123123 // Save collection with updated published record ID
124124 const saveUpdatedResult =
125125 await this.collectionRepository.save(collection);
···70707171 // Handle publishing based on options
7272 if (options?.skipPublishing && options?.publishedRecordIds) {
7373- const publishedRecordId = options.publishedRecordIds.get(collectionId.getStringValue());
7373+ const publishedRecordId = options.publishedRecordIds.get(
7474+ collectionId.getStringValue(),
7575+ );
7476 if (publishedRecordId) {
7577 // Skip publishing and use provided record ID
7678 collection.markCardLinkAsPublished(card.cardId, publishedRecordId);
···9698 }
979998100 // Mark the card link as published in the collection
9999- collection.markCardLinkAsPublished(card.cardId, publishLinkResult.value);
101101+ collection.markCardLinkAsPublished(
102102+ card.cardId,
103103+ publishLinkResult.value,
104104+ );
100105 }
101106102107 // Save the updated collection