↓ Skip to main content
GitLab, OpenTofu, HCL and Nix
Geometric facade of a modern office building
  1. Posts/

GitLab, OpenTofu, HCL and Nix

··22 mins·
Table of Contents

TL;DR: I use OpenTofu and Terranix to manage GitLab settings as code, so changes can be reviewed in Git and manual edits do not quietly become the new normal.

OpenTofu started as a community fork of Terraform. They share the same basic idea: describe the desired state in code, then let a provider bring the real service closer to it through its API. Their paths are starting to differ, but the workflow in this article should make sense to anyone familiar with either tool. It gives us a reviewable history of what we asked the service to do, alongside a way to compare that with what it is actually doing.

I use Terranix to write that configuration in Nix and generate the input OpenTofu expects. This post focuses on GitLab, but the point is broader: keep service settings (e.g., AWS, GitHub, …) in Git, and let plans and automation expose changes before they turn into unexplained differences in the live system.

Managing a few GitLab projects by hand is OKayish. Managing hundreds of repositories, users and permissions is a different story.

The problem: ClickOps drift and inconsistent projects
#

Each GitLab project can have its own settings, including permissions, memberships, protected branches, merge request approval rules and custom hooks.

Note

Even finding all those settings can be a challenge. After years of using the GitLab UI, I still come across settings pages I have never seen before, #truestory. At this point, I suspect the settings menu unlocks new levels with each release !

I have no idea of what I am clickopsing meme

Managing these settings by hand through GitLab’s web interface is what I would call “ClickOps”: clicking through the interface to change live settings. This is an imperative approach: somebody uses GitLab’s web interface to make changes, one action at a time. A declarative approach is the opposite: it describes the desired configuration, and automation works out how to apply it to GitLab.

ClickOps makes changes hard to track. Well, not quite true: there might be some audit events, depending on the system and its available features. But these do not necessarily provide a complete, versioned record. To illustrate this, there is no way in GitLab’s audit event log to know when a repository has been created and by whom.

Infrastructure As Click - Always Has Been
Caution

Those reluctant to adopt a more “GitOps-oriented approach” might argue that ClickOps is quicker, and they might be right. The idea behind Infrastructure as Code (IaC) is to keep the desired configuration in version control, where changes can be reviewed, approved and reverted, making the process transparent, traceable and auditable.

At scale, the complexity grows linearly with each added GitLab project or group and the work becomes much harder totally impossible: every project brings its own settings to track and keep consistent.

I see at least two problems here:

  • ClickOps causes configuration drift: the live settings can diverge from the record of what we intended.
  • Projects become inconsistent: settings or security policies that should be shared can vary from one repository to another.

In an enterprise setting, such inconsistencies can violate organisational policy. Since GitLab does not provide a general way to disable edits in its web interface, teams need controls over who can make changes and regular checks to detect and correct drift.

Forms, settings, buttons everywhere meme
How do you know whether all projects follow the same security policies ? Who changed a particular permission, and why ? When did they change it ? How can you reproduce a configuration ? How do you detect settings that have changed outside the agreed process ? Most of all, how do you keep track of each and every single user and their memberships ?

From ClickOps to DevOps
#

Based on Wikipedia’s definition, DevOps is the integration and automation of software development and information technology operations.

For this GitLab use case, that means treating operational settings just like application code. The goal is not simply to replace clicks with a script, but to make changes repeatable and traceable through automation.

So how could we put that into practice ?

The most direct option would have been to do some scripting and call GitLab’s API using REST (e.g., with curl). That would work for a few settings, but GitLab’s API is large. The script would soon need to handle many resource types, API details and edge cases. We would also need to build our own mechanism to compare the desired configuration with the live state and keep track of that state. Reimplementing that machinery did not seem worthwhile, reinventing the wheel is pointless especially when better tools already exist.

GitLabForm was the next option I evaluated. It worked for the proof of concept, but it did not offer the resource lifecycle and planning workflow we wanted.

Tip

GitLabForm was not available in Nix, I fixed this along the way in NixOS/nixpkgs#573811

The natural next step was Terraform/OpenTofu with the GitLab provider. The provider is maintained by GitLab itself, which made it an obvious and natural choice for this project.

Note

OpenTofu, a fork of Terraform website, is used throughout the rest of this article. You could replace it with Terraform if needed. I prefer OpenTofu because it offers a fully open-source alternative, stricto sensu according to the Open Source Initiative (OSI) definition.

