Swift SDK

Swift Quick Start

WoWSQL Swift SDK — Get started in minutes

Get your API keys: In your WoWSQL project dashboard, go to Settings → API to find your anon key (wowsql_anon_...) and service role key (wowsql_service_...). The anon key is safe for client-side use (respects Row Level Security). The service role key bypasses RLS and should only be used server-side.

Installation

Official package: View on GitHub →

Swift Package Manager: Add to your Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.iOS(.v13), .macOS(.v10_15)],
    dependencies: [
        .package(url: "https://github.com/WoWSQL/WoWSQL-swift.git", from: "3.10.0")
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: ["WOWSQL"]
        ),
    ]
)

Or in Xcode: File → Add Package Dependencies → https://github.com/WoWSQL/WoWSQL-swift

Initialize Client

import WOWSQL

// Initialize client with your anon key (respects Row Level Security)
// URL resolves to: https://myproject.wowsqlconnect.com
// SDK talks to PostgREST at /rest/v1/
let client = WOWSQLClient(
    projectUrl: "myproject",
    apiKey: "wowsql_anon_your_key_here"
)

// Or with full URL
let clientFull = WOWSQLClient(
    projectUrl: "https://myproject.wowsqlconnect.com",
    apiKey: "wowsql_anon_your_key_here"
)

// Always use select() before get()/execute(). All calls are async/await.
let users: QueryResponse<[String: AnyCodable]> = try await client.table("users").select("*").get()

Query Data

Basic Queries

typealias Row = [String: AnyCodable]

let users: QueryResponse<Row> = try await client.table("users").select("*").get()
let adults: QueryResponse<Row> = try await client.table("users").select("*").gte("age", AnyCodable(18)).get()
let names: QueryResponse<Row> = try await client.table("users").select("id", "name").get()

Advanced Queries

let activeUsers: QueryResponse<Row> = try await client.table("users")
    .select("*")
    .filter("status", .eq, AnyCodable("active"))
    .filter("age", .gt, AnyCodable(18))
    .order("created_at", .desc)
    .limit(20)
    .offset(0)
    .get()
print("Found \(activeUsers.count) users")

Filter Helper Methods

let users: QueryResponse<Row> = try await client.table("users")
    .eq("status", AnyCodable("active"))
    .gt("age", AnyCodable(18))
    .like("email", "%@gmail.com")
    .inList("role", [AnyCodable("admin"), AnyCodable("moderator")])
    .between("created_at", AnyCodable("2024-01-01"), AnyCodable("2024-12-31"))
    .get()

GROUP BY and Aggregates

Basic GROUP BY

let result: QueryResponse<Row> = try await client.table("products")
    .select("category", "COUNT(*) as count", "AVG(price) as avg_price")
    .groupBy("category")
    .get()

HAVING Clause

let result: QueryResponse<Row> = try await client.table("products")
    .select("category", "COUNT(*) as count")
    .groupBy("category")
    .having("COUNT(*)", "gt", AnyCodable(10))
    .get()

CRUD Operations

Create Record

let created = try await client.table("users").create([
    "name": AnyCodable("John Doe"),
    "email": AnyCodable("john@example.com"),
    "age": AnyCodable(30)
])
print("Created ID: \(created.id)")

Update Record

_ = try await client.table("users").update(AnyCodable(123), data: ["email": AnyCodable("newemail@example.com")])

Delete Record

_ = try await client.table("users").delete(AnyCodable(123))

Get Single Record by ID

let user = try await client.table("users").getById(AnyCodable(123))
print("User: \(user["name"] ?? AnyCodable("")))")

Get First Record

let first: Row? = try await client.table("users").filter("status", .eq, AnyCodable("active")).first()
if let u = first { print("First: \(u["name"] ?? AnyCodable(""))") }

Filter Operators

Available operators: eq, neq, gt, gte, lt, lte, like, is, in, not_in, between, not_between. Use .eq("col", value), .filter("col", "op", value), etc.

Error Handling

do {
    let users: QueryResponse<Row> = try await client.table("users").select("*").get()
    print("Success: \(users.data.count) users")
} catch {
    print("Error: \(error.localizedDescription)")
}

Authentication

Authentication endpoints are served at /auth/v1/. Use your anon key for client-side auth operations.

Sign Up & Sign In

// Auth endpoints: POST /auth/v1/signup, POST /auth/v1/login
// Header: apikey: wowsql_anon_your_key_here
let auth = ProjectAuthClient(projectUrl: "myproject", apiKey: "wowsql_anon_your_key_here")
let signUp = try await auth.signUp(email: "user@example.com", password: "SecurePassword123", fullName: "John Doe")
print(signUp.session?.accessToken ?? "")   // session is optional since 3.10.0
let signIn = try await auth.signIn(email: "user@example.com", password: "SecurePassword123")
if signIn.requires2FA {
    // TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
    print("2FA required, method: \(signIn.method ?? "")")
} else {
    print(signIn.session?.accessToken ?? "")
}

