C# SDK
C# / .NET Quick Start
WoWSQL · WOWSQL.SDK · PostgreSQL REST API
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
NuGet: WOWSQL.SDK on nuget.org →
.NET CLI:
dotnet add package WOWSQL.SDK
# pin this release: dotnet add package WOWSQL.SDK --version 3.10.0
Package Manager:
Install-Package WOWSQL.SDK
PackageReference:
<PackageReference Include="WOWSQL.SDK" Version="3.10.0" />
Targets .NET Standard 2.1 (the NuGet package ships only lib/netstandard2.1): works with .NET Core 3.0+ and .NET 5+, and with Mono/Xamarin/Unity versions that support .NET Standard 2.1. .NET Framework does not support .NET Standard 2.1.
Namespace
All types are in using WOWSQL; — class names are WOWSQLClient, ProjectAuthClient, WOWSQLStorage, WOWSQLSchema.
Initialize database client
using WOWSQL;
using System.Collections.Generic;
using System.Threading.Tasks;
// URL resolves to: https://myproject.wowsqlconnect.com
// SDK talks to PostgREST at /rest/v1/
var client = new WOWSQLClient(
projectUrl: "myproject", // slug or full URL
apiKey: "wowsql_anon_your_key_here",
baseDomain: "wowsqlconnect.com",
secure: true,
timeout: 30,
verifySsl: true
);
// Query: always Select() before GetAsync() on the builder
var users = await client.Table("users").Select("*").GetAsync();
Query data
Basic
var all = await client.Table("users").Select("*").GetAsync();
var adults = await client.Table("users").Select("*").Gte("age", 18).GetAsync();
var names = await client.Table("users").Select("id", "name").GetAsync();
Chained filters & paging
var active = await client.Table("users")
.Select("*")
.Eq("status", "active")
.Gt("age", 18)
.Order("created_at", "desc")
.Limit(20)
.Offset(0)
.GetAsync();
System.Console.WriteLine($"Count: {active.Count}");
foreach (var row in active.Data)
System.Console.WriteLine(row["name"]);
GROUP BY / HAVING (POST when needed)
var stats = await client.Table("products")
.Select("category", "COUNT(*) as count", "AVG(price) as avg_price")
.GroupBy("category")
.Having("COUNT(*)", "gt", 10)
.GetAsync();
CRUD
var created = await client.Table("users").CreateAsync(new Dictionary<string, object> {
["name"] = "Jane",
["email"] = "jane@example.com"
});
// created.Id, created.Message
await client.Table("users").UpdateAsync(created.Id, new Dictionary<string, object> {
["email"] = "new@example.com"
});
await client.Table("users").DeleteAsync(created.Id);
var one = await client.Table("users").GetByIdAsync(123);
var first = await client.Table("users").Eq("status", "active").FirstAsync();
Error handling
try
{
var res = await client.Table("users").Select("*").GetAsync();
}
catch (WOWSQLException ex)
{
if (ex.StatusCode == 401) { /* bad API key */ }
}
finally { client.Dispose(); }
// or
using (var c = new WOWSQLClient("myproject", "wowsql_anon_your_key_here"))
{
var r = await c.Table("users").Select("*").GetAsync();
}
Authentication — ProjectAuthClient
Authentication endpoints are served at /auth/v1/. Use your anon key for client-side auth operations.
// Auth endpoints: POST /auth/v1/signup, POST /auth/v1/login
// Header: apikey: wowsql_anon_your_key_here
using (var auth = new ProjectAuthClient(
projectUrl: "myproject",
apiKey: "wowsql_anon_your_key_here",
baseDomain: "wowsqlconnect.com"))
{
var session = await auth.SignUpAsync(
"user@example.com",
"SecurePassword123!",
"Full Name",
new Dictionary<string, object> { ["k"] = "v" });
auth.SetSession(session.Session.AccessToken, session.Session.RefreshToken);
var login = await auth.SignInAsync("user@example.com", "SecurePassword123!");
if (login.Requires2fa)
{
// TOTP is on for this user: finish with VerifyTwoFactorLoginAsync (see Two-Factor Authentication)
Console.WriteLine($"2FA required, method: {login.Method}");
}
await auth.ForgotPasswordAsync("user@example.com");
}
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.SignUpAsync(email: "user@example.com", password: "SecurePassword123!", captchaToken: turnstileToken);
await auth.SignInAsync("user@example.com", "SecurePassword123!", turnstileToken);
await auth.ForgotPasswordAsync("user@example.com", turnstileToken);
await auth.SendOtpAsync(email: "user@example.com", phone: null, purpose: "login", captchaToken: turnstileToken);
await auth.SendMagicLinkAsync(email: "user@example.com", captchaToken: turnstileToken);
await auth.ResendVerificationAsync("user@example.com", turnstileToken);
OTP Authentication
Email: SendOtpAsync / VerifyOtpAsync. Phone SMS: SendOtpPhoneAsync / VerifyOtpPhoneAsync.
Provide exactly one channel. Phone OTP requires Auth → SMS / Phone OTP (Twilio, Fast2SMS, or MSG91).
// Email OTP
await auth.SendOtpAsync("user@example.com", "login");
var emailLogin = await auth.VerifyOtpAsync("user@example.com", "123456", "login");
// Phone OTP (SMS) — E.164
await auth.SendOtpPhoneAsync("+15551234567", "login");
var phoneLogin = await auth.VerifyOtpPhoneAsync("+15551234567", "123456", "login");
Console.WriteLine(phoneLogin);
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=.
using (var auth = new ProjectAuthClient(projectUrl: "myproject", apiKey: "wowsql_anon_your_key_here"))
{
// ── Step 1: Get the authorization URL ────────────────────────────────────
var oauth = await auth.GetOAuthAuthorizationUrlAsync(
"google",
"https://yourapp.com/auth/callback"
);
// Redirect the browser to oauth.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: 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
var result = await auth.ExchangeOAuthCallbackAsync(
"google",
"authorization_code_from_callback",
"https://yourapp.com/auth/callback" // optional; the server exchanges the code with the project callback URL
);
Console.WriteLine($"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)
var me = await auth.GetUserAsync();
// Refresh tokens → POST /auth/v1/refresh-token (uses the stored refresh token)
var refreshed = await auth.RefreshTokenAsync();
Console.WriteLine(refreshed.Session?.AccessToken);
// Read or restore the stored session
var session = auth.GetSession();
auth.SetSession("saved_access_token", "saved_refresh_token");
// Sign out → POST /auth/v1/logout
await auth.LogoutAsync();
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": "..."}). C# 3.10.0 also does not wrap /settings, anonymous sign-in, identity linking or email change.
// 1. Enrol (user is signed in)
var setup = await auth.SetupTotpAsync(); // POST /auth/v1/2fa/setup-totp → JObject
// Show setup["qr_code_url"] (a PNG data URI when available) or setup["otpauth_uri"]
var enabled = await auth.EnableTotpAsync("123456"); // POST /auth/v1/2fa/verify-and-enable
Console.WriteLine(enabled["backup_codes"]); // shown once
// 2. Sign in when 2FA is on
var login = await auth.SignInAsync("user@example.com", "SecurePassword123!");
if (login.Requires2fa)
{
// VerifyTwoFactorLoginAsync(pendingToken, code = null, backupCode = null) → POST /auth/v1/2fa/verify-login
var done = await auth.VerifyTwoFactorLoginAsync(login.PendingToken, code: "123456");
Console.WriteLine(done.Session?.AccessToken);
}
// 3. Manage
var status = await auth.GetTwoFactorStatusAsync(); // enabled, method
await auth.DisableTotpAsync(code: "123456"); // or backupCode: "..."
Storage — WOWSQLStorage (buckets)
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).
using (var storage = new WOWSQLStorage(
projectUrl: "myproject",
apiKey: "wowsql_service_your_key_here",
baseDomain: "wowsqlconnect.com",
timeout: 60))
{
// Upload — POST /storage/v1/object/{bucket}/{path}
var bucket = await storage.CreateBucketAsync("uploads", isPublic: false);
var file = await storage.UploadFromPathAsync(@".\doc.pdf", "uploads", path: "docs/doc.pdf");
// List — POST /storage/v1/object/list/{bucket}
var files = await storage.ListFilesAsync("uploads", prefix: "docs/", limit: 100, offset: 0);
// Public URL — GET /storage/v1/object/public/{bucket}/{path}
var url = storage.GetPublicUrl("uploads", "docs/doc.pdf");
// Usage — GET .../stats (GetQuotaAsync is an alias of GetStatsAsync)
var quota = await storage.GetQuotaAsync();
}
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.
var unsub = client.Realtime.Subscribe("messages", change =>
{
Console.WriteLine($"{change["event"]} {change["new"]}");
});
var channel = client.Realtime.Channel("chat");
channel.OnBroadcast("typing", msg => Console.WriteLine(msg["payload"]));
channel.OnPresence(msg => Console.WriteLine(channel.PresenceState().Count));
channel.Subscribe();
await channel.Track(new { user = "alice" });
await channel.Send("typing", new { user = "alice" });
unsub();
channel.Unsubscribe();
client.Realtime.Disconnect();
Schema — WOWSQLSchema (service key only)
Requires service role key. The API requires the primaryKey column to be type UUID (not SERIAL). Use ["type"] = "UUID" with ["auto_increment"] = true for DEFAULT gen_random_uuid(). Raw CREATE TABLE via ExecuteSQL must not use integer/SERIAL primary keys.
using (var schema = new WOWSQLSchema(
projectUrl: "myproject",
serviceKey: "wowsql_service_your_key_here",
baseDomain: "wowsqlconnect.com"))
{
await schema.CreateTableAsync("items", new List<Dictionary<string, object>> {
new Dictionary<string, object> { ["name"] = "id", ["type"] = "UUID", ["auto_increment"] = true },
new Dictionary<string, object> { ["name"] = "title", ["type"] = "TEXT" }
}, primaryKey: "id");
var tables = await schema.ListTablesAsync();
}
Catch SchemaPermissionException if a non-service key is used.
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
NuGet package readme
Recent packages embed README.md so the gallery shows documentation. Unsigned packages show NU3004 under dotnet nuget verify — that is normal; publishing still works.