It gave us several benefits:

  • A stateful model of managed resources
  • A plan showing proposed changes before applying them
  • Dependencies between resources
  • Provider schema validation
  • The ability to import existing resources without recreating them
  • Fully supported and integrated in GitLab through state management and merge request reporting features, out of the box

OpenTofu can read configuration written in HashiCorp Configuration Language (HCL) or in its equivalent JSON syntax.

Composing large configurations with HCL
#

HCL is perfectly capable of describing a resource, and offers variables, expressions, conditionals and reusable modules. Where I felt the friction was one level up: assembling a configuration maintained by several people. HCL gives us iteration over data and module boundaries, but those abstractions need to be designed in advance and connected explicitly. A resource also has one declaration point and another file cannot simply add a few fields to it. I wanted to split ownership across files, share defaults and catch conflicting definitions early, without first building a separate framework to glue all those pieces together.

I guess you already know where I am going ?

This is a job for a language that can compose values and reuse functions. Nix is one way to do it, alongside options such as CUE, KCL and Jsonnet. Nix was already part of our everyday tooling, which made it a natural option to consider. More importantly, its Nix module system gives me the merging and override behaviour I was missing, and Terranix turns that result into OpenTofu configuration.

Nix VS HCL, distracted boyfriend meme

Nix gives us functions, abstractions, composition and a module system for building the configuration before OpenTofu runs. It can also make the tooling environment reproducible: we can pin the tools and versions used to evaluate the modules and run OpenTofu, ensuring the same behaviour in CI and on a developer’s machine.

That’s where Terranix repository enters. It uses the Nix module system to compose Terraform-compatible configuration in Nix:

C(GoninitfxliagfbuirlAaePtsIi)onTeuOWGevspiiraiettrlnnhlaugTanaobittfxehuPseroNNviiixxdemmroodduulleessystemconfig.tf.json
  1. Nix describes the configuration we want
  2. Terranix evaluates the Nix modules
  3. A config.tf.json JSON configuration files is built
  4. OpenTofu reads this file directly, compares the configuration with the current state and creates a plan
  5. OpenTofu communicates with the GitLab API

I really like this division of work. Nix handles configuration composition. OpenTofu handles resource lifecycle management. For me, the real game changer is using Nix as a configuration generator: it gives us flexibility and simplicity.

Not convinced ? Read on…

Consider a simplified GitLab project declaration in HCL:

1resource "gitlab_project" "example" {
2  name             = "example"
3  description      = "Example project"
4  namespace_id     = 123
5  visibility_level = "private"
6}

Imagine that one team manages basic project details, another manages repository settings, and the security team sets memberships. In HCL, splitting a single resource across files requires designing module interfaces and wiring inputs and outputs yourself, with all the accompanying boilerplate.

If you need more configuration to organise your configuration than to define the configuration, that may be a sign of poor design or a poor choice of technology.

With Terranix’s NixOS-style module system, several modules can contribute definitions to the same resource directly once those modules are included in the evaluation.

Composing a resource across multiple files
#

Suppose we have a file called project.nix, another file, project-settings.nix, can contribute additional properties to the very same resource, and yet another file project-example-settings.nix can override the default description:

1{ lib, ... }:
2{
3  resource.gitlab_project.example = {
4    name = "example";
5    path = "project-foobar";
6    description = lib.mkDefault "Example project";
7    visibility_level = "private";
8  };
9}
 1{
 2  resource.gitlab_project.example = {
 3    topics = [
 4      "devsecops"
 5      "automation"
 6    ];
 7
 8    issues_enabled = true;
 9  };
10}
1{
2  resource.gitlab_project.example.description = "Managed declaratively with Nix and OpenTofu !";
3}

When these files are included in the same Terranix module evaluation, the Nix module system merges their declarations into one configuration, applying option types and priorities rather than relying on file load order.

Semantic merging and conflict detection
#

The Nix module system uses option types to combine definitions. Depending on the type, it can join lists, merge attribute sets by key or report a conflict. If two or more modules give the same option different values at the same priority, Nix reports an error rather than choosing one value silently.

These rules help when managing a large configuration. They let us set defaults, add custom settings and find conflicts early.

A conflict found before deployment is just a potential future incident that got cancelled.

Using a module fixed-point
#

Just FYI, This is one of my favourite features of the Nix language.

