Skip to main content

Installation

Add InsForge to your Swift Package Manager dependencies:
dependencies: [
    .package(url: "https://github.com/insforge/insforge-swift.git", from: "0.0.7")
]
import InsForge

let insforge = InsForgeClient(
    baseURL: URL(string: "https://your-app.insforge.app")!,
    anonKey: "your-anon-key"
)

Enable Logging (Optional)

For debugging, you can configure the SDK log level and destination:
let options = InsForgeClientOptions(
    global: .init(
        logLevel: .debug,
        logDestination: .osLog,
        logSubsystem: "com.example.MyApp"
    )
)

let insforge = InsForgeClient(
    baseURL: URL(string: "https://your-app.insforge.app")!,
    anonKey: "your-anon-key",
    options: options
)
Log Levels:
LevelDescription
.traceMost verbose, includes all internal details
.debugDetailed information for debugging
.infoGeneral operational information (default)
.warningWarnings that don’t prevent operation
.errorErrors that affect functionality
.criticalCritical failures
Log Destinations:
DestinationDescription
.consoleStandard output (print)
.osLogApple’s unified logging system (recommended for iOS/macOS)
.noneDisable logging
.customProvide your own LogHandler factory
Use .info or .error in production to avoid exposing sensitive data in logs.

connect()

Establish a WebSocket connection to the realtime server.

Example

do {
    try await insforge.realtime.connect()
    print("Connected to realtime server")
} catch {
    print("Connection failed: \(error)")
}

subscribe()

Subscribe to a channel to receive messages.

Parameters

  • to (String) - Channel name (e.g., “orders:123”, “chat:room-1”)
  • onMessage (Closure) - Callback for receiving messages

Example

await insforge.realtime.subscribe(to: "chat:lobby") { message in
    print("Event: \(message.eventName ?? "")")
    print("Channel: \(message.channelName ?? "")")

    if let payload = message.payload {
        print("Data: \(payload)")
    }

    if let senderId = message.senderId {
        print("From: \(senderId)")
    }
}

unsubscribe()

Unsubscribe from a channel.

Example

await insforge.realtime.unsubscribe(from: "chat:lobby")

publish()

Publish a message to a channel.

Parameters

  • to (String) - Channel name
  • event (String) - Event name
  • payload ([String: Any]) - Message payload as dictionary

Example

try await insforge.realtime.publish(
    to: "chat:lobby",
    event: "message.new",
    payload: [
        "text": "Hello everyone!",
        "author": currentUser.name,
        "timestamp": Date().timeIntervalSince1970
    ]
)

disconnect()

Disconnect from the realtime server.

Example

await insforge.realtime.disconnect()

Channel API

The high-level Channel API provides a more structured way to work with realtime features including broadcast messages and Postgres changes.

channel()

Get or create a channel instance.
let channel = await insforge.realtime.channel("chat:lobby")

removeChannel()

Remove a channel.
await insforge.realtime.removeChannel("chat:lobby")

RealtimeChannel

subscribe()

Subscribe to the channel. Must be called before receiving messages.
let channel = await insforge.realtime.channel("chat:lobby")
try await channel.subscribe()

unsubscribe()

Unsubscribe from the channel.
await channel.unsubscribe()

Broadcast Messages

Listening for Broadcasts

Use broadcast(event:) to receive broadcast messages as an AsyncStream.
let channel = await insforge.realtime.channel("chat:lobby")
try await channel.subscribe()

// Listen for specific event
for await message in await channel.broadcast(event: "new_message") {
    print("Event: \(message.event)")
    print("Payload: \(message.payload)")

    // Decode to typed struct
    if let chatMessage = try? message.decode(ChatMessage.self) {
        print("Message: \(chatMessage.text)")
    }
}

// Listen for all events (wildcard)
for await message in await channel.broadcast(event: "*") {
    print("Received: \(message.event)")
}

Sending Broadcasts

Send broadcast messages with typed or dictionary payloads.
// With Encodable struct
struct ChatMessage: Codable {
    let text: String
    let author: String
}

try await channel.broadcast(
    event: "new_message",
    message: ChatMessage(text: "Hello!", author: "John")
)

