Fabriq
1.x
Docs/Fabriq/Remote protocol
Open

Reading8 min
Updated2 Aug 2026
Sourcev1/(operations)/remote-protocol.mdx
Warning

Experimental — in development. The remote protocol is a working, fully-tested skeleton, not a released surface. The write, relational-read and subscribe planes plus edge authentication are wired and round-trip-tested over real gRPC and mTLS; the maintained live-query, blob and interactive-transaction planes are not built yet (their accessors return

Unsupported
<code>
  remote.ErrNotImplemented
</code>
). See Architecture Decisions for the full status.

Fabriq is, first and foremost, an embedded library: a service calls fabriq.Open, owns its own pools to Postgres/Redis/FalkorDB/Elasticsearch/the object store, and holds the facade in-process. The remote protocol adds an optional second topology without changing that: a central, connection-owning fabriq deployment that other backend services talk to over gRPC.

The seam that makes this possible is that the facade is an interface (core/query.Fabric). A remote client only has to implement that interface over a wire, so application call sites are identical whether the facade is embedded or remote.

Why01

  • Connection consolidation — one tier owns the datastore pools instead of N services × M engines.

  • Centralized policy — schema registry, migrations, RLS, authz and caching live in one place and one version.

  • Decoupled deploys — roll out a fabriq change without rebuilding every service against a new library version.

The cost is a network hop per call, a tier to run HA, and a protocol to version. Reach for it when those trade against connection/operational pressure; keep embedding when you want in-process transactional composition.

Topology02

The same fabriq binary runs in distinct roles; the worker plane (relay, projections, reconciler) always lives on the connection-owning tier, never on a remote client.

RolegRPC serverRunWorkerDB poolsScales on
Service — embeddedfalseownsrequest load
Service — remote clientholds 1 channeln/anonerequest load
fabriq-serveronfalseownsRPCs/streams (behind a gRPC LB)
fabriq-workerofftrueownsevent throughput (consumer groups)

Modules03

The protocol is split across two Go modules so the gRPC dependency tree stays out of the core module — a service that only embeds the library never pulls in google.golang.org/grpc.

  • remote (part of github.com/xraph/fabriq) — transport-neutral. The Fabric client (implements query.Fabric), the server-side Handler (wraps a real facade), the codec-neutral Transport seam, and an in-process Loopback for testing. No gRPC import.

  • remote/grpc (its own go.mod, package remotegrpc) — the gRPC binding: a Client implementing remote.Transport, Register for a *grpc.Server, and the edge-auth interceptors. It frames the envelope over gRPC/HTTP-2 with a pass-through bytes codec, so gRPC supplies multiplexing, deadlines and mTLS while the envelope format stays owned above the seam.

Serving04

The server is the connection-owning tier: open the facade as usual, wrap it in a Handler, and register it on a gRPC server with TLS credentials and an auth interceptor.

// fabriq-server: owns the DB pools
f, stores, err := fabriq.Open(ctx, reg, cfg)
h := remote.NewHandler(f, reg)

// Identity is authenticated at the EDGE: the Authenticator stamps the tenant
// (and any app principal/claims the authz hooks read) from the verified
// credential — never from a request field.
auth := func(ctx context.Context) (context.Context, error) {
    cert, ok := remotegrpc.ClientCertificate(ctx) // mTLS client cert
    if !ok {
        return nil, errors.New("no client certificate")
    }
    ctx, err := remotegrpc.WithTenant(ctx, cert.Subject.CommonName) // CN → tenant (required)
    if err != nil {
        return nil, err
    }
    // Stamp any principal/claims under your OWN context key — fabriq core
    // defines no principal type; the authz hooks read whatever you put here.
    return app.WithPrincipal(ctx, cert.Subject.SerialNumber), nil
}

creds := credentials.NewTLS(&tls.Config{
    Certificates: []tls.Certificate{serverCert},
    ClientCAs:    caPool,
    ClientAuth:   tls.RequireAndVerifyClientCert,
})
srv := grpc.NewServer(append(remotegrpc.ServerOptions(auth), grpc.Creds(creds))...)
remotegrpc.Register(srv, h)
_ = srv.Serve(lis)