HCL expressions can refer to values in an OpenTofu configuration, such as variables and resource attributes. Terranix adds another useful mechanism: each Nix module receives a config parameter with the “fixed-point” of the combined module evaluation. Modules can inspect this shared configuration to decide which definitions to contribute.

Note

The module system computes config as a fixed-point of all module definitions. This means a module can use the final value of an option, even when another module defines or overrides it. Nix’s lazy evaluation makes this kind of recursive configuration possible: expressions are evaluated only when their values are needed.

Here, project.nix declares a project and sets archived to false by default. In the separate branch-protection.nix module, the main branch is protected only if the project’s final configuration says it is still active:

 1{ lib, ... }:
 2{
 3  resource = {
 4    gitlab_project.example = {
 5      name = "example";
 6      path = "project-example";
 7      archived = lib.mkDefault false;
 8    };
 9  };
10}
 1{ config, lib, ... }:
 2{
 3  resource = {
 4    gitlab_branch_protection.project_example_main =
 5      lib.mkIf (!config.resource.gitlab_project.example.archived)
 6        {
 7          branch = "main";
 8          # lib.tf.ref is provided by Terranix and explained further down
 9          project = lib.tf.ref "gitlab_project.example.id";
10        };
11  };
12}

Because the modules share the final configuration, branch-protection.nix can read the project’s resolved archived value. If another module sets archived = true, that value overrides the default and Nix leaves out the branch protection resource.

HCL also has conditional expressions and supports count and for_each for conditional resources, but it lacks a Nix-style module system with a shared fixed-point config parameter or lib.mkIf. In HCL, you must wire the condition explicitly through variables, expressions or module inputs.

The difference becomes clearer when a policy must apply to every project matching its final settings. Say the security team wants a strict approval rule on every active private project, regardless of which file declares it. In HCL, for_each can create the rules, but it needs a collection to iterate over:

 1variable "projects" {
 2  type = map(object({
 3    id         = string
 4    visibility = string
 5    archived   = bool
 6  }))
 7
 8  default = {
 9    application = {
10      id         = "12345"
11      visibility = "private"
12      archived   = false
13    }
14    documentation = {
15      id         = "23456"
16      visibility = "public"
17      archived   = false
18    }
19    legacy = {
20      id         = "34567"
21      visibility = "private"
22      archived   = true
23    }
24  }
25}
26
27resource "gitlab_project_approval_rule" "strict" {
28  for_each = {
29    for name, project in var.projects : name => project
30    if project.visibility == "private" && !project.archived
31  }
32
33  project            = each.value.id
34  name               = "Strict policy"
35  approvals_required = 2
36}
 1{ config, lib, ... }:
 2let
 3  # `projects` contains the `gitlab_project` resources where:
 4  #  - visibility level is private
 5  #  - is not archived
 6  projects = lib.filterAttrs (
 7    _: project: project.visibility_level == "private" && !project.archived
 8  ) config.resource.gitlab_project;
 9in
10{
11  resource.gitlab_project_approval_rule = lib.mapAttrs' (
12    name: _:
13    lib.nameValuePair "strict_${name}" {
14      project = lib.tf.ref "gitlab_project.${name}.id";
15      name = "Strict policy";
16      approvals_required = 2;
17    }
18  ) projects;
19}

In strict-approval-rules.tf, the HCL example is valid, but var.projects is a separate inventory that must stay in sync with the gitlab_project resources. HCL cannot build that map by enumerating independent project resource declarations across the root module: resource references let us read a known resource’s attributes, but they do not expose a collection of all resources of that type.

So now we have to maintain an inventory of the projects and the projects themselves. What could possibly go wrong ?

By contrast, strict-approval-rules.nix uses Terranix to filter config.resource.gitlab_project directly and generate one approval rule for each match.

Because config holds the merged project definitions—including values contributed or overridden by other modules—there is no separate project inventory to maintain and no duplication, making it more natural and less error-prone in the long run. This example shows a clear boundary between the languages: HCL can parameterise and iterate over data we provide, while the Nix module system can derive new resources from its composed configuration.

Building higher-level abstractions
#

Beyond splitting individual resources across files, the Nix module system lets us define our own typed, domain-specific options using lib.mkOption. Instead of repeating low-level declarations (e.g., gitlab_project, gitlab_branch_protection, gitlab_project_approval_rule, …) for hundreds of repositories, we can expose a higher-level interface tailored to our organisation’s policies.

