OpenFief Give your AI agents a fief, not the kingdom.

Architecture

How OpenFief's parts fit together in M0, what is built today, and which page explains each part.

OpenFief runs in the owner’s own Kubernetes cluster. A core written in Go owns the record of every holding and transaction. Everything that talks to the outside world is a plugin: its own container, with only the permissions the owner granted it. An operator turns plugin resources into running, isolated workloads, and an egress proxy is the only way out to the internet. This page shows the whole design for M0 (the first milestone, a tracker with Polish imports) and marks what is built today.

The system and its neighbors#

The owner reaches OpenFief through kubectl port-forward by default (D-28), and installs plugins with the openfief command-line tool using their own kubeconfig (D-46). OpenFief reaches out only through the egress proxy, and only to hosts a plugin’s grant allows (D-32).

Owner's Kubernetes cluster

browser via port-forward,
openfief CLI

egress proxy,
granted hosts only

digest-pinned images

Owner

OpenFief
namespace openfief

IBKR Flex Web Service

NBP
exchange rates

GUS
inflation data

EODHD
end-of-day prices

Ghostfolio
owner's own install

Container registry
signed images

Owner's Kubernetes cluster

browser via port-forward,
openfief CLI

egress proxy,
granted hosts only

digest-pinned images

Owner

OpenFief
namespace openfief

IBKR Flex Web Service

NBP
exchange rates

GUS
inflation data

EODHD
end-of-day prices

Ghostfolio
owner's own install

Container registry
signed images

Parts, as designed and as built#

Green parts are built, amber parts are partly built, and gray parts are decided but not built yet. The table below says what exists in each.

namespace openfief

Plugins, one pod each

plugins build on

applies resources

creates and constrains

allow-lists

calls connectors

outbound HTTPS

openfief CLI

Operator
Plugin + PluginGrant

Egress proxy
Smokescreen

Core service
ledger, valuation, read API,
connector scheduler

PostgreSQL

Built-in UI view

Ghostfolio view

IBKR Flex connector

XTB export parser

Treasury bond connector

Market-data connector

NBP FX

Go SDK
connector contract

namespace openfief

Plugins, one pod each

plugins build on

applies resources

creates and constrains

allow-lists

calls connectors

outbound HTTPS

openfief CLI

Operator
Plugin + PluginGrant

Egress proxy
Smokescreen

Core service
ledger, valuation, read API,
connector scheduler

PostgreSQL

Built-in UI view

Ghostfolio view

IBKR Flex connector

XTB export parser

Treasury bond connector

Market-data connector

NBP FX

Go SDK
connector contract

