Live State logolive-state

Server

The server is used to expose the Router to the world, it creates both the HTTP and WebSocket handlers. It also handles persistence.

Creating a server

You can create a server using the server function, which takes a schema, router and a storage:

server.ts
import { server, SQLStorage } from "@live-state/sync/server";
import { schema } from "./schema";
import { appRouter } from "./router";
import { Pool } from "pg";

const lsServer = server({
  router: appRouter,
  storage: new SQLStorage(
    new Pool({
      connectionString: "postgresql://admin:admin@localhost:5442/live-state",
    })
  ),
  schema,
  contextProvider: async ({ transport, headers, cookies, query }) => {
    // Generate context from request data
    const token = headers.authorization?.replace("Bearer ", "");
    const user = token ? await validateToken(token) : null;

    return {
      userId: user?.id,
      user,
      transport,
      requestId: generateId(),
    };
  },
  middlewares: [
    // Global middleware for all routes
    async ({ req, next }) => {
      console.log(`${req.type} ${req.resource}`);
      return next(req);
    },
  ],
});

Context providers

Context providers generate request context from incoming HTTP headers, cookies, and query parameters. This context is then available to all middlewares, authorization handlers, and custom mutations.

Basic context provider

const lsServer = server({
  router: appRouter,
  storage,
  schema,
  contextProvider: async ({ transport, headers, cookies, query }) => {
    return {
      transport, // "HTTP" or "WEBSOCKET"
      userAgent: headers["user-agent"],
      sessionId: cookies.sessionId,
      apiKey: query.apiKey,
    };
  },
});

Server middlewares

Server-level middlewares run before route-specific middlewares and apply to all routes.

const loggingMiddleware = async ({ req, next }) => {
  const start = Date.now();
  console.log(`→ ${req.type} ${req.resource}`, {
    context: req.context,
    input: req.input,
  });

  try {
    const result = await next(req);
    const duration = Date.now() - start;
    console.log(`← ${req.type} ${req.resource} (${duration}ms)`);
    return result;
  } catch (error) {
    const duration = Date.now() - start;
    console.error(`✗ ${req.type} ${req.resource} (${duration}ms)`, error);
    throw error;
  }
};

const lsServer = server({
  router: appRouter,
  storage,
  schema,
  middlewares: [loggingMiddleware],
});

Adapters

The server can be served using different adapters. As of right now, the only adapter is the Express adapter.

Express

The Express adapter requires JSON and query string parsing. And as it's also a WebSocket server, it requires the use of express-ws.

express-server.ts
import express from "express";
import expressWs from "express-ws";
import { expressAdapter } from "@live-state/sync/server";
import { lsServer } from "./server";
import cors from "cors";

const { app } = expressWs(express());

app
  .use(express.urlencoded({ extended: true }))
  .use(express.json())
  .use(cors());

expressAdapter(app, lsServer);

app.listen(5001, () => console.log("api running on 5001"));