SWAIN

Documentation

Everything you need to integrate the API your database deserves.

Quickstart

  1. Sign in to the SWAIN platform and connect your database.
  2. Select tables and configure auth & security.
  3. Generate and deploy your backend.

Basic API Call Examples

Simple GET Request
curl -H "Authorization: Bearer <JWT>" \
  "https://api.your-domain.com/api/user?page=1&pageSize=25&sort=name:asc"
JavaScript (Fetch API)
const token = 'YOUR_JWT_TOKEN';
const baseUrl = 'https://api.your-domain.com';

async function getUsers() {
  const response = await fetch(`${baseUrl}/api/user?page=1&pageSize=25&sort=name:asc`, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  const data = await response.json();
  return data;
}

// Usage
getUsers()
  .then(users => console.log('Users:', users))
  .catch(error => console.error('Error:', error));
TypeScript with Types
interface User {
  id: number;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  created_at: string;
}

interface ApiResponse<T> {
  data: T[];
  total: number;
  page: number;
  page_size: number;
  total_pages: number;
}

class SwainAPI {
  private baseUrl = 'https://api.your-domain.com';
  private token: string;

  constructor(token: string) {
    this.token = token;
  }

  async getUsers(page = 1, pageSize = 25): Promise<ApiResponse<User>> {
    const response = await fetch(
      `${this.baseUrl}/api/user?page=${page}&pageSize=${pageSize}&sort=name:asc`,
      {
        headers: {
          'Authorization': `Bearer ${this.token}`,
          'Content-Type': 'application/json'
        }
      }
    );

    if (!response.ok) {
      throw new Error(`API Error: ${response.status}`);
    }

    return response.json();
  }
}

// Usage
const api = new SwainAPI('YOUR_JWT_TOKEN');
api.getUsers().then(response => {
  console.log(`Found ${response.data.length} users`);
});
Python (requests)
import requests
from typing import Dict, List, Optional

class SwainAPI:
    def __init__(self, base_url: str, token: str):
        self.base_url = base_url
        self.headers = {
            'Authorization': f'Bearer {token}',
            'Content-Type': 'application/json'
        }
    
    def get_users(self, page: int = 1, page_size: int = 25, 
                  sort: str = "name:asc") -> Dict:
        """Fetch users with pagination."""
        params = {
            'page': page,
            'pageSize': page_size,
            'sort': sort
        }
        
        response = requests.get(
            f"{self.base_url}/api/user",
            headers=self.headers,
            params=params
        )
        response.raise_for_status()
        return response.json()
    
    def filter_users(self, expressions: List[Dict], 
                    projections: Optional[List[str]] = None) -> Dict:
        """Filter users with complex queries."""
        filter_data = {
            'expressions': expressions
        }
        if projections:
            filter_data['projections'] = projections
        
        response = requests.post(
            f"{self.base_url}/api/user/filter",
            headers=self.headers,
            json=filter_data
        )
        response.raise_for_status()
        return response.json()

# Usage
api = SwainAPI('https://api.your-domain.com', 'YOUR_JWT_TOKEN')

# Simple get
users = api.get_users(page=1, page_size=25)
print(f"Found {len(users['data'])} users")

# Complex filter
active_users = api.filter_users(
    expressions=[
        {'field': 'status', 'operator': 'eq', 'value': 'active'}
    ],
    projections=['id', 'name', 'email']
)
Go (net/http)
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type User struct {
    ID        int    `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Status    string `json:"status"`
    CreatedAt string `json:"created_at"`
}

type ApiResponse struct {
    Data       []User `json:"data"`
    Total      int64  `json:"total"`
    Page       int    `json:"page"`
    PageSize   int    `json:"page_size"`
    TotalPages int64  `json:"total_pages"`
}

type SwainClient struct {
    BaseURL string
    Token   string
    Client  *http.Client
}

func NewSwainClient(baseURL, token string) *SwainClient {
    return &SwainClient{
        BaseURL: baseURL,
        Token:   token,
        Client:  &http.Client{},
    }
}

func (c *SwainClient) GetUsers(page, pageSize int) (*ApiResponse, error) {
    url := fmt.Sprintf("%s/api/user?page=%d&pageSize=%d&sort=name:asc",
        c.BaseURL, page, pageSize)
    
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("Authorization", "Bearer "+c.Token)
    req.Header.Set("Content-Type", "application/json")
    
    resp, err := c.Client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    
    var apiResponse ApiResponse
    if err := json.Unmarshal(body, &apiResponse); err != nil {
        return nil, err
    }
    
    return &apiResponse, nil
}

// Usage
func main() {
    client := NewSwainClient("https://api.your-domain.com", "YOUR_JWT_TOKEN")
    
    response, err := client.GetUsers(1, 25)
    if err != nil {
        panic(err)
    }
    
    fmt.Printf("Found %d users\n", len(response.Data))
}
Ruby (HTTParty)
require 'httparty'
require 'json'

class SwainAPI
  include HTTParty
  
  def initialize(base_url, token)
    @base_url = base_url
    @headers = {
      'Authorization' => "Bearer #{token}",
      'Content-Type' => 'application/json'
    }
  end
  
  def get_users(page: 1, page_size: 25, sort: 'name:asc')
    query = {
      page: page,
      pageSize: page_size,
      sort: sort
    }
    
    response = self.class.get(
      "#{@base_url}/api/user",
      headers: @headers,
      query: query
    )
    
    handle_response(response)
  end
  
  def filter_users(expressions:, projections: nil)
    body = { expressions: expressions }
    body[:projections] = projections if projections
    
    response = self.class.post(
      "#{@base_url}/api/user/filter",
      headers: @headers,
      body: body.to_json
    )
    
    handle_response(response)
  end
  
  private
  
  def handle_response(response)
    case response.code
    when 200..299
      response.parsed_response
    else
      raise "API Error: #{response.code} - #{response.message}"
    end
  end
end

# Usage
api = SwainAPI.new('https://api.your-domain.com', 'YOUR_JWT_TOKEN')

# Simple get request
users = api.get_users(page: 1, page_size: 25)
puts "Found #{users['data'].length} users"

# Filter active users
active_users = api.filter_users(
  expressions: [
    { field: 'status', operator: 'eq', value: 'active' }
  ],
  projections: ['id', 'name', 'email']
)
PHP (Guzzle)
<?php
require 'vendor/autoload.php';

use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;

class SwainAPI {
    private $client;
    private $baseUrl;
    private $token;
    
    public function __construct(string $baseUrl, string $token) {
        $this->baseUrl = $baseUrl;
        $this->token = $token;
        $this->client = new Client([
            'base_uri' => $baseUrl,
            'headers' => [
                'Authorization' => 'Bearer ' . $token,
                'Content-Type' => 'application/json'
            ]
        ]);
    }
    
    public function getUsers(int $page = 1, int $pageSize = 25, 
                            string $sort = 'name:asc'): array {
        try {
            $response = $this->client->get('/api/user', [
                'query' => [
                    'page' => $page,
                    'pageSize' => $pageSize,
                    'sort' => $sort
                ]
            ]);
            
            return json_decode($response->getBody(), true);
        } catch (RequestException $e) {
            throw new Exception('API Error: ' . $e->getMessage());
        }
    }
    
    public function filterUsers(array $expressions, 
                               ?array $projections = null): array {
        $body = ['expressions' => $expressions];
        if ($projections) {
            $body['projections'] = $projections;
        }
        
        try {
            $response = $this->client->post('/api/user/filter', [
                'json' => $body
            ]);
            
            return json_decode($response->getBody(), true);
        } catch (RequestException $e) {
            throw new Exception('API Error: ' . $e->getMessage());
        }
    }
}

// Usage
$api = new SwainAPI('https://api.your-domain.com', 'YOUR_JWT_TOKEN');

// Get users
$users = $api->getUsers(1, 25);
echo "Found " . count($users['data']) . " users\n";

// Filter active users
$activeUsers = $api->filterUsers(
    [
        ['field' => 'status', 'operator' => 'eq', 'value' => 'active']
    ],
    ['id', 'name', 'email']
);
C# (.NET HttpClient)
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using System.Collections.Generic;

public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public string Status { get; set; }
    [JsonPropertyName("created_at")] public DateTime CreatedAt { get; set; }
}

public class ApiResponse<T>
{
    [JsonPropertyName("data")] public List<T> Data { get; set; }
    [JsonPropertyName("total")] public long Total { get; set; }
    [JsonPropertyName("page")] public int Page { get; set; }
    [JsonPropertyName("page_size")] public int PageSize { get; set; }
    [JsonPropertyName("total_pages")] public long TotalPages { get; set; }
}

public class SwainClient
{
    private readonly HttpClient _httpClient;
    private readonly string _baseUrl;
    
    public SwainClient(string baseUrl, string token)
    {
        _baseUrl = baseUrl;
        _httpClient = new HttpClient();
        _httpClient.DefaultRequestHeaders.Authorization = 
            new AuthenticationHeaderValue("Bearer", token);
        _httpClient.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));
    }
    
    public async Task<ApiResponse<User>> GetUsersAsync(
        int page = 1, int pageSize = 25, string sort = "name:asc")
    {
        var url = $"{_baseUrl}/api/user?page={page}&pageSize={pageSize}&sort={sort}";
        
        var response = await _httpClient.GetAsync(url);
        response.EnsureSuccessStatusCode();
        
        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<ApiResponse<User>>(json);
    }
    
    public async Task<ApiResponse<User>> FilterUsersAsync(
        object filterExpression)
    {
        var url = $"{_baseUrl}/api/user/filter";
        var json = JsonSerializer.Serialize(filterExpression);
        var content = new StringContent(json, Encoding.UTF8, "application/json");
        
        var response = await _httpClient.PostAsync(url, content);
        response.EnsureSuccessStatusCode();
        
        var responseJson = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<ApiResponse<User>>(responseJson);
    }
}

// Usage
class Program
{
    static async Task Main()
    {
        var client = new SwainClient(
            "https://api.your-domain.com", 
            "YOUR_JWT_TOKEN"
        );
        
        // Get users
        var users = await client.GetUsersAsync(1, 25);
        Console.WriteLine($"Found {users.Data.Count} users");
        
        // Filter active users
        var filter = new
        {
            expressions = new[]
            {
                new { field = "status", @operator = "eq", value = "active" }
            },
            projections = new[] { "id", "name", "email" }
        };
        
        var activeUsers = await client.FilterUsersAsync(filter);
        Console.WriteLine($"Found {activeUsers.Data.Count} active users");
    }
}

Complex Filter Example

Filter JSON Structure
{
  "expressions": [
    { "field": "status", "operator": "eq", "value": "active" },
    {
      "relationship": "customer",
      "expressions": [
        { "field": "region", "operator": "in", "value": ["EU","US"] }
      ],
      "include": true
    }
  ],
  "projections": ["id", "name", "status", "created_at"]
}

Endpoints

MethodPathDescription
GET/api/{model}List with pagination & sorting
GET/api/{model}/{id}Fetch a single record
POST/api/{model}Create a new record
PUT/api/{model}/{id}Update a record
DELETE/api/{model}/{id}Delete a record
POST/api/{model}/filterAdvanced JSON filtering & relation loading
PUT/api/{model}/filterBulk update by filter
DELETE/api/{model}/filterBulk delete by filter
POST/api/{model}/countCount entities by filter
GET/api/{model}/schemaGet model schema information
GET/api/modelsList all available models
GET/api/dynamic_swaggerGet OpenAPI/Swagger documentation

Note: Runtime routes take precedence over inline code annotations. For example, counting uses /api/{model}/count at runtime and in Swagger UI (/swagger/), even if some annotations reference /{model}/filter/count.

Filtering

Send JSON to /api/{model}/filter or include JSON body in GET /api/{model} requests. The filter uses three expression types: FieldExpression for simple comparisons, LogicalExpression for combining with AND/OR/NOT, and RelationshipExpression for filtering on related entities. Multiple expressions at the root level are combined with AND by default. Pagination parameters (page, pageSize, sort) are passed as query parameters even for POST requests.

Operators Reference

OperatorDescriptionExampleNotes
eqEquals{"field":"status","operator":"eq","value":"active"}
neqNot equals{"field":"age","operator":"neq","value":30}
gtGreater than{"field":"age","operator":"gt","value":30}
gteGreater or equal{"field":"age","operator":"gte","value":18}
ltLess than{"field":"price","operator":"lt","value":1000}
lteLess or equal{"field":"age","operator":"lte","value":65}
likePattern match{"field":"name","operator":"like","value":"John%"}Use % for wildcards
not_likeNot like{"field":"email","operator":"not_like","value":"%spam%"}Use % for wildcards
containsSubstring match{"field":"description","operator":"contains","value":"important"}Auto-wraps with %
inIn array{"field":"status","operator":"in","value":["active","pending"]}Value must be array
not_inNot in array{"field":"id","operator":"not_in","value":[1,2,3]}Value must be array
is_nullIs NULL{"field":"deleted_at","operator":"is_null"}No value or null value
is_not_nullIs not NULL{"field":"deleted_at","operator":"is_not_null"}No value or null value
cURL (POST Filter)
# Complex filtering with POST
curl -X POST "https://api.your-domain.com/api/user/filter?page=1&pageSize=50" \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "expressions": [
      {"field": "status", "operator": "eq", "value": "active"}
    ]
  }'

# GET requests can also accept JSON body for filtering
curl -X GET "https://api.your-domain.com/api/user?page=1&pageSize=50" \
  -H "Authorization: Bearer <JWT>" \
  -H "Content-Type: application/json" \
  -d '{
    "expressions": [
      {"field": "active", "operator": "eq", "value": true}
    ]
  }'
Python
import requests

token = "YOUR_JWT"
base = "https://api.your-domain.com"
q = {
  "expressions": [
    {
      "operator": "OR",
      "expressions": [
        {"field": "email", "operator": "like", "value": "%@example.com"},
        {"field": "created_at", "operator": "gte", "value": "2024-01-01"}
      ]
    }
  ]
}
# Query params for pagination
params = {"page": 1, "pageSize": 50}
r = requests.post(f"{base}/api/user/filter", json=q, params=params, headers={
  "Authorization": f"Bearer {token}"
})
r.raise_for_status()
print(r.json())
Advanced Filter with Functions
{
  "expressions": [
    {
      "field": "email",
      "operator": "like",
      "value": "%@example.com",
      "func_name": "LOWER"
    },
    {
      "operator": "AND",
      "expressions": [
        {"field": "age", "operator": "gte", "value": 18},
        {"field": "status", "operator": "in", "value": ["active", "pending"]}
      ]
    }
  ]
}
Aggregation Query
{
  "expressions": [
    {"field": "status", "operator": "eq", "value": "active"}
  ],
  "aggregations": {
    "functions": [
      {"type": "COUNT", "field": "id", "alias": "total_count"},
      {"type": "SUM", "field": "amount", "alias": "total_amount"},
      {"type": "AVG", "field": "rating", "alias": "avg_rating"}
    ],
    "group_by": ["category", "region"]
  }
}
Complex Combined Filter
{
  "expressions": [
    {"field": "active", "operator": "eq", "value": true},
    {
      "operator": "OR",
      "expressions": [
        {"field": "age", "operator": "lt", "value": 25},
        {"field": "age", "operator": "gt", "value": 50}
      ]
    },
    {
      "relationship": "addresses",
      "include": true,
      "expressions": [
        {"field": "city", "operator": "like", "value": "New%"}
      ]
    }
  ],
  "projections": ["id", "name", "age", "active"]
}
// Returns active users who are either under 25 or over 50,
// and have at least one address in a city starting with "New"

Advanced Features

Available field functions: UPPER, LOWER, TRIM for text manipulation; SUM, AVG, MIN, MAX, COUNT for aggregations. Relationship filters support nested queries and selective field inclusion.

Relationship Filtering

Use RelationshipExpression to filter on related entities. The scope parameter controls filtering behavior:

  • "scope": "filterParent" (default) - Only returns parent entities that have matching child records. If no child matches, the parent is excluded.
  • "scope": "filterChild" - Returns all parent entities, but filters the included child records to only those matching the criteria.
Nested Relationship Filter
{
  "expressions": [
    {
      "relationship": "orders",
      "expressions": [
        {"field": "total", "operator": "gte", "value": 1000},
        {
          "relationship": "items",
          "expressions": [
            {"field": "quantity", "operator": "gt", "value": 5}
          ],
          "include": false,
          "scope": "filterParent"
        }
      ],
      "include": true,
      "scope": "filterChild",
      "projections": ["id", "total", "created_at"]
    }
  ]
}
// Returns users with their orders (filtered to only orders >= 1000),
// where those orders have items with quantity > 5

Authentication

Use JWT or API key headers. Row‑Level Security (RLS) can reference JWT claims to enforce per‑model conditions. For multi‑tenant apps, include tenant IDs in tables and define RLS rules in the platform.

Headers
Authorization: Bearer <JWT>
X-API-Key: <API_KEY>

Real‑time

Connect to WebSocket endpoints for real-time updates. Available events: created, updated, deleted.

WebSocket Connection Examples

JavaScript WebSocket
// WebSocket connection manager
class SwainWebSocket {
  constructor(baseUrl, token) {
    this.baseUrl = baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
    this.token = token;
    this.connections = {};
  }
  
  subscribe(model, event, callback) {
    const url = `${this.baseUrl}/ws/${model}/${event}?token=${this.token}`;
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      console.log(`Connected to ${model}/${event}`);
    };
    
    ws.onmessage = (e) => {
      const msg = JSON.parse(e.data);
      callback(msg.data ?? msg);
    };
    
    ws.onerror = (error) => {
      console.error(`WebSocket error for ${model}/${event}:`, error);
    };
    
    ws.onclose = () => {
      console.log(`Disconnected from ${model}/${event}`);
      // Implement reconnection logic here
      setTimeout(() => this.subscribe(model, event, callback), 5000);
    };
    
    this.connections[`${model}/${event}`] = ws;
    return ws;
  }
  
  unsubscribe(model, event) {
    const key = `${model}/${event}`;
    if (this.connections[key]) {
      this.connections[key].close();
      delete this.connections[key];
    }
  }
  
  closeAll() {
    Object.values(this.connections).forEach(ws => ws.close());
    this.connections = {};
  }
}

// Usage
const ws = new SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN');

// Subscribe to order events
ws.subscribe('orders', 'created', (order) => {
  console.log('New order created:', order);
  // Update UI with new order
});

ws.subscribe('orders', 'updated', (order) => {
  console.log('Order updated:', order);
  // Update existing order in UI
});

ws.subscribe('orders', 'deleted', (resp) => {
  console.log('Order deleted:', resp.id);
  // Remove order from UI
});
TypeScript WebSocket
interface WebSocketMessage<T> {
  event: 'created' | 'updated' | 'deleted';
  data: T;
}

interface Order {
  id: number;
  customerId: number;
  total: number;
  status: string;
  created_at: string;
}

class SwainWebSocket {
  private baseUrl: string;
  private token: string;
  private connections: Map<string, WebSocket> = new Map();
  private reconnectDelays: Map<string, number> = new Map();
  
  constructor(baseUrl: string, token: string) {
    this.baseUrl = baseUrl.replace('https://', 'wss://').replace('http://', 'ws://');
    this.token = token;
  }
  
  subscribe<T>(
    model: string, 
    event: 'created' | 'updated' | 'deleted',
    callback: (data: T) => void
  ): WebSocket {
    const key = `${model}/${event}`;
    const url = `${this.baseUrl}/ws/${model}/${event}?token=${this.token}`;
    
    // Close existing connection if any
    this.unsubscribe(model, event);
    
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      console.log(`✅ Connected to ${key}`);
      this.reconnectDelays.set(key, 1000); // Reset delay
    };
    
    ws.onmessage = (e: MessageEvent) => {
      try {
        const message: WebSocketMessage<T> = JSON.parse(e.data);
        callback(message.data);
      } catch (error) {
        console.error(`Failed to parse message for ${key}:`, error);
      }
    };
    
    ws.onerror = (error: Event) => {
      console.error(`❌ WebSocket error for ${key}:`, error);
    };
    
    ws.onclose = (e: CloseEvent) => {
      console.log(`🔌 Disconnected from ${key} (code: ${e.code})`);
      
      if (!e.wasClean) {
        // Exponential backoff for reconnection
        const delay = this.reconnectDelays.get(key) || 1000;
        console.log(`Reconnecting in ${delay}ms...`);
        
        setTimeout(() => {
          this.subscribe(model, event, callback);
        }, delay);
        
        // Increase delay for next reconnection (max 30 seconds)
        this.reconnectDelays.set(key, Math.min(delay * 2, 30000));
      }
    };
    
    this.connections.set(key, ws);
    return ws;
  }
  
  unsubscribe(model: string, event: string): void {
    const key = `${model}/${event}`;
    const ws = this.connections.get(key);
    
    if (ws) {
      ws.close(1000, 'Client closing connection');
      this.connections.delete(key);
      this.reconnectDelays.delete(key);
    }
  }
  
  closeAll(): void {
    this.connections.forEach((ws, key) => {
      ws.close(1000, 'Client closing all connections');
    });
    this.connections.clear();
    this.reconnectDelays.clear();
  }
}

// Usage with TypeScript
const ws = new SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN');

// Subscribe to typed order events
ws.subscribe<Order>('orders', 'created', (order) => {
  console.log(`New order #${order.id} - Total: $${order.total}`);
});