// With dictionary
try await channel.broadcast(
    event: "typing",
    message: ["userId": userId, "isTyping": true]
)

Postgres Changes

Listen for real-time database changes using typed actions.

Action Types

  • InsertAction<T> - New record inserted
  • UpdateAction<T> - Record updated (includes record and oldRecord)
  • DeleteAction<T> - Record deleted (includes oldRecord)
  • SelectAction<T> - Record selected
  • AnyAction<T> - Any of the above

Listening for Changes

struct Todo: Codable, Sendable {
    let id: String
    let title: String
    let completed: Bool
}

let channel = await insforge.realtime.channel("db-changes")
try await channel.subscribe()

// Listen for inserts only
for await action in await channel.postgresChange(
    InsertAction<Todo>.self,
    schema: "public",
    table: "todos"
) {
    print("New todo: \(action.record.title)")
}

// Listen for updates
for await action in await channel.postgresChange(
    UpdateAction<Todo>.self,
    schema: "public",
    table: "todos"
) {
    print("Updated: \(action.oldRecord.title) -> \(action.record.title)")
}

// Listen for deletes
for await action in await channel.postgresChange(
    DeleteAction<Todo>.self,
    schema: "public",
    table: "todos"
) {
    print("Deleted: \(action.oldRecord.title)")
}

// Listen for any change
for await action in await channel.postgresChange(
    AnyAction<Todo>.self,
    schema: "public",
    table: "todos"
) {
    switch action {
    case .insert(let insert):
        print("Inserted: \(insert.record.title)")
    case .update(let update):
        print("Updated: \(update.record.title)")
    case .delete(let delete):
        print("Deleted: \(delete.oldRecord.title)")
    case .select(let select):
        print("Selected: \(select.record.title)")
    }
}

Models Reference

RealtimeMessage

public struct RealtimeMessage: Codable, Sendable {
    let id: String?
    let eventName: String?
    let channelName: String?
    let payload: [String: AnyCodable]?
    let senderType: String?
    let senderId: String?
    let createdAt: Date?
}

BroadcastMessage

public struct BroadcastMessage: Sendable {
    let event: String
    let payload: [String: AnyCodable]
    let senderId: String?

    // Decode payload to typed struct
    func decode<T: Decodable>(_ type: T.Type) throws -> T
}

InsertAction

public struct InsertAction<Record: Codable & Sendable>: Codable, Sendable {
    let type: String           // "INSERT"
    let schema: String
    let table: String
    let record: Record
    let commitTimestamp: String?
}

UpdateAction

public struct UpdateAction<Record: Codable & Sendable>: Codable, Sendable {
    let type: String           // "UPDATE"
    let schema: String
    let table: String
    let record: Record         // New values
    let oldRecord: Record      // Previous values
    let commitTimestamp: String?
}

DeleteAction

public struct DeleteAction<Record: Codable & Sendable>: Codable, Sendable {
    let type: String           // "DELETE"
    let schema: String
    let table: String
    let oldRecord: Record      // Deleted record
    let commitTimestamp: String?
}

Channel

public struct Channel: Codable, Sendable {
    let id: String
    let pattern: String
    let description: String?
    let webhookUrls: [String]?
    let enabled: Bool
    let createdAt: Date
    let updatedAt: Date
}

SwiftUI Integration

Chat Room View

import SwiftUI

struct ChatRoomView: View {
    @StateObject private var viewModel = ChatViewModel()
    @State private var messageText = ""

    var body: some View {
        VStack {
            ScrollView {
                LazyVStack(alignment: .leading, spacing: 8) {
                    ForEach(viewModel.messages, id: \.id) { message in
                        ChatMessageView(message: message)
                    }
                }
                .padding()
            }

            HStack {
                TextField("Message", text: $messageText)
                    .textFieldStyle(.roundedBorder)

                Button("Send") {
                    Task {
                        await viewModel.sendMessage(messageText)
                        messageText = ""
                    }
                }
            }
            .padding()
        }
        .task {
            await viewModel.connect()
        }
        .onDisappear {
            Task {
                await viewModel.disconnect()
            }
        }
    }
}

