PHP SDK

PHP Quick Start

WoWSQL PHP 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 Packagist →

composer require wowsql/wowsql-sdk
# pin this release: composer require wowsql/wowsql-sdk:^3.10

Initialize Client

The SDK talks to PostgREST at /rest/v1/. Your project URL resolves to https://myproject.wowsqlconnect.com.

<?php
require 'vendor/autoload.php';

use WOWSQL\WOWSQLClient;

// Initialize client with your anon key (respects Row Level Security)
// new WOWSQLClient($projectUrl, $apiKey, $baseDomain = 'wowsqlconnect.com', $secure = true, $timeout = 30, $verifySsl = true)
$client = new WOWSQLClient(
    'myproject',                    // resolves to https://myproject.wowsqlconnect.com
    'wowsql_anon_your_key_here'     // anon key from Settings → API
);

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

// Always use select() before get()
$users = $client->table('users')->select('*')->get();

Query Data

Basic Queries

// Get all records
$users = $client->table('users')->select('*')->get();

// With filters
$adults = $client->table('users')
    ->select('*')
    ->gte('age', 18)
    ->get();

// Select specific columns
$names = $client->table('users')
    ->select('id', 'name')
    ->get();

Advanced Queries

// Multiple filters with sorting
$activeUsers = $client->table('users')
    ->select('*')
    ->eq('status', 'active')
    ->gt('age', 18)
    ->orderBy('created_at', true)  // desc
    ->limit(20)
    ->offset(0)
    ->get();

echo "Found " . $activeUsers['count'] . " users\n";
foreach ($activeUsers['data'] as $user) {
    echo $user['name'] . "\n";
}

Filter Helper Methods

// Using helper methods
$users = $client->table('users')
    ->eq('status', 'active')
    ->gt('age', 18)
    ->like('email', '%@gmail.com')
    ->isNotNull('email_verified_at')
    ->get();

GROUP BY and Aggregates

Basic GROUP BY

// Group by category with counts
$result = $client->table('products')
    ->select('category', 'COUNT(*) as count', 'AVG(price) as avg_price')
    ->groupBy('category')
    ->get();

// Group by date
$result2 = $client->table('orders')
    ->select('DATE(created_at) as date', 'COUNT(*) as orders', 'SUM(total) as revenue')
    ->groupBy('DATE(created_at)')
    ->orderBy('date', true)
    ->get();

HAVING Clause

// Filter aggregated results
$result = $client->table('products')
    ->select('category', 'COUNT(*) as count')
    ->groupBy('category')
    ->having('COUNT(*)', 'gt', 10)
    ->get();

CRUD Operations

Create Record

$result = $client->table('users')->create([
    'name'  => 'John Doe',
    'email' => 'john@example.com',
    'age'   => 30,
]);
echo "Created user with ID: " . $result['id'] . "\n";

Update Record

$client->table('users')->update(123, [
    'email' => 'newemail@example.com',
]);
echo "User updated successfully\n";

Delete Record

$client->table('users')->delete(123);
echo "User deleted successfully\n";

Get Single Record by ID

$user = $client->table('users')->getById(123);
echo "User: " . $user['name'] . "\n";

Get First Record

$user = $client->table('users')
    ->eq('status', 'active')
    ->first();

if ($user) {
    echo "First active user: " . $user['name'] . "\n";
}

Filter Operators

Available operators for filtering data:

OperatorDescriptionExample (PHP)
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')
isIS NULL / IS NOT NULL->isNull('deleted_at') / ->isNotNull('email')
inIN operator->in('category', ['electronics', 'books'])
not_inNOT IN->notIn('status', ['deleted', 'archived'])
betweenBETWEEN->between('price', 10, 100)

Error Handling

try {
    $users = $client->table('users')->select('*')->get();
    echo "Success: " . count($users['data']) . " users";
} catch (\WOWSQL\StorageException $e) {           // catch the subclass first
    echo "Storage error: " . $e->getMessage();
} catch (\WOWSQL\WOWSQLException $e) {
    echo "Database error (" . $e->getStatusCode() . "): " . $e->getMessage();
}

Authentication

Auth endpoints are served at /auth/v1/. Use your anon key as the apikey header.

Sign Up & Sign In

use WOWSQL\ProjectAuthClient;

// new ProjectAuthClient($projectUrl, $apiKey, $baseDomain = 'wowsqlconnect.com', ...)
$auth = new ProjectAuthClient(
    'myproject',
    'wowsql_anon_your_key_here'     // anon key — POST /auth/v1/signup, POST /auth/v1/login
);

// Sign up (POST /auth/v1/signup) → AuthResponse
$result = $auth->signUp('user@example.com', 'SecurePassword123', 'John Doe');
echo "Access token: " . $result->session->accessToken . "\n";

