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 Centrifugo WebSocket ┌──────────────┴───────────────────────┐ Browser — Svelte 5 reactive store 1 record → 100 UI locations updated └──────────────────────────────────────┘
< 5ms
Diff generation
0
Full re-renders
3
Lines to subscribe
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.

Normalized Data Store

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

📡

Two Update Streams

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

🔌

Pluggable Transport

Centrifugo, raw WebSocket, SSE — swap via interface. Batch publishing, retry with backoff, disconnect management built-in.

🧠

Smart Invalidation

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

♻️

RefCount GC

No subscribers on a row? Auto-evicted. No memory leaks, no unbounded growth. Seance dies — all subscriptions cleaned up.

🔒

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.

🏷️

TypeScript Codegen

Auto-generated tables.d.ts + views.d.ts. Full type safety from Go graph definitions to Svelte components.

🧪

Testable

Querier interface decouples from pgx. Mock everything. 100+ tests with race detector. Integration tests with testcontainers.

Three steps to reactive data
main.go
// 1. Create engine engine := arcana.New(arcana.Config{ Pool: pgPool, Transport: arcana.NewCentrifugoTransport(url, key), AuthFunc: myAuthFunc, }) // 2. Register graphs engine.Register(UsersListGraph, OrdersGraph) // 3. Start engine.Start(ctx) http.Handle("/arcana", engine.Handler())

1. Wire it up

Three lines to connect Arcana to your existing Go application. Bring your own PostgreSQL pool, pick a transport, define auth.

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

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 }, }
App.svelte
import { arcana } from "$lib/arcana"; const orders = arcana.subscribe("orders_list", { status: "active", limit: 20 }); // orders.data updates automatically // when any order changes in the DB {#each orders.data as ref} <OrderRow order={arcana.row("orders", ref.id)} /> {/each}

3. Subscribe from frontend

One line. No event listeners. No manual refetching. Data arrives as normalized diffs — Svelte 5 applies them with zero full re-renders.

  • Type-safe params and results from codegen
  • Auto-unsubscribe on component destroy
  • Offline-first with IndexedDB adapter
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 orders = arcana.subscribe( "orders_list", { status: "active", limit: 20 } ) // That's it. // Data syncs automatically. // Reconnect handled. // Offline supported. // Types generated.
1 line per screen × 100 screens = 100 lines
Architecture
Arcana is an embeddable library, not a service. It runs inside your Go process with zero external dependencies beyond PostgreSQL and a transport.
Browser / Svelte
Mobile / Capacitor
Node.js / SSR
WebSocket / SSE
Centrifugo / Custom WS / SSE
HTTP API / table_diff + view_diff
Registry
DataStore
Invalidator
DiffEngine
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"}, }), })
Endpoints & Configuration
HTTP Endpoints
// Mount mux.Handle("/arcana/", http.StripPrefix( "/arcana", engine.Handler())) POST /subscribe // Subscribe to graph POST /unsubscribe // Unsubscribe POST /sync // Reconnect sync GET /active // List subscriptions GET /schema // Table definitions GET /health // Engine stats // Subscribe request: { "view": "user_list", "params": {"org_id": "uuid", "limit": 20}} // Response: { "ok": true, "data": { "params_hash": "a1b2c3", "version": 1, "refs": [{"table":"users", "id":"u1"}], "tables": {"users": {"u1": {...}}} }}

REST API & Config

All endpoints require authentication via AuthFunc. Responses use the envelope format.

Config
arcana.Config{ Pool: arcana.PgxQuerier(pool), Transport: transport, AuthFunc: myAuthFunc, // Tuning InvalidationDebounce: 50*time.Millisecond, MaxSubscriptionsPerSeance: 100, SnapshotThreshold: 50, GCInterval: time.Minute, }
Frontend integration
Drop-in TypeScript client with Svelte 5 adapter, offline persistence, and auto-generated types.
ArcanaClient
import { ArcanaClient, IndexedDBStorageAdapter } from "@arcana/sdk"; const client = new ArcanaClient({ apiUrl: "/arcana", transport: centrifugoTransport, storage: new IndexedDBStorageAdapter(), }); await client.connect("workspace-uuid"); const handle = await client.subscribe( "user_list", { org_id: "...", limit: 20 }, { onUpdate: () => renderUI() } ); handle.data // Ref[] — view structure handle.version // current version client.getRow("users", "u1") // row data handle.destroy(); // clean up client.disconnect();

SDK Features

  • Normalized storegetRow(table, id) for any row across views
  • Offline persistence — IndexedDB adapter caches state, auto-syncs on reconnect
  • Svelte 5 adaptercreateSubscription() with $state reactivity
  • Type codegengo run ./cmd/arcana-gen generates tables.d.ts + views.d.ts
  • Custom transport — implement TransportClient interface for any WS/SSE
  • Connection status — connected, connecting, disconnected, offline

Params & Identity

Type-safe parameter definitions with validation. Identity context in every factory for authorization.

// Inside Factory: wsID := arcana.WorkspaceID(ctx) user := arcana.User(ctx) if !user.HasPermission("read:users") { return nil, arcana.ErrForbidden } // Errors: arcana.ErrForbidden // 403 arcana.ErrNotFound // 404 arcana.ErrInvalidParams // 400
Params API
Params: arcana.ParamSchema{ "org_id": arcana.ParamUUID().Required(), "limit": arcana.ParamInt().Default(50), "status": arcana.ParamString(). OneOf("active", "archived"). Build(), "verbose": arcana.ParamBool().Default(false), }, // In factory: orgID := p.UUID("org_id") limit := p.Int("limit") // 50 if omitted status := p.String("status") raw := p.Raw() // map[string]any
Learn Arcana
Start building reactive apps today
One Go library. One subscribe call. Real-time data sync that just works.