For example, a module can define a custom my_org.projects option that expands a concise project declaration into all the underlying OpenTofu resources required by our security standards:

 1{
 2  my_org.projects.plop = {
 3    group = "foobar";
 4    complianceProfile = "strict";
 5    defaultMembers = [
 6      {
 7        username = "alice";
 8        accessLevel = "developer";
 9      }
10      {
11        username = "basile";
12        accessLevel = "reporter";
13      }
14    ];
15  };
16}

Behind the scenes, another Nix module reads config.my_org.projects and generates the corresponding GitLab project, protected branches, merge request approval rules and default project memberships. The module must also declare the my_org.projects option and the shape of its values. Here is a simplified example that declares the option and generates the underlying resources:

The Nix module in details
  1# modules/project-policy.nix
  2{ config, lib, ... }:
  3let
  4  projects = config.my_org.projects or { };
  5  strictProjects = lib.filterAttrs (_: project: project.complianceProfile == "strict") projects;
  6in
  7{
  8  config.resource = {
  9    # See: https://search.opentofu.org/provider/gitlabhq/gitlab/latest/docs/resources/branch_protection
 10    gitlab_branch_protection = lib.mapAttrs' (
 11      name: _project:
 12      lib.nameValuePair "${name}_main" {
 13        allowed_to_merge = [ { access_level = "maintainer"; } ];
 14        allowed_to_push = [ { access_level = "no one"; } ];
 15        branch = "main";
 16        project = lib.tf.ref "gitlab_project.${name}.id";
 17      }
 18    ) strictProjects;
 19
 20    # See: https://search.opentofu.org/provider/gitlabhq/gitlab/latest/docs/resources/project
 21    gitlab_project = lib.mapAttrs' (
 22      name: project:
 23      lib.nameValuePair name {
 24        name = name;
 25        namespace_id = lib.tf.ref "gitlab_group.${project.group}.id";
 26        path = name;
 27        visibility_level = "private";
 28      }
 29    ) projects;
 30
 31    # See: https://search.opentofu.org/provider/gitlabhq/gitlab/latest/docs/resources/project_approval_rule
 32    gitlab_project_approval_rule = lib.mapAttrs' (
 33      name: _project:
 34      lib.nameValuePair "${name}_strict" {
 35        approvals_required = 2;
 36        name = "Strict policy";
 37        project = lib.tf.ref "gitlab_project.${name}.id";
 38      }
 39    ) strictProjects;
 40
 41    # See: https://search.opentofu.org/provider/gitlabhq/gitlab/latest/docs/resources/project_membership
 42    gitlab_project_membership = lib.listToAttrs (
 43      lib.concatLists (
 44        lib.mapAttrsToList (
 45          projectName: project:
 46          map (
 47            member:
 48            lib.nameValuePair "${projectName}_${toString member.username}" {
 49              access_level = member.accessLevel;
 50              project = lib.tf.ref "gitlab_project.${projectName}.id";
 51              user_id = lib.tf.ref "data.gitlab_user.${member.username}.id";
 52            }
 53          ) (project.defaultMembers or [ ])
 54        ) projects
 55      )
 56    );
 57  };
 58
 59  options.my_org.projects = lib.mkOption {
 60    default = { };
 61    description = "Projects defined through the organisation's project interface.";
 62
 63    type = lib.types.attrsOf (
 64      lib.types.submodule {
 65        options = {
 66          complianceProfile = lib.mkOption {
 67            default = "standard";
 68            description = "Project compliance profile.";
 69
 70            type = lib.types.enum [
 71              "standard"
 72              "strict"
 73            ];
 74          };
 75
 76          defaultMembers = lib.mkOption {
 77            default = [ ];
 78            description = "Members to add to the project by default.";
 79
 80            type = lib.types.listOf (
 81              lib.types.submodule {
 82                options = {
 83                  accessLevel = lib.mkOption {
 84                    description = "Project access level for this member.";
 85
 86                    type = lib.types.enum [
 87                      "guest"
 88                      "reporter"
 89                      "developer"
 90                      "maintainer"
 91                    ];
 92                  };
 93
 94                  username = lib.mkOption {
 95                    description = "GitLab username to add to the project.";
 96                    type = lib.types.str;
 97                  };
 98                };
 99              }
100            );
101          };
102
103          group = lib.mkOption {
104            description = "Name of the existing GitLab group.";
105            type = lib.types.str;
106          };
107        };
108      }
109    );
110  };
111}

