Skip to main content
The official Swift SDK for TrailBase provides a type-safe, async/await client for accessing your TrailBase backend from iOS, macOS, watchOS, and tvOS applications.

Installation

Swift Package Manager

Add TrailBase to your Package.swift:
dependencies: [
    .package(url: "https://github.com/trailbaseio/trailbase.git", branch: "main")
]
Or add it via Xcode:
  1. File → Add Package Dependencies
  2. Enter: https://github.com/trailbaseio/trailbase.git
  3. Select the TrailBase product

Platform Support

  • iOS 13.0+
  • macOS 10.15+
  • watchOS 6.0+
  • tvOS 13.0+
  • Mac Catalyst 13.0+

Initialization

Basic Client

import TrailBase

let client = try Client(site: "https://your-server.trailbase.io")

Client with Tokens

import TrailBase

let tokens = Tokens(
    auth_token: "your-auth-token",
    refresh_token: "your-refresh-token",
    csrf_token: "your-csrf-token"
)

let client = try await Client.withTokens(
    site: "https://your-server.trailbase.io",
    tokens: tokens
)

Authentication

Login

do {
    let tokens = try await client.login(
        email: "[email protected]",
        password: "password"
    )
    print("Auth token: \(tokens.auth_token)")
    
    if let user = client.user() {
        print("Logged in as: \(user.email)")
    }
} catch {
    print("Login failed: \(error)")
}

Logout

try await client.logout()

Current User

if let user = client.user() {
    print("User ID: \(user.sub)")
    print("Email: \(user.email)")
}

Access Tokens

if let tokens = client.tokens() {
    // Persist tokens for later use
    let encoder = JSONEncoder()
    if let data = try? encoder.encode(tokens) {
        UserDefaults.standard.set(data, forKey: "trailbase_tokens")
    }
}

Refresh Token

try await client.refreshAuthToken()

Record API

Define Your Record Types

import Foundation

struct Post: Codable {
    let id: String
    let title: String
    let content: String
    let author_id: String
    let created_at: Int64
}

struct NewPost: Codable {
    let title: String
    let content: String
}

List Records

let posts = RecordApi(client: client, name: "posts")

let pagination = Pagination(
    cursor: nil,
    limit: 10,
    offset: 0
)

let response: ListResponse<Post> = try await posts.list(
    pagination: pagination,
    order: ["-created_at"],
    count: true
)

print("Records: \(response.records.count)")
print("Total count: \(response.total_count ?? 0)")
print("Next cursor: \(response.cursor ?? "none")")

Read a Record

// String ID
let post: Post = try await posts.read(recordId: .string("post-id"))

// Integer ID
let post: Post = try await posts.read(recordId: .int(123))

// With expanded relationships
let postWithAuthor: Post = try await posts.read(
    recordId: .string("post-id"),
    expand: ["author"]
)

print("Title: \(post.title)")

Create a Record

let newPost = NewPost(
    title: "Hello World",
    content: "My first post from Swift"
)

let postId = try await posts.create(record: newPost)
print("Created post with ID: \(postId)")

Update a Record

struct PostUpdate: Codable {
    let title: String
}

let update = PostUpdate(title: "Updated Title")
try await posts.update(recordId: .string("post-id"), record: update)

Delete a Record

try await posts.delete(recordId: .string("post-id"))

Filtering

// Simple equality filter
let response: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(column: "author_id", value: userId)
    ]
)

// With comparison operators
let weekAgo = Date().addingTimeInterval(-7 * 24 * 60 * 60)
let timestamp = String(Int64(weekAgo.timeIntervalSince1970))

let recentPosts: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(
            column: "created_at",
            op: .GreaterThan,
            value: timestamp
        )
    ]
)

// LIKE operator for text search
let searchResults: ListResponse<Post> = try await posts.list(
    filters: [
        .Filter(
            column: "title",
            op: .Like,
            value: "%search%"
        )
    ]
)

// AND composite filter
let filtered: ListResponse<Post> = try await posts.list(
    filters: [
        .And(filters: [
            .Filter(column: "status", value: "published"),
            .Filter(column: "author_id", value: userId)
        ])
    ]
)

// OR composite filter
let filtered: ListResponse<Post> = try await posts.list(
    filters: [
        .Or(filters: [
            .Filter(column: "category", value: "tech"),
            .Filter(column: "category", value: "science")
        ])
    ]
)

Available Comparison Operators

public enum CompareOp {
    case Equal
    case NotEqual
    case LessThan
    case LessThanEqual
    case GreaterThan
    case GreaterThanEqual
    case Like
    case Regexp
    case StWithin      // Geospatial
    case StIntersects  // Geospatial
    case StContains    // Geospatial
}

RecordId

public enum RecordId: CustomStringConvertible {
    case string(String)
    case int(Int64)
    
    public var description: String {
        switch self {
        case .string(let id): return id
        case .int(let id): return String(id)
        }
    }
}

