···109109 created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
110110 updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
111111 )`,
112112+113113+ // Follows table (no dependencies)
114114+ sql`CREATE TABLE IF NOT EXISTS follows (
115115+ follower_id TEXT NOT NULL,
116116+ target_id TEXT NOT NULL,
117117+ target_type TEXT NOT NULL,
118118+ created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
119119+ PRIMARY KEY (follower_id, target_id, target_type)
120120+ )`,
112121 ];
113122114123 // Execute table creation queries in order
···238247 `);
239248 await db.execute(sql`
240249 CREATE INDEX IF NOT EXISTS collection_cards_collection_id_idx ON collection_cards(collection_id);
250250+ `);
251251+252252+ // Follows table indexes
253253+ await db.execute(sql`
254254+ CREATE INDEX IF NOT EXISTS idx_follows_follower ON follows(follower_id);
255255+ `);
256256+ await db.execute(sql`
257257+ CREATE INDEX IF NOT EXISTS idx_follows_target ON follows(target_id, target_type);
241258 `);
242259}
···11+import { Result } from 'src/shared/core/Result';
22+import { Follow } from '../Follow';
33+import { FollowTargetType } from '../value-objects/FollowTargetType';
44+55+export interface IFollowsRepository {
66+ /**
77+ * Get all followers of a specific target (user or collection).
88+ *
99+ * @param targetId - The ID of the entity being followed (User DID or Collection UUID)
1010+ * @param targetType - The type of entity being followed (USER or COLLECTION)
1111+ * @returns Array of Follow records (can be empty if no followers)
1212+ *
1313+ * Example:
1414+ * getFollowers('did:plc:alice123', FollowTargetType.USER)
1515+ * → [Follow{ followerId: DID('did:plc:bob456'), targetId: 'did:plc:alice123', ... }]
1616+ */
1717+ getFollowers(
1818+ targetId: string,
1919+ targetType: FollowTargetType,
2020+ ): Promise<Result<Follow[]>>;
2121+2222+ /**
2323+ * Get all followers of multiple collections (combined, deduplicated at use case level).
2424+ *
2525+ * @param collectionIds - Array of collection UUIDs
2626+ * @returns Array of Follow records (can be empty)
2727+ *
2828+ * Example:
2929+ * getFollowersOfCollections(['uuid-1', 'uuid-2'])
3030+ * → [
3131+ * Follow{ followerId: DID('did:plc:bob456'), targetId: 'uuid-1', ... },
3232+ * Follow{ followerId: DID('did:plc:carol789'), targetId: 'uuid-2', ... }
3333+ * ]
3434+ *
3535+ * Notes:
3636+ * - Returns empty array if collectionIds is empty
3737+ * - Results may include duplicates if a user follows multiple input collections
3838+ * (deduplication happens at use case level)
3939+ */
4040+ getFollowersOfCollections(collectionIds: string[]): Promise<Result<Follow[]>>;
4141+}
···11// Repositories
22export * from './repositories/DrizzleUserRepository';
33+export * from './repositories/DrizzleFollowsRepository';
3445// Services
56export * from '../../atproto/infrastructure/services/AtProtoOAuthProcessor';
···11121213// Schema
1314export * from './repositories/schema/user.sql';
1515+export * from './repositories/schema/follows.sql';
1416export * from './repositories/schema/authToken.sql';
1517export * from './repositories/schema/authState.sql';
1618export * from './repositories/schema/authSession.sql';