PartDecided inBuilt todayExplained in
OperatorD-13, D-15 to D-17, D-42 to D-45, D-56, D-57cmd/operator and the reconciler in internal/plugins/operator, plus install manifests in deploy/. It deploys an image only when a trusted signer signed its digest, checked offline with sigstore-go against a bundled Sigstore trust root (internal/plugins/cosign; cmd/sigstore-trusted-root refreshes the root). The trusted signers are OpenFief’s CI, the publishers pinned in the plugin catalog (internal/plugins/catalog, empty today) and any the owner adds in a ConfigMapOperator
Egress proxyD-32, D-63cmd/egress-proxy, embedding Smokescreen through BuildProxy. It checks each plugin’s openfief-egress token with a TokenReview, caches accepted reviews, and allows only the hosts the grant namesEgress
Plugin modelD-11, D-16, D-42 to D-49, D-59Manifests, grants, effective permissions and workload compilation in internal/plugins. The connector contract (openfief.connector.v1) and the Go SDK for plugin authors in sdk/goPlugins
LedgerD-18, D-19, D-23, D-33 to D-35Domain, use cases and the PostgreSQL store in internal/ledger, including the guard against seeding an account twice; the core service runs itLedger
Valuation and FXD-20, D-21, D-25, D-36Prices, FX lookups, valuation bases and the NBP client in internal/valuation; stored valuations and the River revaluation queue (D-36) in internal/valuation/postgresValuation
Treasury bondsD-9, D-25, D-37Polish bond rules in internal/jurisdiction/plTreasury bonds
IngestD-7, D-8, D-23, D-49, D-59Import batches and the IBKR Flex mapping and pull in internal/ingest, still a package of the core rather than a plugin. The core’s side of connector plugins: the client in internal/ingest/connectrpc, the scheduler, watermarks and connector bindings in internal/ingest/app and internal/ingest/postgres, instance discovery in internal/ingest/kube, and the per-instance tokens (D-59) in internal/identity/kubeIngest
Shared kernel(code organization)Money, currency, quantity and dates in internal/kernelKernel
PortfolioD-52 to D-54, D-61Sleeves and sleeve kinds with their default roles and install defaults in internal/portfolio; the model portfolio is not builtPortfolio
Demo datasetGR-12A synthetic dataset in internal/demo, loaded by cmd/demo-seed through the same ingest, ledger and valuation paths real data takes. It refuses any database without its own demo markerDemo
Read APID-6, D-7, D-30, D-36, D-50, D-62The openfief.views.v1.ReadService protobuf service and its generated OpenAPI document; the queries, totals and allocation in internal/views; the Connect and REST /v1 handler, through Vanguard, in internal/views/connectapi. The core service serves itRead API
Core serviceD-2, D-14, D-27, D-28, D-36, D-50cmd/core, installed by deploy/core (below). It migrates every context’s schema, runs the River revaluation workers, and serves the read API on port 8080 with script tokens from internal/identity as its only accepted tokens, plus unauthenticated /healthz and /readyz. core token issue, list and revoke manage the tokens. In a cluster it syncs every running connector instance on a schedule (D-49), as the ingest page explains; core connector bind and core connector sync bind an instance to an account and sync one now. No connector plugin is installed on the demo yet, so its data enters through cmd/demo-seedIdentity, Read API
UI, Ghostfolio view, CLI, XTB parser, market dataD-3 to D-5, D-30, D-46, D-51Not builtStatus

The status page gives the same picture decision by decision, from the requirements in kibi.

Bounded contexts#

The code is organized by domain, following domain-driven design (Eric Evans, 2003). Each bounded context owns its terms, types and data. In this designed map from code-organization.md, arrows point from a context to the contexts it reads:

ingest

ledger

valuation

jurisdiction/pl

views

portfolio

plugins

identity

kernel (shared)

ingest

ledger

valuation

jurisdiction/pl

views

portfolio

plugins

identity

kernel (shared)

The code map draws the same map from the actual imports on every build. Comparing the two is a quick architecture check. Today the code has two arrows the design lacks: ledger imports portfolio to attach accounts to sleeves, and views imports jurisdiction/pl to tell a treasury bond series from a listed instrument in the allocation.

In the cluster#

Everything lives in the openfief namespace. A cluster admin installs the custom resource definitions once. The operator’s permissions are a Role in that namespace only (D-15). The install ships no Ingress, LoadBalancer or tunnel (D-28); only the demo environment adds its own HTTPS route, below.

Cluster

namespace openfief

DSN from Secret
openfief-db-app

connect port

Sync, with a TokenRequest token
for the instance (D-59)

TokenRequest, reads Plugins
and Services

reads selector and ports

watches

creates

writes

reads

HTTPS_PROXY, egress token
as the proxy password

granted hosts only

CRDs
plugins.openfief.io
plugingrants.openfief.io

Deployment openfief-operator
Role: namespace only

Deployment openfief-core
Service openfief-core:
http 8080, connect 8081

PostgreSQL
CloudNativePG openfief-db

Deployment egress-proxy
Service egress-proxy

Plugin pod
own ServiceAccount, Secrets,
NetworkPolicy

ConfigMap
egress allow-lists

Internet

Kubernetes API

Cluster

namespace openfief

DSN from Secret
openfief-db-app

connect port

Sync, with a TokenRequest token
for the instance (D-59)

TokenRequest, reads Plugins
and Services

reads selector and ports

watches

creates

writes

reads

HTTPS_PROXY, egress token
as the proxy password

granted hosts only

CRDs
plugins.openfief.io
plugingrants.openfief.io

Deployment openfief-operator
Role: namespace only

Deployment openfief-core
Service openfief-core:
http 8080, connect 8081

PostgreSQL
CloudNativePG openfief-db

