HyperbasisHyperbasisDocs
Home

HBStorage

The main persistence API for saving, loading, and syncing spaces and anchors.

Overview

HBStorage is the primary interface for all persistence operations. It handles local file storage and optional cloud sync.

let storage = HBStorage()
// or
let storage = HBStorage(config: .default)

Initialization

Local-only storage (default)

let storage = HBStorage()

With cloud sync

let storage = HBStorage(config: HBStorageConfig(
    backend: .supabase(
        url: "https://xxx.supabase.co",
        anonKey: "eyJ..."
    ),
    syncStrategy: .onSave,
    compression: .balanced
))

Space Operations

Save a space

public func save(_ space: HBSpace) async throws
let worldMap = try await arSession.currentWorldMap
let space = try HBSpace(name: "Living Room", worldMap: worldMap)
try await storage.save(space)

Load a space by ID

public func loadSpace(id: UUID) async throws -> HBSpace?
if let space = try await storage.loadSpace(id: savedId) {
    let worldMap = try space.arWorldMap()
    // Use worldMap for relocalization
}

Load all spaces

public func loadAllSpaces() async throws -> [HBSpace]
let spaces = try await storage.loadAllSpaces()
let mostRecent = spaces.max(by: { $0.updatedAt < $1.updatedAt })

Delete a space

public func deleteSpace(id: UUID) async throws
Warning

Deleting a space also deletes all anchors associated with it. This operation cannot be undone.

Anchor Operations

Save an anchor

public func save(_ anchor: HBAnchor) async throws
let anchor = HBAnchor(
    spaceId: space.id,
    transform: entity.transform.matrix,
    metadata: ["text": .string("Hello")]
)
try await storage.save(anchor)

Load anchors for a space

public func loadAnchors(spaceId: UUID, includeDeleted: Bool = false) async throws -> [HBAnchor]
// Active anchors only
let anchors = try await storage.loadAnchors(spaceId: space.id)

// Include soft-deleted anchors
let allAnchors = try await storage.loadAnchors(spaceId: space.id, includeDeleted: true)

Load a single anchor

public func loadAnchor(id: UUID) async throws -> HBAnchor?

Delete an anchor

public func deleteAnchor(id: UUID) async throws

This performs a soft delete - the anchor's deletedAt is set but it remains in storage.

Purge deleted anchors

public func purgeDeletedAnchors(before: Date) async throws
// Permanently remove anchors deleted more than 30 days ago
let cutoff = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: cutoff)

Sync Operations

Manual sync

public func sync() async throws

Only needed if using .manual sync strategy:

do {
    try await storage.sync()
    print("Sync complete")
} catch HBStorageError.cloudNotConfigured {
    print("Cloud not configured")
}

Sync status

// Check if cloud is configured
public var isCloudEnabled: Bool

// Number of pending operations
public var pendingOperationCount: Int

Versioning Operations

Get timeline for a space

public func timeline(spaceId: UUID) async throws -> HBTimeline
let timeline = try await storage.timeline(spaceId: space.id)
print("Events: \(timeline.events.count)")
print("Time span: \(timeline.startDate) to \(timeline.endDate)")

Get anchors at a point in time

public func anchorsAt(spaceId: UUID, date: Date) async throws -> [HBAnchor]
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: Date())!
let pastAnchors = try await storage.anchorsAt(spaceId: space.id, date: yesterday)

Generate a diff

public func diff(spaceId: UUID, from: Date, to: Date) async throws -> HBDiff
let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!
let diff = try await storage.diff(spaceId: space.id, from: oneWeekAgo, to: Date())
print(diff.summary) // "3 added, 1 removed"

Get anchor history

public func history(anchorId: UUID) async throws -> [HBAnchorEvent]
let events = try await storage.history(anchorId: anchor.id)
for event in events {
    print("v\(event.version): \(event.type) at \(event.timestamp)")
}

Rollback to previous version

@discardableResult
public func rollback(anchorId: UUID, toVersion: Int) async throws -> HBAnchor
// Restore to version 2
let restored = try await storage.rollback(anchorId: anchor.id, toVersion: 2)
Note

