Skip to content

FOR DEVELOPERS · SEVEN PACKAGES · FREE · MIT

Build with the OpenSeat SDK

The free seat map SDK for your own platform. Seven focused TypeScript packages: a layout schema, a canvas renderer, React components, a full designer, booking rules, a REST adapter and design tokens. Install them from npm; no account, no API key, no fees.

npm i @openseat/react @openseat/core @openseat/adapter-rest konva react react-dom
first npm release pending
  • Free · MIT licensed
  • TypeScript
  • ES modules
  • React 18.2+ / 19
  • Strict CSP, no eval

186 seats · drawn by layoutToSvg at build time

Stage
Seat map rendered by layoutToSvg
Tickets.tsx
<BookingMap
  layout={layout}
  statuses={statuses}
  rules={{ maxSeats: 6, preventOrphans: true }}
  categoryPrices={{ vip: "₹1,499", stalls: "₹499" }}
/>

ROW A · QUICKSTART

From npm install to booking map in three steps

OpenSeat is free and MIT licensed. Install the packages, render a booking map, then connect it to your backend. There is nothing to sign up for.
npm

First npm release pending. The seven packages are implemented and tested, and will be published to the public npm registry under @openseat. Until then the install command below won't resolve. The API may still change before 1.0.

API reference
  1. Install from npm

    Install only the parts you need. Each package ships ES modules and TypeScript declarations; konva, react and react-dom are peer dependencies (React 18.2+ or 19, Konva 10).

    Want to see it first? Open the live demo

    Terminal
    npm i @openseat/react @openseat/core @openseat/adapter-rest \
      konva react react-dom
    
    # pnpm add … and yarn add … work the same way.
    # Embedding the designer too? Add @openseat/editor.
  2. Render a booking map

    Generate a hall in code, load live statuses through an adapter and let BookingMap handle selection, rules and accessibility. This example is type-checked against the packages in CI.

    Embedding the designer? Import its stylesheet once: import "@openseat/editor/styles.css". @openseat/react needs no CSS.

    Venue.tsx
    import { useState } from "react";
    import { createLayout, generateStraightRows } from "@openseat/core";
    import { createRestAdapter } from "@openseat/adapter-rest";
    import { BookingMap, useInventory } from "@openseat/react";
    
    const layout = createLayout({ id: "hall", name: "Main hall", width: 1000, height: 800 });
    layout.objects = generateStraightRows({
      idPrefix: "stalls",
      rowCount: 10,
      seatsPerRow: 20,
      origin: { x: 100, y: 100 },
      seatSpacing: 28,
      rowSpacing: 32,
      aisleAfter: [10],
    }).objects;
    
    const api = createRestAdapter({ baseUrl: "https://api.example.com" });
    
    export function Venue() {
      const { statuses } = useInventory(api, "show-2026-11-20");
      const [selection, setSelection] = useState<readonly string[]>([]);
      return (
        <BookingMap
          layout={layout}
          statuses={statuses}
          selectedSeatIds={selection}
          onSelectionChange={(next) => setSelection(next)}
          rules={{ maxSeats: 8, preventOrphans: true }}
        />
      );
    }
    A seat shown as available can be taken a moment later. Only a successful hold from your server reserves it.
  3. Connect your backend

    Your server owns holds, bookings and payments. Implement the four BookingAdapter methods against the API you already have, or follow the reference REST contract and use createRestAdapter unchanged.

    BookingAdapter referenceREST contract

    api.ts
    import { createRestAdapter } from "@openseat/adapter-rest";
    
    // Your API follows the reference REST contract…
    export const api = createRestAdapter({
      baseUrl: "https://api.example.com",
      credentials: "include", // cookie sessions; or pass getAccessToken for a bearer token
      realtime: { kind: "poll", intervalMs: 5000 },
    });
    
    // …or implement BookingAdapter from @openseat/booking yourself:
    // getStatuses, holdSeats, confirmHold and releaseHold.

API reference

Every export of all seven packages, plus the REST contract, generated from their TypeScript types so it always matches the code you install.

Open the API reference

ROW B · PACKAGES

Seven packages. Install the parts you need.