ServerOptions installs unary and stream interceptors that run the Authenticator and enrich the call's context before the handler runs. The returned context must carry a tenant (the binding rejects one that doesn't); TenantOnly(func(ctx) (string, error)) adapts a tenant-only resolver when you need no principal. BearerToken covers token-based auth as an alternative to ClientCertificate. fabriq core defines no principal type — the authz hooks read whatever you stamp.

Calling05

A client holds one multiplexed gRPC channel and a remote.Fabric over it. The call sites match the embedded facade exactly.

// a backend service: no DB pools, one channel
cc, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(credentials.NewTLS(clientTLS)))
var fab query.Fabric = remote.New(remotegrpc.NewClient(cc))

// writes — atomic, server-side
res, _ := fab.Exec(ctx, command.Command{
    Entity: "asset", Op: command.OpCreate, Payload: &domain.Asset{Name: "Pump 7"},
})

// reads
var a domain.Asset
_ = fab.Relational().Get(ctx, "asset", res.AggID, &a)
_ = fab.Relational().List(ctx, "asset", query.ListQuery{
    Where: query.Where{query.Eq("kind", "pump")}, OrderBy: "name", Limit: 50,
}, &assets)

// subscribe — server-streamed, conflated deltas
deltas, _ := fab.Subscribe(ctx, query.SubscribeScope{Entity: "asset", Scope: "site", ID: siteID})

// live query — a maintained window: snapshot + enter/leave/move/update deltas
snap, live, h, _ := fab.LiveQuery(ctx, livequery.LiveQuery{
    Entity: "asset", Where: query.Where{query.Eq("kind", "pump")},
    Sort: []livequery.SortKey{{Column: "name"}}, Limit: 50,
})
defer h.Close() // Reanchor (deep scroll) is a follow-on

// blob — chunked upload/download (never buffers a whole object); large/hot
// bytes can skip this tier via the presign bypass.
info, _ := fab.Blob().Put(ctx, "k1", reader, blob.PutOpts{ContentType: "image/png"})
rc, _, _ := fab.Blob().Get(ctx, info.Key)
defer rc.Close()
Note

For typed repositories over a remote facade, use

Unsupported
<code>
  query.For[T](reg, fab.Relational())
</code>
rather than
Unsupported
<code>
  fabriq.For[T]
</code>
— the latter is bound to the concrete embedded facade. The fabriq error taxonomy travels in-band: a remote
Unsupported
<code>
  Exec
</code>
still returns an
Unsupported
<code>
  errors.Is
</code>
-matchable
Unsupported
<code>
  ErrVersionConflict
</code>
/
Unsupported
<code>
  ErrNotFound
</code>
.

Plane status06

PlaneWired
Write — Exec, ExecBatch
Read — Get, GetMany, List
Retrieval — Graph.Query, Search.Search, Vector.Similar (agent recall)
Subscribe (server-streamed deltas)
Live query — snapshot + deltas (maintained window)✅ (Reanchor / deep-scroll pending — needs bidi)
Blob — chunked Put/Get, Head/Delete, presign bypass✅ (List/Copy/multipart pending)
Edge auth — mTLS + bearer, tenant stamping
Raw SQL Queryheld — remoting arbitrary SQL is a deliberate policy decision
Timeseries, Spatial portsnot yet
Interactive (multi-round-trip) Txnon-goal — ExecBatch is the transaction surface

Two seams to remember when extending it: the wire envelope is protobuf (generated into remote/fabriqpb from remote/proto/fabriq/v1/fabriq.proto, messages-only so core stays grpc-free) while payload bodies stay opaque JSON; and every new unary method must be registered in both Handler.Dispatch and the gRPC ServiceDesc — gRPC routes by the latter.

Security07

Only backends connect (no browser), so mTLS is realistic and strong: every client presents a certificate. Tenant and principal arrive in the call's transport metadata and are authenticated by the server-edge interceptor — never trusted from a field in the request body. The embedded facade then enforces RLS and authz exactly as it does in-process. This is a strict upgrade over the in-process trust-the-context model.