Flutter SDK

Flutter / Dart Quick Start

WoWSQL Flutter 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 pub.dev →

dart pub add wowsql

# or add it to pubspec.yaml:
# dependencies:
#   wowsql: ^3.10.0

Platform support

The SDK works with pure Dart, Flutter mobile and desktop, and Flutter web (including dart2wasm). Since 3.9.4 all HTTP goes through package:http, so auth (including the Turnstile captchaToken), data, storage and schema calls all work in the browser.

  • On web, add your page origin to the CORS allowlist for your project host.
  • storage.uploadFromPath(...) and storage.downloadToFile(...) need dart:io (VM, mobile and desktop only). On web, use storage.upload(...) and storage.download(...) with bytes.
  • Storage downloads keep binary bytes intact.

Initialize Client

import 'package:wowsql/wowsql.dart';

void main() async {
  // Initialize client with your anon key (respects Row Level Security)
  // URL resolves to: https://myproject.wowsqlconnect.com
  // SDK talks to PostgREST at /rest/v1/
  final client = WOWSQLClient(
    projectUrl: 'myproject',
    apiKey: 'wowsql_anon_your_key_here',
  );

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

  // Always use select() before get()/execute()
  final users = await client.table('users').select(['*']).get();
  for (final user in users.data) {
    print(user['name']);
  }
}

Query Data

Basic Queries

// Get all records
final users = await client.table('users').select(['*']).get();

// With filters
final adults = await client.table('users').select(['*']).gte('age', 18).get();

// Select specific columns
final names = await client.table('users').select(['id', 'name']).get();

Advanced Queries

// Multiple filters with sorting
final activeUsers = await client.table('users')
    .select(['*'])
    .eq('status', 'active')
    .gt('age', 18)
    .order('created_at', SortDirection.desc)
    .limit(20)
    .offset(0)
    .get();

print('Found ${activeUsers.count} users');
for (final user in activeUsers.data) {
  print(user['name']);
}

Filter Helper Methods

// Using helper methods: eq, gt, like, in, between
final users = await client.table('users')
    .eq('status', 'active')
    .gt('age', 18)
    .like('email', '%@gmail.com')
    .in_('role', ['admin', 'moderator'])
    .between('created_at', '2024-01-01', '2024-12-31')
    .get();

GROUP BY and Aggregates

Basic GROUP BY

final result = await client.table('products')
    .select(['category', 'COUNT(*) as count', 'AVG(price) as avg_price'])
    .groupBy(['category'])
    .get();

final result2 = await client.table('orders')
    .select(['DATE(created_at) as date', 'COUNT(*) as orders', 'SUM(total) as revenue'])
    .groupBy(['DATE(created_at)'])
    .orderBy('date', SortDirection.desc)
    .get();

HAVING Clause

final result = await client.table('products')
    .select(['category', 'COUNT(*) as count'])
    .groupBy(['category'])
    .having('COUNT(*)', 'gt', 10)
    .get();

Multiple ORDER BY

final result = await client.table('products')
    .select(['*'])
    .orderByMultiple([
      OrderByItem(column: 'category', direction: SortDirection.asc),
      OrderByItem(column: 'price', direction: SortDirection.desc),
    ])
    .get();

CRUD Operations

Create Record

final result = await client.table('users').create({
  'name': 'John Doe',
  'email': 'john@example.com',
  'age': 30,
});
print('Created user with ID: ${result.id}');

Update Record

await client.table('users').update(123, {'email': 'newemail@example.com'});
print('User updated successfully');

Delete Record

await client.table('users').delete(123);
print('User deleted successfully');

Get Single Record by ID

final user = await client.table('users').getById(123);
print('User: ${user['name']}');

Get First Record

final user = await client.table('users').eq('status', 'active').first();
if (user != null) {
  print('First active user: ${user['name']}');
}

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

try {
  final users = await client.table('users').select(['*']).get();
  print('Success: ${users.data.length} users');
} on WOWSQLException catch (e) {
  if (e.statusCode == 401) print('Authentication failed — check your API key');
  else if (e.statusCode == 404) print('Table or resource not found');
  else print('Error (${e.statusCode}): ${e.message}');
}

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
final auth = ProjectAuthClient(projectUrl: 'myproject', apiKey: 'wowsql_anon_your_key_here');

final result = await auth.signUp(
  email: 'user@example.com',
  password: 'SecurePassword123',
  fullName: 'John Doe',
);
print('Access token: ${result.session?.accessToken}');