Deployment egress-proxy
Service egress-proxy

Plugin pod
own ServiceAccount, Secrets,
NetworkPolicy

ConfigMap
egress allow-lists

Internet

Kubernetes API

The install manifests are in deploy/: crds/ for the definitions and the namespace, operator/, egress-proxy/ and core/ for the three workloads. The core’s Service openfief-core is a ClusterIP with two named ports on the same container port 8080: http (8080), the read API and health endpoints, reached with kubectl port-forward by default (D-28), and connect (8081), the port the operator hands to plugins (D-49). Its NetworkPolicy admits plugin pods and lets the core reach only DNS, the database, plugin pods on their Connect port (8080) and the Kubernetes API server. A pod reaches the API server through Service kubernetes, which is translated to the API server’s own address before NetworkPolicy applies, so the rule names that port: 6443, as on k3s, to any address; an overlay narrows it to the node’s address or changes the port. The core’s pods mount a Kubernetes API token, and its Role (deploy/core/rbac.yaml) allows only create on its own serviceaccounts/token (resourceNames openfief-core) and get, list and watch on Plugins and Services, all in the openfief namespace. The database URL comes from CloudNativePG’s Secret openfief-db-app, key uri (D-14, D-29). Every workload’s pod sets runAsNonRoot, so each image (cmd/<name>/Dockerfile) runs as a numeric user, 65532:65532, distroless’s nonroot: the kubelet refuses to start a container whose image names its user, because it can’t tell that a name isn’t root. The operator and egress pages walk through them.

The demo environment#

deploy/demo runs the same install on one Hetzner machine with k3s, holding synthetic data only (GR-12). Nothing pushes to it: cloud-init installs Flux, and Flux pulls each layer below, in dependency order. Five layers follow main in git. The openfief layer instead follows a signed OCI artifact that CI publishes after each push to main, with the images pinned to the digests whose signatures verified (D-17). The HTTPS route is the demo’s own, at demo.openfief.dev on k3s’s bundled Traefik with a Let’s Encrypt certificate, to Service openfief-core, port http; a demo-only NetworkPolicy lets Traefik reach the core. Creating and destroying demos is in demo.md.

Hetzner machine, k3s

git: main

signed OCI artifact
digest-pinned images

Flux
installed by cloud-init

demo-crds
deploy/crds

demo-cnpg
CloudNativePG operator

demo-database
one-instance cluster

demo-cert-manager

demo-ingress
Let's Encrypt issuer

demo-routes
Traefik route to the core

demo-openfief
operator, egress proxy,
core, demo-seed Job

Hetzner machine, k3s

git: main

signed OCI artifact
digest-pinned images

Flux
installed by cloud-init

demo-crds
deploy/crds

demo-cnpg
CloudNativePG operator

demo-database
one-instance cluster

demo-cert-manager

demo-ingress
Let's Encrypt issuer

demo-routes
Traefik route to the core

demo-openfief
operator, egress proxy,
core, demo-seed Job

Arrows between layers are Flux dependsOn edges. The CI side, from signed images to the published artifact, is on the delivery pipeline page.

The synthetic data comes from deploy/demo-data, a Job that runs cmd/demo-seed against the demo database; demo-openfief applies it next to the core. The demo runs the Job without --reset-demo and the core without its default sleeves, so the two can start together; demo.md explains why and how to refresh the data.

Main flows#

Each flow is drawn on the page that explains it:

  • Importing a broker statement: the IBKR Flex pull or a connector plugin’s delivery, mapping, and one ledger transaction per import. See Ingest.
  • Revaluing after an import: the import queues revaluation jobs in the same transaction, and a River worker stores the new valuations. See Valuation.
  • Recording and deriving holdings: versioned transactions, FIFO lots and balance assertions. See Ledger.
  • Valuing a bond lot: interest periods, indexation, early-redemption fee and Belka tax. See Treasury bonds.
  • Running a plugin: from a Plugin and its PluginGrant to an isolated pod. See Operator.
  • A plugin calling out: token, allow-list and address checks in the proxy. See Egress.
  • From commit to release: hooks, CI, signed images and the demo deploy. See Delivery pipeline.

Source: docs/architecture.md · last changed 2026-10-28 in 4152730