v0.2.0 · Open Source · MIT License

Reactive data sync
for Go backends

Subscribe to a view. Get live updates. Zero boilerplate. Arcana syncs your PostgreSQL data to any frontend through normalized diffs — one record changed, every display updated.

$ go get github.com/FrankFMY/arcana
// Your app mutates data as usual db.Exec("UPDATE orders SET status = 'paid'") engine.Notify(ctx, arcana.Change{Table: "orders", RowID: id}) ┌──────────────────────────────────────┐ Arcana Engine registry invalidate diff send └──────────────┬───────────────────────┘ table_diff Built-in WebSocket Browser ┌──────────────┴───────────────────────┐ React / Vue / Svelte 1 record → 100 UI locations updated └──────────────────────────────────────┘
0
External dependencies
3
Framework adapters
< 5ms
Diff generation
100%
Type-safe
Everything you need for real-time sync
No more hand-rolling WebSocket hubs, event mappers, and subscription managers. Define views, subscribe, done.

Built-in WebSocket

Zero-dependency real-time transport. Two auth modes: HTTP upgrade (cookies/headers) and token-based (SPA/mobile). Subscribe, mutate, sync — all over a single connection.

📡

Server-Side Mutations

Register write operations with RegisterMutation(). Execute via HTTP or WebSocket. Typed parameter validation + automatic reactive invalidation.

🔌

React / Vue / Svelte

First-class adapters for all three frameworks. useView, useRow, useMutation, useStatus — reactive hooks with SSR support and offline persistence.

🧠

DevTools Panel

Built-in dark-themed dashboard. Graphs, mutations, subscriptions, WS connections, data store rows — all visible at a glance with 2-second auto-refresh.

📊

Normalized Data Store

One record, one source of truth, 100 display locations. Change a company name once — every view using it updates instantly via normalized diffs.

📋

Two Update Streams

table_diff for data changes (broadcast to workspace), view_diff for structural changes (private per session). Minimal traffic, maximum precision.

💡

Smart Invalidation

Column-level filtering. Only re-compute views that depend on changed columns. Explicit notify, pg_notify, or WAL — your choice.

🔒

Security by Design

Graphs are the security boundary. No graph, no data. Factory functions = authorization logic. 409 masking hides resource existence.

📴

Offline Support

IndexedDB adapter. Load from cache when offline. Reconnect with version vectors — get only what changed. Zero data loss.

Three steps to reactive data
main.go
// 1. Create engine (zero config WS transport) engine := arcana.New(arcana.Config{ Pool: arcana.PgxQuerier(pgPool), AuthFunc: myAuthFunc, }) // 2. Register graphs + mutations engine.Register(UsersListGraph, OrdersGraph) engine.RegisterMutation(CreateTask) // 3. Start + mount engine.Start(ctx) mux.Handle("/arcana/", http.StripPrefix( "/arcana", engine.Handler())) mux.Handle("/ws", engine.WSHandler())

1. Wire it up

Built-in WebSocket transport — no Centrifugo, no Redis, no external services. Just Go + PostgreSQL.

  • No framework lock-in — works with Chi, Gin, stdlib
  • No schema changes required in your database
  • Auth pluggable — cookies, JWT, API keys, token-based

2. Define a graph

A graph is a named query with dependencies. Plain Go — no DSL, no YAML, no magic.

  • Factory returns refs (structure) + tables (normalized data)
  • Deps declare which tables trigger re-computation
  • ParamSchema validates and types client inputs