For this example, group names an existing gitlab_group resource, and defaultMembers is an optional list of usernames and access levels. The rule protects main from direct pushes and allows Maintainers to merge approved merge requests. Developers can push to unprotected working branches and GitLab does not restrict those pushes to the branch’s creator. Teams get a small, validated vocabulary for common patterns, which lowers the cognitive load: contributors can use the organisation’s project options without learning every provider resource Terranix can expose. They can still use raw resource.* attributes whenever a project needs something bespoke.

At last, a cosy place to put all those opinions about how a project should be configured or to turn every managerial whim into a typed Nix option, or both !
Important

This abstraction layer also directly addresses one of the biggest friction points when adopting Nix: the learning curve. Colleagues and project maintainers do not need to understand module fixpoints, evaluation semantics or deep Nix language internals to contribute. For their everyday needs, declaring a new repository or onboarding a member looks just like writing a clean, self-documenting data structure.

Organising hundreds of repositories
#

The way we organise files has another benefit: the source tree can mirror the GitLab group and project hierarchy, making it easy to find the settings for a given project.

src/
├── default.nix
├── user.nix
└── group-foobar/
    ├── group.nix
    └── project-plop/
        ├── project.nix
        ├── project-membership.nix
        ├── branch-protection.nix
        └── approval-rules.nix

The Nix module system does not require a fixed directory structure. We can organise modules by team, project or responsibility, then compose them into one configuration.

In this setup, src/default.nix automatically discovers all .nix files in the tree and imports them into a single Terranix evaluation. Adding a new project or splitting a file requires no boilerplate registration: directories organise files for humans, but they do not create separate configuration scopes. By contrast, OpenTofu loads HCL files from a single directory into the root module, while nested directories form separate modules that must be wired and called explicitly.

Directories are for humans. Thankfully, Nix does not care where I put things.

As a result, moving or renaming a file does not change the OpenTofu resource address as long as the resource declaration remains the same, and adding or removing files does not require any specific binding either.

Important

While this kind of opinionated structure conveys semantics, it can be reorganised to adapt to the problem at hand, match the functional needs and grow organically.

Bringing existing GitLab resources under management
#

An existing GitLab instance may already contain hundreds of repositories, groups and user memberships. Creating them by hand is not practical. For an initial migration, scripts can query the GitLab API and generate Nix declarations alongside OpenTofu import blocks.

For example, with project.nix and project-import.nix:

1{
2  resource = {
3    gitlab_project.my_project = {
4      name = "My project";
5      path = "my-project";
6    };
7  };
8}
1{
2  import = [
3    {
4      id = "my-group/my-project";
5      to = "gitlab_project.my_project";
6    }
7  ];
8}
Warning

OpenTofu uses these import declarations to add existing objects to its state. Keeping them in clear and separate identifiable files makes cleanup trivial once imported.

These *-import.nix files are scaffolding: useful to get started, satisfying to delete.

Such scripts are run once to bootstrap the configuration, not as a routine synchronisation tool. Keep the generated resource declarations in Git and make later customisations in separate files. After the initial migration, every change goes through a merge request workflow before GitLab CI applies the configuration with OpenTofu.

GitOps, plans and drift detection
#

The OpenGitOps principles describe 4 properties of a GitOps system: its desired state is declarative, versioned and immutable, pulled automatically, and continuously reconciled.

The generated config.tf.json file is never committed to Git, keeping build artifacts out of version control so the repository remains clean. Instead, the GitLab CI pipeline evaluates the Nix modules and renders config.tf.json on the fly. The pipeline then validates the generated configuration (via tofu validate), creates OpenTofu plans posted as comments on merge requests, and applies changes from the default branch after merge (via tofu apply). OpenTofu stores its state in GitLab’s HTTP backend, with state locking to prevent concurrent modifications.

Scheduled pipelines also check for configuration drift. If someone changes a managed setting by hand in the GitLab web interface, a later run detects the difference, sends a notification and restores the settings.

Worry not, the UI is still there. It just stopped being the source of truth.
Important

The configuration repository is more than a backup, it is our single source of truth: the Nix files define the settings GitLab should use, making project policies and user access easy to review and audit.

Despite that, I really wish GitLab had a way to prevent editing settings through its UI. That would simplify management by moving from a “convergent” to a more “congruent” configuration model.

A match made in heaven
#

