Operations

Security

Understand the built-in protections, then put the right boundary around the server before anything untrusted can reach it.

Table of Contents

Sirannon gives you secure primitives, and the network server is intentionally low-level: it can execute SQL sent by a client. Treat it as a database endpoint, not a public application API, unless you add an explicit application security boundary around it.

Built-in protections

  • The driver layer keeps parameterised values separate from the SQL text. Keep user input in params and never concatenate it into SQL strings.
  • CDC table and column names are validated against a strict allowlist regex and escaped, so change tracking never becomes an injection path.
  • Migration and backup paths reject null bytes, .. segments, and control characters before filesystem access.
  • HTTP request bodies and WebSocket messages are capped at 1 MB by default to reduce memory-exhaustion risk, and maxBodyBytes raises or lowers that single limit across both transports.
  • Remote errors return a machine-readable code and message with stack traces and internal details stripped.
  • Read and write operations use separate connection pools, and read-only databases enforce immutability at the connection level.

Choose a deployment boundary

For production, put the server behind one of these boundaries:

  • A server-side application layer that exposes domain actions, such as createOrder or markPaid, instead of arbitrary SQL.
  • A private network boundary where only trusted services reach the Sirannon server.
  • A hook layer that allows only specific statements, tenants, and tables.

Never expose unrestricted /db/:id/query, /db/:id/execute, /db/:id/transaction, or the /db/:id WebSocket route directly to untrusted browsers.

Authenticate HTTP routes

onRequest runs before database routes and denies a request by returning a status, code, and message; returning undefined allows it. Health endpoints bypass it. Build server.ts around an engine and one open database, then guard every request with 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('orders', './data/orders.db')
 
const server = createServer(sirannon, {
  port: 9876,
  onRequest: ({ headers }) => { 
    if (headers.authorization !== `Bearer ${process.env.SIRANNON_API_TOKEN}`) {
      return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
    }
  },
})
 
await server.listen()

A matching client.ts sends the same token through headers, so every HTTP route reaches the server already authenticated.

client.ts
import { SirannonClient } from '@delali/sirannon-db/client'
 
const client = new SirannonClient('https://db.example.com', {
  transport: 'http',
  headers: { Authorization: `Bearer ${process.env.SIRANNON_API_TOKEN}` },
})
 
const orders = client.database('orders')

Authenticate WebSocket upgrades

Browsers cannot attach an arbitrary Authorization header to a WebSocket, so the bearer check alone leaves the /db/:id WebSocket route open. Widen the same onRequest hook: read method and path to spot the upgrade, validate the Origin header against an explicit allowlist, and accept a short-lived value in Sec-WebSocket-Protocol that the browser can send. The header check still guards the HTTP routes.

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('orders', './data/orders.db')
 
const server = createServer(sirannon, {
  port: 9876,
  onRequest: ({ headers }) => { 
    if (headers.authorization !== `Bearer ${process.env.SIRANNON_API_TOKEN}`) {
      return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
    }
  },
  onRequest: ({ headers, method, path }) => { 
    const isWebSocketUpgrade = method === 'GET' && path.startsWith('/db/')
    if (!isWebSocketUpgrade) {
      if (headers.authorization !== `Bearer ${process.env.SIRANNON_API_TOKEN}`) {
        return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid or missing token' }
      }
      return
    }
 
    if (headers.origin !== 'https://app.example.com') {
      return { status: 403, code: 'FORBIDDEN_ORIGIN', message: 'Forbidden origin' }
    }
 
    const protocols = (headers['sec-websocket-protocol'] ?? '').split(',').map((value) => value.trim())
    if (!protocols.includes(String(process.env.SIRANNON_WS_PROTOCOL))) {
      return { status: 401, code: 'UNAUTHORIZED', message: 'Invalid WebSocket credentials' }
    }
  },
})
 
await server.listen()

A browser client passes the same value through webSocketProtocols. Keep it URL-safe and short-lived.

client.ts
import { SirannonClient } from '@delali/sirannon-db/client'
 
const client = new SirannonClient('https://db.example.com', { 
  transport: 'http',
  headers: { Authorization: `Bearer ${process.env.SIRANNON_API_TOKEN}` },
})
const client = new SirannonClient('wss://db.example.com', { 
  transport: 'websocket',
  webSocketProtocols: [String(process.env.SIRANNON_WS_PROTOCOL)],
})
 
const orders = client.database('orders')

TLS, CORS, and access control

The built-in server binds plain HTTP and WebSocket. For any traffic outside a trusted local network, terminate TLS upstream with a reverse proxy or platform edge, and use https:// and wss:// client URLs. Without TLS, credentials, SQL text, parameters, and CDC payloads are sent in cleartext.

CORS is disabled by default. Enable it only for browser clients that need direct HTTP access, and restrict origins to trusted domains; cors: true allows every origin and belongs in local development only. CORS does not protect WebSocket upgrades, so validate the Origin header in onRequest.

Authentication identifies the caller; it does not make arbitrary SQL safe. Prefer application endpoints that perform domain operations, allow only known statement shapes when clients must use the data API, and keep tenant identifiers and ownership rules on the server side.

Checklist

  • Bind the server to 127.0.0.1 or a private interface unless a proxy enforces TLS and access control.
  • Use HTTPS and WSS for non-local traffic.
  • Authenticate every HTTP database route and every WebSocket upgrade.
  • Validate the WebSocket Origin against an explicit allowlist.
  • Keep SQL behind application actions or a strict allowlist.
  • Keep user input in SQL parameters, never in interpolated strings.
  • Restrict CORS to known origins.
  • Redact authorisation headers, cookies, and WebSocket auth values from logs, and avoid logging full SQL when it can contain sensitive data.