Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nook-browser/Nook/llms.txt

Use this file to discover all available pages before exploring further.

Nook uses observable models for runtime state and SwiftData entities for persistence. Models are designed to be reactive, type-safe, and compatible with SwiftUI’s observation system.

Model categories

Observable models

Runtime state using @Observable macro for automatic UI updates

SwiftData entities

Persistent storage using @Model macro for database integration

Configuration objects

Immutable or singleton configs (e.g., BrowserConfig)

Value types

Simple structs for data transfer (e.g., snapshots, DTOs)

Core observable models

Tab

Location: Nook/Models/Tab/Tab.swift:18 Pattern: Hybrid @Observable + ObservableObject (dual observation for legacy compatibility) The Tab model represents a browser tab with its state, webview, and metadata.
@MainActor
public class Tab: NSObject, Identifiable, ObservableObject, WKDownloadDelegate {
    public let id: UUID
    var url: URL
    var name: String
    var favicon: SwiftUI.Image
    var spaceId: UUID?
    var profileId: UUID?
    var index: Int
    
    // Pin state
    var isPinned: Bool = false        // Global pinned (essentials)
    var isSpacePinned: Bool = false   // Space-level pinned
    var folderId: UUID?               // Folder membership
    
    // Ephemeral state
    var isEphemeral: Bool { resolveProfile()?.isEphemeral ?? false }
    
    // Loading state
    var loadingState: LoadingState = .idle
    
    // Published properties for Combine compatibility
    @Published var canGoBack: Bool = false
    @Published var canGoForward: Bool = false
    @Published var hasPlayingAudio: Bool = false
    @Published var isAudioMuted: Bool = false
    @Published var hasPlayingVideo: Bool = false
}
Key features:
  • Lazy webview: Tab.webView computed property creates webview on first access
  • Favicon caching: Global LRU cache with persistent disk storage (Tab.swift:56-70)
  • Loading states: didStartProvisionalNavigation, didCommit, didFinish, didFail
  • Media state: Audio/video playback tracking, mute control, PiP support
  • OAuth flow tracking: Special handling for sign-in popup windows

Space

Location: Nook/Models/Space/Space.swift:16 Pattern: @Observable (Swift Observation) Spaces organize tabs into themed workspaces with custom gradients.
@MainActor
@Observable
public class Space: NSObject, Identifiable {
    public let id: UUID
    var name: String
    var icon: String
    var color: NSColor
    var gradient: SpaceGradient
    var activeTabId: UUID?
    var profileId: UUID?
    var isEphemeral: Bool = false
}
Related models:
  • SpaceGradient (SpaceGradient.swift) — Gradient configuration with nodes and colors
  • GradientNode (GradientNode.swift) — Individual gradient stop with position/color

Profile

Location: Nook/Models/Profile/Profile.swift:16 Pattern: @Observable (Swift Observation) Profiles provide isolated browsing contexts with separate data stores.
@MainActor
@Observable
final class Profile: NSObject, Identifiable {
    let id: UUID
    var name: String
    var icon: String
    let dataStore: WKWebsiteDataStore  // Isolated per profile
    
    var isEphemeral: Bool = false      // Incognito profile
    var isDefault: Bool { name.lowercased() == "default" }
    
    // Cached stats
    private(set) var cachedCookieCount: Int = 0
    private(set) var cachedRecordCount: Int = 0
}
Key features:
  • Data isolation: Each profile gets a unique WKWebsiteDataStore(forIdentifier: profileId) (Profile.swift:82-98)
  • Ephemeral profiles: Use WKWebsiteDataStore.nonPersistent() for incognito mode
  • Stats caching: Cookie/record counts cached for performance
Factory method (Profile.swift:66):
static func createEphemeral() -> Profile {
    let profile = Profile(
        id: UUID(),
        name: "Incognito",
        icon: "eye.slash",
        dataStore: .nonPersistent()
    )
    profile.isEphemeral = true
    return profile
}

BrowserWindowState

Location: Nook/Models/BrowserWindowState.swift:15 Pattern: @Observable (Swift Observation) Represents per-window UI state, allowing multiple windows with independent selections.
@MainActor
@Observable
class BrowserWindowState {
    let id: UUID
    