ws.subscribe<Order>('orders', 'updated', (order) => {
  console.log(`Order #${order.id} updated - Status: ${order.status}`);
});

ws.subscribe<{ id: number }>('orders', 'deleted', (data) => {
  console.log(`Order #${data.id} was deleted`);
});
Python WebSocket
import asyncio
import json
import websockets
from typing import Callable, Dict, Optional
from dataclasses import dataclass
from datetime import datetime

@dataclass
class Order:
    id: int
    customer_id: int
    total: float
    status: str
    created_at: str

class SwainWebSocket:
    def __init__(self, base_url: str, token: str):
        self.base_url = base_url.replace('https://', 'wss://').replace('http://', 'ws://')
        self.token = token
        self.connections: Dict[str, websockets.WebSocketClientProtocol] = {}
        self.tasks: Dict[str, asyncio.Task] = {}
    
    async def subscribe(
        self,
        model: str,
        event: str,
        callback: Callable,
        retry_delay: int = 5
    ):
        """Subscribe to WebSocket events with automatic reconnection."""
        key = f"{model}/{event}"
        url = f"{self.base_url}/ws/{model}/{event}?token={self.token}"
        
        while True:
            try:
                async with websockets.connect(url) as websocket:
                    self.connections[key] = websocket
                    print(f"✅ Connected to {key}")
                    
                    async for message in websocket:
                        try:
                            payload = json.loads(message)
                            await callback(payload.get('data', payload))
                        except json.JSONDecodeError as e:
                            print(f"Failed to parse message: {e}")
                        except Exception as e:
                            print(f"Error in callback: {e}")
                            
            except websockets.exceptions.ConnectionClosed as e:
                print(f"🔌 Connection closed for {key}: {e}")
                if key in self.connections:
                    del self.connections[key]
                    
            except Exception as e:
                print(f"❌ WebSocket error for {key}: {e}")
            
            # Reconnection logic
            print(f"Reconnecting to {key} in {retry_delay} seconds...")
            await asyncio.sleep(retry_delay)
            retry_delay = min(retry_delay * 2, 30)  # Exponential backoff
    
    async def unsubscribe(self, model: str, event: str):
        """Close a specific WebSocket connection."""
        key = f"{model}/{event}"
        
        if key in self.connections:
            await self.connections[key].close()
            del self.connections[key]
        
        if key in self.tasks:
            self.tasks[key].cancel()
            del self.tasks[key]
    
    async def close_all(self):
        """Close all WebSocket connections."""
        for ws in self.connections.values():
            await ws.close()
        
        for task in self.tasks.values():
            task.cancel()
        
        self.connections.clear()
        self.tasks.clear()