Error Handling

do {
    let post: Post = try await posts.read(recordId: .string("post-id"))
} catch ClientError.invalidStatusCode(let code, let body) {
    print("HTTP \(code): \(body ?? "no body")")
} catch ClientError.invalidJwt {
    print("Invalid JWT token")
} catch ClientError.unauthenticated {
    print("Not authenticated")
} catch {
    print("Error: \(error)")
}

ClientError Enum

public enum ClientError: Error {
    case invalidUrl
    case invalidStatusCode(code: Int, body: String?)
    case invalidResponse(String?)
    case invalidJwt
    case unauthenticated
    case invalidFilter(String)
}

Type Definitions

User

public struct User: Hashable, Equatable {
    let sub: String
    let email: String
}

Tokens

public struct Tokens: Codable, Hashable, Equatable, Sendable {
    let auth_token: String
    let refresh_token: String?
    let csrf_token: String?
}

ListResponse

public struct ListResponse<T: Decodable>: Decodable {
    public let cursor: String?
    public let total_count: Int64?
    public let records: [T]
}

Pagination

public struct Pagination {
    public var cursor: String? = nil
    public var limit: UInt? = nil
    public var offset: UInt? = nil
    
    public init(cursor: String? = nil, limit: UInt? = nil, offset: UInt? = nil)
}

SwiftUI Integration

import SwiftUI
import TrailBase

struct Post: Codable, Identifiable {
    let id: String
    let title: String
    let content: String
}

class PostsViewModel: ObservableObject {
    @Published var posts: [Post] = []
    @Published var loading = false
    @Published var error: String?
    
    private var client: Client
    
    init() {
        self.client = try! Client(site: "https://your-server.trailbase.io")
    }
    
    func loadPosts() async {
        loading = true
        error = nil
        
        do {
            let posts = RecordApi(client: client, name: "posts")
            let response: ListResponse<Post> = try await posts.list(
                pagination: Pagination(limit: 20),
                order: ["-created_at"]
            )
            
            await MainActor.run {
                self.posts = response.records
                self.loading = false
            }
        } catch {
            await MainActor.run {
                self.error = error.localizedDescription
                self.loading = false
            }
        }
    }
}

struct PostsView: View {
    @StateObject private var viewModel = PostsViewModel()
    
    var body: some View {
        NavigationView {
            Group {
                if viewModel.loading {
                    ProgressView()
                } else if let error = viewModel.error {
                    Text("Error: \(error)")
                } else {
                    List(viewModel.posts) { post in
                        VStack(alignment: .leading) {
                            Text(post.title)
                                .font(.headline)
                            Text(post.content)
                                .font(.subheadline)
                                .foregroundColor(.gray)
                        }
                    }
                }
            }
            .navigationTitle("Posts")
            .task {
                await viewModel.loadPosts()
            }
        }
    }
}

Best Practices

Use async/await for all network operations. The SDK is built with Swift’s modern concurrency in mind.
Store tokens securely using Keychain Services, not UserDefaults. Consider using a package like KeychainAccess.
The client automatically refreshes auth tokens before they expire.

Example Application

import Foundation
import TrailBase

struct Post: Codable {
    let id: String
    let title: String
    let content: String
    let published: Bool
}

@main
struct TrailBaseExample {
    static func main() async throws {
        // Initialize client
        let url = ProcessInfo.processInfo.environment["TRAILBASE_URL"] 
            ?? "http://localhost:4000"
        let client = try Client(site: url)
        
        // Login
        do {
            _ = try await client.login(
                email: ProcessInfo.processInfo.environment["TRAILBASE_EMAIL"]!,
                password: ProcessInfo.processInfo.environment["TRAILBASE_PASSWORD"]!
            )
            
            if let user = client.user() {
                print("Logged in as: \(user.email)")
            }
        } catch {
            print("Login failed: \(error)")
            return
        }
        
        // List posts
        let posts = RecordApi(client: client, name: "posts")
        
        let response: ListResponse<Post> = try await posts.list(
            pagination: Pagination(limit: 10),
            order: ["-created_at"],
            filters: [
                .Filter(column: "published", value: "true")
            ]
        )
        
        print("\nFound \(response.records.count) posts:")
        for post in response.records {
            print("- \(post.title)")
        }
        
        // Create a new post
        struct NewPost: Codable {
            let title: String
            let content: String
            let published: Bool
        }
        
        let newPost = NewPost(
            title: "Hello from Swift",
            content: "This post was created using the TrailBase Swift SDK",
            published: true
        )
        
        let newPostId = try await posts.create(record: newPost)
        print("\nCreated new post with ID: \(newPostId)")
        
        // Read the post
        let post: Post = try await posts.read(recordId: newPostId)
        print("Post title: \(post.title)")
        
        // Logout
        try await client.logout()
        print("\nLogged out")
    }
}

Build docs developers (and LLMs) love