graphs/orders.go
var OrdersList = arcana.GraphDef{ Key: "orders_list", Params: arcana.ParamSchema{ "status": arcana.ParamString(), "limit": arcana.ParamInt().Default(50), }, Deps: []arcana.TableDep{ {Table: "orders", Columns: []string{"status"}}, }, Factory: func(ctx context.Context, q arcana.Querier, p arcana.Params, ) (*arcana.Result, error) { rows, _ := q.Query(ctx, ` SELECT id, number, status, total FROM orders WHERE org_id = $1`, arcana.WorkspaceID(ctx)) // ... scan, result.AddRow + AddRef }, }
import { useView, useRow } from "@arcana/react"; function OrdersList() { const { data, loading } = useView("orders_list", { status: "active", limit: 20 }); if (loading) return <Spinner />; return data.map(ref => <OrderRow key={ref.id} id={ref.id} /> ); }
import { useView, useRow } from "@arcana/vue"; const { data, loading } = useView("orders_list", () => ({ status: "active", limit: 20 })); <!-- Template --> <Spinner v-if="loading" /> <template v-else> <OrderRow v-for="ref in data" :key="ref.id" :id="ref.id" /> </template>
import { arcana } from "$lib/arcana"; const orders = arcana.subscribe("orders_list", { status: "active", limit: 20 }); // orders.data updates automatically {#each orders.data as ref} <OrderRow order={arcana.row("orders", ref.id)} /> {/each}

3. Subscribe from frontend

One hook. No event listeners. No manual refetching. Data arrives as normalized diffs — your framework applies them with zero full re-renders.

  • React, Vue 3, and Svelte 5 adapters
  • Auto-unsubscribe on component destroy
  • Offline-first with IndexedDB adapter
  • Type-safe params from codegen
Without Arcana vs With Arcana

Without Arcana

// fetch data const res = await fetch("/api/orders") const orders = await res.json() // subscribe to events ws.on("order.created", (e) => { ... }) ws.on("order.updated", (e) => { ... }) ws.on("order.deleted", (e) => { ... }) ws.on("order.paid", (e) => { ... }) ws.on("order.shipped", (e) => { ... }) // map events to state // handle reconnect // handle stale data // handle pagination // repeat for every screen...
~50 lines per screen × 100 screens = 5,000 lines

With Arcana

const { data } = useView( "orders_list", { status: "active", limit: 20 } ) // That's it. // Data syncs automatically. // Reconnect handled. // Offline supported. // Types generated.
1 hook per screen × 100 screens = 100 lines
Mutations with auto-invalidation
Define server-side write operations. Execute via HTTP or WebSocket. Changed data automatically triggers reactive updates for all subscribers.
mutations.go
var CreateTask = arcana.MutationDef{ Key: "create_task", Params: arcana.ParamSchema{ "title": arcana.ParamString().Required(), "status": arcana.ParamString().Default("todo"), }, Handler: func(ctx context.Context, q arcana.Querier, p arcana.Params, ) (*arcana.MutationResult, error) { id := uuid.New() q.Exec(ctx, `INSERT INTO tasks ...`, id, p.String("title")) return &arcana.MutationResult{ Data: map[string]any{"id": id}, Changes: []arcana.Change{{ Table: "tasks", RowID: id, }}, }, nil }, } engine.RegisterMutation(CreateTask)

Define once, call from anywhere

Mutations are registered on the server with typed parameters and a handler function. The result declares which rows changed — Arcana handles the rest.

  • HTTPPOST /mutate with {"action":"create_task","params":{...}}
  • WebSocket{"type":"mutate","action":"create_task"} over the same WS connection
  • Auto-invalidation — Changes trigger the diff pipeline, all affected views update
  • FrontenduseMutation("create_task") in React/Vue/Svelte
Architecture
Arcana is an embeddable library, not a service. It runs inside your Go process with zero external dependencies beyond PostgreSQL.
React
Vue 3
Svelte 5
Mobile / Capacitor
Built-in WebSocket / Centrifugo / Custom
Registry
DataStore
Invalidator
Mutations
table_diff + view_diff + mutate
WSTransport
DevTools
HTTP Handler
SQL / pg_notify / WAL
PostgreSQL
Three ways to detect changes
Choose the mode that fits your architecture. All three feed into the same invalidation pipeline — you can even combine them.

1. Explicit Notify

Call engine.Notify() after mutations. Simple, full control.

db.Exec("UPDATE users SET name=$1", name) engine.NotifyTable(ctx, "users", id, []string{"name"})
📢

2. pg_notify Triggers

Auto-generated triggers. Zero app instrumentation after setup.

arcana.EnsureTriggers(ctx, pool, "arcana_changes", engine.Registry().RepTable()) engine := arcana.New(arcana.Config{ ChangeDetector: arcana.NewPGNotifyListener(cfg), })
🛰

3. WAL Replication

Logical replication stream. Captures all DML. Most reliable.

engine := arcana.New(arcana.Config{ ChangeDetector: arcana.NewWALListener( arcana.WALConfig{ ConnString: connStr, SlotName: "arcana_slot", Tables: []string{"users"}, }), })
Frontend integration
Drop-in TypeScript client with React, Vue, and Svelte adapters, offline persistence, and auto-generated types.
ArcanaClient + WS Transport
import { ArcanaClient } from "@arcana/sdk"; import { ArcanaWSTransport } from "@arcana/ws"; const transport = new ArcanaWSTransport({ url: "ws://localhost:8080/ws", }); const client = new ArcanaClient({ apiUrl: "/arcana", transport, }); await client.connect("workspace-uuid"); // Subscribe, mutate, disconnect const handle = await client.subscribe( "orders", { limit: 20 }, { onUpdate: () => refresh() } ); await client.mutate("create_task", { title: "Ship feature" });

SDK Features

  • WebSocket transport — subscribe, mutate, sync over a single WS connection with auto-reconnect
  • React adapteruseView, useRow, useMutation, useStatus hooks with SSR support
  • Vue 3 adapter — composables with shallowRef reactivity and onUnmounted cleanup
  • Svelte 5 adaptercreateSubscription() with $state reactivity
  • Offline persistence — IndexedDB adapter caches state, auto-syncs on reconnect
  • Type codegengo run ./cmd/arcana-gen generates tables.d.ts + views.d.ts
Endpoints & Configuration
HTTP + WebSocket Endpoints
// Mount mux.Handle("/arcana/", http.StripPrefix( "/arcana", engine.Handler())) mux.Handle("/ws", engine.WSHandler()) POST /subscribe // Subscribe to graph POST /unsubscribe // Unsubscribe POST /sync // Reconnect sync POST /mutate // Execute mutation GET /active // List subscriptions GET /schema // Table definitions GET /health // Engine stats // WebSocket protocol (same WS connection): // subscribe, unsubscribe, sync, mutate // Request/reply with id + reply_to

REST + WebSocket API

All HTTP endpoints require authentication via AuthFunc. WebSocket supports HTTP upgrade auth or token-based auth for SPAs.

DevTools
// Mount DevTools panel mux.Handle("/devtools/", http.StripPrefix("/devtools", engine.DevToolsHandler())) // GET /devtools/ → dashboard // GET /devtools/state → JSON state
Learn Arcana
Start building reactive apps today
One Go library. One subscribe call. Built-in WebSocket. React, Vue, Svelte. Real-time data sync that just works.