final login = await auth.signIn(
  email: 'user@example.com',
  password: 'SecurePassword123',
);
if (login.requires2fa) {
  // TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
  print('2FA required, method: ${login.method}');
} else {
  print('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.

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

Password Reset

await auth.forgotPassword(email: 'user@example.com');
await auth.resetPassword(token: 'reset_token_from_email', newPassword: 'NewSecurePassword123');

OTP Authentication

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

// Email OTP
await auth.sendOtp(email: 'user@example.com', purpose: 'login');
final emailLogin = await auth.verifyOtp(
  email: 'user@example.com',
  otp: '123456',
  purpose: 'login',
);

// Phone OTP (SMS)
await auth.sendOtp(phone: '+15551234567', purpose: 'login');
final phoneLogin = await auth.verifyOtp(
  phone: '+15551234567',
  otp: '123456',
  purpose: 'login',
);
print('Logged in: $phoneLogin');
await auth.sendMagicLink(email: 'user@example.com', purpose: 'login');
await auth.verifyEmail(token: 'verification_token_from_email');
await auth.resendVerification(email: 'user@example.com');

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 Map<String, dynamic> with authorization_url, provider, backend_callback_url, frontend_redirect_uri
final oauth = await auth.getOAuthAuthorizationUrl(
  provider: 'google',
  redirectUri: 'https://yourapp.com/auth/callback',
);
// Launch the URL in a browser / in-app WebView:
//   launchUrl(Uri.parse(oauth['authorization_url']));
// 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=...
// Use flutter_web_auth_2 or a deep-link handler to intercept the URI and extract tokens.


// ── (Advanced) Manual code exchange — only if you handle the callback yourself
final result = await auth.exchangeOAuthCallback(
  provider: 'google',
  code: 'authorization_code_from_callback',
  redirectUri: 'https://yourapp.com/auth/callback',  // optional; the server exchanges the code with the project callback URL
);
print('OAuth login successful: ${result.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)
final me = await auth.getUser();

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

// Read or restore the stored session
final session = await auth.getSession();
await auth.setSession(accessToken: 'saved_access_token', refreshToken: 'saved_refresh_token');

// Sign out → POST /auth/v1/logout
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.

Flutter 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)
final setup = await auth.setupTotp();            // POST /auth/v1/2fa/setup-totp
// Show setup['qr_code_url'] (a PNG data URI when available) or setup['otpauth_uri']
final enabled = await auth.enableTotp('123456'); // POST /auth/v1/2fa/verify-and-enable
print(enabled['backup_codes']);                  // shown once

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

// 3. Manage
final status = await auth.getTwoFactorStatus();               // {enabled, method}
final rotated = await auth.regenerateRecoveryCodes('123456');
await auth.disableTotp(code: '123456');                       // or backupCode: '...'

Storage Operations

Initialize Storage

final storage = WOWSQLStorage(
  projectUrl: 'myproject',
  apiKey: 'wowsql_anon_your_key_here',
);

File Operations

// Upload a local file (VM / mobile / desktop only — uses dart:io)
final result = await storage.uploadFromPath('path/to/document.pdf', bucket: 'documents', path: 'uploads');
print('Uploaded: ${result.key}');

// Upload from bytes (works on every platform, including Flutter web)
// requires: import 'dart:typed_data';
final bytes = Uint8List.fromList('Hello, WoWSQL!'.codeUnits);
final upload = await storage.upload('documents', bytes, path: 'uploads', fileName: 'hello.txt', contentType: 'text/plain');
print('Uploaded: ${upload.key}');

// Public URL (public buckets)
final url = storage.getPublicUrl('documents', 'uploads/document.pdf');
print('Public URL: $url');

// Download bytes (every platform)
final data = await storage.download('documents', 'uploads/document.pdf');

// Save straight to disk (VM / mobile / desktop only)
await storage.downloadToFile('documents', 'uploads/document.pdf', '/tmp/document.pdf');

// List files
final files = await storage.listFiles('documents', prefix: 'uploads/', limit: 100);
for (final file in files) {
  print('${file.path}: ${file.size} bytes');
}

// Delete file
await storage.deleteFile('documents', 'uploads/document.pdf');

// Check usage (GET .../stats). getQuota() calls .../quota, which returns the same fields as /stats.
final stats = await storage.getStats();
print('Files: ${stats['total_files']}');
print('Used: ${stats['total_size_gb']} 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.

final unsub = client.realtime.subscribe(
  table: 'messages',
  callback: (change) => print('${change.event} ${change.newRow ?? change.oldRow}'),
);

final channel = client.realtime.channel('chat');
channel.on('broadcast', {'event': 'typing'}, (msg) => print(msg['payload']));
channel.on('presence', {}, (_) => print(channel.presenceState()));
channel.subscribe();
await channel.track({'user': 'alice'});
await 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 autoIncrement: 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.

final schema = WOWSQLSchema(
  projectUrl: 'myproject',
  serviceKey: 'wowsql_service_your_key_here',
);

await schema.createTable(
  name: 'items',
  columns: [
    ColumnDefinition(name: 'id', type: 'UUID', autoIncrement: true),
    ColumnDefinition(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