    // Active selections
    var currentTabId: UUID?
    var currentSpaceId: UUID?
    var currentProfileId: UUID?
    var activeTabForSpace: [UUID: UUID] = [:]
    
    // UI state
    var sidebarWidth: CGFloat = 250
    var isSidebarVisible: Bool = true
    var isCommandPaletteVisible: Bool = false
    var urlBarFrame: CGRect = .zero
    
    // Toast state
    var profileSwitchToast: BrowserManager.ProfileSwitchToast?
    var isShowingProfileSwitchToast: Bool = false
    
    // Incognito state
    var isIncognito: Bool = false
    var ephemeralProfile: Profile?
    var ephemeralSpaces: [Space] = []
    
    // References
    weak var tabManager: TabManager?
    weak var commandPalette: CommandPalette?
    var window: NSWindow?
}
Usage: Each ContentView creates a BrowserWindowState and registers it with WindowRegistry.

SwiftData entities

Schema definition

All entities are registered in BrowserManager.swift:29-37:
static let schema = Schema([
    SpaceEntity.self,
    ProfileEntity.self,
    TabEntity.self,
    FolderEntity.self,
    TabsStateEntity.self,
    HistoryEntity.self,
    ExtensionEntity.self,
])

TabEntity

Location: Nook/Models/Tab/TabsModel.swift:12 Persists tab state across app launches.
@Model
final class TabEntity {
    @Attribute(.unique) var id: UUID
    var urlString: String
    var name: String
    var isPinned: Bool
    var isSpacePinned: Bool
    var index: Int
    var spaceId: UUID?
    var profileId: UUID?      // For global pinned tabs
    var folderId: UUID?       // For folder membership
    
    // Navigation state tracking
    var currentURLString: String?
    var canGoBack: Bool = false
    var canGoForward: Bool = false
}
Persistence flow:
  1. TabManager creates snapshot: TabPersistenceActor.SnapshotTab
  2. PersistenceActor upserts: SnapshotTabTabEntity
  3. On restore: TabEntityTab

SpaceEntity

Location: Nook/Models/Space/SpaceModels.swift Persists space configuration.
@Model
final class SpaceEntity {
    @Attribute(.unique) var id: UUID
    var name: String
    var icon: String
    var index: Int
    var gradientData: Data?   // Encoded SpaceGradient
    var activeTabId: UUID?
    var profileId: UUID?
}

ProfileEntity

Location: Nook/Models/Profile/ProfileEntity.swift Persists profile metadata.
@Model
final class ProfileEntity {
    @Attribute(.unique) var id: UUID
    var name: String
    var icon: String
    var index: Int
}
Profile data stores are not persisted as entities. Each profile’s WKWebsiteDataStore is automatically managed by WebKit based on the profile UUID.

FolderEntity

Location: Nook/Models/Tab/TabsModel.swift:61 Persists tab folders within spaces.
@Model
final class FolderEntity {
    @Attribute(.unique) var id: UUID
    var name: String
    var icon: String
    var color: String
    var spaceId: UUID
    var isOpen: Bool
    var index: Int
}

HistoryEntity

Location: Nook/Models/History/HistoryEntity.swift Persists browsing history entries.
@Model
final class HistoryEntity {
    @Attribute(.unique) var id: UUID
    var urlString: String
    var title: String
    var visitDate: Date
    var profileId: UUID
}

ExtensionEntity

Location: Nook/Models/Extension/ExtensionModels.swift Persists installed browser extensions.
@Model
final class ExtensionEntity {
    @Attribute(.unique) var id: String  // Extension unique identifier
    var displayName: String
    var version: String
    var isEnabled: Bool
    var installDate: Date
    var manifestData: Data
}

Configuration models

BrowserConfig

Location: Nook/Models/BrowserConfig/BrowserConfig.swift:12 Pattern: Singleton Provides shared WKWebViewConfiguration for all webviews.
class BrowserConfiguration {
    static let shared = BrowserConfiguration()
    
    lazy var webViewConfiguration: WKWebViewConfiguration = {
        let config = WKWebViewConfiguration()
        config.websiteDataStore = WKWebsiteDataStore.default()
        config.preferences.javaScriptCanOpenWindowsAutomatically = true
        config.mediaTypesRequiringUserActionForPlayback = []
        config.preferences.isElementFullscreenEnabled = true
        // ... extension controller set by ExtensionManager
        return config
    }()
    
