Networked access

Server

Expose any Sirannon instance over HTTP and WebSocket with one function call, backed by uWebSockets.js.

Table of Contents

The server export turns a Sirannon instance into a network service. Applications query it over HTTP, and WebSocket connections add real-time change subscriptions. This page builds one file, server.ts, step by step. You stand up a minimal loopback server, then add the options that shape how it accepts connections.

Start a server

Open a database on the Sirannon instance, pass that instance to createServer, and call listen. Start server.ts with the driver, the instance, an open database, and a server on the default port.

server.ts
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
import { createServer } from '@delali/sirannon-db/server'
 
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
await sirannon.open('app', './data/app.db')
 
const server = createServer(sirannon)
await server.listen()

With no options, the server binds 127.0.0.1 on port 9876. That default keeps it on the loopback interface, so nothing outside the host can reach it until you decide to expose it. HTTP request bodies and WebSocket messages are capped at 1 MB by default to reduce memory-exhaustion risk, a single limit you can raise or lower with maxBodyBytes, and remote errors return machine-readable codes without stack traces or internal details.

Choose a port and screen requests

createServer(sirannon, options) accepts a port, a cors origin, a maxBodyBytes cap, and an onRequest gate that runs before every HTTP request and WebSocket upgrade. Return { status, code, message } from onRequest to deny, or return nothing to allow. Set an explicit port and turn away requests that lack a bearer token.

server.ts
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
import { createServer } from '@delali/sirannon-db/server'
 
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
await sirannon.open('app', './data/app.db')
 
const server = createServer(sirannon) 
const server = createServer(sirannon, { 
  port: 9876,
  onRequest: ({ headers }) => {
    if (headers.authorization !== 'Bearer local-dev-token') {
      return { status: 401, code: 'UNAUTHORIZED', message: 'Missing or invalid token' }
    }
  },
})
await server.listen()

The onRequest gate is the extension point for authentication. This example checks a fixed token to show the shape. The full picture is in the security guide, covering Authorization: Bearer headers, WebSocket credentials over Sec-WebSocket-Protocol, TLS termination, and SQL access control. Read it before exposing the server beyond localhost.

Cap the request size

maxBodyBytes sets the largest HTTP request body and the largest WebSocket message the server accepts, in bytes. One value governs both transports. It must be a positive integer and defaults to 1_048_576 (1 MB). Set it to match the biggest write you expect, and keep it small enough that a single request cannot exhaust memory.

server.ts
import { Sirannon } from '@delali/sirannon-db'
import { betterSqlite3 } from '@delali/sirannon-db/driver/better-sqlite3'
import { createServer } from '@delali/sirannon-db/server'
 
const driver = betterSqlite3()
const sirannon = new Sirannon({ driver })
await sirannon.open('app', './data/app.db')
 
const server = createServer(sirannon, {
  port: 9876,
  maxBodyBytes: 4 * 1024 * 1024, 
  onRequest: ({ headers }) => {
    if (headers.authorization !== 'Bearer local-dev-token') {
      return { status: 401, code: 'UNAUTHORIZED', message: 'Missing or invalid token' }
    }
  },
})
await server.listen()

createServer throws a SirannonError with code INVALID_MAX_BODY_BYTES when the value is not a positive integer. Once the server is running, a request or message that goes over the cap is rejected with PAYLOAD_TOO_LARGE rather than read into memory. A bulk load that exceeds the cap must be split into several sequential loads.

Three write shapes

The server accepts writes in three shapes over both transports. Reach for each one when the write in front of you matches it.

  • Use a transaction to run several different statements that must all succeed or all fail together, such as a debit on one row and a credit on another.
  • Use a batch to run one statement many times with different values, such as inserting a thousand rows into the same table. It costs less than a transaction of a thousand near-identical statements, and it stays all-or-nothing.
  • Use a load for a large, from-scratch import. It relaxes durability while the rows go in and restores it afterwards, so it trades power-loss safety during the load for speed; if the process dies mid-load, you run it again. The bulk load guide covers the durability levels and the summary it returns.

HTTP routes

MethodPathDescription
POST/db/:id/queryExecutes a SELECT and returns { rows }.
POST/db/:id/executeExecutes a mutation and returns { changes, lastInsertRowId }.
POST/db/:id/transactionExecutes many statements atomically in one transaction and returns { results }.
POST/db/:id/batchApplies one statement over many parameter sets in one transaction and returns { results }.
POST/db/:id/loadBulk-loads rows with relaxed durability and returns { rowsLoaded, changes }.
GET/healthReports liveness.
GET/health/readyReports readiness with per-database status.

WebSocket protocol

Connect to ws://host:port/db/:id and send JSON messages. Every message carries a type and a client-chosen id, and every reply echoes that id. The same query, execute, transaction, batch, and load shapes run over the socket, and the server dispatches CDC change events to subscribers in real time, which is what powers the client SDK's live subscriptions.

Inbound typeFieldsReply
querysql, params?{ type: 'result', data: { rows } }
executesql, params?{ type: 'result', data: { changes, lastInsertRowId } }
transactionstatements, writeConcern?{ type: 'result', data: { results } }
batchsql, paramsBatch, writeConcern?{ type: 'result', data: { results } }
loadsql, paramsBatch, durability?{ type: 'result', data: { rowsLoaded, changes } }
subscribetable, filter?{ type: 'subscribed' }, then change events
unsubscribenone{ type: 'unsubscribed' }

The transaction, batch, and load messages run every statement server-side in one transaction and reply once. The server never holds the write lock across a network round-trip, so it does not accept an interactive transaction where the client sends BEGIN, then more statements, then COMMIT over separate messages. A single slow or dead client would otherwise freeze every write to the database.