# cfgsync `cfgsync` is a small library stack for bootstrap-time config delivery. The library solves one problem: nodes need to identify themselves, wait until configuration is ready, fetch the files they need, write them locally, and then continue startup. `cfgsync` owns that transport and serving loop. The application using it still decides what “ready” means and what files should be generated. That split is the point of the design: - `cfgsync` owns registration, polling, payload transport, and file delivery. - the application adapter owns readiness policy and artifact generation. The result is a reusable library without application-specific bootstrap logic leaking into core crates. ## How it works The normal flow is registration-backed serving. Each node first sends a registration containing: - a stable node identifier - its IP address - optional typed application metadata The server stores registrations and builds a `RegistrationSnapshot`. The application provides a `RegistrationSnapshotMaterializer`, which receives that snapshot and decides whether configuration is ready yet. If the materializer returns `NotReady`, the node keeps polling. If it returns `Ready`, cfgsync serves one payload containing: - node-local files for the requesting node - optional shared files that every node should receive The node then writes those files locally and continues startup. That is the main model. Everything else is a variation of it. ## Precomputed artifacts Some systems already know the final artifacts before any node starts. That still fits the same model. In that case the server simply starts with precomputed `MaterializedArtifacts`. Nodes still register and fetch through the same protocol, but the materializer already knows the final outputs. Registration becomes an identity and readiness gate, not a source of topology discovery. This is why cfgsync no longer needs a separate “static mode” as a first-class concept. Precomputed serving is just registration-backed serving with an already-known result. ## Crate layout ### `cfgsync-artifacts` This crate contains the file-level data model: - `ArtifactFile` for a single file - `ArtifactSet` for a group of files If all you need is “what files exist and how are they grouped”, this is the crate to look at. ### `cfgsync-core` This crate contains the protocol and the low-level HTTP implementation. Important types here are: - `NodeRegistration` - `RegistrationPayload` - `NodeArtifactsPayload` - `CfgsyncClient` - `NodeConfigSource` It also defines the HTTP contract: - `POST /register` - `POST /node` The server answers with either a payload, `NotReady`, or `Missing`. ### `cfgsync-adapter` This crate defines the application-facing seam. The key types are: - `RegistrationSnapshot` - `RegistrationSnapshotMaterializer` - `MaterializedArtifacts` - `MaterializationResult` The adapter’s job is simple: given the current registration snapshot, decide whether artifacts are ready, and if they are, return them. The crate also contains reusable wrappers around that seam: - `CachedSnapshotMaterializer` - `PersistingSnapshotMaterializer` - `RegistrationConfigSource` These exist because caching and result persistence are generic orchestration concerns, not application-specific logic. ### `cfgsync-runtime` This crate provides the operational entrypoints. Use it when you want to run cfgsync rather than define its protocol: - client-side fetch/write helpers - server config loading - direct serving helpers such as `serve(...)` This is the crate that should feel like the normal “start here” path for users integrating cfgsync into a real system. ## Artifact model The adapter usually thinks in full snapshots, but cfgsync serves one node at a time. The materializer returns `MaterializedArtifacts`, which contain: - node-local artifacts keyed by node identifier - optional shared artifacts When one node fetches config, cfgsync resolves that node’s local files, merges in the shared files, and returns a single payload. That is why applications usually do not need a second “shared config” endpoint. Shared files can travel in the same payload as node-local files. ## The adapter boundary The adapter is where application semantics belong. In practice, the adapter should define: - the typed registration payload - the readiness rule - the conversion from registration snapshots into artifacts - any shared artifact generation the application needs Typical examples are: - waiting for `n` initial nodes - deriving peer lists from registrations - generating one node-local config file per node - generating one shared deployment file for all nodes What does not belong in cfgsync core is equally important. Generic cfgsync should not understand: - application-specific topology semantics - genesis or deployment generation rules for one protocol - application-specific command/state-machine logic - domain-specific ideas of what a node “really is” Those belong in the adapter or in the consuming application. ## Minimal integration path For a new application, the shortest sensible path is: 1. define a typed registration payload 2. implement `RegistrationSnapshotMaterializer` 3. return node-local and optional shared artifacts 4. serve them with `serve(...)` 5. use `CfgsyncClient` or the runtime helpers on the node side That gives you the main value of the library without forcing extra application logic into cfgsync itself. ## Minimal example A minimal standalone example lives in: - `cfgsync/runtime/examples/minimal_cfgsync.rs` It shows the smallest complete path: - registration snapshot materializer - cfgsync server - node artifact generation ## Code sketch Typed registration payload: ```rust use cfgsync_core::NodeRegistration; #[derive(serde::Serialize)] struct MyNodeMetadata { network_port: u16, api_port: u16, } let registration = NodeRegistration::new("node-1", "127.0.0.1".parse().unwrap()) .with_metadata(&MyNodeMetadata { network_port: 3000, api_port: 18080, })?; ``` Snapshot materializer: ```rust use cfgsync_adapter::{ DynCfgsyncError, MaterializationResult, MaterializedArtifacts, RegistrationSnapshot, RegistrationSnapshotMaterializer, }; use cfgsync_artifacts::{ArtifactFile, ArtifactSet}; struct MyMaterializer; impl RegistrationSnapshotMaterializer for MyMaterializer { fn materialize_snapshot( &self, registrations: &RegistrationSnapshot, ) -> Result { if registrations.len() < 2 { return Ok(MaterializationResult::NotReady); } let nodes = registrations.iter().map(|registration| { ( registration.identifier.clone(), ArtifactSet::new(vec![ArtifactFile::new( "/config.yaml", format!("id: {}\n", registration.identifier), )]), ) }); Ok(MaterializationResult::ready( MaterializedArtifacts::from_nodes(nodes), )) } } ``` Serving: ```rust use cfgsync_runtime::serve; # async fn run() -> anyhow::Result<()> { serve(4400, MyMaterializer).await?; # Ok(()) # } ``` Fetching and writing artifacts: ```rust use cfgsync_runtime::{OutputMap, fetch_and_write}; # async fn run(registration: cfgsync_core::NodeRegistration) -> anyhow::Result<()> { let outputs = OutputMap::new() .route("/config.yaml", "/node-data/node-1/config.yaml") .route("deployment-settings.yaml", "/node-data/shared/deployment-settings.yaml"); fetch_and_write(®istration, "http://127.0.0.1:4400", &outputs).await?; # Ok(()) # } ``` ## Compatibility The intended public API is what the crate roots reexport today. Some older compatibility paths still exist internally to avoid breaking current in-repo consumers, but they are not the main model and should not be treated as the recommended public surface.