The Likeness Search system uses eight core database tables, all defined using Drizzle ORM and following the project's conventions (nanoid IDs, snake_case columns, timestamps with timezone).
All schemas are located in: packages/db/src/schema/
File: packages/db/src/schema/likenessAssets.ts
Stores uploaded images and voice samples with AI-extracted search tags.
Key Fields:
id - Primary key (nanoid)accountId - Foreign key to account.idtype - Enum: IMAGE or VOICEcontentUrl - Media filename (e.g., photo-a3f2.jpg), resolved to proxy URL via Media SystemsearchTags - JSON field storing AI-extracted tagsvoiceSampleUrl - Media filename for generated voice sample (VOICE assets only)tagAttemptCount - Number of failed AI extraction attempts (max 5)tagLastAttemptAt - Timestamp of last attemptAccess Functions: packages/db/src/access/likenessAssets.ts
getAssetById(id)listAssetsByAccountId(accountId)getUnprocessedAssets(accountId, maxAttempts)createAsset(data)updateAssetSearchTags(id, searchTags)updateAssetVoiceSample(id, voiceSampleUrl)incrementTagAttemptCount(id)resetTagAttemptCount(id)File: packages/db/src/schema/likenessSearch.ts
SQL search index with 30+ enum fields for exact filtering.
Key Fields:
id - Primary key (nanoid)accountId - Foreign key to account.id (unique)category, gender, country, targetAudiencenumberOfFollowers, primaryPlatform, isVerified, contentNiche, engagementRatebirthYear, height, weight, bodyTypehairColor, hairLength, hairTypeeyeColor, skinColor, ethnicity, faceShape, facialHair, wearGlasseshasTattoos, hasPiercingsvoicePitch, voiceTone, accent, primaryLanguageAccess Functions: packages/db/src/access/likenessSearch.ts
upsertSearchIndex(accountId, data)getSearchIndexByAccountId(accountId)deleteSearchIndex(accountId)searchByFilters(filters, limit, offset)File: packages/db/src/schema/likenessSearchVector.ts
pgvector embeddings for semantic similarity search fallback.
Key Fields:
accountId - Primary key, foreign key to account.idcontent - Text representation of tags (normalized for embedding)embedding - pgvector column (1536 dimensions, OpenAI text-embedding-3-small)Access Functions: packages/db/src/access/likenessSearchVector.ts
vectorUpsert(accountId, content, embedding)vectorSearch(queryEmbedding, topK, maxDistance)vectorSearchWithImageAssets(queryEmbedding, opts)vectorSearchWithVoiceAssets(queryEmbedding, opts)deleteFromVectorIndex(accountId)File: packages/db/src/schema/likenessNeedIndexingQueue.ts
Event queue driving the indexing pipeline.
Key Fields:
id - Primary key (nanoid)accountId - Foreign key to account.idelementType - Enum: IMAGE_ASSET, VOICE_ASSET, VOICE_TERM, IMAGE_TERM, LIKENESS_TERM, PROFILE_DESCRIPTION, SOCIAL_DATAelementId - Optional ID of the element that triggered the eventstatus - Enum: PENDING, IN_PROGRESS, COMPLETED, FAILED, DISCARDED, TIMEOUT, AWAITING_RETRYinfo - JSON field for additional metadataretryCount - Number of retry attempts (max 5)retryAt - Timestamp for retry (when status is AWAITING_RETRY)Indexes:
(status, createdAt) - For getting oldest PENDING events(accountId, status) - For checking active events per account(status, retryAt) - For processing retryable eventsAccess Functions: packages/db/src/access/likenessNeedIndexingQueue.ts
addToQueue(accountId, elementType, elementId, info)getOldestPendingEvent()getEventById(eventId)markEventInProgress(eventId)markEventCompleted(eventId)markEventFailed(eventId, errorInfo)markEventDiscarded(eventId, reason)markEventAwaitingRetry(eventId, retryAt)markTimedOutEvents()processRetryableEvents()hasActiveEventForAccount(accountId)markOtherPendingEventsAsDiscarded(accountId, excludeEventId)getEventRetryCount(eventId)File: packages/db/src/schema/accountSocialLinks.ts
Social media links and platform-specific follower counts.
Key Fields:
id - Primary key (nanoid)accountId - Foreign key to account.idplatform - Enum: INSTAGRAM, TIKTOK, TWITTER, YOUTUBE, LINKEDIN, FACEBOOK, TWITCH, SNAPCHAT, ONLYFANSurl - Social media profile URLfollowersCount - Platform-specific follower count (updated via scraping)Unique Constraint: (accountId, platform) - One link per platform per account
Access Functions: packages/db/src/access/accountSocialLinks.ts
getSocialLinksByAccountId(accountId)getSocialLinkByAccountAndPlatform(accountId, platform)upsertSocialLink(data)deleteSocialLink(id)getTotalFollowersCount(accountId)File: packages/db/src/schema/scrapes.ts
Social media scraping results and retry tracking.
Key Fields:
id - Primary key (nanoid)accountId - Foreign key to account.idlinkhash - Hash of the scraped URL (for deduplication)link - The scraped URLname - Scraped name/usernameavatar - Scraped avatar URLfollowers - Scraped follower counterror - Error message if scraping failedrawData - JSON field storing raw scraping responseattemptCount - Number of failed scrape attempts (max 5)lastAttemptAt - Timestamp of last attemptUnique Constraint: (accountId, linkhash) - One scrape record per account-link combination
Indexes:
(accountId) - For getting all scrapes for an accountAccess Functions: packages/db/src/access/scrapes.ts
getScrapesByAccountId(accountId)upsertScrape(data)incrementScrapeAttemptCount(accountId, linkhash)resetScrapeAttemptCount(accountId, linkhash)File: packages/db/src/schema/elevenLabs.ts
ElevenLabs voice clones linked to accounts. This table stores voice IDs created on ElevenLabs and is used system-wide, not just for likeness search.
Key Fields:
accountId - Foreign key to account.id (part of composite primary key)voiceId - ElevenLabs voice ID (part of composite primary key)modelId - TTS model ID (e.g., "eleven_multilingual_v2")stability - Voice stability setting (0-100)similarityBoost - Similarity boost setting (0-100)style - Style setting (0-100)useSpeakerBoost - Enable speaker boost (boolean)voiceTitle - Custom title for the voicevoiceExample - Media filename for TTS sample audiocreatedAt - Timestamp when voice was createdupdatedAt - Timestamp when voice was last updatedPrimary Key: Composite (accountId, voiceId) - Allows multiple voice clones per account
Foreign Key: accountId references account.id with ON DELETE CASCADE
Access Functions: packages/db/src/access/elevenLabs.ts
getVoiceByAccountAndVoiceId(accountId, voiceId)listVoicesByAccountId(accountId)upsertVoice(data)updateVoiceExample(accountId, voiceId, voiceExampleUrl)deleteVoiceByAccountAndVoiceId(accountId, voiceId)deleteAllVoicesByAccountId(accountId)Usage:
voiceExample stores the generated TTS sample URL for playbackFile: packages/db/src/schema/media.ts
Central registry for all uploaded files. Stores S3 coordinates and a unique filename used as the public reference. Content is served via the proxy endpoint GET /api/media/[filename] — see Media System.
Key Fields:
id - Primary key (nanoid)filename - Unique public reference: {base}-{4char}.{ext} (e.g., photo-a3f2.jpg)originalFilename - Original file names3Key, s3Bucket, s3Region - S3 metadatacontentType - MIME typestatus - Enum: UPLOADING, COMPLETE, SOFT_DELETE, DELETEAccess Functions: packages/db/src/access/media.ts
createMedia(data)getMediaByFilename(filename)getMediaById(id)updateMediaStatus(id, status)File: packages/db/src/schema/account.ts (existing)
Used by the search system for:
displayName - Required for base requirements checkimageUrl - Required for base requirements checkslug - Used in search resultsdescription - Can contribute tags to search indexFile: packages/db/src/schema/ipTerms.ts (existing)
Used by the search system for:
The system uses 23 PostgreSQL enum types, all defined in the schema files:
likeness_search_category - MODELS, ACTORS, MUSICIANS, etc.likeness_search_gender - MALE, FEMALE, NON_BINARYlikeness_search_country - USA, UK, etc.likeness_search_target_audience - AGE_18_35, LUXURY, FITNESSlikeness_search_content_niche - FASHION, FITNESS, BEAUTY, etc.likeness_search_primary_platform - INSTAGRAM, TIKTOK, YOUTUBE, etc.account_social_links_platform - Same as primary platformlikeness_search_body_type - SLIM, ATHLETIC, MUSCULAR, etc.likeness_search_hair_color - BLACK, BROWN, BLONDE, etc.likeness_search_hair_length - SHORT, MEDIUM, LONGlikeness_search_hair_type - STRAIGHT, CURLY, WAVY, KINKYlikeness_search_eye_color - BLUE, BROWN, GREEN, HAZELlikeness_search_skin_color - FAIR, MEDIUM, DARKlikeness_search_ethnicity - CAUCASIAN, AFRICAN_AMERICAN, ASIAN, etc.likeness_search_face_shape - OVAL, ROUND, SQUARE, etc.likeness_search_facial_hair - CLEAN_SHAVEN, BEARD_FULL, etc.likeness_search_voice_pitch - VERY_HIGH, HIGH, MEDIUM, LOW, VERY_LOWlikeness_search_voice_tone - WARM, AUTHORITATIVE, FRIENDLY, etc.likeness_search_accent - AMERICAN_GENERAL, BRITISH_RP, etc.likeness_search_language - ENGLISH, SPANISH, FRENCH, etc.likeness_need_indexing_queue_element_type - Event trigger typeslikeness_need_indexing_queue_status - Event status valueslikeness_assets_type - IMAGE, VOICEmedia_status - UPLOADING, COMPLETE, SOFT_DELETE, DELETESeveral tables use JSONB fields for flexible data storage:
likeness_assets.searchTags - AI-extracted tags (validated by Zod schemas)likeness_need_indexing_queue.info - Additional event metadatascrapes.rawData - Raw scraping response dataThe likeness_search_vector.embedding field uses PostgreSQL's vector type (pgvector extension):
text-embedding-3-small)<=> operator)All tables use text primary keys with nanoid generation (except likeness_search_vector which uses accountId as primary key).
All tables reference account.id with ON DELETE CASCADE behavior (handled by Drizzle).
likeness_search.accountId - One search index entry per accountlikeness_search_vector.accountId - One vector entry per accountaccount_social_links(accountId, platform) - One link per platform per accountscrapes(accountId, linkhash) - One scrape record per account-link combinationeleven_labs(accountId, voiceId) - Composite primary key (allows multiple voices per account)media.filename - Unique index for proxy lookuplikeness_need_indexing_queue:
(status, createdAt) - For FIFO event processing(accountId, status) - For checking active events(status, retryAt) - For processing retryable eventsscrapes:
(accountId) - For getting all scrapes for an accountLatest Migration File: packages/db/drizzle/0009_modern_daredevil.sql
The migrations create:
eleven_labs tableTo Apply: Run npm run db:migrate in packages/db
All TypeScript types are defined in packages/types/src/types/:
LikenessAssets - Asset type with all fieldsNewLikenessAssets - Type for creating assets (omits auto-generated fields)LikenessSearch - Search index type with all enum fieldsSearchFilters - Search filter interfaceFormattedSearchResult - Enriched search result typeLikenessQueueEvent - Queue event typeLikenessQueueElementType - Event trigger type unionLikenessQueueStatus - Event status type unionAccountSocialLink - Social link typeNewAccountSocialLink - Type for creating social linksElevenLabsVoice - ElevenLabs voice clone typeNewElevenLabsVoice - Type for creating voice clonesMedia - Media record typeMediaStatus - UPLOADING | COMPLETE | SOFT_DELETE | DELETEImportant: Tables are never exposed directly. All database access goes through access functions in packages/db/src/access/. This ensures:
Example:
// ✅ Correct - Use access function
import { getAssetById } from "@zooly/app-db";
const asset = await getAssetById(assetId);
// ❌ Wrong - Don't access table directly
import { likenessAssetsTable } from "@zooly/app-db";
const asset = await db.select().from(likenessAssetsTable)...On This Page
OverviewSchema LocationTable RelationshipsCore Tables1. likeness_assets2. likeness_search3. likeness_search_vector4. likeness_need_indexing_queue5. account_social_links6. scrapes7. eleven_labs8. mediaSupporting Tablesaccountip_termsEnum TypesCategory & BasicSocialPhysical & VisualVoiceQueueData TypesJSON FieldsVector TypeIndexes and ConstraintsPrimary KeysForeign KeysUnique ConstraintsIndexesMigrationType DefinitionsAccess Pattern