# Usage example
async def main():
    ws = SwainWebSocket('https://api.your-domain.com', 'YOUR_JWT_TOKEN')
    
    # Define callbacks
    async def on_order_created(data):
        order = Order(**data)
        print(f"New order #{order.id} - Total: ${order.total}")
    
    async def on_order_updated(data):
        order = Order(**data)
        print(f"Order #{order.id} updated - Status: {order.status}")
    
    async def on_order_deleted(data):
        print(f"Order #{data['id']} was deleted")
    
    # Create subscription tasks
    tasks = [
        asyncio.create_task(ws.subscribe('orders', 'created', on_order_created)),
        asyncio.create_task(ws.subscribe('orders', 'updated', on_order_updated)),
        asyncio.create_task(ws.subscribe('orders', 'deleted', on_order_deleted))
    ]
    
    try:
        # Run all subscriptions concurrently
        await asyncio.gather(*tasks)
    except KeyboardInterrupt:
        print("Shutting down...")
        await ws.close_all()

# Run the async main function
if __name__ == "__main__":
    asyncio.run(main())
Swift (iOS)
import Foundation

// MARK: - Models
struct Order: Codable {
    let id: Int
    let customerId: Int
    let total: Double
    let status: String
    let created_at: String
}

struct DeleteResponse: Codable {
    let id: Int?
}