A buyer page that imports @openseat/react and @openseat/booking never loads the editor or the templates; our CI size check keeps a booking-only page under 200 kB gzipped for initial load.
  • @openseat/core

    Browser + Node

    Layout schema (Zod), validation, serialisation, migrations, geometry, generators, edit operations and the inventory manifest.

    • createLayout
    • validateLayout
    • createBlockSection
    • generateCurvedRows
    • arenaPreset
    • diffManifests

    @openseat/core/templates: the 104 venue templates in a separate, lazy entry point.

    Depends on
    zod
  • @openseat/theme-default

    Anywhere

    Design tokens and seat-state styles for the canvas, plus interface tokens as CSS custom properties.

    • createTheme
    • defaultTheme
    • uiTokens
    • uiTokenCssVariables
    Depends on
    nothing
  • @openseat/booking

    Browser + Node

    Statuses, selection rules, the hold lifecycle, cart totals, adapter contracts and an in-memory demo adapter.

    • BookingAdapter
    • buildCart
    • createInMemoryBookingAdapter
    • BookingError
    Depends on
    core
  • @openseat/renderer-konva

    Browser (SVG: Node too)

    Canvas rendering, hit testing, pan, zoom and pinch, selection, drag and transform, and SVG export.

    • SeatMapRenderer
    • layoutToSvg
    • fitBounds
    • zoomAt

    layoutToSvg builds a string, so it also runs in Node with no canvas.

    Depends on
    core, theme-default
    Peers
    konva
  • @openseat/react

    Browser (client components)

    React bindings: the buyer booking map, a controlled seat map, a quantity picker for zones and hooks.

    • BookingMap
    • SeatMap
    • AreaQuantityPicker
    • useInventory
    • useHold
    • useLayout
    Depends on
    core, booking, renderer-konva, theme-default
    Peers
    react, react-dom, konva
  • @openseat/editor

    Browser (client only)

    LayoutEditor: the full designer with its store, history, snapping, shortcuts and panels. One stylesheet.

    • LayoutEditor
    • createEditorStore
    • @openseat/editor/styles.css
    Depends on
    core, react, renderer-konva, theme-default, zustand
    Peers
    react, react-dom, konva
  • @openseat/adapter-rest

    Browser + Node

    Optional BookingAdapter plus layout load and save for the reference REST contract: If-Match revisions, SSE or polling.

    • createRestAdapter
    • DEFAULT_PATHS
    Depends on
    core, booking, zod
  • How they stack

    Each layer only depends on the layers below it.

    1. L4 · Appeditor
    2. L3 · Bindingsreactadapter-rest
    3. L2 · Enginesbookingrenderer-konva
    4. L1 · Foundationcoretheme-default

ROW C · ARCHITECTURE

Two surfaces, one document.

Organisers edit a layout in LayoutEditor. Each save sends your server the layout, the revision it started from and an inventory manifest. Publishing an event snapshots one revision and seeds an inventory row per seat and per area. Buyers load that layout and live availability through a BookingAdapter, BookingMap draws it, and your server holds the chosen places atomically before payment.
  1. Organiser

    LayoutEditor

    @openseat/editor

    Draws the venue, sets ticket types

  2. onSave

    Your backend or the OpenSeat server

    { layout, baseRevision, force, manifest }

    Layouts (revisioned) → publish → event snapshot → inventory, holds, bookings, payments

  3. Buyer

    BookingMap

    @openseat/react

    Loads layout + availability, then holdSeats → confirmHold through a BookingAdapter

Where the truth lives
DataOwner
Venue geometry, ticket typesThe layout document
PricesYour event's price list (categoryPrices)
AvailabilityYour inventory (SeatStatusRecord[])
Holds and bookingsYour server (countdown follows its expiresAt)
Selection, cartThe browser: never trusted

The rule

The SDK only displays and selects. Your backend owns inventory truth, atomic holds, hold expiry, booking confirmation, payment verification and pricing.

API reference

ROW D · CODE SAMPLES

Recipes

Copy-ready snippets for the things teams build first. Every call is a real export of the packages, checked against their type declarations.

Recipe 01@openseat/react · @openseat/adapter-rest

Booking map with live inventory and holds

