Pular para o conteúdo
🍎 iOS / SWIFT

SuperDB com iOS nativo.

SDK Swift ainda não disponível — use URLSession + Keychain diretamente com a REST API hoje.

🚧

SDK Swift ainda não disponível. Não existe um pacote Swift Package Manager oficial para o SuperDB ainda.

Enquanto isso, a integração via URLSession funciona perfeitamente — o SuperDB usa APIs REST padrão HTTP/JSON. Precisa do SDK Swift? Conte pelo suporte →

Abordagem: URLSession + Keychain

No iOS nativo, o fluxo é:

  1. Fazer login via URLSession chamando a API REST do SuperDB
  2. Armazenar os tokens no Keychain do iOS — o equivalente seguro ao SecureStore do Expo. São dois: access_token (sessão) e data_plane_token (dados)
  3. Mandar o data_plane_token em Authorization: Bearer nas chamadas a api.superdb.com.br — o access_token não é aceito lá

Se preferir usar Alamofire, a lógica é idêntica — apenas a camada de HTTP muda.

Serviço de autenticação

SuperDBAuth.swift
import Foundation
import Security

struct AuthResponse: Codable {
    let accessToken: String      // sessão — auth.superdb.com.br
    let dataPlaneToken: String   // dados  — api.superdb.com.br
    let refreshToken: String
    let user: UserInfo?

    enum CodingKeys: String, CodingKey {
        case accessToken    = "access_token"
        case dataPlaneToken = "data_plane_token"
        case refreshToken   = "refresh_token"
        case user
    }
}

struct UserInfo: Codable {
    let id: String
    let email: String?
}

class SuperDBAuth {
    static let shared = SuperDBAuth()

    private let baseURL = "https://auth.superdb.com.br"
    private let anonKey = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    // O slug do projeto. É ELE que diz ao servidor de qual tenant se trata — a
    // anon key não faz isso. Sem este header, toda chamada de auth devolve
    // 404 tenant_not_found, fácil de confundir com URL errada.
    private let projeto = "meu-projeto"
    private let tokenKey = "superdb_access_token"
    private let dataPlaneKey = "superdb_data_plane_token"

    private var defaultHeaders: [String: String] {
        ["Content-Type": "application/json", "X-SuperDB-Project": projeto, "apikey": anonKey]
    }

    // MARK: - Keychain helpers
    // São duas contas no Keychain porque são dois tokens com destinos
    // diferentes: sessão e dados.
    private func save(_ token: String, conta: String) {
        let data = Data(token.utf8)
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: conta,
            kSecValueData as String: data
        ]
        SecItemDelete(query as CFDictionary)
        SecItemAdd(query as CFDictionary, nil)
    }

    private func read(conta: String) -> String? {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: conta,
            kSecReturnData as String: true,
            kSecMatchLimit as String: kSecMatchLimitOne
        ]
        var result: AnyObject?
        SecItemCopyMatching(query as CFDictionary, &result)
        guard let data = result as? Data else { return nil }
        return String(data: data, encoding: .utf8)
    }

    /// Guarde os DOIS depois do login: `save(r.accessToken, ...)` e
    /// `save(r.dataPlaneToken, ...)`.
    func saveTokens(_ r: AuthResponse) {
        save(r.accessToken, conta: tokenKey)
        save(r.dataPlaneToken, conta: dataPlaneKey)
    }

    /// Sessão — auth.superdb.com.br.
    func getToken() -> String? { read(conta: tokenKey) }

    /// Dados — api.superdb.com.br. Vale 1 hora; renove em
    /// POST /auth/v1/token/refresh com o refresh_token.
    func getDataPlaneToken() -> String? { read(conta: dataPlaneKey) }

    func deleteToken() {
        let query: [String: Any] = [
            kSecClass as String: kSecClassGenericPassword,
            kSecAttrAccount as String: tokenKey
        ]
        SecItemDelete(query as CFDictionary)
    }

    // MARK: - Auth
    func signIn(email: String, password: String) async throws -> AuthResponse {
        var request = URLRequest(url: URL(string: "\(baseURL)/auth/v1/signin")!)
        request.httpMethod = "POST"
        defaultHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
        request.httpBody = try JSONEncoder().encode(["email": email, "password": password])

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }

        let authResponse = try JSONDecoder().decode(AuthResponse.self, from: data)
        saveTokens(authResponse)   // guarda a sessão E o token de dados
        return authResponse
    }

    func signUp(email: String, password: String) async throws -> AuthResponse {
        var request = URLRequest(url: URL(string: "\(baseURL)/auth/v1/signup")!)
        request.httpMethod = "POST"
        defaultHeaders.forEach { request.setValue($1, forHTTPHeaderField: $0) }
        request.httpBody = try JSONEncoder().encode(["email": email, "password": password])

        let (data, response) = try await URLSession.shared.data(for: request)
        guard let httpResponse = response as? HTTPURLResponse,
              (200...201).contains(httpResponse.statusCode) else {
            throw URLError(.badServerResponse)
        }
        return try JSONDecoder().decode(AuthResponse.self, from: data)
    }

    func signOut() async {
        deleteToken()
    }
}

Queries no banco de dados

SuperDBDatabase.swift
class SuperDBDatabase {
    // Dados: api.superdb.com.br (NÃO auth.superdb.com.br)
    // Path: raiz da tabela, sem prefixo /rest/v1/
    private let dataURL = "https://api.superdb.com.br"

    func fetch<T: Decodable>(
        from table: String,
        type: T.Type,
        query: String = ""
    ) async throws -> [T] {
        // É o data_plane_token, não o access_token. Mandar o de sessão
        // aqui devolve 401 — os dois vêm no mesmo corpo do login.
        guard let token = SuperDBAuth.shared.getDataPlaneToken() else {
            throw URLError(.userAuthenticationRequired)
        }

        // Correto: api.superdb.com.br/ (sem /rest/v1/)
        // O tenant sai do próprio token; não há header de schema a montar.
        let urlString = "\(dataURL)/\(table)\(query)"
        var request = URLRequest(url: URL(string: urlString)!)
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")

        let (data, _) = try await URLSession.shared.data(for: request)
        return try JSONDecoder().decode([T].self, from: data)
    }
}

// Uso:
struct Post: Codable {
    let id: Int
    let title: String
    let body: String?
}

// Em um ViewModel ou View:
let db = SuperDBDatabase()
let posts = try await db.fetch(
    from: "posts",
    type: Post.self,
    query: "?order=created_at.desc&limit=20"
)

Alternativa com Alamofire

Se preferir usar Alamofire (Swift Package Manager: https://github.com/Alamofire/Alamofire):

Com Alamofire
import Alamofire

// Dados pedem o data_plane_token — não o de sessão.
let token = SuperDBAuth.shared.getDataPlaneToken() ?? ""

// Dados: api.superdb.com.br/ (sem /rest/v1/)
AF.request(
    "https://api.superdb.com.br/posts",
    headers: ["Authorization": "Bearer \(token)"]
)
.responseDecodable(of: [Post].self) { response in
    switch response.result {
    case .success(let posts):
        print("Posts: \(posts)")
    case .failure(let error):
        print("Erro: \(error)")
    }
}

Quando o SDK Swift estiver disponível

Essa página ajudou?