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
<code>
remote.ErrNotImplemented
</code>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.
| Role | gRPC server | RunWorker | DB pools | Scales on |
| Service — embedded | — | false | owns | request load |
| Service — remote client | holds 1 channel | n/a | none | request load |
| fabriq-server | on | false | owns | RPCs/streams (behind a gRPC LB) |
| fabriq-worker | off | true | owns | event 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 ofgithub.com/xraph/fabriq) — transport-neutral. TheFabricclient (implementsquery.Fabric), the server-sideHandler(wraps a real facade), the codec-neutralTransportseam, and an in-processLoopbackfor testing. No gRPC import.remote/grpc(its owngo.mod, packageremotegrpc) — the gRPC binding: aClientimplementingremote.Transport,Registerfor 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()For typed repositories over a remote facade, use
<code>
query.For[T](reg, fab.Relational())
</code><code>
fabriq.For[T]
</code><code>
Exec
</code><code>
errors.Is
</code><code>
ErrVersionConflict
</code><code>
ErrNotFound
</code>Plane status06
| Plane | Wired |
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 Query | held — remoting arbitrary SQL is a deliberate policy decision |
| Timeseries, Spatial ports | not yet |
Interactive (multi-round-trip) Tx | non-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.