Java SDK

Java Quick Start

WoWSQL Java 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-java). Kotlin uses a separate artifact: com.wowsql:wowsql-sdk.

Requirements: Java 11 or later. Classes live in the com.wowsql package (WOWSQLClient, ProjectAuthClient, WOWSQLStorage, WOWSQLSchema, WOWSQLException); response models are in com.wowsql.models.

Maven:

<dependency>
  <groupId>com.wowsql</groupId>
  <artifactId>wowsql-sdk-java</artifactId>
  <version>3.10.0</version>
</dependency>

Gradle:

implementation 'com.wowsql:wowsql-sdk-java: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.WOWSQLClient;
import com.wowsql.models.QueryResponse;
import java.util.*;

// Initialize client — URL resolves to https://myproject.wowsqlconnect.com
// SDK communicates with PostgREST at /rest/v1/
WOWSQLClient client = new WOWSQLClient(
    "https://myproject.wowsqlconnect.com",
    "wowsql_anon_your_key_here"
);

// Always use select() before get()/execute()
QueryResponse users = client.table("users").select("*").limit(10).get();

Query Data

Basic Queries

// Get all records
QueryResponse users = client.table("users").select("*").get();

// With filters
QueryResponse adults = client.table("users")
    .select("*")
    .filter("age", "gte", 18)
    .get();

// Select specific columns
QueryResponse names = client.table("users").select("id", "name").get();

Advanced Queries

// Multiple filters with sorting
QueryResponse activeUsers = client.table("users")
    .select("*")
    .filter("status", "eq", "active")
    .filter("age", "gt", 18)
    .order("created_at", "desc")
    .limit(20)
    .offset(0)
    .get();

System.out.println("Found " + activeUsers.getCount() + " users");
for (Map<String, Object> user : activeUsers.getData()) {
    System.out.println(user.get("name"));
}

Filter Helper Methods

// Using helper methods: eq, gt, gte, lt, lte, like, in, between
QueryResponse users = client.table("users")
    .eq("status", "active")
    .gt("age", 18)
    .like("email", "%@gmail.com")
    .in("role", Arrays.<Object>asList("admin", "moderator"))
    .between("created_at", "2024-01-01", "2024-12-31")
    .get();

GROUP BY and Aggregates

Basic GROUP BY

// Group by category with counts
QueryResponse result = client.table("products")
    .select("category", "COUNT(*) as count", "AVG(price) as avg_price")
    .groupBy("category")
    .get();

// Group by date
QueryResponse result2 = client.table("orders")
    .select("DATE(created_at) as date", "COUNT(*) as orders", "SUM(total) as revenue")
    .groupBy("DATE(created_at)")
    .order("date", "desc")          // or .orderBy("date", true) — true = descending
    .get();

HAVING Clause

QueryResponse result = client.table("products")
    .select("category", "COUNT(*) as count")
    .groupBy("category")
    .having("COUNT(*)", "gt", 10)
    .get();

CRUD Operations

Create Record

import com.wowsql.models.CreateResponse;

Map<String, Object> row = new HashMap<>();
row.put("name", "John Doe");
row.put("email", "john@example.com");
row.put("age", 30);
CreateResponse result = client.table("users").create(row);
System.out.println("Created user with ID: " + result.getId());

Update Record

client.table("users").update(123, Map.of("email", "newemail@example.com"));
System.out.println("User updated successfully");

Delete Record

client.table("users").delete(123);
System.out.println("User deleted successfully");

Get Single Record by ID

Map<String, Object> user = client.table("users").getById(123);
System.out.println("User: " + user.get("name"));

Get First Record

Map<String, Object> first = client.table("users")
    .filter("status", "eq", "active")
    .first();
if (first != null) {
    System.out.println("First active user: " + first.get("name"));
}

Filter Operators

Available operators for filtering data:

OperatorDescriptionExample (Java)
eqEquals.filter("status", "eq", "active")
neqNot equals.filter("status", "neq", "deleted")
gtGreater than.filter("age", "gt", 18)
gteGreater than or equal.filter("age", "gte", 18)
ltLess than.filter("price", "lt", 100)
lteLess than or equal.filter("price", "lte", 100)
likePattern matching.filter("name", "like", "john")
inIN operator.in("category", Arrays.asList("electronics", "books"))
not_inNOT IN.notIn("status", Arrays.asList("deleted", "archived"))
betweenBETWEEN.between("price", 10, 100)

Error Handling

import com.wowsql.WOWSQLException;

try {
    QueryResponse users = client.table("users").select("*").get();
    System.out.println("Success: " + users.getData().size() + " users");
} catch (WOWSQLException e) {
    Integer status = e.getStatusCode();   // may be null for network errors
    if (status != null && status == 401) System.out.println("Authentication failed — check your API key");
    else if (status != null && status == 404) System.out.println("Table or resource not found");
    else System.out.println("Error (" + status + "): " + e.getMessage());
}

