Skip to content

Database & Service Layer

Database Module (lib/database)

💡 Multi-Database Support: The project supports SQLite (default), MySQL, and PostgreSQL through the DatabaseAdapter interface. Configure database.type in config.yml to switch — all repository layer code remains unchanged.

Extensibility Convention:

  • To add a new database, create an adapter file in src/lib/database/ implementing the DatabaseAdapter interface, then add the corresponding case in connection.ts's createAndInitializeAdapter()
  • SQL dialect differences are handled centrally in sql-dialect.ts; add a conversion function for new dialects
  • Repository layer uses async + DatabaseAdapter API exclusively, with no need to worry about underlying drivers
FileResponsibility
adapter.tsDatabaseAdapter interface definition with unified query/queryOne/execute/exec/transaction APIs
sql-dialect.tsSQL dialect conversion utility handling INSERT OR REPLACE/IGNORE, @param named parameters, ?$1 positional parameters, etc.
sqlite-adapter.tsSQLite adapter wrapping better-sqlite3 as async API with built-in Mutex serialization and transaction reentry support
mysql-adapter.tsMySQL adapter using mysql2/promise connection pool
postgresql-adapter.tsPostgreSQL adapter using pg.Pool connection pool
connection.tsConnection management factory creating the appropriate adapter based on config.yml database.type, global singleton + concurrent init dedup
schema.tsCreates all data tables (CREATE TABLE IF NOT EXISTS, compatible with all three databases)
migrations.tsDetects schema changes (via hasColumn/hasTable), auto-executes ALTER TABLE, idempotent
seed.tsInitializes sample tags, sample sites, and default theme config for empty databases

Repository Pattern (lib/services)

SiteRepository — Site Data Access

typescript
// Get paginated site list (isolated by owner_id)
// Search matches: name, description, tag name, enabled recommendation context, notes, todos
function getPaginatedCards(options: {
  ownerId: string;
  scope: "all" | "tag";
  tagId?: string | null;
  query?: string | null;
  cursor?: string | null;
}): PaginatedCards

// Get all sites (isolated by ownerId when provided)
function getAllSitesForAdmin(ownerId?: string): Site[]

// Get/Create/Update/Delete single site
function getSiteById(id: string): Site | null
function createSite(input: {..., ownerId: string}): Site | null
function updateSite(input: {...}): Site | null
function deleteSite(id: string): void

// Update memo fields only (lightweight update)
function updateSiteMemo(id: string, data: { siteNotes?; siteNotesAiEnabled?; siteTodos?; siteTodosAiEnabled? }): void

// Update recommendation context field only
function updateSiteRecommendContext(id: string, context: string): void

// Rebuild search text (merge searchable fields into search_text column)
function recomputeSearchText(siteId: string): void

// Sorting / Online check / Social cards / Note cards
function reorderSitesGlobal(siteIds: string[]): void
function reorderSitesInTag(tagId: string, siteIds: string[]): void
function updateSiteOnlineStatus(siteId: string, isOnline: boolean): void
function getSocialCardCount(ownerId?: string): number
function getNoteCardCount(ownerId?: string): number
function deleteAllSocialCardSites(ownerId: string): void
function deleteAllNoteCardSites(ownerId: string): void

TagRepository — Tag Data Access

typescript
function getVisibleTags(ownerId: string): Tag[]
function getTagById(id: string): Tag | null
function createTag(input: {..., ownerId: string}): Tag
function updateTag(input: {...}): Tag | null
function deleteTag(id: string): void
function reorderTags(tagIds: string[]): void
function restoreTagSites(tagId: string, siteIds: string[]): void

AppearanceRepository — Appearance Data Access

typescript
function getAppearances(ownerId: string): Record<ThemeMode, ThemeAppearance>
function updateAppearances(ownerId: string, appearances: {...}): void
function getDefaultTheme(): ThemeMode
function getAppSettings(): AppSettings
function updateAppSettings(settings: {...}): AppSettings
function getVirtualTagSortOrders(): Record<string, number>
function saveVirtualTagSortOrders(orders: Record<string, number>): void
function insertVirtualTagsBySortOrder(tags, virtualTags): void
async function injectVirtualTags(tags: Tag[], ownerId: string): Promise<void>

AssetRepository — Asset Data Access

typescript
function createAsset(input): { id, kind, url }
function getAsset(id: string): StoredAsset | null
function deleteAsset(id: string): void
function getNoteAttachments(noteId: string): StoredAsset[]
function associateAssetsWithNote(assetIds: string[], noteId: string): void
function findOrphanNoteAssets(referencedAssetIds: Set<string>): StoredAsset[]

Other Repositories

RepositoryFileResponsibility
CardRepositorycard-repository.tsSocial card data access (compatible with cards table + sites table social cards)
UserRepositoryuser-repository.tsRegistered user CRUD, password hash verification, role management, OAuth user creation, data copying
OAuthRepositoryoauth-repository.tsOAuth account binding/unbinding/queries
SnapshotRepositorysnapshot-repository.tsSnapshot create/restore/delete/rename/expiry cleanup

Service Layer

ServiceFileResponsibility
ConfigServiceconfig-service.tsReset default config, reset user data, incremental/overwrite config import from ZIP
DataPortabilityServicedata-portability-service.tsUser data portability: extensible export, clean/incremental/overwrite import, HMAC-SHA256 signature verification
SearchServicesearch-service.tsSearch suggestions
NotificationRepositorynotification-repository.tsNotification config CRUD + Webhook dispatch
OnlineCheckScheduleronline-check-scheduler.tsOnline check scheduled scheduler (daily 4 AM batch check + retries)
UrlOnlineCacheRepositoryurl-online-cache-repository.tsURL online status cache management
OAuthProvidersoauth-providers.tsOAuth provider config read/write, authorization URL construction, token exchange (server-only)