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:

OperatorDescriptionExample (Kotlin)
eqEquals.eq("status", "active")
neqNot equals.neq("status", "deleted")
gtGreater than.gt("age", 18)
gteGreater than or equal.gte("age", 18)
ltLess than.lt("price", 100)
lteLess than or equal.lte("price", 100)
likePattern matching.like("name", "john")
inIN operator.in("category", listOf("electronics", "books"))
not_inNOT IN.notIn("status", listOf("deleted", "archived"))
betweenBETWEEN.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")
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",
)