@MainActor
class ChatViewModel: ObservableObject {
    @Published var messages: [ChatMessage] = []

    private let roomId = "room-1"
    private var channel: RealtimeChannel?
    private var messageTask: Task<Void, Never>?

    struct ChatMessage: Codable, Identifiable {
        let id: String
        let sender: String
        let text: String
        let timestamp: Date
    }

    func connect() async {
        do {
            try await insforge.realtime.connect()

            channel = await insforge.realtime.channel("chat:\(roomId)")
            try await channel?.subscribe()

            // Start listening for messages
            messageTask = Task {
                guard let channel = channel else { return }
                for await message in await channel.broadcast(event: "new_message") {
                    if let chatMessage = try? message.decode(ChatMessage.self) {
                        messages.append(chatMessage)
                    }
                }
            }
        } catch {
            print("Connection error: \(error)")
        }
    }

    func sendMessage(_ text: String) async {
        guard let channel = channel else { return }

        let message = ChatMessage(
            id: UUID().uuidString,
            sender: currentUser.name,
            text: text,
            timestamp: Date()
        )

        do {
            try await channel.broadcast(event: "new_message", message: message)
        } catch {
            print("Send error: \(error)")
        }
    }

    func disconnect() async {
        messageTask?.cancel()
        await channel?.unsubscribe()
        await insforge.realtime.disconnect()
    }
}

Order Tracking View

struct OrderTrackingView: View {
    let orderId: String
    @State private var status: String = "pending"
    @State private var updates: [StatusUpdate] = []

    struct StatusUpdate: Codable, Identifiable {
        let id: String
        let status: String
        let timestamp: Date
    }

    var body: some View {
        VStack {
            Text("Order Status: \(status)")
                .font(.headline)

            List(updates) { update in
                VStack(alignment: .leading) {
                    Text(update.status)
                        .font(.subheadline)
                    Text(update.timestamp, style: .time)
                        .font(.caption)
                }
            }
        }
        .task {
            await subscribeToUpdates()
        }
    }

    func subscribeToUpdates() async {
        do {
            try await insforge.realtime.connect()

            await insforge.realtime.subscribe(to: "order:\(orderId)") { message in
                if message.eventName == "status_changed",
                   let payload = message.payload,
                   let newStatus = payload["status"]?.value as? String {
                    Task { @MainActor in
                        status = newStatus
                        updates.append(StatusUpdate(
                            id: UUID().uuidString,
                            status: newStatus,
                            timestamp: Date()
                        ))
                    }
                }
            }
        } catch {
            print("Error: \(error)")
        }
    }
}

Database Sync View

struct TodoListView: View {
    @State private var todos: [Todo] = []
    private var changeTask: Task<Void, Never>?

    struct Todo: Codable, Sendable, Identifiable {
        let id: String
        let title: String
        let completed: Bool
    }

    var body: some View {
        List(todos) { todo in
            HStack {
                Text(todo.title)
                Spacer()
                if todo.completed {
                    Image(systemName: "checkmark.circle.fill")
                }
            }
        }
        .task {
            await loadTodos()
            await subscribeToChanges()
        }
    }

    func loadTodos() async {
        do {
            todos = try await insforge.database
                .from("todos")
                .select()
                .order("createdAt", ascending: false)
                .execute()
        } catch {
            print("Load error: \(error)")
        }
    }

    func subscribeToChanges() async {
        do {
            try await insforge.realtime.connect()

            let channel = await insforge.realtime.channel("todos-sync")
            try await channel.subscribe()

            // Listen for any changes to todos table
            for await action in await channel.postgresChange(
                AnyAction<Todo>.self,
                schema: "public",
                table: "todos"
            ) {
                await MainActor.run {
                    switch action {
                    case .insert(let insert):
                        todos.insert(insert.record, at: 0)
                    case .update(let update):
                        if let index = todos.firstIndex(where: { $0.id == update.record.id }) {
                            todos[index] = update.record
                        }
                    case .delete(let delete):
                        todos.removeAll { $0.id == delete.oldRecord.id }
                    case .select:
                        break
                    }
                }
            }
        } catch {
            print("Subscription error: \(error)")
        }
    }
}