    // Profile-specific config derivation
    func webViewConfiguration(for profile: Profile) -> WKWebViewConfiguration {
        let config = webViewConfiguration.copy() as! WKWebViewConfiguration
        config.userContentController = freshUserContentController()
        config.websiteDataStore = profile.dataStore
        return config
    }
}
Critical pattern (BrowserConfig.swift:92-102):
  • All webview configs MUST derive from the base config via .copy()
  • This ensures extension controller and process pool are shared
  • Direct instantiation of WKWebViewConfiguration() breaks extension support
See WebView coordination for details.

Value types and snapshots

PersistenceActor snapshots

Location: Nook/Managers/TabManager/TabManager.swift:33-82 Lightweight, serializable representations for atomic persistence.
struct SnapshotTab: Codable {
    let id: UUID
    let urlString: String
    let name: String
    let index: Int
    let spaceId: UUID?
    let isPinned: Bool
    let isSpacePinned: Bool
    let profileId: UUID?
    let folderId: UUID?
    let currentURLString: String?
    let canGoBack: Bool
    let canGoForward: Bool
}

struct SnapshotSpace: Codable { ... }
struct SnapshotFolder: Codable { ... }
struct SnapshotState: Codable { ... }

struct Snapshot: Codable {
    let spaces: [SnapshotSpace]
    let tabs: [SnapshotTab]
    let folders: [SnapshotFolder]
    let state: SnapshotState
}
Purpose: Decouple persistence from observable models to avoid actor isolation issues.

Model conversion patterns

Observable → Entity

// TabManager creates snapshots
let snapshotTabs = tabs.map { tab in
    PersistenceActor.SnapshotTab(
        id: tab.id,
        urlString: tab.url.absoluteString,
        name: tab.name,
        // ...
    )
}

// PersistenceActor upserts entities
func upsertTab(in ctx: ModelContext, tab: SnapshotTab) throws {
    let existing = try ctx.fetch(
        FetchDescriptor<TabEntity>(predicate: #Predicate { $0.id == tab.id })
    ).first
    
    if let entity = existing {
        entity.urlString = tab.urlString
        entity.name = tab.name
        // ...
    } else {
        let entity = TabEntity(id: tab.id, urlString: tab.urlString, ...)
        ctx.insert(entity)
    }
}

Entity → Observable

// TabManager restores from entities
func loadTabs() {
    let entities = try context.fetch(FetchDescriptor<TabEntity>())
    self.tabs = entities.map { entity in
        Tab(
            id: entity.id,
            url: URL(string: entity.urlString) ?? defaultURL,
            name: entity.name,
            index: entity.index,
            spaceId: entity.spaceId
        )
    }
}

Observable patterns

Swift Observation (@Observable)

Modern approach used by Space, Profile, BrowserWindowState, WebViewCoordinator, WindowRegistry.
@MainActor
@Observable
class Profile {
    var name: String  // Automatically tracked
    var icon: String  // Automatically tracked
    
    // Explicit non-tracking
    @ObservationIgnored
    private var cache: [String: Any] = [:]
}
Usage in SwiftUI:
@Environment(Profile.self) private var profile
// or
@Bindable var space: Space

Combine (@Published)

Legacy approach used by BrowserManager, Tab (hybrid).
@MainActor
final class BrowserManager: ObservableObject {
    @Published var currentTab: Tab?
    @Published var spaces: [Space] = []
}
Usage in SwiftUI:
@EnvironmentObject var browserManager: BrowserManager
// or
@ObservedObject var tab: Tab

Best practices

Use @Observable for new modelsSwift Observation (@Observable) is the modern approach with better performance and cleaner syntax. Only use @Published for legacy compatibility.
Keep models MainActor-confinedAll models that interact with SwiftUI must be @MainActor to prevent data races.
Use snapshots for async persistenceDon’t pass observable models directly to actors. Convert to Codable snapshots first to avoid actor isolation errors.
Validate entity → model conversionsAlways handle missing/corrupt data when restoring from SwiftData. Use default values or skip invalid entities.

Next steps

State management

Learn about @Observable vs @Published and persistence patterns

WebView coordination

Understand how BrowserConfig and profiles interact with webviews