struct WebSocketMessage<T: Codable>: Codable {
    let event: String
    let data: T
}

// MARK: - WebSocket Manager
class SwainWebSocket: NSObject {
    private let baseURL: String
    private let token: String
    private var connections: [String: URLSessionWebSocketTask] = [:]
    private var session: URLSession!
    
    init(baseURL: String, token: String) {
        self.baseURL = baseURL
            .replacingOccurrences(of: "https://", with: "wss://")
            .replacingOccurrences(of: "http://", with: "ws://")
        self.token = token
        super.init()
        self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
    }
    
    func subscribe<T: Codable>(
        model: String,
        event: String,
        type: T.Type,
        callback: @escaping (T) -> Void
    ) {
        let key = "\(model)/\(event)"
        let urlString = "\(baseURL)/ws/\(model)/\(event)?token=\(token)"
        
        guard let url = URL(string: urlString) else {
            print("Invalid URL: \(urlString)")
            return
        }
        
        // Close existing connection if any
        unsubscribe(model: model, event: event)
        
        let webSocketTask = session.webSocketTask(with: url)
        connections[key] = webSocketTask
        
        // Start receiving messages
        receiveMessage(key: key, type: type, callback: callback)
        
        // Connect
        webSocketTask.resume()
        print("✅ Connecting to \(key)")
    }
    