useInventory keeps statuses fresh through the adapter (polling by default, server-sent events here). useHold runs the hold lifecycle with a countdown that follows your server's expiry.

  • Rules (maxSeats, preventOrphans) are checked in the browser for a smooth experience and again on your server.
  • categoryPrices is display text only: the SDK never computes prices.
Tickets.tsx
import { useState } from "react";
import type { LayoutDocument } from "@openseat/core";
import { createRestAdapter } from "@openseat/adapter-rest";
import { BookingMap, useHold, useInventory } from "@openseat/react";

// Create the adapter once (module scope or useState), never inline in render.
const api = createRestAdapter({
  baseUrl: `${location.origin}/api`,
  credentials: "same-origin",
  realtime: { kind: "sse", url: (id) => `${location.origin}/api/v1/events/${encodeURIComponent(id)}/stream` },
});

export function Tickets({ layout, eventId }: { layout: LayoutDocument; eventId: string }) {
  const { statuses } = useInventory(api, eventId); // live over server-sent events
  const hold = useHold(api, eventId, { maxTickets: 6, layout });
  const [selection, setSelection] = useState<readonly string[]>([]);

  return (
    <>
      <BookingMap
        layout={layout}
        statuses={statuses}
        selectedSeatIds={selection}
        onSelectionChange={(next) => setSelection(next)}
        rules={{ maxSeats: 6, preventOrphans: true }}
        categoryPrices={{ vip: "₹1,499", stalls: "₹499", balcony: "₹299" }}
      />
      <button disabled={!hold.canHold || selection.length === 0} onClick={() => hold.hold(selection)}>
        Hold {selection.length} seats
      </button>
      {hold.state.status === "held" ? <p>Held for {Math.ceil(hold.remainingMs / 1000)} s</p> : null}
    </>
  );
}

@openseat/react API reference

Recipe 02@openseat/editor

Embed LayoutEditor with save and revision conflicts

The full designer in your organiser dashboard. Saves carry the base revision and an inventory manifest; the REST adapter turns a 409 into a conflict the editor resolves with the user.

  • The editor fills its container: give it a height (100dvh in a dashboard).
  • Client-only: mount it in a client component, never server-render it.
OrganiserPage.tsx
import { useState } from "react";
import type { LayoutDocument } from "@openseat/core";
import { createRestAdapter } from "@openseat/adapter-rest";
import { LayoutEditor } from "@openseat/editor";
import "@openseat/editor/styles.css";

const api = createRestAdapter({
  baseUrl: "https://tickets.example.com/api",
  credentials: "include", // staff cookie session
  sendManifest: true, // PUT body: { layout, manifest }
});

export function OrganiserPage({ layout }: { layout: LayoutDocument }) {
  const [prices, setPrices] = useState<Record<string, string>>({ vip: "₹1,499", general: "₹299" });
  return (
    <div style={{ height: "100dvh" }}>
      <LayoutEditor
        initialLayout={layout}
        // Sends If-Match: "<baseRevision>". A 409/412 resolves as { status: "conflict", remote },
        // and the editor offers "Load their version" or "Overwrite with mine" (force: true).
        onSave={(request) => api.saveLayout(request)}
        categoryPrices={prices}
        onCategoryPricesChange={setPrices}
      />
    </div>
  );
}

@openseat/editor API reference

Recipe 03@openseat/core

Generate parametric sections in code

createBlockSection builds exactly what the designer's Section tool makes. Change rows, seats per row or curvature later and the section regenerates while seats that still fit keep their IDs.

  • generateStraightRows and generateCurvedRows are simpler generators for plain rows and arcs.
  • A category sets its colour with the key colour (British spelling), not color.
hall.ts
import { createLayout, createBlockSection, updateSectionSeating, validateLayout } from "@openseat/core";

const empty = createLayout({
  id: "hall",
  name: "Main hall",
  width: 1200,
  height: 800,
  categories: [{ id: "general", name: "General", colour: "#2563EB" }], // "colour", not "color"
});

const { layout, sectionId } = createBlockSection(empty, {
  position: { x: 600, y: 400 },
  categoryId: "general",
  seating: {
    kind: "block",
    rows: 10,
    seatsPerRow: 20,
    seatSpacing: 24,
    rowSpacing: 30,
    curvature: 20, // -100..100; > 0 arcs around the stage in front
    align: "center",
    aisles: [10],
    aisleWidth: 30,
    seatShape: { kind: "circle", radius: 9 },
    seatLabeling: { format: "numeric", start: 1, step: 1, direction: "ltr" },
  },
});