// Sign in (POST /auth/v1/login): returns a session, or a 2FA challenge
$login = $auth->signIn('user@example.com', 'SecurePassword123');
if ($login->requires2fa) {
    // TOTP is on for this user: finish with verifyTwoFactorLogin (see Two-Factor Authentication)
    echo "2FA required, method: " . $login->method . "\n";
} else {
    echo "Access token: " . $login->session->accessToken . "\n";
}

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('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', 'login', null, $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

Provide exactly one of email or phone. Signature: sendOtp($email, $purpose, $phone) / verifyOtp($email, $otp, $purpose, $newPassword, $phone). Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91).

// Email OTP
$auth->sendOtp('user@example.com', 'login');
$emailLogin = $auth->verifyOtp('user@example.com', '123456', 'login');

// Phone OTP (SMS) — E.164
$auth->sendOtp(null, 'login', '+15551234567');
$phoneLogin = $auth->verifyOtp(null, '123456', 'login', null, '+15551234567');
echo "Logged in\n";

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 ─────────────────────────
$result = $auth->getOAuthAuthorizationUrl(
    'google',
    'https://yourapp.com/auth/callback'
);
// Redirect the browser:
// header('Location: ' . $result['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
$authResult = $auth->exchangeOAuthCallback(
    'google',
    'authorization_code_from_callback',
    'https://yourapp.com/auth/callback'  // optional; the server exchanges the code with the project callback URL
);
echo "OAuth login successful: " . $authResult->user->id . "\n";

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

// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
$refreshed = $auth->refreshToken();
echo $refreshed->session->accessToken;

// Read or restore the stored session
$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.

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

// 2. Sign in when 2FA is on
$login = $auth->signIn('user@example.com', 'SecurePassword123');
if ($login->requires2fa) {
    // verifyTwoFactorLogin($pendingToken, $code = null, $backupCode = null) → POST /auth/v1/2fa/verify-login
    $done = $auth->verifyTwoFactorLogin($login->pendingToken, '123456');
    echo $done->session->accessToken;
}

// 3. Manage
$status = $auth->getTwoFactorStatus();              // enabled, method
$rotated = $auth->regenerateRecoveryCodes('123456');
$auth->disableTotp('123456');                       // or disableTotp(null, 'backup-code')

Storage Operations

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/.

Storage endpoints are served at /storage/v1/.

use WOWSQL\WOWSQLStorage;

// new WOWSQLStorage($projectUrl, $apiKey, $projectSlug = '', $baseUrl = '', $baseDomain = 'wowsqlconnect.com', ...)
$storage = new WOWSQLStorage(
    'myproject',
    'wowsql_anon_your_key_here'     // anon key
);

File Operations

// Upload a local file: uploadFromPath($filePath, $bucketName = 'default', $path = null)
// $path is the object path; its folder part ("uploads") is sent as the folder query parameter.
$file = $storage->uploadFromPath('path/to/document.pdf', 'documents', 'uploads/document.pdf');
echo "Uploaded: " . $file->path . "\n";

// Public URL (public buckets): /storage/v1/object/public/{bucket}/{path}
$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, or save straight to disk
$bytes = $storage->download('documents', 'uploads/document.pdf');
$storage->downloadToFile('documents', 'uploads/document.pdf', '/tmp/document.pdf');

// List files: listFiles($bucketName, $prefix = null, $limit = 100, $offset = 0)
$files = $storage->listFiles('documents', 'uploads/');
foreach ($files as $f) {
    echo $f->path . ": " . round($f->sizeMb(), 2) . " MB\n";
}

// Delete file: deleteFile($bucketName, $filePath)
$storage->deleteFile('documents', 'uploads/document.pdf');

// Check usage (GET .../stats; getQuota() is an alias of getStats())
$stats = $storage->getStats();
echo $stats->totalFiles . " files, " . $stats->totalSizeGb . " GB used";

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 access.

Realtime

Subscribe to INSERT / UPDATE / DELETE, broadcast ephemeral events, and track presence. Uses the same anon or service_role key as REST. Requires textalk/websocket. PHP uses a blocking listen() loop — run it in a worker process.

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.

$rt = $client->realtime();
$rt->subscribe('messages', function ($change) {
    echo $change['event'];
});

$channel = $rt->channel('chat');
$channel->on('broadcast', ['event' => 'typing'], function ($msg) {
    print_r($msg['payload']);
});
$channel->subscribe();
$channel->track(['user' => 'alice']);
$channel->send('typing', ['user' => 'alice']);
$rt->listen();

Schema DDL (WOWSQLSchema)

Requires service role key. createTable requires the primary key column to be type 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.

use WOWSQL\WOWSQLSchema;

$schema = new WOWSQLSchema('myproject', 'wowsql_service_your_key_here');   // service role key — bypasses RLS
$schema->createTable('items', [
    ['name' => 'id', 'type' => 'UUID', 'auto_increment' => true],
    ['name' => 'title', 'type' => 'TEXT'],
], 'id');