    private func receiveMessage<T: Codable>(
        key: String,
        type: T.Type,
        callback: @escaping (T) -> Void
    ) {
        guard let task = connections[key] else { return }
        
        task.receive { [weak self] result in
            switch result {
            case .success(let message):
                switch message {
                case .string(let text):
                    if let data = text.data(using: .utf8) {
                        do {
                            let decoded = try JSONDecoder().decode(
                                WebSocketMessage<T>.self,
                                from: data
                            )
                            DispatchQueue.main.async {
                                callback(decoded.data)
                            }
                        } catch {
                            print("Decoding error: \(error)")
                        }
                    }
                case .data(let data):
                    do {
                        let decoded = try JSONDecoder().decode(
                            WebSocketMessage<T>.self,
                            from: data
                        )
                        DispatchQueue.main.async {
                            callback(decoded.data)
                        }
                    } catch {
                        print("Decoding error: \(error)")
                    }
                @unknown default:
                    break
                }
                
                // Continue receiving messages
                self?.receiveMessage(key: key, type: type, callback: callback)
                
            case .failure(let error):
                print("❌ WebSocket error for \(key): \(error)")
                // Implement reconnection logic
                DispatchQueue.global().asyncAfter(deadline: .now() + 5) { [weak self] in
                    let components = key.split(separator: "/")
                    if components.count == 2 {
                        self?.subscribe(
                            model: String(components[0]),
                            event: String(components[1]),
                            type: type,
                            callback: callback
                        )
                    }
                }
            }
        }
    }
    
