Kotlin SDK
Kotlin Quick Start
WoWSQL Kotlin 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 Maven Central → (com.wowsql:wowsql-sdk — this is the only published Maven artifact.)
// Gradle (Kotlin DSL)
implementation("com.wowsql:wowsql-sdk:3.10.0")
Initialize Client
The SDK talks to PostgREST at /rest/v1/ under the hood. Your project URL resolves to https://myproject.wowsqlconnect.com.
import com.wowsql.sdk.WowSQLClient
// Initialize client — URL resolves to https://myproject.wowsqlconnect.com
// SDK communicates with PostgREST at /rest/v1/
val client = WowSQLClient(
projectUrl = "myproject",
apiKey = "wowsql_anon_your_key_here"
)
// Or with full URL
val clientFull = WowSQLClient(
projectUrl = "https://myproject.wowsqlconnect.com",
apiKey = "wowsql_anon_your_key_here"
)
// Always use select() before get()/execute()
val users = client.table<Map<String, Any?>>("users").select("*").get()
Query Data
Basic Queries
// Get all records
val users = client.table("users").select("*").get()
// With filters
val adults = client.table("users").select("*").gte("age", 18).get()
// Select specific columns
val names = client.table("users").select("id", "name").get()
Advanced Queries
// Multiple filters with sorting
val activeUsers = client.table("users")
.select("*")
.eq("status", "active")
.gt("age", 18)
.order("created_at", "desc")
.limit(20)
.offset(0)
.get()
println("Found ${activeUsers.count} users")
activeUsers.data.forEach { println(it["name"]) }
Filter Helper Methods
// Using helper methods: eq, gt, like, in, between
val users = client.table("users")
.eq("status", "active")
.gt("age", 18)
.like("email", "%@gmail.com")
.in("role", listOf("admin", "moderator"))
.between("created_at", "2024-01-01", "2024-12-31")
.get()
GROUP BY and Aggregates
Basic GROUP BY
// Group by category with counts
val result = client.table("products")
.select("category", "COUNT(*) as count", "AVG(price) as avg_price")
.groupBy("category")
.get()
// Group by date
val result2 = client.table("orders")
.select("DATE(created_at) as date", "COUNT(*) as orders", "SUM(total) as revenue")
.groupBy("DATE(created_at)")
.orderBy("date", "desc")
.get()
HAVING Clause
val result = client.table("products")
.select("category", "COUNT(*) as count")
.groupBy("category")
.having("COUNT(*)", "gt", 10)
.get()
CRUD Operations
Create Record
val result = client.table("users").create(mapOf(
"name" to "John Doe",
"email" to "john@example.com",
"age" to 30
))
println("Created user with ID: ${result["id"]}")
Update Record
client.table("users").update(123, mapOf("email" to "newemail@example.com"))
println("User updated successfully")
Delete Record
client.table("users").delete(123)
println("User deleted successfully")
Get Single Record by ID
val user = client.table("users").getById(123)
println("User: ${user?.get("name")}")
Get First Record
val user = client.table("users").eq("status", "active").first()
user?.let { println("First active user: ${it["name"]}") }
Filter Operators
Available operators for filtering data:
| Operator | Description | Example (Kotlin) |
|---|---|---|
eq | Equals | .eq("status", "active") |
neq | Not equals | .neq("status", "deleted") |
gt | Greater than | .gt("age", 18) |
gte | Greater than or equal | .gte("age", 18) |
lt | Less than | .lt("price", 100) |
lte | Less than or equal | .lte("price", 100) |
like | Pattern matching | .like("name", "john") |
in | IN operator | .in("category", listOf("electronics", "books")) |
not_in | NOT IN | .notIn("status", listOf("deleted", "archived")) |
between | BETWEEN | .between("price", 10, 100) |
Error Handling
try {
val users = client.table("users").select("*").get()
println("Success: ${users.data.size} users")
} catch (e: WowSQLException) {
when (e.statusCode) {
401 -> println("Authentication failed — check your API key")
404 -> println("Table or resource not found")
else -> println("Error (${e.statusCode}): ${e.message}")
}
}
Authentication
Auth endpoints are served at /auth/v1/. Use the anon key for client-side auth operations.
Sign Up & Sign In
val auth = ProjectAuthClient(projectUrl = "myproject", apiKey = "wowsql_anon_your_key_here")
// Sign up — POST /auth/v1/signup
val result = auth.signUp("user@example.com", "SecurePassword123", "John Doe", null)
println("Access token: ${result.session?.accessToken}") // session is nullable since 3.10.0
// Sign in — POST /auth/v1/login (returns a session, or a 2FA challenge)
val login = auth.signIn("user@example.com", "SecurePassword123")
if (login.requires2fa) {
// TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
println("2FA required, method: ${login.method}")
} else {
println("Access token: ${login.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.
auth.signUp(email = "user@example.com", password = "SecurePassword123", captchaToken = turnstileToken)
auth.signIn(email = "user@example.com", password = "SecurePassword123", captchaToken = turnstileToken)
auth.forgotPassword(email = "user@example.com", captchaToken = turnstileToken)
auth.sendOtp(email = "user@example.com", captchaToken = turnstileToken)
auth.sendMagicLink(email = "user@example.com", captchaToken = turnstileToken)
auth.resendVerification(email = "user@example.com", captchaToken = turnstileToken)
Password Reset
auth.forgotPassword("user@example.com")
auth.resetPassword("reset_token_from_email", "NewSecurePassword123")
OTP Authentication
Provide exactly one of email or phone.
Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91).
// Email OTP
auth.sendOtp(email = "user@example.com", purpose = "login")
val emailLogin = auth.verifyOtp(email = "user@example.com", otp = "123456", purpose = "login")
// Phone OTP (SMS)
auth.sendOtp(phone = "+15551234567", purpose = "login")
val phoneLogin = auth.verifyOtp(phone = "+15551234567", otp = "123456", purpose = "login")
println("Logged in: $phoneLogin")
Magic Link & Email Verification
auth.sendMagicLink("user@example.com", "login")
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: Server/ViewModel — get the authorization URL ─────────────────────
val oauthUrl = auth.getOAuthAuthorizationUrl(
"google",
"https://yourapp.com/auth/callback"
)
// Open a browser / custom tab to oauthUrl.authorizationUrl
// Google calls: https://<slug>.wowsqlconnect.com/auth/v1/oauth/google/callback
// WowSQL exchanges the code and redirects to your frontend_redirect_uri.
// ── Step 2: App — handle the redirect ────────────────────────────────────────
// WowSQL redirects to: https://yourapp.com/auth/callback?access_token=...&refresh_token=...
// Use an Android deep-link / intent-filter to intercept the URI and extract the tokens.
// ── (Advanced) Manual code exchange — only if you handle the callback yourself
val result = auth.exchangeOAuthCallback(
"google",
"authorization_code_from_callback",
"https://yourapp.com/auth/callback" // optional; the server exchanges the code with the project callback URL
)
println("OAuth login successful: ${result.user?.id ?: result.session?.accessToken}")
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)
val me = auth.getUser()
// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
val refreshed = auth.refreshToken()
println(refreshed.session?.accessToken)
// Read or restore the stored session
val tokens = auth.getSession() // mapOf("access_token" to ..., "refresh_token" to ...)
auth.setSession("saved_access_token", "saved_refresh_token")
// Sign out → POST /auth/v1/logout
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.
Not wrapped in this SDK in 3.10.0: rotating recovery codes (POST /auth/v1/2fa/recovery-codes). Call the REST endpoint directly with the apikey header and Authorization: Bearer <access token>; the body is {"code": "123456"} (disable also accepts {"backup_code": "..."}). Kotlin 3.10.0 also does not wrap /settings, anonymous sign-in, identity linking or email change.
// 1. Enrol (user is signed in)
val setup = auth.setupTotp() // POST /auth/v1/2fa/setup-totp
// Show setup["qr_code_url"] (a PNG data URI when available) or setup["otpauth_uri"]
val enabled = auth.enableTotp("123456") // POST /auth/v1/2fa/verify-and-enable
println(enabled["backup_codes"]) // shown once
// 2. Sign in when 2FA is on
val login = auth.signIn("user@example.com", "SecurePassword123")
if (login.requires2fa) {
// POST /auth/v1/2fa/verify-login
val done = auth.verifyTwoFactorLogin(login.pendingToken!!, code = "123456")
// or: backupCode = "one-of-your-backup-codes"
println(done.session?.accessToken)
}
// 3. Manage
val status = auth.getTwoFactorStatus() // enabled, method
auth.disableTotp(code = "123456") // or backupCode = "..."
With ViewModel (Android)
Use the client inside an Android ViewModel with coroutines:
class UsersViewModel(
private val client: WowSQLClient
) : ViewModel() {
private val _users = MutableLiveData<List<Map<String, Any?>>>()
val users: LiveData<List<Map<String, Any?>>> = _users
fun loadUsers() {
viewModelScope.launch {
try {
val response = client.table("users")
.select("*")
.limit(10)
.execute()
_users.value = response.data
} catch (e: Exception) {
// Handle error
}
}
}
}
Storage Operations
Storage endpoints are served at /storage/v1/.
Initialize Storage
The SDK storage client calls /api/v1/storage/projects/{project_slug}/… on your project host (buckets, files, stats); the REST routes below are served at /storage/v1/.
val storage = WowSQLStorage(
projectUrl = "myproject",
apiKey = "wowsql_anon_your_key_here"
)
File Operations
// Upload a local file: uploadFromPath(filePath, bucketName, path)
// path is the object path; its folder part ("uploads") is sent as the folder query parameter.
val result = storage.uploadFromPath("path/to/document.pdf", "documents", "uploads/document.pdf")
println("Uploaded: ${result.path} (${result.size} bytes)")
// Public URL (public buckets): /storage/v1/object/public/{bucket}/{path}
val url = storage.getPublicUrl("documents", "uploads/document.pdf")
// Expiring signed URLs (getFileUrl in earlier versions of this page) are not part of the SDK in 3.10.0.
// Use getPublicUrl for public buckets, or download() with your API key for private ones.
// Download bytes
val bytes = storage.download("documents", "uploads/document.pdf")
// List files: listFiles(bucketName, prefix, limit, offset)
val files = storage.listFiles("documents", prefix = "uploads/")
files.forEach { println("${it.path}: ${it.size} bytes") }
// Delete file: deleteFile(bucketName, filePath)
storage.deleteFile("documents", "uploads/document.pdf")
// Check usage (GET .../stats; getQuota() is an alias of getStats())
val stats = storage.getStats()
println("${stats.totalFiles} files, ${stats.totalSizeGb} GB used")
Download or View File
Via REST:
- Upload:
POST /storage/v1/object/{bucket}/{path} - Download:
GET /storage/v1/object/{bucket}/{path} - List:
POST /storage/v1/object/list/{bucket} - Public URL:
GET /storage/v1/object/public/{bucket}/{path}
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.
val unsub = client.realtime().subscribe("messages", { change ->
println("${change["event"]} ${change["new"]}")
}, schema = "public", event = "*")
val channel = client.realtime().channel("chat")
channel.onBroadcast("typing") { println(it["payload"]) }
channel.onPresence { println(channel.presenceState()) }
channel.subscribe()
channel.track(mapOf("user" to "alice"))
channel.send("typing", mapOf("user" to "alice"))
unsub()
channel.unsubscribe()
client.realtime().disconnect()
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.
val schema = WowSQLSchema(projectUrl = "myproject", apiKey = "wowsql_service_your_key_here")
schema.createTable(
tableName = "items",
columns = listOf(
mapOf("name" to "id", "type" to "UUID", "auto_increment" to true),
mapOf("name" to "title", "type" to "TEXT"),
),
primaryKey = "id",
)