diff --git a/Makefile.am b/Makefile.am
index ab901df757..8dc9a3438b 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -552,6 +552,7 @@ SCM_TESTS =					\
   tests/services.scm				\
   tests/services/file-sharing.scm		\
   tests/services/configuration.scm		\
+  tests/services/configuration/generic-ini.scm	\
   tests/services/lightdm.scm			\
   tests/services/linux.scm			\
   tests/services/telephony.scm			\
diff --git a/gnu/local.mk b/gnu/local.mk
index ce16d37e2b..a05d70aff3 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -670,6 +670,7 @@ GNU_SYSTEM_MODULES =				\
   %D%/services/cgit.scm			\
   %D%/services/ci.scm				\
   %D%/services/configuration.scm		\
+  %D%/services/configuration/generic-ini.scm	\
   %D%/services/cuirass.scm			\
   %D%/services/cups.scm				\
   %D%/services/databases.scm			\
diff --git a/gnu/services/configuration/generic-ini.scm b/gnu/services/configuration/generic-ini.scm
new file mode 100644
index 0000000000..338a35f90d
--- /dev/null
+++ b/gnu/services/configuration/generic-ini.scm
@@ -0,0 +1,129 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (gnu services configuration generic-ini)
+  #:use-module (gnu services configuration)
+  #:use-module (guix gexp)
+  #:use-module (srfi srfi-171)
+  #:use-module (ice-9 match)
+  #:export (ini-entry?
+            list-of-ini-entries?
+
+            serialize-ini-configuration))
+
+;;;
+;;; Generic INI serializer
+;;;
+
+
+;;;
+;;; Predicates
+;;;
+
+;; This is the same format used in SRFI-233 but without comment support.
+(define ini-entry?
+  (match-lambda
+    (((? symbol) (? symbol) (? string)) #t)
+    (_ #f)))
+
+(define list-of-ini-entries
+  (list-of ini-entry?))
+
+;;
+;; Overall design document
+;;
+;; This module implements a generic INI serializer for a record-type defined
+;; using define-configuration.
+;; It expects that the serialize-<type> procedures return a list with
+;; three elements of the form:
+;;    (list section key value)
+;; Where ‘section’ and ‘key’ are symbols and ‘value’ is a string.
+;; The fields within define-configuration do not have to be ordered in,
+;; any way whatsoever as the ‘serialize-ini’ will group them up automatically.
+;; This implies that no assumptions should be made regarding the order of the
+;; values in the serializied INI output.
+;;
+;; Additional notes:
+;; Q: Why not replace rcons with string-append and forego the ungexp-splice?
+;; A: The transduction happens outside of the G-Exp while the final string-append
+;;    takes place in the G-Exp.
+;;
+;; Debugging tips: Open a REPL and try one transducer at a time from
+;; ‘ini-transducer’.
+;;
+
+(define (add-section-header partition)
+  (let ((header (caar partition)))
+    (cons (list header)
+          partition)))
+
+(define serializer
+  (match-lambda
+    ((section)
+     #~(format #f "[~a]~%" '#$section))
+    ((section key value)
+     #~(format #f "~a=~a~%" '#$key #$value))
+    ((? string? s)  ; used for the newline between sections
+     s)))
+
+(define ini-transducer
+  (compose (tpartition car)
+           (tmap add-section-header)
+           (tadd-between '("\n"))
+           tconcatenate
+           (tmap serializer)))
+
+;; A “first-pass” serialization is performed and sorted in order
+;; to group up the fields by “section” before passing through the
+;; transducer.
+(define (serialize-ini-configuration config fields)
+  (let* ((srfi-233-IR
+          ;; First pass: “serialize” into a (disordered) list of
+          ;; SRFI-233 entries.
+          (list-transduce (base-transducer config) rcons fields))
+         (comparator (lambda (x y)
+                       ;; Sort the SRFI-233 entries by section.
+                       (string<=? (symbol->string (car x))
+                                  (symbol->string (car y)))))
+         (sorted-entries (sort srfi-233-IR comparator)))
+    #~(string-append
+       #$@(list-transduce ini-transducer rcons sorted-entries))))
+
+;; FIXME:RFC:
+;;       generic-ini- prefixed serializing procs?
+;;       (perhaps prefixed as generic-ini: ?)
+;; Example procedures:
+;;
+(define* (generic-ini-serialize-string field-name value #:key section)
+  (list section field-name value))
+
+;; field-name-transform can be used to “uglify” a field-name,
+;; e.g. want-ipv6?  ->  want_ipv6
+(define* (generic-ini-serialize-boolean field-name value #:key section
+                            (field-name-transform identity))
+  (list section (field-name-transform field-name)
+        (if value "true" "false")))
+
+
+;;; FIXME: delete this before inclusion, these are notes for the first RFC.
+;;;
+;;; Left out for now (but readily extendable):
+;;;  * Custom leading (presumed to be whitespace) characters for entries
+;;;    à la gitconfig, mostly pretty-printing purposes
+;;;  * Configurable delimiter (\n, \r\n, \0, ...)
+;;;  * Configurable Key-value separator (this is usually =)
diff --git a/tests/services/configuration/generic-ini.scm b/tests/services/configuration/generic-ini.scm
new file mode 100644
index 0000000000..c04acdcd46
--- /dev/null
+++ b/tests/services/configuration/generic-ini.scm
@@ -0,0 +1,106 @@
+;;; GNU Guix --- Functional package management for GNU
+;;; Copyright © 2023 Bruno Victal <mirai@makinata.eu>
+;;;
+;;; This file is part of GNU Guix.
+;;;
+;;; GNU Guix is free software; you can redistribute it and/or modify it
+;;; under the terms of the GNU General Public License as published by
+;;; the Free Software Foundation; either version 3 of the License, or (at
+;;; your option) any later version.
+;;;
+;;; GNU Guix is distributed in the hope that it will be useful, but
+;;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;;; GNU General Public License for more details.
+;;;
+;;; You should have received a copy of the GNU General Public License
+;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (tests services configuration generic-ini)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu services configuration generic-ini)
+  #:use-module (guix diagnostics)
+  #:use-module (guix gexp)
+  #:use-module (guix store)
+  #:autoload (guix i18n) (G_)
+  #:use-module (srfi srfi-34)
+  #:use-module (srfi srfi-64)
+  #:use-module (srfi srfi-71))
+
+;;; Tests for the (gnu services configuration generic-ini) module.
+
+(test-begin "generic-ini serializer")
+
+
+(define expected-output "\
+[featured]
+favourite=true
+track=Deep affection
+artist=Magistina Saga
+album=Creaith Anthem
+
+[main]
+enabled=true
+")
+
+
+;;;
+;;; Serializers
+;;;
+(define (strip-trailing-?-character field-name)
+  "Drop rightmost '?' character"
+  (let ((str (symbol->string field-name)))
+    (if (string-suffix? "?" str)
+        (string->symbol (string-drop-right str 1))
+        field-name)))
+
+(define* (serialize-string field-name value #:key section)
+  (list section field-name value))
+
+(define* (serialize-boolean field-name value #:key section)
+  (list section (strip-trailing-?-character field-name)
+        (if value "true" "false")))
+
+
+;;;
+;;; Record-type definition
+;;;
+
+(define-configuration foo-configuration
+  (album
+   (string "Creaith Anthem")
+   "Lorem Ipsum …"
+   (serializer-kwargs '(#:section featured)))
+
+  (artist
+   (string "Magistina Saga")
+   "Lorem Ipsum …"
+   (serializer-kwargs '(#:section featured)))
+
+  (track
+   (string "Deep affection")
+   "Lorem Ipsum …"
+   (serializer-kwargs '(#:section featured)))
+
+  (favourite?
+   (boolean #t)
+   "Lorem Ipsum …"
+   (serializer-kwargs '(#:section featured)))
+
+  (enabled?
+   (boolean #t)
+   "Lorem Ipsum …"
+   (serializer-kwargs '(#:section main))))
+
+(test-equal "Well-formed INI output from serialize-ini"
+  expected-output
+  ;; Serialize the above into a string, properly resolving any potential
+  ;; nested G-Exps as well.
+  (let* ((serialized-ini
+          (serialize-ini-configuration (foo-configuration)
+                                       foo-configuration-fields))
+         (lowered conn (with-store store
+                         ((lower-gexp serialized-ini) store))))
+    (eval (lowered-gexp-sexp lowered) (current-module))))
+
+(test-end)