After we had built this system, I read An Introduction to OpenTofu Symbol Libraries. Just like Nix with flakes (OK the pun was easy!), Symbol Libraries are an experimental OpenTofu feature that lets teams define reusable functions, types and values in HCL. The article’s DNS example defines an HCL record type and a function that formats its records.

The equivalent HCL and Nix examples below formats the same record during Terranix evaluation:

 1typedef "dns_recordset" {
 2  type = object({
 3    name    = string
 4    type    = string
 5    ttl     = number
 6    records = list(string)
 7  })
 8}
 9
10function "format_dns_recordset" {
11  type = string
12
13  parameter "recordset" {
14    type = symbols::dns_recordset()
15  }
16
17  locals {
18    entries = [for record in param.recordset.records :
19      "record_set ${param.recordset.name} is a ${param.recordset.type} with a ttl of ${param.recordset.ttl} and entry \"${record}\""
20    ]
21  }
22
23  return = join("\n", local.entries)
24}
 1let
 2  recordset = {
 3    name = "Example";
 4    type = "A";
 5    ttl = 500;
 6    records = [ "10.10.10.10" ];
 7  };
 8
 9  formatDnsRecordset =
10    dns:
11    builtins.concatStringsSep "\n" (
12      map (
13        record:
14        "record_set ${dns.name} is a ${dns.type} with a ttl of ${toString dns.ttl} and entry \"${record}\""
15      ) dns.records
16    );
17in
18{
19  locals.formatted_recordset = formatDnsRecordset recordset;
20}

Which Terranix renders as a static OpenTofu local before OpenTofu even runs:

1locals {
2  formatted_recordset = "record_set Example is a A with a ttl of 500 and entry \"10.10.10.10\""
3}

The 2 snippets above run at different stages: the HCL snippet defines a reusable type and formatter evaluated by OpenTofu, whereas the Nix snippet computes the value ahead of time during Terranix evaluation. That example resonated with me. Seeing OpenTofu add reusable functions and types to HCL reinforces my view that configuration at scale often needs the expressive tools of a general-purpose language.

Warning

Unlike the HCL example with typedef, the Nix language itself has no built-in type system for functions or values. Type checking is only available when defining Nix module options (e.g., via lib.mkOption).

Because Nix runs first, it cannot inspect values that only become known during an OpenTofu plan or apply. For those, we emit an OpenTofu reference expression with lib.tf.ref (as seen in the earlier branch-protection example):

1resource "gitlab_branch_protection" "main" {
2  project            = gitlab_project.example.id
3  branch             = "main"
4  push_access_level  = "maintainer"
5  merge_access_level = "developer"
6}
1{ lib, ... }:
2{
3  resource.gitlab_branch_protection.main = {
4    project = lib.tf.ref "gitlab_project.example.id";
5    branch = "main";
6    push_access_level = "maintainer";
7    merge_access_level = "developer";
8  };
9}

This is not a reason for everyone to stop using HCL. Symbol Libraries suit teams that want reusable logic inside OpenTofu. Nix and Terranix suit teams that want to compose configuration before OpenTofu runs, with a pinned and reproducible tooling environment.

Final thoughts
#

Moving away from ClickOps transformed how we manage GitLab on a daily basis. Creating a new repository, onboarding a team member or rolling out a stricter branch-protection policy across hundreds of projects is now a small, readable merge request rather than a series of manual clicks.

You click, I commit, we are not the same meme

Combining Nix, Terranix and OpenTofu gives us the best of both worlds:

  • Nix and Terranix bring a powerful module system, semantic merging, early conflict detection and custom domain abstractions, without forcing a rigid directory layout.
  • OpenTofu brings battle-tested state management, execution plans, drift detection and a rich ecosystem of providers.

Every permission, project setting and group membership now lives in Git. Reviewed by peers, validated in CI and continuously reconciled against live state.

Convinced now ? If not, then I did not explain myself well 😄 !

Acknowledgements
#

Many thanks to Simon Shine and “Bobbeh” for reviewing an early draft and sharing their feedback. Any opinions or mistakes that remain are mine.

Pol Dellaiera
Author
Pol Dellaiera
Pol Dellaiera is a software engineer and technology enthusiast with a passion for programming, open-source software, and innovative technologies. With years of experience in the tech industry, Pol has developed a deep understanding of various programming languages, frameworks, and tools. He enjoys sharing his knowledge through writing, speaking at conferences, and contributing to open-source projects. In his free time, Pol explores new technologies, experiments with coding projects, and engages with the tech community to stay updated on the latest trends and advancements.

Related