Rollback creates a new restored event - it doesn't delete history. The full event log is preserved.

Data Management

Clear local storage

public func clearLocalStorage() throws
Error

This permanently deletes all local data including spaces and anchors. Use with caution.

Get storage size

public func localStorageSize() throws -> Int

Returns total size of local storage in bytes.

Storage Size Expectations

Understanding typical storage requirements helps with planning and cleanup:

Size by Content

ContentTypical SizeNotes
1 space (world map)3-30 MBDepends on environment size
1 anchor0.5-2 KBVaries by metadata complexity
100 anchors50-200 KBNegligible compared to spaces
1000 anchors0.5-2 MBStill small vs world maps
Pending sync operation~1 KBQueued operations

Key insight: World maps dominate storage. Anchors are negligible.

Realistic Scenarios

ScenarioSpacesAnchorsEst. Total Size
Single room, 10 notes1105-15 MB
Home with 3 rooms35015-50 MB
Office with many markers520030-100 MB
Heavy usage over months201000100-400 MB

Monitoring Storage

func checkStorageHealth() throws {
    let size = try storage.localStorageSize()
    let sizeInMB = Double(size) / 1_000_000

    print("Storage: \(String(format: "%.1f", sizeInMB)) MB")

    if sizeInMB > 100 {
        print("Consider cleaning up old spaces")
    }
}

Cleanup Strategies

Purge Old Deleted Anchors

Soft-deleted anchors remain in storage until explicitly purged:

// Remove anchors deleted more than 30 days ago
let cutoff = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
try await storage.purgeDeletedAnchors(before: cutoff)

Remove Unused Spaces

Delete spaces that haven't been used recently:

let ninetyDaysAgo = Calendar.current.date(byAdding: .day, value: -90, to: Date())!

for space in try await storage.loadAllSpaces() {
    if space.updatedAt < ninetyDaysAgo {
        // Check if it has any active anchors
        let anchors = try await storage.loadAnchors(spaceId: space.id)
        if anchors.isEmpty {
            try await storage.deleteSpace(id: space.id)
            print("Deleted unused space: \(space.name ?? space.id.uuidString)")
        }
    }
}

Automatic Cleanup on Launch

func performMaintenanceIfNeeded() async throws {
    let lastCleanup = UserDefaults.standard.object(forKey: "lastCleanup") as? Date
    let oneWeekAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())!

    guard lastCleanup == nil || lastCleanup! < oneWeekAgo else { return }

    // Purge old deleted anchors
    let thirtyDaysAgo = Calendar.current.date(byAdding: .day, value: -30, to: Date())!
    try await storage.purgeDeletedAnchors(before: thirtyDaysAgo)

    UserDefaults.standard.set(Date(), forKey: "lastCleanup")
}

User-Initiated Cleanup

Let users manage their storage:

struct StorageManagementView: View {
    @State private var storageSize: Int = 0
    @State private var spaces: [HBSpace] = []

    var body: some View {
        List {
            Section("Storage Used") {
                Text(ByteCountFormatter.string(
                    fromByteCount: Int64(storageSize),
                    countStyle: .file
                ))
            }

            Section("Saved Spaces") {
                ForEach(spaces) { space in
                    HStack {
                        Text(space.name ?? "Unnamed")
                        Spacer()
                        Text(space.worldMapSizeFormatted)
                            .foregroundColor(.secondary)
                    }
                }
                .onDelete(perform: deleteSpaces)
            }
        }
    }
}

Internal Flow

When save(_ space:) is called:

  1. Compress worldMapData using configured compression level
  2. Create internal HBStorageSpace with compression flag
  3. Save to HBLocalStore (always)
  4. If syncStrategy == .onSave and cloud is configured:
    • Upload to HBCloudStore
    • On failure: queue operation for retry

When loadSpace(id:) is called:

  1. Try loading from HBLocalStore
  2. If not found and cloud is configured, try HBCloudStore
  3. If found in cloud, cache locally
  4. Decompress worldMapData
  5. Return HBSpace

Next Steps