Docs

Quickstart: iOS (SwiftUI)

Build the notes app in SwiftUI: keep the session in the Keychain, follow sign-in state with auth.changes, and list and add the signed-in user's notes.

Updated Oct 24, 2026

A SwiftUI app (iOS 15+) where people sign up, sign in, add notes and see only their own. The SDK uses Foundation only.

#Before you start

Do steps 1 to 3 of the Quickstart first: create a project, copy its project URL and anon key (Quickstart), and create the notes table with its row rules (Quickstart). Every signed-in user then sees and changes only their own notes, whatever the app code does.

#1. Create the app and add the package

In Xcode create an App project (SwiftUI, Swift). Then File › Add Package Dependencies… and add the Sovaria package, product Sovaria.

https://github.com/sovaria/sovaria-swift.git   (from: 1.0.0)

#2. Keep the session in the Keychain

import Foundation
import Security
import Sovaria

final class KeychainStore: SessionStorage, @unchecked Sendable {
    private func query(_ key: String) -> [String: Any] {
        [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "sovaria", kSecAttrAccount as String: key]
    }
    func read(_ key: String) async -> String? {
        var q = query(key)
        q[kSecReturnData as String] = true
        var out: AnyObject?
        guard SecItemCopyMatching(q as CFDictionary, &out) == errSecSuccess, let data = out as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }
    func write(_ key: String, _ value: String) async {
        SecItemDelete(query(key) as CFDictionary)
        var q = query(key)
        q[kSecValueData as String] = Data(value.utf8)
        q[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock
        SecItemAdd(q as CFDictionary, nil)
    }
    func delete(_ key: String) async { SecItemDelete(query(key) as CFDictionary) }
}

#3. Connect and follow sign-in state

import Foundation
import Sovaria

@MainActor
final class NotesModel: ObservableObject {
    @Published var signedIn = false
    @Published var notes: [[String: Any]] = []
    @Published var error = ""
    private(set) var sovaria: Sovaria?

    func start() async {
        do {
            // Restores the saved session (refreshing it if it's about to expire).
            let client = try await Sovaria.initialize(url: "https://<project-ref>.sovarias.in",
                                                      anonKey: "<anon-key>", storage: KeychainStore())
            sovaria = client
            for await state in client.auth.changes {      // first event: .initialSession
                signedIn = state.session != nil
                if signedIn { await load() }
            }
        } catch { self.error = "\(error)" }
    }

    func signIn(_ email: String, _ password: String, create: Bool) async {
        guard let sovaria else { return }
        do {
            if create { _ = try await sovaria.auth.signUp(email: email, password: password) }
            else { _ = try await sovaria.auth.signInWithPassword(email: email, password: password) }
        } catch let e as SovariaError { error = e.message } catch { self.error = "\(error)" }
    }

    // Rows come back keyed by column name. The row rules add "where owner_id = auth.uid()".
    func load() async {
        do { notes = try await sovaria?.db.sql("select id, body from notes order by id desc").rows ?? [] }
        catch { self.error = "\(error)" }
    }

    func add(_ body: String) async {
        do {
            _ = try await sovaria?.db.sql("insert into notes (body) values ($1)", [body])
            await load()
        } catch { self.error = "\(error)" }
    }

    func signOut() async { try? await sovaria?.auth.signOut() }
}

#4. The screens

import SwiftUI

struct ContentView: View {
    @StateObject private var model = NotesModel()
    @State private var email = "", password = "", draft = ""

    var body: some View {
        Group {
            if model.signedIn {
                List {
                    HStack {
                        TextField("Write a note", text: $draft)
                        Button("Add") { Task { await model.add(draft); draft = "" } }
                    }
                    ForEach(model.notes.indices, id: \.self) { i in Text(model.notes[i]["body"] as? String ?? "") }
                }
                .toolbar { Button("Sign out") { Task { await model.signOut() } } }
            } else {
                Form {
                    TextField("Email", text: $email).textInputAutocapitalization(.never)
                    SecureField("Password", text: $password)
                    Button("Sign in") { Task { await model.signIn(email, password, create: false) } }
                    Button("Create account") { Task { await model.signIn(email, password, create: true) } }
                }
            }
            if !model.error.isEmpty { Text(model.error).foregroundStyle(.red) }
        }
        .task { await model.start() }
    }
}

#5. Run it

Pick a simulator and press Run (⌘R). Create an account, add a note, sign out and back in: the note is still there, and a second account never sees it.

#Next steps

#Coming from Firebase or Supabase?

PlatformWhat this is called there
FirebaseFirebase iOS (Swift) setup
SupabaseSupabase Swift quickstart
SovariaQuickstart: iOS (SwiftUI)

Moving an existing app? Migrate from Supabase or Firebase imports users (with their passwords), data, files and rules.