    func unsubscribe(model: String, event: String) {
        let key = "\(model)/\(event)"
        if let task = connections[key] {
            task.cancel(with: .goingAway, reason: nil)
            connections.removeValue(forKey: key)
        }
    }
    
    func closeAll() {
        connections.values.forEach { task in
            task.cancel(with: .goingAway, reason: nil)
        }
        connections.removeAll()
    }
}

// MARK: - URLSessionWebSocketDelegate
extension SwainWebSocket: URLSessionWebSocketDelegate {
    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didOpenWithProtocol protocol: String?
    ) {
        print("✅ WebSocket connected")
    }
    
    func urlSession(
        _ session: URLSession,
        webSocketTask: URLSessionWebSocketTask,
        didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
        reason: Data?
    ) {
        print("🔌 WebSocket disconnected with code: \(closeCode)")
    }
}

// MARK: - Usage
class OrderManager {
    let webSocket = SwainWebSocket(
        baseURL: "https://api.your-domain.com",
        token: "YOUR_JWT_TOKEN"
    )
    
    func startListening() {
        // Subscribe to order events
        webSocket.subscribe(
            model: "orders",
            event: "created",
            type: Order.self
        ) { order in
            print("New order #\(order.id) - Total: $\(order.total)")
            // Update UI
        }
        
        webSocket.subscribe(
            model: "orders",
            event: "updated",
            type: Order.self
        ) { order in
            print("Order #\(order.id) updated - Status: \(order.status)")
            // Update UI
        }
        
        webSocket.subscribe(
            model: "orders",
            event: "deleted",
            type: DeleteResponse.self
        ) { data in
            if let orderId = data.id {
                print("Order #\(orderId) was deleted")
                // Update UI
            }
        }
    }
    