// Change a parameter later: seats that still have a slot keep their IDs.
const { layout: bigger, report } = updateSectionSeating(layout, sectionId, { rows: 14, curvature: 35 });
console.log(validateLayout(bigger).valid, report.removedSeatIds); // true []

@openseat/core API reference

Recipe 04@openseat/core

A 9,758-seat arena in one call

arenaPreset returns the options for a complete bowl: rink with markings, lower bowl, boxes, upper bowl and fan-shaped corners. estimateArenaSeats is exact, so you can show or cap a seat count before generating.

  • Generates in about 36 ms (median, Node 26, Apple M5).
  • Every generated section stays editable.
arena.ts
import { arenaPreset, createArenaLayout, estimateArenaSeats } from "@openseat/core";

const options = { ...arenaPreset("hockey"), idPrefix: "a", center: { x: 0, y: 0 } };
console.log(estimateArenaSeats(options)); // 9758, exact and effectively free

const arena = createArenaLayout({ ...options, layoutId: "arena" });
// Presets: "hockey" | "basketball" | "concert-in-round" | "football"

@openseat/core API reference

Recipe 05@openseat/core/templates

Start from one of 104 venue templates

Search by venue or by event, type the expected audience and get a layout fitted to it, with wheelchair spaces, Not for sale places and suggested prices in ₹. Any setup can also be built as sections only.

  • A separate entry point, so booking pages never pay for it.
  • summary.notes lists every adjustment in plain words.
templates.ts
// A separate, lazy entry point: buyer pages never load it.
const { buildTemplate, searchTemplates } = await import("@openseat/core/templates");

const [hit] = searchTemplates("farewell banquet", { limit: 1 }); // social.banquet-hall.rounds-dance

const { layout, summary } = buildTemplate("performance.auditorium.annual-day", null, {
  name: "Annual Day 2026",
  targetCapacity: 400, // fits rows to the audience
  zonesOnly: false, // true: "Sections only (no seats)", sold by quantity
});
summary.totalCapacity; // 408
summary.notes; // every clamp, in plain words

@openseat/core API reference

Recipe 06@openseat/renderer-konva

Export a layout to SVG, even in Node

layoutToSvg returns a standalone, deterministic SVG string with no canvas or DOM: posters, emails, previews and server-side thumbnails. The seat map at the top of this page was drawn this way at build time.

  • Text is XML-escaped; no ids, no defs, no inline styles.
  • Pass a theme or seatStates to colour it.
export-svg.ts
import { writeFileSync } from "node:fs";
import { buildTemplate } from "@openseat/core/templates";
import { layoutToSvg } from "@openseat/renderer-konva"; // not @openseat/core

const { layout } = buildTemplate("performance.auditorium.annual-day", null, { name: "Annual Day 2026" });
writeFileSync("annual-day.svg", layoutToSvg(layout)); // standalone, deterministic, no canvas or DOM

@openseat/renderer-konva API reference

Recipe 07@openseat/theme-default

Make it look like your product

Two sets of tokens: a canvas Theme for seats, sections, zones and stages, passed to BookingMap, SeatMap, LayoutEditor and layoutToSvg; and interface tokens as CSS custom properties for the chrome around the map.

  • Seat states keep a shape marker, so they never rely on colour alone.
  • Setting properties works under a strict CSP.
theme.tsx
import { createTheme } from "@openseat/theme-default";
import { BookingMap } from "@openseat/react";
import type { LayoutDocument } from "@openseat/core";

// Canvas tokens: deep-merged over the default theme, never mutated.
const theme = createTheme({
  fontFamily: "Geist, system-ui, sans-serif",
  canvas: { background: "#FFFFFF" },
  seat: { selected: { fill: "#4F46E5", stroke: "#3730A3" } },
});

export const Buyer = ({ layout }: { layout: LayoutDocument }) => <BookingMap layout={layout} theme={theme} />;
app.css
/* The designer's chrome reads CSS custom properties (or set the shared --os-ui-* tokens). */
.os-editor {
  --os-accent: #4f46e5;
  --os-font: "Geist", system-ui, sans-serif;
  --os-radius: 12px;
}