Authentication

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

Sign Up & Sign In

import com.wowsql.ProjectAuthClient;
import com.wowsql.models.AuthResponse;

ProjectAuthClient auth = new ProjectAuthClient(
    "https://myproject.wowsqlconnect.com",
    "wowsql_anon_your_key_here"
);

// Sign up — POST /auth/v1/signup
AuthResponse result = auth.signUp("user@example.com", "SecurePassword123", "John Doe", null);
System.out.println("Access token: " + result.getSession().getAccessToken());

// Sign in — POST /auth/v1/login (returns a session, or a 2FA challenge)
AuthResponse login = auth.signIn("user@example.com", "SecurePassword123");
if (login.isRequires2fa()) {
    // TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
    System.out.println("2FA required, method: " + login.getMethod());
} else {
    System.out.println("Access token: " + login.getSession().getAccessToken());
}

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 overloads are unchanged.

auth.signUp("user@example.com", "SecurePassword123", null, null, turnstileToken);
auth.signIn("user@example.com", "SecurePassword123", turnstileToken);
auth.forgotPassword("user@example.com", turnstileToken);
auth.sendOtp("user@example.com", null, "login", turnstileToken);
auth.sendMagicLink("user@example.com", "login", turnstileToken);
auth.resendVerification("user@example.com", turnstileToken);

Password Reset

auth.forgotPassword("user@example.com");
auth.resetPassword("reset_token_from_email", "NewSecurePassword123");

OTP Authentication

Added in com.wowsql:wowsql-sdk-java:3.9.1. Email: sendOtp(email, purpose) / verifyOtp(email, otp, purpose, newPassword). Phone SMS: sendOtpPhone(phone, purpose) / verifyOtpPhone(phone, otp, purpose, newPassword). Provide exactly one channel (email or phone). Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91). purpose: login, signup, or password_reset (pass newPassword for password reset).

// Email OTP
auth.sendOtp("user@example.com", "login");
AuthResponse emailLogin = (AuthResponse) auth.verifyOtp(
    "user@example.com", "123456", "login", null
);
System.out.println("Logged in: " + emailLogin.getUser().getId());

// Phone OTP (SMS) — E.164, e.g. +15551234567
auth.sendOtpPhone("+15551234567", "login");
AuthResponse phoneLogin = (AuthResponse) auth.verifyOtpPhone(
    "+15551234567", "123456", "login", null
);
System.out.println("Logged in: " + phoneLogin.getUser().getId());

// Optional: combined overload — exactly one of email or phone
auth.sendOtp(null, "+15551234567", "login");
AuthResponse login = (AuthResponse) auth.verifyOtp(
    null, "+15551234567", "123456", "login", null
);

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-side — get the authorization URL ─────────────────────────
// Returns a Map with keys: authorization_url, provider, backend_callback_url, frontend_redirect_uri
Map<String, String> oauth = auth.getOAuthAuthorizationUrl(
    "google",
    "https://yourapp.com/auth/callback"
);
// Redirect the browser to oauth.get("authorization_url")
// Google calls: https://<slug>.wowsqlconnect.com/auth/v1/oauth/google/callback
// WoWSQL exchanges the code and redirects to your frontend_redirect_uri.

// ── Step 2: Frontend — read tokens from the redirect ────────────────────────
// WoWSQL redirects to: https://yourapp.com/auth/callback?access_token=...&refresh_token=...

// ── (Advanced) Manual code exchange — only if you handle the callback yourself
AuthResponse result = auth.exchangeOAuthCallback(
    "google",
    "authorization_code_from_callback",
    "https://yourapp.com/auth/callback"  // optional; the server exchanges the code with the project callback URL
);
System.out.println("OAuth login successful: " + result.getUser().getId());

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)
AuthUser me = auth.getUser();

// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
AuthResponse refreshed = auth.refreshToken();
System.out.println(refreshed.getSession().getAccessToken());

// Read or restore the stored session
Map<String, String> tokens = auth.getSession();
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.

// 1. Enrol (user is signed in; the stored access token is used)
Map<String, Object> setup = auth.setupTotp();          // POST /auth/v1/2fa/setup-totp
// Show setup.get("qr_code_url") (a PNG data URI when available) or setup.get("otpauth_uri")
Map<String, Object> enabled = auth.enableTotp("123456"); // POST /auth/v1/2fa/verify-and-enable
System.out.println(enabled.get("backup_codes"));       // shown once