    func stopListening() {
        webSocket.closeAll()
    }
}
Kotlin (Android)
import okhttp3.*
import com.google.gson.Gson
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import java.util.concurrent.TimeUnit

// Data classes
data class Order(
    val id: Int,
    val customerId: Int,
    val total: Double,
    val status: String,
    val created_at: String
)

data class WebSocketMessage<T>(
    val event: String,
    val data: T
)

// WebSocket Manager
class SwainWebSocket(
    private val baseUrl: String,
    private val token: String
) {
    private val client = OkHttpClient.Builder()
        .pingInterval(30, TimeUnit.SECONDS)
        .build()
    
    private val gson = Gson()
    private val connections = mutableMapOf<String, WebSocket>()
    private val reconnectDelays = mutableMapOf<String, Long>()
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    
    inline fun <reified T> subscribe(
        model: String,
        event: String,
        crossinline callback: (T) -> Unit
    ): Flow<T> {
        val key = "$model/$event"
        val flow = Channel<T>(Channel.UNLIMITED)
        
        val wsUrl = baseUrl
            .replace("https://", "wss://")
            .replace("http://", "ws://")
        
        val url = "$wsUrl/ws/$model/$event?token=$token"
        
        val request = Request.Builder()
            .url(url)
            .build()
        
        val listener = object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                super.onOpen(webSocket, response)
                connections[key] = webSocket
                reconnectDelays[key] = 1000L // Reset delay
                println("✅ Connected to $key")
            }
            
            override fun onMessage(webSocket: WebSocket, text: String) {
                super.onMessage(webSocket, text)
                try {
                    val message = gson.fromJson(
                        text,
                        WebSocketMessage::class.java
                    )
                    val data = gson.fromJson(
                        gson.toJson(message.data),
                        T::class.java
                    )
                    
                    scope.launch {
                        flow.send(data)
                        withContext(Dispatchers.Main) {
                            callback(data)
                        }
                    }
                } catch (e: Exception) {
                    println("Failed to parse message: ${e.message}")
                }
            }
            
            override fun onClosing(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosing(webSocket, code, reason)
                webSocket.close(1000, null)
                connections.remove(key)
                println("🔌 Closing $key: $reason")
            }
            
            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                super.onClosed(webSocket, code, reason)
                connections.remove(key)
                println("🔌 Closed $key: $reason")
            }
            
            override fun onFailure(
                webSocket: WebSocket,
                t: Throwable,
                response: Response?
            ) {
                super.onFailure(webSocket, t, response)
                connections.remove(key)
                println("❌ WebSocket error for $key: ${t.message}")
                
                // Reconnection with exponential backoff
                val delay = reconnectDelays[key] ?: 1000L
                println("Reconnecting in ${delay}ms...")
                
                scope.launch {
                    delay(delay)
                    subscribe<T>(model, event, callback)
                }
                
                // Increase delay for next reconnection (max 30 seconds)
                reconnectDelays[key] = minOf(delay * 2, 30000L)
            }
        }
        
        // Close existing connection if any
        unsubscribe(model, event)
        
        // Create new WebSocket connection
        client.newWebSocket(request, listener)
        
        return flow.receiveAsFlow()
    }
    
    fun unsubscribe(model: String, event: String) {
        val key = "$model/$event"
        connections[key]?.close(1000, "Client closing connection")
        connections.remove(key)
        reconnectDelays.remove(key)
    }
    
    fun closeAll() {
        connections.values.forEach { ws ->
            ws.close(1000, "Client closing all connections")
        }
        connections.clear()
        reconnectDelays.clear()
        scope.cancel()
    }
}