CAPTCHA (Turnstile)

Optional. Only required when dashboard Attack Protection → Turnstile is enabled. Create a Cloudflare Turnstile widget for your app domain and paste the site key and secret in Attack Protection (WoWSQL does not share a platform widget). GET /auth/v1/settings returns your captcha.site_key. Render Turnstile with that key, then pass the token. Omit it when captcha is off — existing calls are unchanged.

try await auth.signUp(email: "user@example.com", password: "SecurePassword123", captchaToken: turnstileToken)
try await auth.signIn(email: "user@example.com", password: "SecurePassword123", captchaToken: turnstileToken)
try await auth.forgotPassword(email: "user@example.com", captchaToken: turnstileToken)
try await auth.sendOtp(email: "user@example.com", captchaToken: turnstileToken)
try await auth.sendMagicLink(email: "user@example.com", captchaToken: turnstileToken)
try await auth.resendVerification(email: "user@example.com", captchaToken: turnstileToken)

Password Reset

_ = try await auth.forgotPassword(email: "user@example.com")
_ = try await auth.resetPassword(token: "reset_token", newPassword: "NewSecurePassword123")

OTP Authentication

Provide exactly one of email or phone. Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91).

// Email OTP
try await auth.sendOtp(email: "user@example.com", purpose: "login")
let emailLogin = try await auth.verifyOtp(email: "user@example.com", otp: "123456", purpose: "login")

// Phone OTP (SMS) — E.164
try await auth.sendOtp(phone: "+15551234567", purpose: "login")
let phoneLogin = try await auth.verifyOtp(phone: "+15551234567", otp: "123456", purpose: "login")
print(phoneLogin)

OAuth Authentication

Setup checklist — do this once per provider, per project:
1. Dashboard → Project → Auth → Providers: enable the provider, paste Client ID and Client Secret (no leading/trailing spaces).
2. Copy the Redirect URI shown: https://<slug>.wowsqlconnect.com/auth/v1/oauth/<provider>/callback and register it in your provider's developer console.
3. The callback URL is handled entirely by WoWSQL — your app does not send an API key on it. WoWSQL exchanges the code and redirects to your frontend_redirect_uri with ?access_token=&refresh_token=.

// ── Step 1: Get the authorization URL ────────────────────────────────────────
// Returns [String: String] with authorization_url, provider, backend_callback_url, frontend_redirect_uri
let oauth = try await auth.getOAuthAuthorizationUrl(provider: "google", redirectUri: "https://yourapp.com/auth/callback")
// Open oauth["authorization_url"] in ASWebAuthenticationSession or SFSafariViewController
// Google calls: https://<slug>.wowsqlconnect.com/auth/v1/oauth/google/callback
// WoWSQL exchanges the code and redirects to your redirectUri.

// ── Step 2: App — handle the redirect ────────────────────────────────────────
// WoWSQL redirects to: https://yourapp.com/auth/callback?access_token=...&refresh_token=...
// Intercept the URL in your ASWebAuthenticationSession callback and parse the tokens.

// ── (Advanced) Manual code exchange — only if you handle the callback yourself
let session = try await auth.exchangeOAuthCallback(provider: "google", code: "authorization_code_from_callback",
                                                   redirectUri: "https://yourapp.com/auth/callback")
print("OAuth login: \(session.user?.id ?? "")")

Session and Token Refresh

Token refresh calls POST /auth/v1/refresh-token with the stored refresh token and stores the new token pair. From 3.10.0 every SDK uses this route; before 3.10.0 the TypeScript and Java SDKs called routes that do not exist, and Flutter and Rust had no refresh method.

// Current user → GET /auth/v1/me (uses the stored access token)
let me = try await auth.getUser()

// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
let refreshed = try await auth.refreshToken()
print(refreshed.session?.accessToken ?? "")

// Read or restore the stored session
let tokens = auth.getSession()   // ["access_token": ..., "refresh_token": ...]
auth.setSession(accessToken: "saved_access_token", refreshToken: "saved_refresh_token")

// Sign out → POST /auth/v1/logout
_ = try await auth.logout()

Two-Factor Authentication (TOTP)

Added in 3.10.0. When a user has TOTP two-factor authentication enabled, sign-in does not return a session. It returns a challenge instead: requires_2fa is true and a short-lived pending_token is set (SDK field names are in the example). Finish the sign-in with the verify call, using a 6-digit code from the authenticator app or one of the user's backup codes. Status, setup, enable, disable and recovery-code calls run as the signed-in user: the SDK sends your API key and the stored access token. Enabling 2FA and rotating recovery codes return new backup codes once; show them to the user and do not store them in plain text.

Swift 3.10.0 does not wrap /settings, anonymous sign-in, identity linking or email change; only the TypeScript, Python and Java SDKs do.

