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