// 2. Sign in when 2FA is on
AuthResponse login = auth.signIn("user@example.com", "SecurePassword123");
if (login.isRequires2fa()) {
    // verifyTwoFactorLogin(pendingToken, code, backupCode) → POST /auth/v1/2fa/verify-login
    AuthResponse done = auth.verifyTwoFactorLogin(login.getPendingToken(), "123456", null);
    System.out.println(done.getSession().getAccessToken());
}

// 3. Manage
Map<String, Object> status = auth.getTwoFactorStatus();          // enabled, method
Map<String, Object> rotated = auth.regenerateRecoveryCodes("123456");
auth.disableTotp("123456", null);                                // or disableTotp(null, "backup-code")

Settings, Anonymous Sign-in, Identity Linking and Email Change

Added in 3.10.0. Anonymous sign-in and identity linking must be enabled in the project Auth settings (anonymous_auth_enabled, manual_linking_enabled); otherwise the server rejects the call. Linking and email change run as the signed-in user.

// Public auth settings → GET /auth/v1/settings
Map<String, Object> settings = auth.getSettings();

// Anonymous sign-in → POST /auth/v1/sign-in-anonymous
AuthResponse anon = auth.signInAnonymous();

// Link another OAuth identity to the signed-in user → POST /auth/v1/oauth/{provider}/link
auth.linkOAuthIdentity("github", "authorization_code_from_callback", "https://yourapp.com/auth/callback");

// Change email → POST /auth/v1/update-email
auth.updateEmail("new@example.com");

Storage Operations

Initialize Storage

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

import com.wowsql.WOWSQLStorage;
import com.wowsql.models.StorageFile;
import java.util.Map;

WOWSQLStorage storage = new WOWSQLStorage(
    "https://myproject.wowsqlconnect.com",
    "wowsql_anon_your_key_here"
);

File Operations

// Upload a local file: uploadFromPath(filePath, bucketName, folder)
// The third argument is the folder (sent as ?folder=); the file keeps its own name.
storage.uploadFromPath("path/to/document.pdf", "documents", "uploads");   // → uploads/document.pdf

// Public URL (public buckets): getPublicUrl(bucketName, path)
String url = storage.getPublicUrl("documents", "uploads/document.pdf");
System.out.println("Public URL: " + url);

// Download bytes, or save straight to disk
byte[] bytes = storage.download("documents", "uploads/document.pdf");
storage.downloadToFile("documents", "uploads/document.pdf", "/tmp/document.pdf");

// List files: listFiles(bucketName, prefix)
List<StorageFile> files = storage.listFiles("documents", "uploads/");
for (StorageFile file : files) {
    System.out.println(file.getPath() + ": " + file.getSize() + " bytes");
}

// Delete file: deleteFile(bucketName, path)
storage.deleteFile("documents", "uploads/document.pdf");

// Check usage (GET .../stats). getQuota() calls .../quota and returns a StorageQuota
// (getTotalFiles(), getTotalSizeBytes(), getTotalSizeGb(), getFileTypes()).
Map<String, Object> stats = storage.getStats();
System.out.println(stats.get("total_files") + " files, " + stats.get("total_size_gb") + " GB used");

Buckets

import com.wowsql.models.StorageBucket;

StorageBucket avatars = storage.createBucket("avatars", true);   // name, isPublic
List<StorageBucket> buckets = storage.listBuckets();
StorageBucket bucket = storage.getBucket("avatars");
storage.updateBucket("avatars", Map.of("public", false));
storage.deleteBucket("avatars");

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.

Runnable unsub = client.realtime().subscribe("public", "messages", "*", change -> {
    System.out.println(change.get("event") + " " + change.get("new"));
});

RealtimeChannel channel = client.realtime().channel("chat");
channel.onBroadcast("typing", msg -> System.out.println(msg.get("payload")));
channel.onPresence(msg -> System.out.println(channel.presenceState()));
channel.subscribe(status -> {});
channel.track(Map.of("user", "alice"));
channel.send("typing", Map.of("user", "alice"));

unsub.run();
channel.unsubscribe();
client.realtime().disconnect();

Schema DDL (WOWSQLSchema)

Requires service role key. createTable requires the primary key column to be type UUID. Use "auto_increment": true 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.

import com.wowsql.WOWSQLSchema;

WOWSQLSchema schema = new WOWSQLSchema("myproject", "wowsql_service_your_key_here");
List<Map<String, Object>> columns = new ArrayList<>();
Map<String, Object> idCol = new HashMap<>();
idCol.put("name", "id");
idCol.put("type", "UUID");
idCol.put("auto_increment", true);
columns.add(idCol);
Map<String, Object> titleCol = new HashMap<>();
titleCol.put("name", "title");
titleCol.put("type", "TEXT");
columns.add(titleCol);
schema.createTable("items", columns, "id");