# App Architecture This page explains how the major Marks runtime pieces fit together. For product-level flows, see [Product workflows](../workflows/product-workflows.md). For setup and verification, see [Development and testing](../operations/development-and-testing.md). ## Targets and source layout `/project.yml` is the XcodeGen project definition. It defines four targets: - `Marks` — the main iOS app. Sources include `/Marks` plus `/MarksWidget/WidgetDataStore.swift` so the app can write widget data. - `ShareExtension` — app extension with `/ShareExtension` plus selected shared model/service files from `/Marks` (`Bookmark`, `ServerConfig`, `LinkdingAPI`, `MarksAuth`, `Log`, `PodcastRequests`). - `MarksWidget` — WidgetKit extension with `/MarksWidget`. - `MarksTests` — unit-test bundle depending on `Marks`. The app is Swift 6.0 and targets iOS 26.0. The main app, share extension, and widget extension share the app group `group.com.magicive.marks` through entitlements and runtime code. ## App entrypoint and navigation shell `/Marks/MarksApp.swift` contains the `@main` app and `MainContainer`. At startup: 1. `MarksApp` loads a saved `ServerConfig` or falls back to a built-in default config. 2. `MainContainer` constructs `LinkdingAPI(config:)`. 3. `MainContainer` stores `BookmarksViewModel(api:cacheKey:)` in `@State`, keeping the model stable across SwiftUI re-renders. 4. A top-level `TabView` presents Bookmarks, Tags, Podcasts, and Search. `MainContainer` also handles: - custom deep links (`marks://bookmark?...`, `marks://podcast?...`), - App Intent handoff through `IntentRouter.shared`, - pending podcast requests queued by the share extension, - foreground refreshes and background podcast progress persistence. ## Central state: `BookmarksViewModel` `/Marks/Views/BookmarksViewModel.swift` is `@Observable @MainActor` and is the core app state object. It owns: - loaded bookmarks, errors, loading flags, pagination URL, search query, and unread filter, - generated smart collections and enrichment progress, - a `LinkdingAPI` actor for server sync, - `ClaudeService` for backend AI/podcast calls, - `PodcastPlayerViewModel` and `PodcastGenerationManager`, - the disk bookmark cache URL. Key side effects during bookmark loading and mutations: - `load()` optionally restores cached bookmarks from Application Support, fetches linkding bookmarks, restores local AI metadata, saves the full unfiltered cache, writes recent bookmarks to `WidgetDataStore`, and indexes bookmarks into Spotlight. - `loadMore()` follows linkding's `next` URL and indexes newly appended bookmarks. - `delete()` and `archive()` mutate linkding, remove local entries, and remove Spotlight entries. - `addBookmark()` saves to linkding, inserts locally, indexes the bookmark, and starts best-effort AI enrichment. - `enrichAll()` and background `startEnrichment()` call AI enrichment, persist AI metadata, and reindex enriched bookmarks. Because one `BookmarksViewModel` instance is shared by all tabs, array mutations and filters have cross-tab effects. ## Linkding integration `/Marks/Services/LinkdingAPI.swift` is an actor wrapping linkding's REST API. It builds URLs from `ServerConfig` (`/Marks/Models/ServerConfig.swift`) and sends `Authorization: Token ...` headers. Implemented operations include: - `fetchBookmarks(search:offset:limit:unread:)` → `GET /api/bookmarks/`, - `fetchBookmarksFromUrl(_:)` for pagination `next` URLs, - `checkBookmark(url:)` → `GET /api/bookmarks/check/`, used by the share extension, - `fetchTags(limit:)` → `GET /api/tags/`, - `createBookmark(_:)` → `POST /api/bookmarks/`, - `updateBookmark(id:update:)` → `PATCH /api/bookmarks/{id}/`, - `deleteBookmark(id:)` → `DELETE /api/bookmarks/{id}/`, - `archiveBookmark(id:)` → `POST /api/bookmarks/{id}/archive/`, - `verifyConnection()` for configuration checks. `Bookmark`, `BookmarkResponse`, create/update payloads, `BookmarkCheck`, tag response models, and `SmartCollection` live in `/Marks/Models/Bookmark.swift`. ## Configuration and app-group sharing `ServerConfig` contains linkding host, optional port, path, token, and HTTPS flag. Its extension stores config in two places: - `UserDefaults.standard` for the main app, - `UserDefaults(suiteName: "group.com.magicive.marks")` for extensions. `ServerConfig.loadShared()` is used by the share extension, which cannot read the main app's standard defaults. Avoid changing config keys or app-group IDs without updating all entitlements, `ServerConfig`, `WidgetDataStore`, and `PodcastRequests` together. Security note: `/Marks/MarksApp.swift` currently includes a built-in default config with a token-like value. Treat it as sensitive; do not copy it into documentation or generated output. ## Backend AI, analytics, and podcasts `/Marks/Services/MarksAuth.swift` registers an anonymous device with the Marks backend and caches a JWT in `UserDefaults.standard`. `ClaudeService` uses that token for backend calls: - bookmark enrichment: `POST /v1/marks/enrich`, - semantic search: `POST /v1/marks/search`, - smart collections: `POST /v1/marks/collections`, - podcast generation: `POST /v1/podcast/generate`, - podcast polling: `GET /v1/podcast/status/{jobId}`, - podcast audio download: `GET /v1/podcast/audio/{jobId}`. `/Marks/Services/AnalyticsService.swift` sends events through the backend path used by `ClaudeService`/`MarksAuth`. Podcast audio is downloaded to Application Support under a `podcasts` directory using a stable hash of the article URL. `PodcastIndex` records metadata in `index.json`; `PodcastLibrary` reads that index for the Podcasts tab and widget metadata. ## On-device Ask and Spotlight The app has two AI paths: 1. **Backend AI** through `ClaudeService` for enrichment, semantic search, smart collections, and podcasts. 2. **On-device RAG** through `/Marks/Services/BookmarkAssistant.swift` for the Ask UI. `BookmarkAssistant` uses `FoundationModels.LanguageModelSession` with `BookmarkSearchTool`. Its instructions require answers to be grounded in bookmarks. Retrieval flows through Spotlight: - `SpotlightIndexer` indexes bookmarks after loads, load-more operations, add, and enrichment. - `SpotlightBookmarkSearch`/`BookmarkSearchTool` search the index for relevant bookmarks. - `BookmarkEntity` also conforms to indexed App Intents concepts for system-level entity discovery. If changing bookmark fields, keep `Bookmark`, `BookmarkEntity`, Spotlight indexing, widget payloads, and row UI in sync. ## Share extension architecture The share extension is implemented by `/ShareExtension/ShareViewController.swift` and `/ShareExtension/ShareView.swift`. It accepts a shared URL/text payload, creates a SwiftUI save card, loads shared `ServerConfig`, checks linkding for an existing bookmark, and lets the user edit tags, notes, read-later state, and a Create Podcast toggle. For suggestions, it combines: - linkding `checkBookmark` metadata and `auto_tags`, - the user's existing tag vocabulary from `fetchTags()`, - backend AI tag suggestions through `/ShareExtension/TagSuggester.swift`. Existing bookmarks are updated rather than duplicated. New bookmarks are created through `LinkdingAPI`. Podcast generation is not performed inside the extension; it queues a `PodcastRequests` item in the app group for the main app to process later. ## Widget architecture and deep links `/MarksWidget/MarksWidgetBundle.swift` registers three widgets: - `RecentBookmarksWidget` (`systemMedium`), - `RandomBookmarkWidget` (`systemSmall`), - `RecentPodcastsWidget` (`systemMedium`). Widgets read only app-group JSON through `/MarksWidget/WidgetDataStore.swift`: - `widget_bookmarks.json`, - `widget_podcasts.json`. They do not call network APIs. Main-app writes trigger `WidgetCenter.shared.reloadAllTimelines()` when WidgetKit is available. Widget links use custom URLs built by `URL.marksBookmark(_:)` and `URL.marksPodcast(_:)`. `MainContainer.handleDeepLink(_:)` opens bookmark URLs in `BrowserView` or starts podcast playback and shows the player. ## App Intents and Shortcuts `/Marks/Intents/MarksAppIntents.swift` defines: - `OpenBookmarkIntent` — opens a selected `BookmarkEntity` in app. - `SearchMarksIntent` — conforms to the system search schema and routes text to the Search tab. - `AddBookmarkIntent` — saves a URL headlessly through linkding and refreshes widget data. - `ShowUnreadIntent` — fetches up to 10 unread bookmarks. - `SummarizeBookmarkIntent` — calls backend AI enrichment for a selected bookmark. - `MarksShortcuts` — registers Add Bookmark, Search, Unread, and Summarize shortcuts. `IntentRouter` bridges intents that need to open UI back into `MainContainer`. `BookmarkEntity` maps between linkding `Bookmark` values and App Intents entities. ## Recent history context Recent git history shows major investment in podcast UX and on-device AI: the latest merge added played/unplayed queues, background podcast generation, share-sheet audio requests, podcast library/index changes, and player improvements. A prior AI commit added on-device RAG, Spotlight indexing, and unified logging. Use those areas cautiously because they span app state, storage, widgets, share extension, and App Intents rather than a single view.