@openseat/theme-default API reference

ROW E · BACKEND

Your backend, or ours

OpenSeat brings no backend, database or payment provider of its own. Connect it to the API you already have, or self-host the free OpenSeat server (public image coming soon).
Bring your own API

Implement the adapter contract

Four methods connect BookingMap and useHold to your API. The typed contracts and the optional REST adapter for the reference API are documented in the API reference.

adapter.ts
import type { Booking, BookingAdapter, Hold, SeatStatusRecord } from "@openseat/booking";

async function json<T>(res: Promise<Response>): Promise<T> {
  return (await res).json() as Promise<T>;
}

// Your API, your rules. (If it follows the reference contract, use createRestAdapter instead.)
export const adapter: BookingAdapter = {
  getStatuses: ({ eventId }) => json<SeatStatusRecord[]>(fetch(`/api/events/${eventId}/seats`)),
  holdSeats: (input) =>
    json<Hold>(fetch(`/api/events/${input.eventId}/holds`, { method: "POST", body: JSON.stringify(input) })),
  confirmHold: (input) =>
    json<Booking>(fetch(`/api/holds/${input.holdId}/confirm`, { method: "POST", body: JSON.stringify(input) })),
  releaseHold: async (holdId) => {
    await fetch(`/api/holds/${holdId}`, { method: "DELETE" });
  },
};

Rules your server must enforce

  • One per-buyer ticket limit across seats, standing places, booths and whole tables
  • Not for sale places are never held or sold, whatever the client sends
  • Holds are all-or-nothing and atomic; retries reuse the idempotency key
  • Prices come from your price list, never from the client
  • Layout saves check If-Match revisions and never remove held or sold seat IDs
  • Rate limits per customer and per IP on holds and confirmations
save-layout.ts
import { buildInventoryManifest, diffManifests, validateLayout } from "@openseat/core";

const result = validateLayout(body.layout); // never trust the client's layout
if (!result.valid || !result.layout) throw new Error("invalid layout"); // result.errors says why
const next = buildInventoryManifest(result.layout);
const { removedSeatIds } = diffManifests(previousManifest, next);
// Refuse the save if any removed seat is held or sold for a live event.
Self-host the OpenSeat server

Fastify and Postgres 16, free to self-host

The OpenSeat server implements the reference contract exactly, so createRestAdapter works against it unchanged. Node 22+, no ORM, no Redis. A public Docker image is coming soon.

Reference REST contract
GET    /v1/events/:eventId/seats          # the event's seats
GET    /v1/events/:eventId/availability   # seat statuses
GET    /v1/events/:eventId/stream         # live updates (SSE)
POST   /v1/events/:eventId/holds          # atomic, all-or-nothing hold
GET    /v1/holds/:holdId                  # hold and its expiresAt
DELETE /v1/holds/:holdId                  # release
POST   /v1/holds/:holdId/confirm          # hold → booking
GET    /v1/layouts/:layoutId              # load a layout and its revision
PUT    /v1/layouts/:layoutId              # save with If-Match
POST   /v1/layouts                        # create
POST   /v1/layouts/:layoutId/validate     # validate (optional)
  • Organiser accounts and events with a short /e/CODE link
  • Holds and bookings with double-booking prevention inside Postgres
  • Live availability over server-sent events
  • Bookings list with CSV export, mark paid, cancel
  • Pay at venue or UPI reference; Razorpay when you add your keys
  • Buyer details erased 180 days after the event by default

The server also ships organiser screens: sign-in, Publish and share and a Bookings page. Or skip those screens and drive the API from your own pages through the REST adapter. Free, with no plans or fees.

ROW F · PERFORMANCE

Fast where it counts

First render, 2,000 seats
~15 ms
Redraw per frame, 2,000 seats
< 3 ms
Seats from one arenaPreset("hockey")
9,758
To generate that arena
~36 ms

Apple M5, headless Chromium and Node 26, October 2026. Very large maps (10,000 seats) pan at about 30 fps when fully zoomed out; level-of-detail drawing is on the roadmap.

See the SDK running before you install it

The demo runs the real generator, designer and booking map in your browser. Then open the API reference and build your own. It's free.