// 1. Enrol (user is signed in)
let setup = try await auth.setupTotp()              // POST /auth/v1/2fa/setup-totp
let uri = setup["otpauth_uri"]?.value as? String    // or setup["qr_code_url"] (PNG data URI when available)
let enabled = try await auth.enableTotp(code: "123456")  // POST /auth/v1/2fa/verify-and-enable
print(enabled["backup_codes"]?.value ?? "")         // shown once

// 2. Sign in when 2FA is on
let login = try await auth.signIn(email: "user@example.com", password: "SecurePassword123")
if login.requires2FA, let pending = login.pendingToken {
    // POST /auth/v1/2fa/verify-login
    let done = try await auth.verifyTwoFactorLogin(pendingToken: pending, code: "123456")
    // or: backupCode: "one-of-your-backup-codes"
    print(done.session?.accessToken ?? "")
}

// 3. Manage
let status = try await auth.getTwoFactorStatus()                  // enabled, method
let rotated = try await auth.regenerateRecoveryCodes(code: "123456")
_ = try await auth.disableTotp(code: "123456")                     // or backupCode: "..."

Storage Operations

Initialize Storage

let storage = WOWSQLStorage(
    projectUrl: "myproject",
    apiKey: "wowsql_anon_your_key_here"
)

File Operations

// Upload a local file
let uploaded = try await storage.uploadFromPath(filePath: "path/to/document.pdf", bucketName: "documents", path: "uploads/document.pdf")

// Public URL (public buckets)
let url = storage.getPublicUrl(bucketName: "documents", filePath: "uploads/document.pdf")

// Download bytes
let data = try await storage.download(bucketName: "documents", filePath: "uploads/document.pdf")

// List files
let files = try await storage.listFiles(bucketName: "documents", prefix: "uploads/")

// Delete file
_ = try await storage.deleteFile(bucketName: "documents", filePath: "uploads/document.pdf")

// Check quota
let quota = try await storage.getQuota()
print("\(quota.totalFiles) files, \(quota.totalSizeGb) GB")

Download or View File

Via REST: use GET /storage/v1/object/{bucket}/{path} for download, or GET /storage/v1/object/public/{bucket}/{path} for public file access.

The SDK storage client itself calls /api/v1/storage/projects/{project_slug}/… on your project host (buckets, files, stats).

Realtime

Subscribe to INSERT / UPDATE / DELETE, broadcast ephemeral events, and track presence. Uses the same anon or service_role key as REST.

WebSocket URL: wss://<project>.wowsqlconnect.com/realtime/v1/websocket?apikey=<wowsql_anon_... or wowsql_service_...>. Auth is always the query param. Enable a table first with POST /realtime/v1/enable and body {"schema_name":"public","table_name":"messages"}. Channel send / presence do not need a Postgres trigger. Payloads are capped at 64 KiB. Missing key closes 4001; invalid key closes 4003 (do not reconnect). Multi-replica deployments need REDIS_URL.

let unsub = client.realtime.subscribe(table: "messages") { change in
    print(change.event, change.newRow ?? change.oldRow as Any)
}

let channel = client.realtime.channel("chat")
channel.onBroadcast(event: "typing") { msg in print(msg["payload"] as Any) }
channel.onPresence { _ in print(channel.presenceState()) }
channel.subscribe()
channel.track(["user": "alice"])
channel.send(event: "typing", payload: ["user": "alice"])

Schema DDL (WOWSQLSchema)

Requires service role key. createTable requires primaryKey to name a column whose type is UUID. Use auto_increment on that column for DEFAULT gen_random_uuid(). Raw CREATE TABLE via execute SQL cannot use SERIAL PRIMARY KEY (or similar integer PK). Existing tables are unchanged.

let schema = WOWSQLSchema(
    projectUrl: "myproject",
    serviceKey: "wowsql_service_your_key_here"
)

_ = try await schema.createTable("items", columns: [
    ["name": "id", "type": "UUID", "auto_increment": true],
    ["name": "title", "type": "TEXT"],
], primaryKey: "id")

Example API Call (REST)

The SDK wraps PostgREST. Here's what the underlying HTTP calls look like:

Query (GET)

GET https://myproject.wowsqlconnect.com/rest/v1/users?select=id,email,name&status=eq.active&order=created_at.desc&limit=10
Headers:
  apikey: wowsql_anon_your_key_here
  Content-Type: application/json

Insert (POST)

POST https://myproject.wowsqlconnect.com/rest/v1/users
Headers:
  apikey: wowsql_anon_your_key_here
  Content-Type: application/json
  Prefer: return=representation

Body:
{
  "name": "John Doe",
  "email": "john@example.com",
  "age": 30
}

Update (PATCH)

PATCH https://myproject.wowsqlconnect.com/rest/v1/users?id=eq.123
Headers:
  apikey: wowsql_anon_your_key_here
  Content-Type: application/json
  Prefer: return=representation

Body:
{
  "email": "newemail@example.com"
}

Delete (DELETE)

DELETE https://myproject.wowsqlconnect.com/rest/v1/users?id=eq.123
Headers:
  apikey: wowsql_anon_your_key_here