// Usage in Android Activity/Fragment
class OrderActivity : AppCompatActivity() {
    private lateinit var webSocket: SwainWebSocket
    private val orders = mutableListOf<Order>()
    private lateinit var adapter: OrderAdapter
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_orders)
        
        webSocket = SwainWebSocket(
            baseUrl = "https://api.your-domain.com",
            token = "YOUR_JWT_TOKEN"
        )
        
        setupRecyclerView()
        startListeningToOrders()
    }
    
    private fun startListeningToOrders() {
        // Subscribe to order created events
        lifecycleScope.launch {
            webSocket.subscribe<Order>("orders", "created") { order ->
                println("New order #${order.id} - Total: $${order.total}")
                orders.add(order)
                adapter.notifyItemInserted(orders.size - 1)
            }.collect { order ->
                // Flow-based processing
                processNewOrder(order)
            }
        }
        
        // Subscribe to order updated events
        lifecycleScope.launch {
            webSocket.subscribe<Order>("orders", "updated") { order ->
                println("Order #${order.id} updated - Status: ${order.status}")
                val index = orders.indexOfFirst { it.id == order.id }
                if (index != -1) {
                    orders[index] = order
                    adapter.notifyItemChanged(index)
                }
            }.collect()
        }
        
        // Subscribe to order deleted events
        lifecycleScope.launch {
            webSocket.subscribe<Map<String, Int>>("orders", "deleted") { data ->
                data["id"]?.let { orderId ->
                    println("Order #$orderId was deleted")
                    val index = orders.indexOfFirst { it.id == orderId }
                    if (index != -1) {
                        orders.removeAt(index)
                        adapter.notifyItemRemoved(index)
                    }
                }
            }.collect()
        }
    }
    
    private fun processNewOrder(order: Order) {
        // Additional processing logic
    }
    
    override fun onDestroy() {
        super.onDestroy()
        webSocket.closeAll()
    }
}

Errors & validation

The API returns structured error responses with appropriate HTTP status codes. All error responses include a JSON object with a message field and optionally additional context. Input validation ensures type safety and proper operator usage. The API supports database transactions for data consistency - if any part of a request fails, the entire operation is rolled back.

HTTP Status Codes

StatusMeaningCommon Causes
200SuccessRequest completed successfully
201CreatedResource created successfully
204No ContentNot used; deletions return 200 with JSON
400Bad RequestInvalid JSON, wrong data type, invalid filter syntax
401UnauthorizedMissing or invalid authentication token
403ForbiddenValid auth but insufficient permissions (RLS violation)
404Not FoundResource doesn't exist or is inaccessible
409ConflictUnique constraint violation, concurrent update conflict
422Unprocessable EntityValidation error, business rule violation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error
503Service UnavailableDatabase connection lost, maintenance mode

Error Response Format

Basic Error
{
  "message": "Invalid operator: ILIKE not supported for field type"
}
Validation Error
{
  "message": "Validation failed",
  "errors": {
    "email": "Invalid email format",
    "age": "Must be a positive integer"
  }
}

Common Error Scenarios

Authentication Error (401)
# Request without authentication
curl https://api.your-domain.com/api/user
Response
{
  "message": "Authentication required"
}
Permission Error (403)
// User tries to access data outside their tenant scope
{
  "message": "Access denied: Row-level security policy violation",
  "resource": "orders",
  "policy": "tenant_isolation"
}
Constraint Violation (409)
// Attempting to create duplicate unique value
{
  "message": "Unique constraint violation",
  "field": "email",
  "value": "user@example.com",
  "constraint": "users_email_unique"
}

Input Validation

The API performs comprehensive validation before executing any database operations:

  • Type Validation: Ensures field values match expected data types
  • Required Fields: Checks that all non-nullable fields are provided
  • Format Validation: Validates emails, URLs, dates, and other formatted data
  • Range Validation: Enforces min/max values for numbers and string lengths
  • Relationship Validation: Verifies foreign key references exist
  • Custom Business Rules: Applies any model-specific validation logic

Transaction Handling

All write operations (CREATE, UPDATE, DELETE) are wrapped in database transactions. This ensures:

  • Atomicity - all changes succeed or all fail
  • Consistency - database constraints are maintained
  • Isolation - concurrent requests don't interfere
  • Durability - committed changes persist

Rate Limiting

APIs include rate limiting to prevent abuse. When limits are exceeded:

Rate Limit Response
// Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995200
Retry-After: 3600
Response Body
{
  "message": "Rate limit exceeded. Try again in 3600 seconds.",
  "retry_after": 3600
}