diff --git a/Knot.files b/Knot.files
index 7157f4c36ad5ca986846ef945e121c884fc763e2..61b6366ec5dd5354e33291b7b78b956b9a806617 100644
--- a/Knot.files
+++ b/Knot.files
@@ -100,6 +100,14 @@ src/contrib/url-parser/url_parser.h
 src/contrib/vpool/vpool.c
 src/contrib/vpool/vpool.h
 src/contrib/wire_ctx.h
+src/knot/catalog/catalog_db.c
+src/knot/catalog/catalog_db.h
+src/knot/catalog/catalog_update.c
+src/knot/catalog/catalog_update.h
+src/knot/catalog/generate.c
+src/knot/catalog/generate.h
+src/knot/catalog/interpret.c
+src/knot/catalog/interpret.h
 src/knot/common/evsched.c
 src/knot/common/evsched.h
 src/knot/common/fdset.c
@@ -268,8 +276,6 @@ src/knot/zone/adjust.c
 src/knot/zone/adjust.h
 src/knot/zone/backup.c
 src/knot/zone/backup.h
-src/knot/zone/catalog.c
-src/knot/zone/catalog.h
 src/knot/zone/contents.c
 src/knot/zone/contents.h
 src/knot/zone/measure.c
diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc
index 7d39438ff4b00d6bee9a2598cfaafd9e4431956d..88b964fa581e0ce8e058b563e21f7640a66c1a73 100644
--- a/src/knot/Makefile.inc
+++ b/src/knot/Makefile.inc
@@ -9,6 +9,14 @@ include_libknotd_HEADERS = \
 	knot/include/module.h
 
 libknotd_la_SOURCES = \
+	knot/catalog/catalog_db.c		\
+	knot/catalog/catalog_db.h		\
+	knot/catalog/catalog_update.c		\
+	knot/catalog/catalog_update.h		\
+	knot/catalog/generate.c			\
+	knot/catalog/generate.h			\
+	knot/catalog/interpret.c		\
+	knot/catalog/interpret.h		\
 	knot/conf/base.c			\
 	knot/conf/base.h			\
 	knot/conf/conf.c			\
@@ -159,8 +167,6 @@ libknotd_la_SOURCES = \
 	knot/zone/adjust.h			\
 	knot/zone/backup.c			\
 	knot/zone/backup.h			\
-	knot/zone/catalog.c			\
-	knot/zone/catalog.h			\
 	knot/zone/contents.c			\
 	knot/zone/contents.h			\
 	knot/zone/measure.h			\
diff --git a/src/knot/catalog/catalog_db.c b/src/knot/catalog/catalog_db.c
new file mode 100644
index 0000000000000000000000000000000000000000..28628bb7a2f11742922d45c5cf3d342ddb1c3f57
--- /dev/null
+++ b/src/knot/catalog/catalog_db.c
@@ -0,0 +1,374 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <urcu.h>
+
+#include "knot/catalog/catalog_db.h"
+#include "knot/common/log.h"
+
+static const MDB_val catalog_iter_prefix = { 1, "" };
+
+size_t catalog_dname_append(knot_dname_storage_t storage, const knot_dname_t *name)
+{
+	size_t old_len = knot_dname_size(storage);
+	size_t name_len = knot_dname_size(name);
+	size_t new_len = old_len - 1 + name_len;
+	if (old_len == 0 || name_len == 0 || new_len > KNOT_DNAME_MAXLEN) {
+		return 0;
+	}
+	memcpy(storage + old_len - 1, name, name_len);
+	return new_len;
+}
+
+int catalog_bailiwick_shift(const knot_dname_t *subname, const knot_dname_t *name)
+{
+	const knot_dname_t *res = subname;
+	while (!knot_dname_is_equal(res, name)) {
+		if (*res == '\0') {
+			return -1;
+		}
+		res = knot_wire_next_label(res, NULL);
+	}
+	return res - subname;
+}
+
+void catalog_init(catalog_t *cat, const char *path, size_t mapsize)
+{
+	knot_lmdb_init(&cat->db, path, mapsize, MDB_NOTLS, NULL);
+}
+
+// does NOT check for catalog zone version by RFC, this is Knot-specific in the cat LMDB !
+static void check_cat_version(catalog_t *cat)
+{
+	if (cat->ro_txn->ret == KNOT_EOK) {
+		MDB_val key = { 8, "\x01version" };
+		if (knot_lmdb_find(cat->ro_txn, &key, KNOT_LMDB_EXACT)) {
+			if (strncmp(CATALOG_VERSION, cat->ro_txn->cur_val.mv_data,
+			            cat->ro_txn->cur_val.mv_size) != 0) {
+				log_warning("unmatching catalog version");
+			}
+		} else if (cat->rw_txn != NULL) {
+			MDB_val val = { strlen(CATALOG_VERSION), CATALOG_VERSION };
+			knot_lmdb_insert(cat->rw_txn, &key, &val);
+		}
+	}
+}
+
+int catalog_open(catalog_t *cat)
+{
+	if (!knot_lmdb_is_open(&cat->db)) {
+		int ret = knot_lmdb_open(&cat->db);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+	}
+	if (cat->ro_txn == NULL) {
+		knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn));
+		if (ro_txn == NULL) {
+			return KNOT_ENOMEM;
+		}
+		knot_lmdb_begin(&cat->db, ro_txn, false);
+		cat->ro_txn = ro_txn;
+	}
+	check_cat_version(cat);
+	return cat->ro_txn->ret;
+}
+
+int catalog_begin(catalog_t *cat)
+{
+	int ret = catalog_open(cat);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+	knot_lmdb_txn_t *rw_txn = calloc(1, sizeof(*rw_txn));
+	if (rw_txn == NULL) {
+		return KNOT_ENOMEM;
+	}
+	knot_lmdb_begin(&cat->db, rw_txn, true);
+	if (rw_txn->ret != KNOT_EOK) {
+		ret = rw_txn->ret;
+		free(rw_txn);
+		return ret;
+	}
+	assert(cat->rw_txn == NULL); // LMDB prevents two existing RW txns at a time
+	cat->rw_txn = rw_txn;
+	check_cat_version(cat);
+	return cat->rw_txn->ret;
+}
+
+int catalog_commit(catalog_t *cat)
+{
+	knot_lmdb_txn_t *rw_txn = rcu_xchg_pointer(&cat->rw_txn, NULL);
+	knot_lmdb_commit(rw_txn);
+	int ret = rw_txn->ret;
+	free(rw_txn);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	// now refresh RO txn
+	knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn));
+	if (ro_txn == NULL) {
+		return KNOT_ENOMEM;
+	}
+	knot_lmdb_begin(&cat->db, ro_txn, false);
+	cat->old_ro_txn = rcu_xchg_pointer(&cat->ro_txn, ro_txn);
+
+	return KNOT_EOK;
+}
+
+void catalog_abort(catalog_t *cat)
+{
+	knot_lmdb_txn_t *rw_txn = rcu_xchg_pointer(&cat->rw_txn, NULL);
+	if (rw_txn != NULL) {
+		knot_lmdb_abort(rw_txn);
+		free(rw_txn);
+	}
+}
+
+void catalog_commit_cleanup(catalog_t *cat)
+{
+	knot_lmdb_txn_t *old_ro_txn = rcu_xchg_pointer(&cat->old_ro_txn, NULL);
+	if (old_ro_txn != NULL) {
+		knot_lmdb_abort(old_ro_txn);
+		free(old_ro_txn);
+	}
+}
+
+int catalog_deinit(catalog_t *cat)
+{
+	assert(cat->rw_txn == NULL);
+	if (cat->ro_txn != NULL) {
+		knot_lmdb_abort(cat->ro_txn);
+		free(cat->ro_txn);
+	}
+	if (cat->old_ro_txn != NULL) {
+		knot_lmdb_abort(cat->old_ro_txn);
+		free(cat->old_ro_txn);
+	}
+	knot_lmdb_deinit(&cat->db);
+	return KNOT_EOK;
+}
+
+int catalog_add(catalog_t *cat, const knot_dname_t *member,
+                const knot_dname_t *owner, const knot_dname_t *catzone)
+{
+	if (cat->rw_txn == NULL) {
+		return KNOT_EINVAL;
+	}
+	int bail = catalog_bailiwick_shift(owner, catzone);
+	if (bail < 0) {
+		return KNOT_EOUTOFZONE;
+	}
+	assert(bail >= 0 && bail < 256);
+	MDB_val key = knot_lmdb_make_key("BN", 0, member); // 0 for future purposes
+	MDB_val val = knot_lmdb_make_key("BBN", 0, bail, owner);
+
+	knot_lmdb_insert(cat->rw_txn, &key, &val);
+	free(key.mv_data);
+	free(val.mv_data);
+	return cat->rw_txn->ret;
+}
+
+int catalog_del(catalog_t *cat, const knot_dname_t *member)
+{
+	if (cat->rw_txn == NULL) {
+		return KNOT_EINVAL;
+	}
+	MDB_val key = knot_lmdb_make_key("BN", 0, member);
+	knot_lmdb_del_prefix(cat->rw_txn, &key); // deletes one record
+	free(key.mv_data);
+	return cat->rw_txn->ret;
+}
+
+static void unmake_val(MDB_val *val, const knot_dname_t **owner, const knot_dname_t **catz)
+{
+	uint8_t zero, shift;
+	knot_lmdb_unmake_key(val->mv_data, val->mv_size, "BBN", &zero, &shift, owner);
+	*catz = *owner + shift;
+}
+
+static int find_threadsafe(catalog_t *cat, const knot_dname_t *member,
+                           const knot_dname_t **owner, const knot_dname_t **catz,
+                           void **tofree)
+{
+	*tofree = NULL;
+	if (cat->ro_txn == NULL) {
+		return KNOT_ENOENT;
+	}
+
+	MDB_val key = knot_lmdb_make_key("BN", 0, member), val = { 0 };
+
+	int ret = knot_lmdb_find_threadsafe(cat->ro_txn, &key, &val, KNOT_LMDB_EXACT);
+	if (ret == KNOT_EOK) {
+		unmake_val(&val, owner, catz);
+		*tofree = val.mv_data;
+	}
+	free(key.mv_data);
+	return ret;
+}
+
+int catalog_get_catz(catalog_t *cat, const knot_dname_t *member,
+                     const knot_dname_t **catz, void **tofree)
+{
+	const knot_dname_t *unused;
+	return find_threadsafe(cat, member, &unused, catz, tofree);
+}
+
+bool catalog_has_member(catalog_t *cat, const knot_dname_t *member)
+{
+	const knot_dname_t *catz;
+	void *tofree = NULL;
+	int ret = catalog_get_catz(cat, member, &catz, &tofree);
+	free(tofree);
+	return (ret == KNOT_EOK);
+}
+
+bool catalog_contains_exact(catalog_t *cat, const knot_dname_t *member,
+                            const knot_dname_t *owner, const knot_dname_t *catz)
+{
+	const knot_dname_t *found_owner, *found_catz;
+	void *tofree = NULL;
+	int ret = find_threadsafe(cat, member, &found_owner, &found_catz, &tofree);
+	if (ret == KNOT_EOK && (!knot_dname_is_equal(owner, found_owner) ||
+	    !knot_dname_is_equal(catz, found_catz))) {
+		ret = KNOT_ENOENT;
+	}
+	free(tofree);
+	return (ret == KNOT_EOK);
+}
+
+typedef struct {
+	catalog_apply_cb_t cb;
+	void *ctx;
+} catalog_apply_ctx_t;
+
+static int catalog_apply_cb(MDB_val *key, MDB_val *val, void *ctx)
+{
+	catalog_apply_ctx_t *iter_ctx = ctx;
+	uint8_t zero;
+	const knot_dname_t *mem = NULL, *ow = NULL, *cz = NULL;
+	knot_lmdb_unmake_key(key->mv_data, key->mv_size, "BN", &zero, &mem);
+	unmake_val(val, &ow, &cz);
+	if (mem == NULL || ow == NULL || cz == NULL) {
+		return KNOT_EMALF;
+	}
+	return iter_ctx->cb(mem, ow, cz, iter_ctx->ctx);
+}
+
+int catalog_apply(catalog_t *cat, const knot_dname_t *for_member,
+                  catalog_apply_cb_t cb, void *ctx, bool rw)
+{
+	MDB_val prefix = knot_lmdb_make_key(for_member == NULL ? "B" : "BN", 0, for_member);
+	catalog_apply_ctx_t iter_ctx = { cb, ctx };
+	knot_lmdb_txn_t *use_txn = rw ? cat->rw_txn : cat->ro_txn;
+	int ret = knot_lmdb_apply_threadsafe(use_txn, &prefix, true, catalog_apply_cb, &iter_ctx);
+	free(prefix.mv_data);
+	return ret;
+}
+
+static bool same_catalog(knot_lmdb_txn_t *txn, const knot_dname_t *catalog)
+{
+	if (catalog == NULL) {
+		return true;
+	}
+	const knot_dname_t *txn_cat = NULL, *unused;
+	unmake_val(&txn->cur_val, &unused, &txn_cat);
+	return knot_dname_is_equal(txn_cat, catalog);
+}
+
+int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to,
+                 const knot_dname_t *cat_only, bool read_rw_txn)
+{
+	if (!knot_lmdb_exists(from)) {
+		return KNOT_EOK;
+	}
+	int ret = knot_lmdb_open(from);
+	if (ret == KNOT_EOK) {
+		ret = knot_lmdb_open(to);
+	}
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+	knot_lmdb_txn_t txn_r = { 0 }, txn_w = { 0 };
+	knot_lmdb_begin(from, &txn_r, read_rw_txn); // using RW txn not to conflict with still-open RO txn
+	knot_lmdb_begin(to, &txn_w, true);
+	knot_lmdb_foreach(&txn_w, (MDB_val *)&catalog_iter_prefix) {
+		if (same_catalog(&txn_w, cat_only)) {
+			knot_lmdb_del_cur(&txn_w);
+		}
+	}
+	knot_lmdb_foreach(&txn_r, (MDB_val *)&catalog_iter_prefix) {
+		if (same_catalog(&txn_r, cat_only)) {
+			knot_lmdb_insert(&txn_w, &txn_r.cur_key, &txn_r.cur_val);
+		}
+	}
+	if (txn_r.ret != KNOT_EOK) {
+		knot_lmdb_abort(&txn_r);
+		knot_lmdb_abort(&txn_w);
+		return txn_r.ret;
+	}
+	knot_lmdb_commit(&txn_r);
+	knot_lmdb_commit(&txn_w);
+	return txn_w.ret;
+}
+
+static void print_dname(const knot_dname_t *d)
+{
+	knot_dname_txt_storage_t tmp;
+	knot_dname_to_str(tmp, d, sizeof(tmp));
+	printf("%s  ", tmp);
+}
+
+static void print_dname3(const char *prefix, const knot_dname_t *a,
+                         const knot_dname_t *b,const knot_dname_t *c)
+{
+	printf("%s", prefix);
+	print_dname(a);
+	print_dname(b);
+	print_dname(c);
+	printf("\n");
+}
+
+static int catalog_print_cb(const knot_dname_t *mem, const knot_dname_t *ow,
+                            const knot_dname_t *cz, void *ctx)
+{
+	print_dname3("", mem, ow, cz);
+	(*(ssize_t *)ctx)++;
+	return KNOT_EOK;
+}
+
+void catalog_print(catalog_t *cat)
+{
+	ssize_t total = 0;
+
+	printf(";; <catalog zone> <record owner> <record zone>\n");
+
+	if (cat != NULL) {
+		int ret = catalog_open(cat);
+		if (ret == KNOT_EOK) {
+			ret = catalog_apply(cat, NULL, catalog_print_cb, &total, false);
+		}
+		if (ret != KNOT_EOK) {
+			printf("Catalog print failed (%s)\n", knot_strerror(ret));
+			return;
+		}
+	}
+
+	printf("Total records: %zd\n", total);
+}
diff --git a/src/knot/catalog/catalog_db.h b/src/knot/catalog/catalog_db.h
new file mode 100644
index 0000000000000000000000000000000000000000..e0c2184d227c1b681692048bf89c1ebb6c41b6ce
--- /dev/null
+++ b/src/knot/catalog/catalog_db.h
@@ -0,0 +1,191 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/journal/knot_lmdb.h"
+#include "libknot/libknot.h"
+
+#define CATALOG_VERSION		"1.0"
+#define CATALOG_ZONE_VERSION	"2" // must be just one char long
+#define CATALOG_ZONES_LABEL	"\x05""zones"
+
+typedef struct catalog {
+	knot_lmdb_db_t db;
+	knot_lmdb_txn_t *ro_txn; // persistent RO transaction
+	knot_lmdb_txn_t *rw_txn; // temporary RW transaction
+
+	// private
+	knot_lmdb_txn_t *old_ro_txn;
+} catalog_t;
+
+/*!
+ * \brief Append a prefix dname to a dname in a storage.
+ *
+ * \return New dname length.
+ */
+size_t catalog_dname_append(knot_dname_storage_t storage, const knot_dname_t *name);
+
+/*!
+ * \brief Return the number of bytes that subname has more than name.
+ *
+ * \return -1 if subname is not subname of name
+ */
+int catalog_bailiwick_shift(const knot_dname_t *subname, const knot_dname_t *name);
+
+/*!
+ * \brief Initialize catalog structure.
+ *
+ * \param cat        Catalog structure.
+ * \param path       Path to LMDB for catalog.
+ * \param mapsize    Mapsize of the LMDB.
+ */
+void catalog_init(catalog_t *cat, const char *path, size_t mapsize);
+
+/*!
+ * \brief Open the catalog LMDB, create it if not exists.
+ *
+ * \param cat   Catlog to be opened.
+ *
+ * \return KNOT_E*
+ */
+int catalog_open(catalog_t *cat);
+
+/*!
+ * \brief Start a temporary RW transaction in the catalog.
+ *
+ * \param cat   Catalog in question.
+ *
+ * \return KNOT_E*
+ */
+int catalog_begin(catalog_t *cat);
+
+/*!
+ * \brief End using the temporary RW txn, refresh the persistent RO txn.
+ *
+ * \param cat   Catalog in question.
+ *
+ * \return KNOT_E*
+ */
+int catalog_commit(catalog_t *cat);
+
+/*!
+ * \brief Abort temporary RW txn.
+ */
+void catalog_abort(catalog_t *cat);
+
+/*!
+ * \brief Free up old txns.
+ *
+ * \note This must be called after catalog_commit() with a delay of synchronnize_rcu().
+ *
+ * \param cat   Catalog.
+ */
+void catalog_commit_cleanup(catalog_t *cat);
+
+/*!
+ * \brief Close the catalog and de-init the structure.
+ *
+ * \param cat   Catalog to be closed.
+ *
+ * \return KNOT_E*
+ */
+int catalog_deinit(catalog_t *cat);
+
+/*!
+ * \brief Add a member zone to the catalog database.
+ *
+ * \param cat       Catalog to be augmented.
+ * \param member    Member zone name.
+ * \param owner     Owner of the PTR record in catalog zone, respective to the member zone.
+ * \param catzone   Name of the catalog zone whose it's the member.
+ *
+ * \return KNOT_E*
+ */
+int catalog_add(catalog_t *cat, const knot_dname_t *member,
+                const knot_dname_t *owner, const knot_dname_t *catzone);
+
+/*!
+ * \brief Delete a member zone from the catalog database.
+ *
+ * \param cat       Catalog to be removed from.
+ * \param member    Member zone to be removed.
+ *
+ * \return KNOT_E*
+ */
+int catalog_del(catalog_t *cat, const knot_dname_t *member);
+
+/*!
+ * \brief Find catz name of the catalog owning this member.
+ *
+ * \note This function may be called in multithreaded operation.
+ *
+ * \param cat       Catalog datatase.
+ * \param member    Member to search for.
+ * \param catz      Out: name of catalog zone it resides in.
+ * \param tofree    Out: a pointer that has to be freed later.
+ *
+ * \return KNOT_E*
+ */
+int catalog_get_catz(catalog_t *cat, const knot_dname_t *member,
+                     const knot_dname_t **catz, void **tofree);
+
+/*!
+ * \brief Check if this member exists in any catalog zone.
+ */
+bool catalog_has_member(catalog_t *cat, const knot_dname_t *member);
+
+/*!
+ * \brief Check if exactly this record (member, owner, catz) is in catalog DB.
+ */
+bool catalog_contains_exact(catalog_t *cat, const knot_dname_t *member,
+                            const knot_dname_t *owner, const knot_dname_t *catz);
+
+typedef int (*catalog_apply_cb_t)(const knot_dname_t *member, const knot_dname_t *owner,
+                                  const knot_dname_t *catz, void *ctx);
+/*!
+ * \brief Iterate through catalog database, applying callback.
+ *
+ * \param cat          Catalog to be iterated.
+ * \param for_member   (Optional) Iterate only on records for this member name.
+ * \param cb           Callback to be called.
+ * \param ctx          Context for this callback.
+ * \param rw           Use read-write transaction.
+ *
+ * \return KNOT_E*
+ */
+int catalog_apply(catalog_t *cat, const knot_dname_t *for_member,
+                  catalog_apply_cb_t cb, void *ctx, bool rw);
+
+/*!
+ * \brief Copy records from one catalog database to other.
+ *
+ * \param from           Catalog DB to copy from.
+ * \param to             Catalog DB to copy to.
+ * \param cat_only       Optional: copy only records for this catalog zone.
+ * \param read_rw_txn    Use RW txn for read operations.
+ *
+ * \return KNOT_E*
+ */
+int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to,
+                 const knot_dname_t *zone_only, bool read_rw_txn);
+
+/*!
+ * \brief Print to stdout whole contents of catalog database (for human).
+ *
+ * \param cat   Catalog database to be printed.
+ */
+void catalog_print(catalog_t *cat);
diff --git a/src/knot/catalog/catalog_update.c b/src/knot/catalog/catalog_update.c
new file mode 100644
index 0000000000000000000000000000000000000000..5e46ddbb64079a3ceab36bc2ac32c6fc446ce467
--- /dev/null
+++ b/src/knot/catalog/catalog_update.c
@@ -0,0 +1,335 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <string.h>
+
+#include "knot/catalog/catalog_update.h"
+#include "knot/common/log.h"
+#include "knot/conf/base.h"
+#include "contrib/macros.h"
+
+int catalog_update_init(catalog_update_t *u)
+{
+	u->upd = trie_create(NULL);
+	if (u->upd == NULL) {
+		return KNOT_ENOMEM;
+	}
+	pthread_mutex_init(&u->mutex, 0);
+	u->error = KNOT_EOK;
+	return KNOT_EOK;
+}
+
+catalog_update_t *catalog_update_new(void)
+{
+	catalog_update_t *u = calloc(1, sizeof(*u));
+	if (u != NULL) {
+		int ret = catalog_update_init(u);
+		if (ret != KNOT_EOK) {
+			free(u);
+			u = NULL;
+		}
+	}
+	return u;
+}
+
+static void catalog_upd_val_free(catalog_upd_val_t *val)
+{
+	free(val->add_owner);
+	free(val->rem_owner);
+	free(val);
+}
+
+static int freecb(trie_val_t *tval, void *unused)
+{
+	UNUSED(unused);
+	catalog_upd_val_t *val = *tval;
+	if (val != NULL) {
+		catalog_upd_val_free(val);
+	}
+	return 0;
+}
+
+void catalog_update_clear(catalog_update_t *u)
+{
+	trie_apply(u->upd, freecb, NULL);
+	trie_clear(u->upd);
+	u->error = KNOT_EOK;
+}
+
+void catalog_update_deinit(catalog_update_t *u)
+{
+	pthread_mutex_destroy(&u->mutex);
+	trie_free(u->upd);
+}
+
+void catalog_update_free(catalog_update_t *u)
+{
+	if (u != NULL) {
+		catalog_update_deinit(u);
+		free(u);
+	}
+}
+
+static catalog_upd_val_t *upd_val_new(const knot_dname_t *member, int bail,
+                                      const knot_dname_t *owner, bool rem)
+{
+	size_t member_size = knot_dname_size(member);
+	size_t owner_size = knot_dname_size(owner);
+	assert(bail <= (int)owner_size);
+
+	catalog_upd_val_t *val = malloc(sizeof(*val) + member_size);
+	if (val == NULL) {
+		return NULL;
+	}
+	val->member = (knot_dname_t *)(val + 1);
+	memcpy(val->member, member, member_size);
+	knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
+	if (owner_cpy == NULL) {
+		free(val);
+		return NULL;
+	}
+	if (rem) {
+		val->type = CAT_UPD_REM;
+		val->add_owner = NULL;
+		val->add_catz = NULL;
+		val->rem_owner = owner_cpy;
+		val->rem_catz = owner_cpy + bail;
+	} else {
+		val->type = CAT_UPD_ADD;
+		val->add_owner = owner_cpy;
+		val->add_catz = owner_cpy + bail;
+		val->rem_owner = NULL;
+		val->rem_catz = NULL;
+	}
+	return val;
+}
+
+static const knot_dname_t *get_uniq(const knot_dname_t *ptr_owner,
+                                    const knot_dname_t *catz)
+{
+	int labels = knot_dname_labels(ptr_owner, NULL);
+	labels -= knot_dname_labels(catz, NULL);
+	assert(labels >= 2);
+	return ptr_owner + knot_dname_prefixlen(ptr_owner, labels - 2, NULL);
+}
+
+static bool same_uniq(const knot_dname_t *owner1, const knot_dname_t *catz1,
+                      const knot_dname_t *owner2, const knot_dname_t *catz2)
+{
+	const knot_dname_t *uniq1 = get_uniq(owner1, catz1), *uniq2 = get_uniq(owner2, catz2);
+	if (*uniq1 != *uniq2) {
+		return false;
+	}
+	return memcmp(uniq1 + 1, uniq2 + 1, *uniq1) == 0;
+}
+
+static int upd_val_update(catalog_upd_val_t *val, int bail,
+                          const knot_dname_t *owner, bool rem)
+{
+	if ((rem  && val->type != CAT_UPD_ADD) ||
+	    (!rem && val->type != CAT_UPD_REM)) {
+		return KNOT_EEXIST;
+	}
+	knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
+	if (owner_cpy == NULL) {
+		return KNOT_ENOMEM;
+	}
+	if (rem) {
+		val->rem_owner = owner_cpy;
+		val->rem_catz = owner_cpy + bail;
+	} else {
+		val->add_owner = owner_cpy;
+		val->add_catz = owner_cpy + bail;
+	}
+	if (same_uniq(val->rem_owner, val->rem_catz, val->add_owner, val->add_catz)) {
+		val->type = CAT_UPD_MINOR;
+	} else {
+		val->type = CAT_UPD_UNIQ;
+	}
+	return KNOT_EOK;
+}
+
+int catalog_update_add(catalog_update_t *u, const knot_dname_t *member,
+                       const knot_dname_t *owner, const knot_dname_t *catzone,
+                       bool remove, catalog_t *check_rem)
+{
+	if (remove && check_rem != NULL &&
+	    !catalog_contains_exact(check_rem, member, owner, catzone)) {
+		return KNOT_EOK;
+		// we need to perform this check immediately because
+		// garbage removal would block legitimate removal
+	}
+
+	int bail = catalog_bailiwick_shift(owner, catzone);
+	if (bail < 0) {
+		return KNOT_EOUTOFZONE;
+	}
+	assert(bail >= 0 && bail <= KNOT_DNAME_MAXLEN);
+
+	knot_dname_storage_t lf_storage;
+	uint8_t *lf = knot_dname_lf(member, lf_storage);
+
+	trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]);
+	if (found != NULL) {
+		catalog_upd_val_t *val = *found;
+		assert(knot_dname_is_equal(val->member, member));
+		return upd_val_update(val, bail, owner, remove);
+	}
+
+	catalog_upd_val_t *val = upd_val_new(member, bail, owner, remove);
+	if (val == NULL) {
+		return KNOT_ENOMEM;
+	}
+	trie_val_t *added = trie_get_ins(u->upd, lf + 1, lf[0]);
+	if (added == NULL) {
+		catalog_upd_val_free(val);
+		return KNOT_ENOMEM;
+	}
+	assert(*added == NULL);
+	*added = val;
+	return KNOT_EOK;
+}
+
+catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member)
+{
+	knot_dname_storage_t lf_storage;
+	uint8_t *lf = knot_dname_lf(member, lf_storage);
+
+	trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]);
+	return found == NULL ? NULL : *(catalog_upd_val_t **)found;
+}
+
+static bool check_member(catalog_upd_val_t *val, conf_t *conf, catalog_t *cat)
+{
+	if (val->type == CAT_UPD_REM || val->type == CAT_UPD_INVALID) {
+		return true;
+	}
+	if (!conf_rawid_exists(conf, C_ZONE, val->add_catz, knot_dname_size(val->add_catz))) {
+		knot_dname_txt_storage_t cat_str;
+		(void)knot_dname_to_str(cat_str, val->add_catz, sizeof(cat_str));
+		log_zone_error(val->member, "catalog template zone '%s' not configured, ignoring", cat_str);
+		return false;
+	}
+	if (conf_rawid_exists(conf, C_ZONE, val->member, knot_dname_size(val->member))) {
+		log_zone_error(val->member, "member zone already configured, ignoring");
+		return false;
+	}
+	if (val->type == CAT_UPD_ADD && catalog_has_member(cat, val->member)) {
+		log_zone_error(val->member, "member zone already configured by catalog, ignoring");
+		return false;
+	}
+	return true;
+}
+
+static int rem_conf_conflict(const knot_dname_t *mem, const knot_dname_t *ow,
+                             const knot_dname_t *cz, void *ctx)
+{
+	if (conf_rawid_exists(conf(), C_ZONE, mem, knot_dname_size(mem))) {
+		return catalog_update_add(ctx, mem, ow, cz, true, NULL);
+	}
+	return KNOT_EOK;
+}
+
+void catalog_update_finalize(catalog_update_t *u, catalog_t *cat)
+{
+	conf_t *cnf = conf();
+
+	catalog_it_t *it = catalog_it_begin(u);
+	while (!catalog_it_finished(it)) {
+		catalog_upd_val_t *val = catalog_it_val(it);
+		if (!check_member(val, cnf, cat)) {
+			val->type = (val->type == CAT_UPD_ADD ? CAT_UPD_INVALID : CAT_UPD_REM);
+		}
+		catalog_it_next(it);
+	}
+	catalog_it_free(it);
+
+	// TODO this takes time. Is it really useful?
+	// it checks if the configuration file has not
+	// changed in the way to create confict with
+	// existing member zone and let config take precendence
+	if (cat->ro_txn != NULL) {
+		(void)catalog_apply(cat, NULL, rem_conf_conflict, u, false);
+	}
+}
+
+int catalog_update_commit(catalog_update_t *u, catalog_t *cat)
+{
+	catalog_it_t *it = catalog_it_begin(u);
+	if (catalog_it_finished(it)) {
+		catalog_it_free(it);
+		return KNOT_EOK;
+	}
+	int ret = catalog_begin(cat);
+	while (!catalog_it_finished(it) && ret == KNOT_EOK) {
+		catalog_upd_val_t *val = catalog_it_val(it);
+		switch (val->type) {
+		case CAT_UPD_ADD:
+		case CAT_UPD_MINOR: // catalog_add will simply update/overwrite existing data
+		case CAT_UPD_UNIQ:
+			ret = catalog_add(cat, val->member, val->add_owner, val->add_catz);
+			break;
+		case CAT_UPD_REM:
+			ret = catalog_del(cat, val->member);
+			break;
+		case CAT_UPD_INVALID:
+			break; // no action
+		default:
+			assert(0);
+			ret = KNOT_ERROR;
+		}
+		catalog_it_next(it);
+	}
+	catalog_it_free(it);
+	if (ret == KNOT_EOK) {
+		ret = catalog_commit(cat);
+	} else {
+		catalog_abort(cat);
+	}
+	return ret;
+}
+
+typedef struct {
+	const knot_dname_t *zone;
+	catalog_update_t *u;
+} del_all_ctx_t;
+
+static int del_all_cb(const knot_dname_t *member, const knot_dname_t *owner,
+                      const knot_dname_t *catz, void *dactx)
+{
+	del_all_ctx_t *ctx = dactx;
+	if (knot_dname_is_equal(catz, ctx->zone)) {
+		// TODO possible speedup by indexing which member zones belong to a catalog zone
+		return catalog_update_add(ctx->u, member, owner, catz, true, NULL);
+	} else {
+		return KNOT_EOK;
+	}
+}
+
+int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone)
+{
+	int ret = catalog_open(cat);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	pthread_mutex_lock(&u->mutex);
+	del_all_ctx_t ctx = { zone, u };
+	ret = catalog_apply(cat, NULL, del_all_cb, &ctx, false);
+	pthread_mutex_unlock(&u->mutex);
+	return ret;
+}
diff --git a/src/knot/catalog/catalog_update.h b/src/knot/catalog/catalog_update.h
new file mode 100644
index 0000000000000000000000000000000000000000..0835a7496561db2dddde42fd8cda0bca5307d7b4
--- /dev/null
+++ b/src/knot/catalog/catalog_update.h
@@ -0,0 +1,151 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "contrib/qp-trie/trie.h"
+#include "knot/catalog/catalog_db.h"
+
+typedef enum {
+	CAT_UPD_INVALID,   // invalid value
+	CAT_UPD_ADD,       // member addition
+	CAT_UPD_REM,       // member removal
+	CAT_UPD_MINOR,     // owner or catzone change, uniqID preserved
+	CAT_UPD_UNIQ,      // uniqID change
+	CAT_UPD_MAX,       // number of options in ths enum
+} catalog_upd_type_t;
+
+typedef struct catalog_upd_val {
+	knot_dname_t *member;     // name of catalog member zone
+	catalog_upd_type_t type;  // what kind of update this is
+
+	knot_dname_t *rem_owner;  // owner of PTR record being removed
+	knot_dname_t *rem_catz;   // catalog zone the member being removed from
+	knot_dname_t *add_owner;  // owner of PTR record being added
+	knot_dname_t *add_catz;   // catalog zone the member being added to
+} catalog_upd_val_t;
+
+typedef struct {
+	trie_t *upd;             // tree of catalog_upd_val_t, that gonna be changed in catalog
+	int error;               // error occured during generating of upd
+	pthread_mutex_t mutex;   // lock for accessing this struct
+} catalog_update_t;
+
+/*!
+ * \brief Initialize catalog update structure.
+ *
+ * \param u   Catalog update to be initialized.
+ *
+ * \return KNOT_EOK, KNOT_ENOMEM
+ */
+int catalog_update_init(catalog_update_t *u);
+catalog_update_t *catalog_update_new(void);
+
+/*!
+ * \brief Clear contents of catalog update structure.
+ *
+ * \param u   Catalog update structure to be cleared.
+ */
+void catalog_update_clear(catalog_update_t *u);
+
+/*!
+ * \brief Free catalog update structure.
+ *
+ * \param u   Catalog update structure.
+ */
+void catalog_update_deinit(catalog_update_t *u);
+void catalog_update_free(catalog_update_t *u);
+
+/*!
+ * \brief Add a new record to catalog update structure.
+ *
+ * \param u         Catalog update.
+ * \param member    Member zone name to be added.
+ * \param owner     Owner of respective PTR record.
+ * \param catzone   Catalog zone holding the member.
+ * \param remove    Add a removal of such record.
+ * \param check_rem Check catalog DB for existing record to be removed.
+ *
+ * \return KNOT_E*
+ */
+int catalog_update_add(catalog_update_t *u, const knot_dname_t *member,
+                       const knot_dname_t *owner, const knot_dname_t *catzone,
+                       bool remove, catalog_t *check_rem);
+
+/*!
+ * \brief Read catalog update record for given member zone.
+ *
+ * \param u          Catalog update.
+ * \param member     Member zone name.
+ * \param remove     Search in remove section.
+ *
+ * \return Found update record for given member zone; or NULL.
+ */
+catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member);
+
+/*!
+ * \brief Catalog update iteration.
+ */
+typedef trie_it_t catalog_it_t;
+
+inline static catalog_it_t *catalog_it_begin(catalog_update_t *u)
+{
+	return trie_it_begin(u->upd);
+}
+
+inline static catalog_upd_val_t *catalog_it_val(catalog_it_t *it)
+{
+	return *(catalog_upd_val_t **)trie_it_val(it);
+}
+
+inline static bool catalog_it_finished(catalog_it_t *it)
+{
+	return it == NULL || trie_it_finished(it);
+}
+
+#define catalog_it_next trie_it_next
+#define catalog_it_free trie_it_free
+
+/*!
+ * \brief CHeck and align Catalog update to avoid conflicts with conf or other catalogs.
+ *
+ * \param u      Catalog update to be aligned in-place.
+ * \param cat    Catalog DB to check against.
+ *
+ * \note Also accesses conf().
+ */
+void catalog_update_finalize(catalog_update_t *u, catalog_t *cat);
+
+/*!
+ * \brief Put changes from Catalog Update into persistent Catalog database.
+ *
+ * \param u      Catalog update to be commited.
+ * \param cat    Catalog to be updated.
+ *
+ * \return KNOT_E*
+ */
+int catalog_update_commit(catalog_update_t *u, catalog_t *cat);
+
+/*!
+ * \brief Add to catalog update removals of all member zones of a single catalog zone.
+ *
+ * \param u      Catalog update to be updated.
+ * \param cat    Catalog database to be iterated.
+ * \param zone   Name of catalog zone whose members gonna be removed.
+ *
+ * \return KNOT_E*
+ */
+int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone);
diff --git a/src/knot/catalog/generate.c b/src/knot/catalog/generate.c
new file mode 100644
index 0000000000000000000000000000000000000000..05230e3ef6e3c853258eae1983526e2ec098efa6
--- /dev/null
+++ b/src/knot/catalog/generate.c
@@ -0,0 +1,245 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "knot/catalog/generate.h"
+#include "knot/common/log.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/zonedb.h"
+#include "contrib/openbsd/siphash.h"
+#include "contrib/wire_ctx.h"
+
+static knot_dname_t *catalog_member_owner(const knot_dname_t *member,
+                                          const knot_dname_t *catzone,
+                                          time_t member_time)
+{
+	SIPHASH_CTX hash;
+	SIPHASH_KEY shkey = { 0 }; // only used for hashing -> zero key
+	SipHash24_Init(&hash, &shkey);
+	SipHash24_Update(&hash, member, knot_dname_size(member));
+	uint64_t u64time = htobe64(member_time);
+	SipHash24_Update(&hash, &u64time, sizeof(u64time));
+	uint64_t hashres = SipHash24_End(&hash);
+
+	char *hexhash = bin_to_hex((uint8_t *)&hashres, sizeof(hashres));
+	if (hexhash == NULL) {
+		return NULL;
+	}
+	size_t hexlen = strlen(hexhash);
+	assert(hexlen == 16);
+	size_t zoneslen = knot_dname_size((uint8_t *)CATALOG_ZONES_LABEL);
+	assert(hexlen <= KNOT_DNAME_MAXLABELLEN && zoneslen <= KNOT_DNAME_MAXLABELLEN);
+	size_t catzlen = knot_dname_size(catzone);
+
+	size_t outlen = hexlen + zoneslen + catzlen;
+	knot_dname_t *out;
+	if (outlen > KNOT_DNAME_MAXLEN || (out = malloc(outlen)) == NULL) {
+		free(hexhash);
+		return NULL;
+	}
+
+	wire_ctx_t wire = wire_ctx_init(out, outlen);
+	wire_ctx_write_u8(&wire, hexlen);
+	wire_ctx_write(&wire, hexhash, hexlen);
+	wire_ctx_write(&wire, CATALOG_ZONES_LABEL, zoneslen);
+	wire_ctx_skip(&wire, -1);
+	wire_ctx_write(&wire, catzone, catzlen);
+	assert(wire.error == KNOT_EOK);
+
+	free(hexhash);
+	return out;
+}
+
+void catalogs_generate(struct knot_zonedb *db_new, struct knot_zonedb *db_old)
+{
+	// general comment: catz->contents!=NULL means incremental update of catalog
+
+	if (db_old != NULL) {
+		knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old);
+		while (!knot_zonedb_iter_finished(it)) {
+			zone_t *zone = knot_zonedb_iter_val(it);
+			knot_dname_t *cg = zone->catalog_gen;
+			if (cg != NULL && knot_zonedb_find(db_new, zone->name) == NULL) {
+				zone_t *catz = knot_zonedb_find(db_new, cg);
+				if (catz != NULL && catz->contents != NULL) {
+					assert(catz->cat_members != NULL); // if this failed to allocate, catz wasn't added to zonedb
+					knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member);
+					if (owner == NULL) {
+						catz->cat_members->error = KNOT_ENOENT;
+						knot_zonedb_iter_next(it);
+						continue;
+					}
+					int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, true, NULL);
+					free(owner);
+					if (ret != KNOT_EOK) {
+						catz->cat_members->error = ret;
+					}
+				}
+			}
+			knot_zonedb_iter_next(it);
+		}
+		knot_zonedb_iter_free(it);
+	}
+
+	knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_new);
+	while (!knot_zonedb_iter_finished(it)) {
+		zone_t *zone = knot_zonedb_iter_val(it);
+		knot_dname_t *cg = zone->catalog_gen;
+		zone_t *catz = cg != NULL ? knot_zonedb_find(db_new, cg) : NULL;
+		if (cg != NULL && catz == NULL) {
+			log_zone_warning(zone->name, "member zone belongs to non-existing catalog zone");
+			continue;
+		}
+		if (cg != NULL && (catz->contents == NULL || knot_zonedb_find(db_old, zone->name) == NULL)) {
+			assert(catz->cat_members != NULL);
+			knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member);
+			if (owner == NULL) {
+				catz->cat_members->error = KNOT_ENOENT;
+				knot_zonedb_iter_next(it);
+				continue;
+			}
+			int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, false, NULL);
+			free(owner);
+			if (ret != KNOT_EOK) {
+				catz->cat_members->error = ret;
+			}
+		}
+		knot_zonedb_iter_next(it);
+	}
+	knot_zonedb_iter_free(it);
+}
+
+static void set_rdata(knot_rrset_t *rrset, uint8_t *data, uint16_t len)
+{
+	knot_rdata_init(rrset->rrs.rdata, len, data);
+	rrset->rrs.size = knot_rdata_size(len);
+}
+
+struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone,
+                                             uint32_t soa_serial)
+{
+	if (u->error != KNOT_EOK) {
+		return NULL;
+	}
+	zone_contents_t *c = zone_contents_new(catzone, true);
+	if (c == NULL) {
+		return c;
+	}
+
+	zone_node_t *unused = NULL;
+	uint8_t invalid[9] = "\x07""invalid";
+	uint8_t version[9] = "\x07""version";
+	uint8_t cat_version[2] = "\x01" CATALOG_ZONE_VERSION;
+
+	// prepare common rrset with one rdata item
+	uint8_t rdata[256] = { 0 };
+	knot_rrset_t rrset;
+	knot_rrset_init(&rrset, (knot_dname_t *)catzone, KNOT_RRTYPE_SOA, KNOT_CLASS_IN, 0);
+	rrset.rrs.rdata = (knot_rdata_t *)rdata;
+	rrset.rrs.count = 1;
+
+	// set catalog zone's SOA
+	uint8_t data[250];
+	assert(sizeof(knot_rdata_t) + sizeof(data) <= sizeof(rdata));
+	wire_ctx_t wire = wire_ctx_init(data, sizeof(data));
+	wire_ctx_write(&wire, invalid, sizeof(invalid));
+	wire_ctx_write(&wire, invalid, sizeof(invalid));
+	wire_ctx_write_u32(&wire, soa_serial);
+	wire_ctx_write_u32(&wire, CATALOG_SOA_REFRESH);
+	wire_ctx_write_u32(&wire, CATALOG_SOA_RETRY);
+	wire_ctx_write_u32(&wire, CATALOG_SOA_EXPIRE);
+	wire_ctx_write_u32(&wire, 0);
+	set_rdata(&rrset, data, wire_ctx_offset(&wire));
+	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
+		goto fail;
+	}
+
+	// set catalog zone's NS
+	unused = NULL;
+	rrset.type = KNOT_RRTYPE_NS;
+	set_rdata(&rrset, invalid, sizeof(invalid));
+	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
+		goto fail;
+	}
+
+	// set catalog zone's version TXT
+	unused = NULL;
+	knot_dname_storage_t owner;
+	if (knot_dname_store(owner, version) == 0 || catalog_dname_append(owner, catzone) == 0) {
+		goto fail;
+	}
+	rrset.owner = owner;
+	rrset.type = KNOT_RRTYPE_TXT;
+	set_rdata(&rrset, cat_version, sizeof(cat_version));
+	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
+		goto fail;
+	}
+
+	// insert member zone PTR records
+	rrset.type = KNOT_RRTYPE_PTR;
+	catalog_it_t *it = catalog_it_begin(u);
+	while (!catalog_it_finished(it)) {
+		catalog_upd_val_t *val = catalog_it_val(it);
+		if (val->add_owner == NULL) {
+			continue;
+		}
+		rrset.owner = val->add_owner;
+		set_rdata(&rrset, val->member, knot_dname_size(val->member));
+		unused = NULL;
+		if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
+			catalog_it_free(it);
+			goto fail;
+		}
+		catalog_it_next(it);
+	}
+	catalog_it_free(it);
+
+	return c;
+fail:
+	zone_contents_deep_free(c);
+	return NULL;
+}
+
+int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu)
+{
+	knot_rrset_t ptr;
+	knot_rrset_init(&ptr, NULL, KNOT_RRTYPE_PTR, KNOT_CLASS_IN, 0);
+	uint8_t tmp[KNOT_DNAME_MAXLEN + sizeof(knot_rdata_t)];
+	ptr.rrs.rdata = (knot_rdata_t *)tmp;
+	ptr.rrs.count = 1;
+
+	int ret = u->error;
+	catalog_it_t *it = catalog_it_begin(u);
+	while (!catalog_it_finished(it) && ret == KNOT_EOK) {
+		catalog_upd_val_t *val = catalog_it_val(it);
+		if (val->type == CAT_UPD_INVALID) {
+			continue;
+		}
+		set_rdata(&ptr, val->member, knot_dname_size(val->member));
+		if (val->type != CAT_UPD_ADD && knot_dname_is_equal(zu->zone->name, val->rem_catz)) {
+			ptr.owner = val->rem_owner;
+			ret = zone_update_remove(zu, &ptr);
+		}
+		if (val->type != CAT_UPD_REM && knot_dname_is_equal(zu->zone->name, val->add_catz)) {
+			ptr.owner = val->add_owner;
+			ret = zone_update_add(zu, &ptr);
+		}
+		catalog_it_next(it);
+	}
+	catalog_it_free(it);
+	return ret;
+}
diff --git a/src/knot/catalog/generate.h b/src/knot/catalog/generate.h
new file mode 100644
index 0000000000000000000000000000000000000000..721c1efa03b72e544c84d47651a88edd3b402d61
--- /dev/null
+++ b/src/knot/catalog/generate.h
@@ -0,0 +1,56 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/catalog/catalog_update.h"
+
+#define CATALOG_SOA_REFRESH	3600
+#define CATALOG_SOA_RETRY	600
+#define CATALOG_SOA_EXPIRE	(INT32_MAX - 1)
+
+struct knot_zonedb;
+
+/*!
+ * \brief Compare old and new zonedb, create incremental catalog upd in each catz->cat_members
+ */
+void catalogs_generate(struct knot_zonedb *db_new, struct knot_zonedb *db_old);
+
+struct zone_contents;
+
+/*!
+ * \brief Generate catalog zone contents from (full) catalog update.
+ *
+ * \param u           Catalog update to read.
+ * \param catzone     Catalog zone name.
+ * \param soa_serial  SOA serial of the generated zone.
+ *
+ * \return Catalog zone contents, or NULL if ENOMEM.
+ */
+struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone,
+                                             uint32_t soa_serial);
+
+struct zone_update;
+
+/*!
+ * \brief Incrementally update catalog zone from catalog update.
+ *
+ * \param u    Catalog update to read.
+ * \param zu   Zone update to be updated.
+ *
+ * \return KNOT_E*
+ */
+int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu);
diff --git a/src/knot/catalog/interpret.c b/src/knot/catalog/interpret.c
new file mode 100644
index 0000000000000000000000000000000000000000..fa45bbe2145e737cdf6469cb6fd5ac2c7dee5598
--- /dev/null
+++ b/src/knot/catalog/interpret.c
@@ -0,0 +1,92 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <pthread.h>
+
+#include "knot/catalog/interpret.h"
+#include "knot/zone/contents.h"
+
+typedef struct {
+	catalog_update_t *u;
+	const knot_dname_t *apex;
+	bool remove;
+	catalog_t *check;
+} cat_upd_ctx_t;
+
+static bool check_zone_version(const zone_contents_t *zone)
+{
+	size_t zone_size = knot_dname_size(zone->apex->owner);
+	knot_dname_t sub[zone_size + 8];
+	memcpy(sub, "\x07""version", 8);
+	memcpy(sub + 8, zone->apex->owner, zone_size);
+
+	const zone_node_t *ver_node = zone_contents_find_node(zone, sub);
+	knot_rdataset_t *ver_rr = node_rdataset(ver_node, KNOT_RRTYPE_TXT);
+	if (ver_rr == NULL) {
+		return false;
+	}
+
+	knot_rdata_t *rdata = ver_rr->rdata;
+	for (int i = 0; i < ver_rr->count; i++) {
+		if (rdata->len == 2 && rdata->data[1] == CATALOG_ZONE_VERSION[0]) {
+			return true;
+		}
+		rdata = knot_rdataset_next(rdata);
+	}
+	return false;
+}
+
+static int cat_update_add_node(zone_node_t *node, void *data)
+{
+	cat_upd_ctx_t *ctx = data;
+	const knot_rdataset_t *ptr = node_rdataset(node, KNOT_RRTYPE_PTR);
+	if (ptr == NULL || ptr->count == 0) {
+		return KNOT_EOK;
+	}
+	knot_rdata_t *rdata = ptr->rdata;
+	int ret = KNOT_EOK;
+	for (int i = 0; ret == KNOT_EOK && i < ptr->count; i++) {
+		const knot_dname_t *member = knot_ptr_name(rdata);
+		ret = catalog_update_add(ctx->u, member, node->owner, ctx->apex,
+		                         ctx->remove, ctx->check);
+		rdata = knot_rdataset_next(rdata);
+	}
+	return ret;
+}
+
+int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone,
+                             bool remove, bool check_ver, catalog_t *check)
+{
+	if (check_ver && !check_zone_version(zone)) {
+		return KNOT_EZONEINVAL;
+	}
+
+	knot_dname_storage_t sub;
+	if (knot_dname_store(sub, (uint8_t *)CATALOG_ZONES_LABEL) == 0 ||
+	    catalog_dname_append(sub, zone->apex->owner) == 0) {
+		return KNOT_EINVAL;
+	}
+
+	if (zone_contents_find_node(zone, sub) == NULL) {
+		return KNOT_EOK;
+	}
+
+	cat_upd_ctx_t ctx = { u, zone->apex->owner, remove, check };
+	pthread_mutex_lock(&u->mutex);
+	int ret = zone_tree_sub_apply(zone->nodes, sub, false, cat_update_add_node, &ctx);
+	pthread_mutex_unlock(&u->mutex);
+	return ret;
+}
diff --git a/src/knot/catalog/interpret.h b/src/knot/catalog/interpret.h
new file mode 100644
index 0000000000000000000000000000000000000000..851c6cf85e5b076f7878406b9a60334eb99a974a
--- /dev/null
+++ b/src/knot/catalog/interpret.h
@@ -0,0 +1,36 @@
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program 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.
+
+    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/catalog/catalog_update.h"
+
+struct zone_contents;
+
+/*!
+ * \brief Iterate over PTR records in given zone contents and add members to catalog update.
+ *
+ * \param u            Catalog update to be updated.
+ * \param zone         Zone contents to be searched for member PTR records.
+ * \param remove       Add removals of found member zones.
+ * \param check_ver    Do check catalog zone version record first.
+ * \param check        Optional: existing catalog database to be checked for existence
+ *                     of such record (useful for removals).
+ *
+ * \return KNOT_E*
+ */
+int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone,
+                             bool remove, bool check_ver, catalog_t *check);
diff --git a/src/knot/conf/conf.c b/src/knot/conf/conf.c
index 8e200f8697c117d3de1402295ad41875f83c5974..c9007bb7d0126f2622d34c69f56523c15f56f6e2 100644
--- a/src/knot/conf/conf.c
+++ b/src/knot/conf/conf.c
@@ -23,9 +23,9 @@
 
 #include "knot/conf/base.h"
 #include "knot/conf/confdb.h"
+#include "knot/catalog/catalog_db.h"
 #include "knot/common/log.h"
 #include "knot/server/dthreads.h"
-#include "knot/zone/catalog.h"
 #include "libknot/libknot.h"
 #include "libknot/yparser/yptrafo.h"
 #include "contrib/macros.h"
@@ -218,11 +218,13 @@ conf_val_t conf_zone_get_txn(
 
 	// Check if this is a catalog member zone.
 	if (conf->catalog != NULL) {
-		knot_dname_storage_t catalog;
-		int ret = catalog_get_zone_threadsafe(conf->catalog, dname, catalog);
+		void *tofree = NULL;
+		const knot_dname_t *catalog;
+		int ret = catalog_get_catz(conf->catalog, dname, &catalog, &tofree);
 		if (ret == KNOT_EOK) {
 			conf_db_get(conf, txn, C_ZONE, C_CATALOG_TPL, catalog,
 			            knot_dname_size(catalog), &val);
+			free(tofree);
 			if (val.code != KNOT_EOK) {
 				CONF_LOG_ZONE(LOG_ERR, catalog,
 				              "catalog zone has no catalog template (%s)",
diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c
index 2dcff11bed72b0b2ea5458d64d0be171f415381d..3d37a4fb5c0502ce23f05a449ef63339eeb248a0 100644
--- a/src/knot/events/handlers/load.c
+++ b/src/knot/events/handlers/load.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -16,6 +16,7 @@
 
 #include <assert.h>
 
+#include "knot/catalog/generate.h"
 #include "knot/common/log.h"
 #include "knot/conf/conf.h"
 #include "knot/dnssec/key-events.h"
diff --git a/src/knot/server/server.h b/src/knot/server/server.h
index 56dd3cd383e3df4eba4dbec052745c6dd45dabcd..0c14e67e1e3d7d540034111d9b0c1720c4f6b354 100644
--- a/src/knot/server/server.h
+++ b/src/knot/server/server.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -17,13 +17,13 @@
 #pragma once
 
 #include "knot/conf/conf.h"
+#include "knot/catalog/catalog_update.h"
 #include "knot/common/evsched.h"
 #include "knot/common/fdset.h"
 #include "knot/journal/knot_lmdb.h"
 #include "knot/server/dthreads.h"
 #include "knot/worker/pool.h"
 #include "knot/zone/backup.h"
-#include "knot/zone/catalog.h"
 #include "knot/zone/zonedb.h"
 
 struct server;
diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c
index 5cee6e4bb209eec7f118a7e689a4f25fb87c1cc7..e1b6a2c5f4017f07834990a571a6ad56ad6417c7 100644
--- a/src/knot/updates/zone-update.c
+++ b/src/knot/updates/zone-update.c
@@ -14,12 +14,12 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
+#include "knot/catalog/interpret.h"
 #include "knot/common/log.h"
 #include "knot/dnssec/zone-events.h"
 #include "knot/updates/zone-update.h"
 #include "knot/zone/adds_tree.h"
 #include "knot/zone/adjust.h"
-#include "knot/zone/catalog.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/zone-diff.h"
 #include "knot/zone/zonefile.h"
diff --git a/src/knot/zone/backup.c b/src/knot/zone/backup.c
index 1e85f52e6f76516e8936f4fa3b77dd2d7cd98442..ef34547607124f49daafafbc44f2b231abe1893a 100644
--- a/src/knot/zone/backup.c
+++ b/src/knot/zone/backup.c
@@ -25,11 +25,11 @@
 
 #include "contrib/files.h"
 #include "knot/zone/backup.h"
+#include "knot/catalog/catalog_db.h"
 #include "knot/common/log.h"
 #include "knot/dnssec/kasp/kasp_zone.h"
 #include "knot/dnssec/kasp/keystore.h"
 #include "knot/journal/journal_metadata.h"
-#include "knot/zone/catalog.h"
 #include "libdnssec/error.h"
 #include "contrib/files.h"
 #include "contrib/string.h"
diff --git a/src/knot/zone/catalog.c b/src/knot/zone/catalog.c
deleted file mode 100644
index 1b3589e1f8b8933fe90db08bd4d5d74ba29328fd..0000000000000000000000000000000000000000
--- a/src/knot/zone/catalog.c
+++ /dev/null
@@ -1,884 +0,0 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
-
-    This program 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.
-
-    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-#include "knot/zone/catalog.h"
-
-#include <assert.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <urcu.h>
-
-#include "contrib/openbsd/siphash.h"
-#include "contrib/string.h"
-#include "contrib/wire_ctx.h"
-
-#include "knot/common/log.h"
-#include "knot/conf/conf.h"
-#include "knot/updates/zone-update.h"
-
-#define CATALOG_VERSION "1.0"
-#define CATALOG_ZONE_VERSION "2" // must be just one char long
-#define CATALOG_ZONES_LABEL "\x05""zones"
-#define CATALOG_SOA_REFRESH 3600
-#define CATALOG_SOA_RETRY 600
-#define CATALOG_SOA_EXPIRE (INT32_MAX - 1)
-
-const MDB_val catalog_iter_prefix = { 1, "" };
-
-knot_dname_t *catalog_member_owner(const knot_dname_t *member,
-                                   const knot_dname_t *catzone,
-                                   time_t member_time)
-{
-	SIPHASH_CTX hash;
-	SIPHASH_KEY shkey = { 0 }; // only used for hashing -> zero key
-	SipHash24_Init(&hash, &shkey);
-	SipHash24_Update(&hash, member, knot_dname_size(member));
-	uint64_t u64time = htobe64(member_time);
-	SipHash24_Update(&hash, &u64time, sizeof(u64time));
-	uint64_t hashres = SipHash24_End(&hash);
-
-	char *hexhash = bin_to_hex((uint8_t *)&hashres, sizeof(hashres));
-	if (hexhash == NULL) {
-		return NULL;
-	}
-	size_t hexlen = strlen(hexhash);
-	assert(hexlen == 16);
-	size_t zoneslen = knot_dname_size((uint8_t *)CATALOG_ZONES_LABEL);
-	assert(hexlen <= KNOT_DNAME_MAXLABELLEN && zoneslen <= KNOT_DNAME_MAXLABELLEN);
-	size_t catzlen = knot_dname_size(catzone);
-
-	size_t outlen = hexlen + zoneslen + catzlen;
-	knot_dname_t *out;
-	if (outlen > KNOT_DNAME_MAXLEN || (out = malloc(outlen)) == NULL) {
-		free(hexhash);
-		return NULL;
-	}
-
-	wire_ctx_t wire = wire_ctx_init(out, outlen);
-	wire_ctx_write_u8(&wire, hexlen);
-	wire_ctx_write(&wire, hexhash, hexlen);
-	wire_ctx_write(&wire, CATALOG_ZONES_LABEL, zoneslen);
-	wire_ctx_skip(&wire, -1);
-	wire_ctx_write(&wire, catzone, catzlen);
-	assert(wire.error == KNOT_EOK);
-
-	free(hexhash);
-	return out;
-}
-
-static bool check_zone_version(const zone_contents_t *zone)
-{
-	size_t zone_size = knot_dname_size(zone->apex->owner);
-	knot_dname_t sub[zone_size + 8];
-	memcpy(sub, "\x07""version", 8);
-	memcpy(sub + 8, zone->apex->owner, zone_size);
-
-	const zone_node_t *ver_node = zone_contents_find_node(zone, sub);
-	knot_rdataset_t *ver_rr = node_rdataset(ver_node, KNOT_RRTYPE_TXT);
-	if (ver_rr == NULL) {
-		return false;
-	}
-
-	knot_rdata_t *rd = ver_rr->rdata;
-	for (int i = 0; i < ver_rr->count; i++) {
-		if (rd->len == 2 && rd->data[1] == CATALOG_ZONE_VERSION[0]) {
-			return true;
-		}
-		rd = knot_rdataset_next(rd);
-	}
-	return false;
-}
-
-void catalog_init(catalog_t *cat, const char *path, size_t mapsize)
-{
-	knot_lmdb_init(&cat->db, path, mapsize, MDB_NOTLS, NULL);
-}
-
-// does NOT check for catalog zone version by RFC, this is Knot-specific in the cat LMDB !
-static void check_cat_version(catalog_t *cat)
-{
-	if (cat->ro_txn->ret == KNOT_EOK) {
-		MDB_val key = { 8, "\x01version" };
-		if (knot_lmdb_find(cat->ro_txn, &key, KNOT_LMDB_EXACT)) {
-			if (strncmp(CATALOG_VERSION, cat->ro_txn->cur_val.mv_data,
-			            cat->ro_txn->cur_val.mv_size) != 0) {
-				log_warning("unmatching catalog version");
-			}
-		} else if (cat->rw_txn != NULL) {
-			MDB_val val = { strlen(CATALOG_VERSION), CATALOG_VERSION };
-			knot_lmdb_insert(cat->rw_txn, &key, &val);
-		}
-	}
-}
-
-int catalog_open(catalog_t *cat)
-{
-	if (!knot_lmdb_is_open(&cat->db)) {
-		int ret = knot_lmdb_open(&cat->db);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-	}
-	if (cat->ro_txn == NULL) {
-		knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn));
-		if (ro_txn == NULL) {
-			return KNOT_ENOMEM;
-		}
-		knot_lmdb_begin(&cat->db, ro_txn, false);
-		cat->ro_txn = ro_txn;
-	}
-	check_cat_version(cat);
-	return cat->ro_txn->ret;
-}
-
-int catalog_begin(catalog_t *cat)
-{
-	int ret = catalog_open(cat);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-	knot_lmdb_txn_t *rw_txn = calloc(1, sizeof(*rw_txn));
-	if (rw_txn == NULL) {
-		return KNOT_ENOMEM;
-	}
-	knot_lmdb_begin(&cat->db, rw_txn, true);
-	if (rw_txn->ret != KNOT_EOK) {
-		ret = rw_txn->ret;
-		free(rw_txn);
-		return ret;
-	}
-	assert(cat->rw_txn == NULL); // LMDB prevents two existing RW txns at a time
-	cat->rw_txn = rw_txn;
-	check_cat_version(cat);
-	return cat->rw_txn->ret;
-}
-
-int catalog_commit(catalog_t *cat)
-{
-	knot_lmdb_txn_t *rw_txn = rcu_xchg_pointer(&cat->rw_txn, NULL);
-	knot_lmdb_commit(rw_txn);
-	int ret = rw_txn->ret;
-	free(rw_txn);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	// now refresh RO txn
-	knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn));
-	if (ro_txn == NULL) {
-		return KNOT_ENOMEM;
-	}
-	knot_lmdb_begin(&cat->db, ro_txn, false);
-	cat->old_ro_txn = rcu_xchg_pointer(&cat->ro_txn, ro_txn);
-
-	return KNOT_EOK;
-}
-
-void catalog_commit_cleanup(catalog_t *cat)
-{
-	knot_lmdb_txn_t *old_ro_txn = rcu_xchg_pointer(&cat->old_ro_txn, NULL);
-	if (old_ro_txn != NULL) {
-		knot_lmdb_abort(old_ro_txn);
-		free(old_ro_txn);
-	}
-}
-
-int catalog_deinit(catalog_t *cat)
-{
-	assert(cat->rw_txn == NULL);
-	if (cat->ro_txn != NULL) {
-		knot_lmdb_abort(cat->ro_txn);
-		free(cat->ro_txn);
-	}
-	if (cat->old_ro_txn != NULL) {
-		knot_lmdb_abort(cat->old_ro_txn);
-		free(cat->old_ro_txn);
-	}
-	knot_lmdb_deinit(&cat->db);
-	return KNOT_EOK;
-}
-
-static int bailiwick_shift(const knot_dname_t *subname, const knot_dname_t *name)
-{
-	const knot_dname_t *res = subname;
-	while (!knot_dname_is_equal(res, name)) {
-		if (*res == '\0') {
-			return -1;
-		}
-		res = knot_wire_next_label(res, NULL);
-	}
-	return res - subname;
-}
-
-int catalog_add(catalog_t *cat, const knot_dname_t *member,
-                const knot_dname_t *owner, const knot_dname_t *catzone)
-{
-	if (cat->rw_txn == NULL) {
-		return KNOT_EINVAL;
-	}
-	int bail = bailiwick_shift(owner, catzone);
-	if (bail < 0) {
-		return KNOT_EOUTOFZONE;
-	}
-	assert(bail >= 0 && bail < 256);
-	MDB_val key = knot_lmdb_make_key("BN", 0, member); // 0 for future purposes
-	MDB_val val = knot_lmdb_make_key("BBN", 0, bail, owner);
-
-	knot_lmdb_insert(cat->rw_txn, &key, &val);
-	free(key.mv_data);
-	free(val.mv_data);
-	return cat->rw_txn->ret;
-}
-
-int catalog_del(catalog_t *cat, const knot_dname_t *member)
-{
-	if (cat->rw_txn == NULL) {
-		return KNOT_EINVAL;
-	}
-	MDB_val key = knot_lmdb_make_key("BN", 0, member);
-	knot_lmdb_del_prefix(cat->rw_txn, &key); // deletes one record
-	free(key.mv_data);
-	return cat->rw_txn->ret;
-}
-
-void catalog_curval(catalog_t *cat, const knot_dname_t **member,
-                    const knot_dname_t **owner, const knot_dname_t **catzone)
-{
-	uint8_t zero, shift;
-	if (member != NULL) {
-		knot_lmdb_unmake_key(cat->ro_txn->cur_key.mv_data, cat->ro_txn->cur_key.mv_size,
-		                     "BN", &zero, member);
-	}
-	const knot_dname_t *ow;
-	knot_lmdb_unmake_curval(cat->ro_txn, "BBN", &zero, &shift, &ow);
-	if (owner != NULL) {
-		*owner = ow;
-	}
-	if (catzone != NULL) {
-		*catzone = ow + shift;
-	}
-}
-
-static void catalog_curval2(MDB_val *key, MDB_val *val, const knot_dname_t **member,
-                            const knot_dname_t **owner, const knot_dname_t **catzone)
-{
-	uint8_t zero, shift;
-	if (member != NULL) {
-		knot_lmdb_unmake_key(key->mv_data, key->mv_size,
-		                     "BN", &zero, member);
-	}
-	const knot_dname_t *ow;
-	knot_lmdb_unmake_key(val->mv_data, val->mv_size, "BBN", &zero, &shift, &ow);
-	if (owner != NULL) {
-		*owner = ow;
-	}
-	if (catzone != NULL) {
-		*catzone = ow + shift;
-	}
-}
-
-int catalog_get_zone(catalog_t *cat, const knot_dname_t *member,
-                     const knot_dname_t **catzone)
-{
-	if (cat->ro_txn == NULL) {
-		return KNOT_ENOENT;
-	}
-
-	MDB_val key = knot_lmdb_make_key("BN", 0, member);
-	if (knot_lmdb_find(cat->ro_txn, &key, KNOT_LMDB_EXACT)) {
-		catalog_curval(cat, NULL, NULL, catzone);
-		free(key.mv_data);
-		return KNOT_EOK;
-	}
-	free(key.mv_data);
-	return MIN(cat->ro_txn->ret, KNOT_ENOENT);
-}
-
-int catalog_get_zone_threadsafe(catalog_t *cat, const knot_dname_t *member,
-                                knot_dname_storage_t catzone)
-{
-	if (cat->ro_txn == NULL) {
-		return KNOT_ENOENT;
-	}
-
-	MDB_val key = knot_lmdb_make_key("BN", 0, member), val = { 0 };
-
-	int ret = knot_lmdb_find_threadsafe(cat->ro_txn, &key, &val, KNOT_LMDB_EXACT);
-	if (ret == KNOT_EOK) {
-		uint8_t zero, shift;
-		const knot_dname_t *ow = NULL;
-		knot_lmdb_unmake_key(val.mv_data, val.mv_size, "BBN", &zero, &shift, &ow);
-		if (knot_dname_store(catzone, ow + shift) == 0) {
-			ret = KNOT_EINVAL;
-		}
-		free(val.mv_data);
-	}
-	free(key.mv_data);
-	return ret;
-}
-
-typedef struct {
-	const knot_dname_t *member;
-	const knot_dname_t *owner;
-	const knot_dname_t *catzone;
-	catalog_find_res_t ret;
-} find_ctx_t;
-
-static int find_cb(MDB_val *key, MDB_val *val, void *fictx)
-{
-	const knot_dname_t *mem, *ow, *cz;
-	catalog_curval2(key, val, &mem, &ow, &cz);
-	find_ctx_t *ctx = fictx;
-	assert(knot_dname_is_equal(mem, ctx->member));
-	if (!knot_dname_is_equal(cz, ctx->catzone)) {
-		ctx->ret = MEMBER_ZONE;
-	} else if (!knot_dname_is_equal(ow, ctx->owner)) {
-		ctx->ret = MEMBER_OWNER;
-	} else {
-		ctx->ret = MEMBER_EXACT;
-	}
-	return KNOT_EOK;
-}
-
-catalog_find_res_t catalog_find(catalog_t *cat, const knot_dname_t *member,
-                                const knot_dname_t *owner, const knot_dname_t *catzone)
-{
-	MDB_val key = knot_lmdb_make_key("BN", 0, member);
-	find_ctx_t ctx = { member, owner, catzone, MEMBER_NONE };
-	int ret = knot_lmdb_apply_threadsafe(cat->ro_txn, &key, false, find_cb, &ctx);
-	free(key.mv_data);
-	switch (ret) {
-	case KNOT_EOK:
-		return ctx.ret;
-	case KNOT_ENOENT:
-		return MEMBER_NONE;
-	default:
-		return MEMBER_ERROR;
-	}
-}
-
-inline static bool same_catalog(knot_lmdb_txn_t *txn, const knot_dname_t *catalog)
-{
-	if (catalog == NULL) {
-		return true;
-	}
-	const knot_dname_t *txn_cat = NULL;
-	catalog_curval2(&txn->cur_key, &txn->cur_val, NULL, NULL, &txn_cat);
-	return knot_dname_is_equal(txn_cat, catalog);
-}
-
-int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to,
-                 const knot_dname_t *zone_only, bool read_rw_txn)
-{
-	if (!knot_lmdb_exists(from)) {
-		return KNOT_EOK;
-	}
-	int ret = knot_lmdb_open(from);
-	if (ret == KNOT_EOK) {
-		ret = knot_lmdb_open(to);
-	}
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-	knot_lmdb_txn_t txn_r = { 0 }, txn_w = { 0 };
-	knot_lmdb_begin(from, &txn_r, read_rw_txn); // using RW txn not to conflict with still-open RO txn
-	knot_lmdb_begin(to, &txn_w, true);
-	knot_lmdb_foreach(&txn_w, (MDB_val *)&catalog_iter_prefix) {
-		if (same_catalog(&txn_w, zone_only)) {
-			knot_lmdb_del_cur(&txn_w);
-		}
-	}
-	knot_lmdb_foreach(&txn_r, (MDB_val *)&catalog_iter_prefix) {
-		if (same_catalog(&txn_r, zone_only)) {
-			knot_lmdb_insert(&txn_w, &txn_r.cur_key, &txn_r.cur_val);
-		}
-	}
-	if (txn_r.ret != KNOT_EOK) {
-		knot_lmdb_abort(&txn_r);
-		knot_lmdb_abort(&txn_w);
-		return txn_r.ret;
-	}
-	knot_lmdb_commit(&txn_r);
-	knot_lmdb_commit(&txn_w);
-	return txn_w.ret;
-}
-
-int catalog_update_init(catalog_update_t *u)
-{
-	u->upd = trie_create(NULL);
-	if (u->upd == NULL) {
-		return KNOT_ENOMEM;
-	}
-	pthread_mutex_init(&u->mutex, 0);
-	u->error = KNOT_EOK;
-	return KNOT_EOK;
-}
-
-catalog_update_t *catalog_update_new()
-{
-	catalog_update_t *u = calloc(1, sizeof(*u));
-	if (u != NULL) {
-		int ret = catalog_update_init(u);
-		if (ret != KNOT_EOK) {
-			free(u);
-			u = NULL;
-		}
-	}
-	return u;
-}
-
-static int freecb(trie_val_t *tval, void *unused)
-{
-	catalog_upd_val_t *val = *tval;
-	if (val != NULL) {
-		freecb((void **)&val->counter, unused);
-		free(val);
-	}
-	return 0;
-}
-
-void catalog_update_clear(catalog_update_t *u)
-{
-	trie_apply(u->upd, freecb, NULL);
-	trie_clear(u->upd);
-	u->error = KNOT_EOK;
-}
-
-void catalog_update_deinit(catalog_update_t *u)
-{
-	pthread_mutex_destroy(&u->mutex);
-	trie_free(u->upd);
-}
-
-void catalog_update_free(catalog_update_t *u)
-{
-	if (u != NULL) {
-		catalog_update_deinit(u);
-		free(u);
-	}
-}
-
-static const knot_dname_t *get_uniq(const knot_dname_t *ptr_owner,
-                                    const knot_dname_t *catz)
-{
-	int labels = knot_dname_labels(ptr_owner, NULL);
-	labels -= knot_dname_labels(catz, NULL);
-	assert(labels >= 2);
-	return ptr_owner + knot_dname_prefixlen(ptr_owner, labels - 2, NULL);
-}
-
-static bool same_uniq(const knot_dname_t *owner1, const knot_dname_t *catz1,
-                      const knot_dname_t *owner2, const knot_dname_t *catz2)
-{
-	const knot_dname_t *uniq1 = get_uniq(owner1, catz1), *uniq2 = get_uniq(owner2, catz2);
-	if (*uniq1 != *uniq2) {
-		return false;
-	}
-	return memcmp(uniq1 + 1, uniq2 + 1, *uniq1) == 0;
-}
-
-static catalog_upd_val_t *new_upd_val(const knot_dname_t *member,
-                                      const knot_dname_t *owner,
-                                      size_t bail, catalog_upd_type_t type,
-                                      catalog_upd_val_t *counter)
-{
-	size_t member_size = knot_dname_size(member);
-	size_t owner_size = knot_dname_size(owner);
-	assert(bail <= owner_size);
-
-	catalog_upd_val_t *val = malloc(sizeof(*val) + member_size + owner_size);
-	if (val == NULL) {
-		return NULL;
-	}
-	val->member = (knot_dname_t *)(val + 1);
-	val->owner = val->member + member_size;
-	val->catzone = val->owner + bail;
-	memcpy(val->member, member, member_size);
-	memcpy(val->owner, owner, owner_size);
-	val->type = type;
-	val->counter = counter;
-	return val;
-}
-
-int catalog_update_add(catalog_update_t *u, const knot_dname_t *member,
-                       const knot_dname_t *owner, const knot_dname_t *catzone,
-                       bool remove)
-{
-	int bail = bailiwick_shift(owner, catzone);
-	if (bail < 0) {
-		return KNOT_EOUTOFZONE;
-	}
-	assert(bail >= 0 && bail < 256);
-
-	knot_dname_storage_t lf_storage;
-	uint8_t *lf = knot_dname_lf(member, lf_storage);
-
-	catalog_upd_type_t type = remove ? MEMB_UPD_REM : MEMB_UPD_ADD;
-	catalog_upd_val_t *counter = NULL;
-
-	trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]);
-	if (found != NULL) {
-		counter = *found;
-		assert(knot_dname_is_equal(counter->member, member));
-		switch (counter->type) {
-		case MEMB_UPD_ADD:
-		case MEMB_UPD_REM:
-			assert(counter->counter == NULL);
-			if (counter->type == type) {
-				return KNOT_ESEMCHECK;
-			}
-			if (knot_dname_is_equal(counter->owner, owner)) { // exact cancelout
-				assert(knot_dname_is_equal(counter->catzone, catzone));
-				trie_del(u->upd, lf + 1, lf[0], NULL);
-				free(counter);
-				return KNOT_EOK;
-			}
-			bool suniq = same_uniq(owner, catzone, counter->owner, counter->catzone);
-			if (type == MEMB_UPD_REM) {
-				counter->type = suniq ? MEMB_UPD_MINOR : MEMB_UPD_UNIQ;
-				counter->counter = new_upd_val(member, owner, bail, type, NULL);
-				return counter->counter != NULL ? KNOT_EOK : KNOT_ENOMEM;
-			}
-			type = suniq ? MEMB_UPD_MINOR : MEMB_UPD_UNIQ;
-			*found = NULL; // counter will be attached to new val
-			break;
-		default:
-			return KNOT_ESEMCHECK;
-		}
-	}
-
-	catalog_upd_val_t *val = new_upd_val(member, owner, bail, type, counter);
-	if (val == NULL) {
-		return KNOT_ENOMEM;
-	}
-	trie_val_t *added = trie_get_ins(u->upd, lf + 1, lf[0]);
-	if (added == NULL) {
-		free(val);
-		return KNOT_ENOMEM;
-	}
-	assert(*added == NULL);
-	*added = val;
-	return KNOT_EOK;
-}
-
-catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member)
-{
-	knot_dname_storage_t lf_storage;
-	uint8_t *lf = knot_dname_lf(member, lf_storage);
-
-	trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]);
-	return found == NULL ? NULL : *(catalog_upd_val_t **)found;
-}
-
-typedef struct {
-	catalog_update_t *u;
-	const knot_dname_t *apex;
-	bool remove;
-	catalog_t *check;
-} cat_upd_ctx_t;
-
-static int cat_update_add_node(zone_node_t *node, void *data)
-{
-	cat_upd_ctx_t *ctx = data;
-	const knot_rdataset_t *ptr = node_rdataset(node, KNOT_RRTYPE_PTR);
-	if (ptr == NULL || ptr->count == 0) {
-		return KNOT_EOK;
-	}
-	knot_rdata_t *rdata = ptr->rdata;
-	int ret = KNOT_EOK;
-	for (int i = 0; ret == KNOT_EOK && i < ptr->count; i++) {
-		const knot_dname_t *member = knot_ptr_name(rdata);
-		if (ctx->check != NULL && ctx->remove &&
-		    catalog_find(ctx->check, member, node->owner, ctx->apex) != MEMBER_EXACT) {
-			rdata = knot_rdataset_next(rdata);
-			continue;
-		}
-		ret = catalog_update_add(ctx->u, member, node->owner, ctx->apex, ctx->remove);
-		rdata = knot_rdataset_next(rdata);
-	}
-	return ret;
-}
-
-static size_t dname_append(knot_dname_storage_t storage, const knot_dname_t *name)
-{
-	size_t old_len = knot_dname_size(storage);
-	size_t name_len = knot_dname_size(name);
-	size_t new_len = old_len - 1 + name_len;
-	if (old_len == 0 || name_len == 0 || new_len > KNOT_DNAME_MAXLEN) {
-		return 0;
-	}
-	memcpy(storage + old_len - 1, name, name_len);
-	return new_len;
-}
-
-int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone,
-                             bool remove, bool check_ver, catalog_t *check)
-{
-	if (check_ver && !check_zone_version(zone)) {
-		return KNOT_EZONEINVAL;
-	}
-
-	knot_dname_storage_t sub;
-	if (knot_dname_store(sub, (uint8_t *)CATALOG_ZONES_LABEL) == 0 ||
-	    dname_append(sub, zone->apex->owner ) == 0) {
-		return KNOT_EINVAL;
-	}
-
-	if (zone_contents_find_node(zone, sub) == NULL) {
-		return KNOT_EOK;
-	}
-
-	cat_upd_ctx_t ctx = { u, zone->apex->owner, remove, check };
-	pthread_mutex_lock(&u->mutex);
-	int ret = zone_tree_sub_apply(zone->nodes, sub, false, cat_update_add_node, &ctx);
-	pthread_mutex_unlock(&u->mutex);
-	return ret;
-}
-
-static void set_rdata(knot_rrset_t *rrset, uint8_t *data, uint16_t len)
-{
-	knot_rdata_init(rrset->rrs.rdata, len, data);
-	rrset->rrs.size = knot_rdata_size(len);
-}
-
-struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone,
-                                             uint32_t soa_serial)
-{
-	if (u->error != KNOT_EOK) {
-		return NULL;
-	}
-	zone_contents_t *c = zone_contents_new(catzone, true);
-	if (c == NULL) {
-		return c;
-	}
-
-	zone_node_t *unused = NULL;
-	uint8_t invalid[9] = "\x07""invalid";
-	uint8_t version[9] = "\x07""version";
-	uint8_t cat_version[2] = "\x01" CATALOG_ZONE_VERSION;
-
-	// prepare common rrset with one rdata item
-	uint8_t rdata[256] = { 0 };
-	knot_rrset_t rrset;
-	knot_rrset_init(&rrset, (knot_dname_t *)catzone, KNOT_RRTYPE_SOA, KNOT_CLASS_IN, 0);
-	rrset.rrs.rdata = (knot_rdata_t *)rdata;
-	rrset.rrs.count = 1;
-
-	// set catalog zone's SOA
-	uint8_t data[250];
-	assert(sizeof(knot_rdata_t) + sizeof(data) <= sizeof(rdata));
-	wire_ctx_t wire = wire_ctx_init(data, sizeof(data));
-	wire_ctx_write(&wire, invalid, sizeof(invalid));
-	wire_ctx_write(&wire, invalid, sizeof(invalid));
-	wire_ctx_write_u32(&wire, soa_serial);
-	wire_ctx_write_u32(&wire, CATALOG_SOA_REFRESH);
-	wire_ctx_write_u32(&wire, CATALOG_SOA_RETRY);
-	wire_ctx_write_u32(&wire, CATALOG_SOA_EXPIRE);
-	wire_ctx_write_u32(&wire, 0);
-	set_rdata(&rrset, data, wire_ctx_offset(&wire));
-	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
-		goto fail;
-	}
-
-	// set catalog zone's NS
-	unused = NULL;
-	rrset.type = KNOT_RRTYPE_NS;
-	set_rdata(&rrset, invalid, sizeof(invalid));
-	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
-		goto fail;
-	}
-
-	// set catalog zone's version TXT
-	unused = NULL;
-	knot_dname_storage_t owner;
-	if (knot_dname_store(owner, version) == 0 || dname_append(owner, catzone) == 0) {
-		goto fail;
-	}
-	rrset.owner = owner;
-	rrset.type = KNOT_RRTYPE_TXT;
-	set_rdata(&rrset, cat_version, sizeof(cat_version));
-	if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
-		goto fail;
-	}
-
-	// insert member zone PTR records
-	rrset.type = KNOT_RRTYPE_PTR;
-	catalog_it_t *it = catalog_it_begin(u);
-	while (!catalog_it_finished(it)) {
-		catalog_upd_val_t *val = catalog_it_val(it);
-		rrset.owner = val->owner;
-		set_rdata(&rrset, val->member, knot_dname_size(val->member));
-		unused = NULL;
-		if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) {
-			catalog_it_free(it);
-			goto fail;
-		}
-		catalog_it_next(it);
-	}
-	catalog_it_free(it);
-
-	return c;
-
-fail:
-	zone_contents_deep_free(c);
-	return NULL;
-}
-
-int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu)
-{
-	knot_rrset_t ptr;
-	knot_rrset_init(&ptr, NULL, KNOT_RRTYPE_PTR, KNOT_CLASS_IN, 0);
-	uint8_t tmp[KNOT_DNAME_MAXLEN + sizeof(knot_rdata_t)];
-	ptr.rrs.rdata = (knot_rdata_t *)tmp;
-	ptr.rrs.count = 1;
-
-	int ret = u->error;
-	catalog_it_t *it = catalog_it_begin(u);
-	while (!catalog_it_finished(it) && ret == KNOT_EOK) {
-		catalog_upd_val_t *val = catalog_it_val(it);
-		bool same_cat = knot_dname_is_equal(zu->zone->name, val->catzone);
-		ptr.owner = val->owner;
-		set_rdata(&ptr, val->member, knot_dname_size(val->member));
-		switch (val->type) {
-		case MEMB_UPD_ADD:
-			if (same_cat) {
-				ret = zone_update_add(zu, &ptr);
-			}
-			break;
-		case MEMB_UPD_REM:
-			if (same_cat) {
-				ret = zone_update_remove(zu, &ptr);
-			}
-			break;
-		case MEMB_UPD_MINOR:
-		case MEMB_UPD_UNIQ:
-			if (val->counter == NULL) {
-				ret = KNOT_ERROR; // some previous error
-			} else if (same_cat) {
-				ret = zone_update_add(zu, &ptr);
-			}
-			if (ret == KNOT_EOK &&
-			    knot_dname_is_equal(zu->zone->name, val->counter->catzone)) {
-				ptr.owner = val->counter->owner;
-				ret = zone_update_remove(zu, &ptr);
-			}
-			break;
-		default:
-			ret = KNOT_EINVAL;
-		}
-		catalog_it_next(it);
-	}
-	catalog_it_free(it);
-	return ret;
-}
-
-typedef struct {
-	const knot_dname_t *zone;
-	catalog_update_t *u;
-} del_all_ctx_t;
-
-static int del_all_cb(MDB_val *key, MDB_val *val, void *dactx)
-{
-	const knot_dname_t *mem, *ow, *cz;
-	catalog_curval2(key, val, &mem, &ow, &cz);
-	del_all_ctx_t *ctx = dactx;
-	if (knot_dname_is_equal(cz, ctx->zone)) {
-		// TODO possible speedup by indexing which member zones belong to a catalog zone
-		return catalog_update_add(ctx->u, mem, ow, cz, true);
-	} else {
-		return KNOT_EOK;
-	}
-}
-
-int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone)
-{
-	int ret = catalog_open(cat);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	pthread_mutex_lock(&u->mutex);
-	del_all_ctx_t ctx = { zone, u };
-	ret = knot_lmdb_apply_threadsafe(cat->ro_txn, &catalog_iter_prefix, true, del_all_cb, &ctx);
-	pthread_mutex_unlock(&u->mutex);
-	return ret;
-}
-
-static void print_dname(const knot_dname_t *d)
-{
-	knot_dname_txt_storage_t tmp;
-	knot_dname_to_str(tmp, d, sizeof(tmp));
-	printf("%s  ", tmp);
-}
-
-static void print_dname3(const char *prefix, const knot_dname_t *a, const knot_dname_t *b,
-                         const knot_dname_t *c)
-{
-	printf("%s", prefix);
-	print_dname(a);
-	print_dname(b);
-	print_dname(c);
-}
-
-void catalog_print(catalog_t *cat)
-{
-	ssize_t total = 0;
-
-	printf(";; <catalog zone> <record owner> <record zone>\n");
-
-	if (cat != NULL) {
-		int ret = catalog_open(cat);
-		if (ret != KNOT_EOK) {
-			printf("Catalog print failed (%s)\n", knot_strerror(ret));
-			return;
-		}
-
-		catalog_foreach(cat) {
-			const knot_dname_t *mem, *ow, *cz;
-			catalog_curval(cat, &mem, &ow, &cz);
-			print_dname3("", mem, ow, cz);
-			total++;
-		}
-	}
-
-	printf("Total zones: %zd\n", total);
-}
-
-void catalog_update_print(catalog_update_t *u)
-{
-	const static char* sign[MEMB_UPD_MAX] = { "! ", "+ ", "- ", "* ", "# " };
-	ssize_t counts[MEMB_UPD_MAX] = { 0 };
-
-	printf(";; <catalog zone> <record owner> <record zone>\n");
-
-	if (u != NULL) {
-		catalog_it_t *it = catalog_it_begin(u);
-		while (!catalog_it_finished(it)) {
-			catalog_upd_val_t *val = catalog_it_val(it);
-			print_dname3(sign[val->type], val->member, val->owner, val->catzone);
-			counts[val->type]++;
-			catalog_it_next(it);
-		}
-		catalog_it_free(it);
-	}
-
-	printf("Total changes:");
-	for (int i = 1; i < MEMB_UPD_MAX; i++) {
-		printf(" %s%zd", sign[i], counts[i]);
-	}
-	printf("\n");
-}
diff --git a/src/knot/zone/catalog.h b/src/knot/zone/catalog.h
deleted file mode 100644
index 22db6d53622d04069cfa990e664066bb0b7f4728..0000000000000000000000000000000000000000
--- a/src/knot/zone/catalog.h
+++ /dev/null
@@ -1,369 +0,0 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
-
-    This program 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.
-
-    This program 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 this program.  If not, see <https://www.gnu.org/licenses/>.
- */
-
-#pragma once
-
-#include <pthread.h>
-
-#include "libknot/libknot.h"
-#include "contrib/qp-trie/trie.h"
-#include "knot/journal/knot_lmdb.h"
-
-typedef struct catalog {
-	knot_lmdb_db_t db;
-	knot_lmdb_txn_t *ro_txn; // persistent RO transaction
-	knot_lmdb_txn_t *rw_txn; // temporary RW transaction
-
-	// private
-	knot_lmdb_txn_t *old_ro_txn;
-} catalog_t;
-
-typedef enum {
-	MEMBER_NONE,   // this member zone is not in any catalog
-	MEMBER_EXACT,  // this member zone precisely matches lookup
-	MEMBER_ZONE,   // this member zone is in different catalog
-	MEMBER_OWNER,  // this member zone is in same catalog with diferent owner
-	MEMBER_ERROR,  // find error code in cat->txn.ret
-} catalog_find_res_t;
-
-typedef struct {
-	trie_t *upd;             // tree of catalog_upd_val_t, that gonna be changed in catalog
-	int error;               // error occured during generating of upd
-	pthread_mutex_t mutex;   // lock for accessing this struct
-} catalog_update_t;
-
-typedef enum {
-	MEMB_UPD_INVALID,   // invalid value
-	MEMB_UPD_ADD,       // member addition
-	MEMB_UPD_REM,       // member removal
-	MEMB_UPD_MINOR,     // owner or catzone change, uniqID preserved
-	MEMB_UPD_UNIQ,      // uniqID change
-	MEMB_UPD_MAX,       // number of options in ths enum
-} catalog_upd_type_t;
-
-typedef struct catalog_upd_val {
-	knot_dname_t *member;     // name of catalog member zone
-	knot_dname_t *owner;      // the owner of PTR record defining the member zone
-	knot_dname_t *catzone;    // the catalog zone the PTR is in
-	catalog_upd_type_t type;  // what kind of update this is
-	struct catalog_upd_val *counter; // original owner/catzone before this update
-} catalog_upd_val_t;
-
-extern const MDB_val catalog_iter_prefix;
-
-/*!
- * \brief Generate owner name for catalog PTR record.
- *
- * \param member        Name of the member zone respective to the PTR record.
- * \param catzone       Catalog zone name to contain the PTR.
- * \param member_time   Timestamp of member zone addition.
- *
- * \return Owner name or NULL on error (e.g. ENOMEM, too long result...).
- *
- * \note Don't forget to free the return value later.
- */
-knot_dname_t *catalog_member_owner(const knot_dname_t *member,
-                                   const knot_dname_t *catzone,
-                                   time_t member_time);
-
-/*!
- * \brief Initialize catalog structure.
- *
- * \param cat        Catalog structure.
- * \param path       Path to LMDB for catalog.
- * \param mapsize    Mapsize of the LMDB.
- */
-void catalog_init(catalog_t *cat, const char *path, size_t mapsize);
-
-/*!
- * \brief Open the catalog LMDB, create it if not exists.
- *
- * \param cat   Catlog to be opened.
- *
- * \return KNOT_E*
- */
-int catalog_open(catalog_t *cat);
-
-/*!
- * \brief Start a temporary RW transaction in the catalog.
- *
- * \param cat   Catalog in question.
- *
- * \return KNOT_E*
- */
-int catalog_begin(catalog_t *cat);
-
-/*!
- * \brief End using the temporary RW txn, refresh the persistent RO txn.
- *
- * \param cat   Catalog in question.
- *
- * \return KNOT_E*
- */
-int catalog_commit(catalog_t *cat);
-
-/*!
- * \brief Free up old txns.
- *
- * \note This must be called after catalog_commit() with a delay of synchronnize_rcu().
- *
- * \param cat   Catalog.
- */
-void catalog_commit_cleanup(catalog_t *cat);
-
-/*!
- * \brief Close the catalog and de-init the structure.
- *
- * \param cat   Catalog to be closed.
- *
- * \return KNOT_E*
- */
-int catalog_deinit(catalog_t *cat);
-
-/*!
- * \brief Add a member zone to the catalog database.
- *
- * \param cat       Catalog to be augmented.
- * \param member    Member zone name.
- * \param owner     Owner of the PTR record in catalog zone, respective to the member zone.
- * \param catzone   Name of the catalog zone whose it's the member.
- *
- * \return KNOT_E*
- */
-int catalog_add(catalog_t *cat, const knot_dname_t *member,
-                const knot_dname_t *owner, const knot_dname_t *catzone);
-
-inline static int catalog_add2(catalog_t *cat, const catalog_upd_val_t *val)
-{
-	return catalog_add(cat, val->member, val->owner, val->catzone);
-}
-
-/*!
- * \brief Delete a member zone from the catalog database.
- *
- * \param cat       Catalog to be removed from.
- * \param member    Member zone to be removed.
- *
- * \return KNOT_E*
- */
-int catalog_del(catalog_t *cat, const knot_dname_t *member);
-
-inline static int catalog_del2(catalog_t *cat, const catalog_upd_val_t *val)
-{
-	assert(val->type != MEMB_UPD_ADD);
-	return catalog_del(cat, val->member);
-}
-
-#define catalog_foreach(cat) knot_lmdb_foreach((cat)->ro_txn, (MDB_val *)&catalog_iter_prefix)
-
-/*!
- * \brief Deserialize a value in catalog database.
- *
- * \param cat       Catalog with cat->txn->cur_val to be deserialized.
- * \param member    Output: member zone.
- * \param owner     Output: PTR owner.
- * \param catzone   Output: catalog zone.
- */
-void catalog_curval(catalog_t *cat, const knot_dname_t **member,
-                    const knot_dname_t **owner, const knot_dname_t **catzone);
-
-/*!
- * \brief Get the catalog zone for known member zone.
- *
- * \param cat        Catalog database.
- * \param member     Member zone name.
- * \param catzone    Catalog zone holding the member zone.
- *
- * \return KNOT_E*
- */
-int catalog_get_zone(catalog_t *cat, const knot_dname_t *member,
-                     const knot_dname_t **catzone);
-
-/*!
- * \brief Get the catalog zone for known member zone.
- *
- * \note This function is safe for multithreaded operation over shared LMDB transaction.
- *
- * \param cat        Catalog database.
- * \param member     Member zone name.
- * \param catzone    Catalog zone holding the member zone.
- *
- * \return KNOT_E*
- */
-int catalog_get_zone_threadsafe(catalog_t *cat, const knot_dname_t *member,
-                                knot_dname_storage_t catzone);
-
-/*!
- * \brief Find specific member record in catalog database.
- *
- * \param cat        Catalog database.
- * \param member     Member zone to be searched for.
- * \param owner      Owner to be searched/verified.
- * \param catzone    Catalog zone to be searched/verified.
- *
- * \return see catalog_find_res_t
- */
-catalog_find_res_t catalog_find(catalog_t *cat, const knot_dname_t *member,
-                                const knot_dname_t *owner, const knot_dname_t *catzone);
-
-/*!
- * \brief Copy records from one catalog database to other.
- *
- * \param from            Catalog DB to copy from.
- * \param to              Catalog db to copy to.
- * \param zone_only       Optional: copy only records for this catalog zone.
- * \param read_rw_txn     Use RW txn for read operations.
- *
- * \return KNOT_E*
- */
-int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to,
-                 const knot_dname_t *zone_only, bool read_rw_txn);
-
-/*!
- * \brief Initialize catalog update structure.
- *
- * \param u   Catalog update to be initialized.
- *
- * \return KNOT_EOK, KNOT_ENOMEM
- */
-int catalog_update_init(catalog_update_t *u);
-catalog_update_t *catalog_update_new(void);
-
-/*!
- * \brief Clear contents of catalog update structure.
- *
- * \param u   Catalog update structure to be cleared.
- */
-void catalog_update_clear(catalog_update_t *u);
-
-/*!
- * \brief Free catalog update structure.
- *
- * \param u   Catalog update structure.
- */
-void catalog_update_deinit(catalog_update_t *u);
-void catalog_update_free(catalog_update_t *u);
-
-/*!
- * \brief Add a new record to catalog update structure.
- *
- * \param u         Catalog update.
- * \param member    Member zone name to be added.
- * \param owner     Owner of respective PTR record.
- * \param catzone   Catalog zone holding the member.
- * \param remove    Add a removal of such record.
- *
- * \return KNOT_E*
- */
-int catalog_update_add(catalog_update_t *u, const knot_dname_t *member,
-                       const knot_dname_t *owner, const knot_dname_t *catzone,
-                       bool remove);
-
-/*!
- * \brief Read catalog update record for given member zone.
- *
- * \param u          Catalog update.
- * \param member     Member zone name.
- * \param remove     Search in remove section.
- *
- * \return Found update record for given member zone; or NULL.
- */
-catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member);
-
-struct zone_contents;
-
-/*!
- * \brief Iterate over PTR records in given zone contents and add members to catalog update.
- *
- * \param u            Catalog update to be updated.
- * \param zone         Zone contents to be searched for member PTR records.
- * \param remove       Add removals of found member zones.
- * \param check_ver    Do check catalog zone version record first.
- * \param check        Optional: existing catalog database to be checked for existence of such record (useful for removals).
- *
- * \return KNOT_E*
- */
-int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone,
-                             bool remove, bool check_ver, catalog_t *check);
-
-/*!
- * \brief Generate catalog zone contents from (full) catalog update.
- *
- * \param u           Catalog update to read.
- * \param catzone     Catalog zone name.
- * \param soa_serial  SOA serial of the generated zone.
- *
- * \return Catalog zone contents, or NULL if ENOMEM.
- */
-struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone,
-                                             uint32_t soa_serial);
-
-struct zone_update;
-
-/*!
- * \brief Incrementally update catalog zone from catalog update.
- *
- * \param u    Catalog update to read.
- * \param zu   Zone update to be updated.
- *
- * \return KNOT_E*
- */
-int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu);
-
-/*!
- * \brief Add to catalog update removals of all member zones of a single catalog zone.
- *
- * \param u      Catalog updat to be updated.
- * \param cat    Catalog database to be iterated.
- * \param zone   Name of catalog zone whose members gonna be removed.
- *
- * \return KNOT_E*
- */
-int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone);
-
-typedef trie_it_t catalog_it_t;
-
-inline static catalog_it_t *catalog_it_begin(catalog_update_t *u)
-{
-	return trie_it_begin(u->upd);
-}
-
-inline static catalog_upd_val_t *catalog_it_val(catalog_it_t *it)
-{
-	return *(catalog_upd_val_t **)trie_it_val(it);
-}
-
-inline static bool catalog_it_finished(catalog_it_t *it)
-{
-	return it == NULL || trie_it_finished(it);
-}
-
-#define catalog_it_next trie_it_next
-#define catalog_it_free trie_it_free
-
-/*!
- * \brief Print to stdout whole contents of catalog database (for human).
- *
- * \param cat   Catalog database to be printed.
- */
-void catalog_print(catalog_t *cat);
-
-/*!
- * \brief Print to stdout whole contents of catalog update (for human).
- *
- * \param u   Catalog update to be printed.
- */
-void catalog_update_print(catalog_update_t *u);
diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h
index 0ae65a504bd5b32289c9b18ff63bc7ae3c2dcb0e..84eb706d6903edf9d58e48d49b05cc2bf421164d 100644
--- a/src/knot/zone/zone.h
+++ b/src/knot/zone/zone.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -17,12 +17,12 @@
 #pragma once
 
 #include "contrib/semaphore.h"
+#include "knot/catalog/catalog_update.h"
 #include "knot/conf/conf.h"
 #include "knot/conf/confio.h"
 #include "knot/journal/journal_basic.h"
 #include "knot/events/events.h"
 #include "knot/updates/changesets.h"
-#include "knot/zone/catalog.h"
 #include "knot/zone/contents.h"
 #include "knot/zone/timers.h"
 #include "libknot/dname.h"
diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c
index 6e852132747bba33b23740aadba4646e2d5fd8e7..865e505de7207118511ebb8ea1e36100e0dc0446 100644
--- a/src/knot/zone/zonedb-load.c
+++ b/src/knot/zone/zonedb-load.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -18,11 +18,11 @@
 #include <unistd.h>
 #include <urcu.h>
 
+#include "knot/catalog/generate.h"
 #include "knot/common/log.h"
 #include "knot/conf/module.h"
 #include "knot/events/replan.h"
 #include "knot/journal/journal_metadata.h"
-#include "knot/zone/catalog.h"
 #include "knot/zone/timers.h"
 #include "knot/zone/zone-load.h"
 #include "knot/zone/zone.h"
@@ -76,65 +76,6 @@ static zone_t *create_zone_from(const knot_dname_t *name, server_t *server)
 	return zone;
 }
 
-static void catalogs_generate(knot_zonedb_t *db_new, knot_zonedb_t *db_old)
-{
-	// general comment: catz->contents!=NULL means incremental update of catalog
-
-	if (db_old != NULL) {
-		knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old);
-		while (!knot_zonedb_iter_finished(it)) {
-			zone_t *zone = knot_zonedb_iter_val(it);
-			knot_dname_t *cg = zone->catalog_gen;
-			if (cg != NULL && knot_zonedb_find(db_new, zone->name) == NULL) {
-				zone_t *catz = knot_zonedb_find(db_new, cg);
-				if (catz != NULL && catz->contents != NULL) {
-					assert(catz->cat_members != NULL); // if this failed to allocate, catz wasn't added to zonedb
-					knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member);
-					if (owner == NULL) {
-						catz->cat_members->error = KNOT_ENOENT;
-						knot_zonedb_iter_next(it);
-						continue;
-					}
-					int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, true);
-					free(owner);
-					if (ret != KNOT_EOK) {
-						catz->cat_members->error = ret;
-					}
-				}
-			}
-			knot_zonedb_iter_next(it);
-		}
-		knot_zonedb_iter_free(it);
-	}
-
-	knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_new);
-	while (!knot_zonedb_iter_finished(it)) {
-		zone_t *zone = knot_zonedb_iter_val(it);
-		knot_dname_t *cg = zone->catalog_gen;
-		zone_t *catz = cg != NULL ? knot_zonedb_find(db_new, cg) : NULL;
-		if (cg != NULL && catz == NULL) {
-			log_zone_warning(zone->name, "member zone belongs to non-existing catalog zone");
-			continue;
-		}
-		if (cg != NULL && (catz->contents == NULL || knot_zonedb_find(db_old, zone->name) == NULL)) {
-			assert(catz->cat_members != NULL);
-			knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member);
-			if (owner == NULL) {
-				catz->cat_members->error = KNOT_ENOENT;
-				knot_zonedb_iter_next(it);
-				continue;
-			}
-			int ret = catalog_update_add(catz->cat_members, zone->name, owner, cg, false);
-			free(owner);
-			if (ret != KNOT_EOK) {
-				catz->cat_members->error = ret;
-			}
-		}
-		knot_zonedb_iter_next(it);
-	}
-	knot_zonedb_iter_free(it);
-}
-
 static zone_t *create_zone_reload(conf_t *conf, const knot_dname_t *name,
                                   server_t *server, zone_t *old_zone)
 {
@@ -301,7 +242,7 @@ static bool check_open_catalog(catalog_t *cat)
 	if (knot_lmdb_exists(&cat->db)) {
 		int ret = catalog_open(cat);
 		if (ret != KNOT_EOK) {
-			log_error("failed to open existing zone catalog");
+			log_error("failed to open persistent zone catalog");
 		} else {
 			return true;
 		}
@@ -319,13 +260,13 @@ static zone_t *reuse_member_zone(zone_t *zone, server_t *server, conf_t *conf,
 	catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zone->name);
 	if (upd != NULL) {
 		switch (upd->type) {
-		case MEMB_UPD_UNIQ:
+		case CAT_UPD_UNIQ:
 			zone_purge(conf, zone, server);
 			knot_sem_wait(&zone->cow_lock);
 			ptrlist_add(expired_contents, zone_expire(zone), NULL);
 			knot_sem_post(&zone->cow_lock);
 			break;
-		case MEMB_UPD_REM:
+		case CAT_UPD_REM:
 			return NULL; // zone to be removed
 		default:
 			break;
@@ -347,7 +288,7 @@ static zone_t *reuse_member_zone(zone_t *zone, server_t *server, conf_t *conf,
 static zone_t *reuse_cold_zone(const knot_dname_t *zname, server_t *server, conf_t *conf)
 {
 	catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zname);
-	if (upd != NULL && upd->type == MEMB_UPD_REM) {
+	if (upd != NULL && upd->type == CAT_UPD_REM) {
 		return NULL; // zone will be removed immediately
 	}
 
@@ -362,10 +303,30 @@ static zone_t *reuse_cold_zone(const knot_dname_t *zname, server_t *server, conf
 	return zone;
 }
 
+typedef struct {
+	knot_zonedb_t *zonedb;
+	server_t *server;
+	conf_t *conf;
+} reuse_cold_zone_ctx_t;
+
+static int reuse_cold_zone_cb(const knot_dname_t *member, const knot_dname_t *owner,
+                              const knot_dname_t *catz, void *ctx)
+{
+	UNUSED(owner);
+	UNUSED(catz);
+	reuse_cold_zone_ctx_t *rcz = ctx;
+
+	zone_t *zone = reuse_cold_zone(member, rcz->server, rcz->conf);
+	if (zone == NULL) {
+		return KNOT_ENOMEM;
+	}
+	return knot_zonedb_insert(rcz->zonedb, zone);
+}
+
 static zone_t *add_member_zone(catalog_upd_val_t *val, knot_zonedb_t *check,
                                server_t *server, conf_t *conf)
 {
-	if (val->type != MEMB_UPD_ADD) {
+	if (val->type != CAT_UPD_ADD) {
 		return NULL;
 	}
 
@@ -377,7 +338,6 @@ static zone_t *add_member_zone(catalog_upd_val_t *val, knot_zonedb_t *check,
 	zone_t *zone = create_zone(conf, val->member, server, NULL);
 	if (zone == NULL) {
 		log_zone_error(val->member, "zone cannot be created");
-		catalog_del2(conf->catalog, val);
 	} else {
 		zone_set_flag(zone, ZONE_IS_CAT_MEMBER);
 		conf_activate_modules(conf, server, zone->name, &zone->query_modules,
@@ -456,60 +416,29 @@ static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server, list_t *expi
 		}
 		knot_zonedb_iter_free(it);
 	} else if (check_open_catalog(&server->catalog)) {
-		catalog_foreach(&server->catalog) {
-			const knot_dname_t *member = NULL, *catzone = NULL;
-			catalog_curval(&server->catalog, &member, NULL, &catzone);
-
-			if (!conf_rawid_exists(conf, C_ZONE, catzone, knot_dname_size(catzone))) {
-				knot_dname_txt_storage_t cat_str;
-				(void)knot_dname_to_str(cat_str, catzone, sizeof(cat_str));
-				log_zone_error(member, "catalog template zone '%s' not configured, ignoring", cat_str);
-				continue;
-			} else if (conf_rawid_exists(conf, C_ZONE, member, knot_dname_size(member))) {
-				log_zone_error(member, "non-catalog zone already configured, ignoring");
-				continue;
-			}
-
-			zone_t *zone = reuse_cold_zone(member, server, conf);
-			if (zone != NULL) {
-				knot_zonedb_insert(db_new, zone);
-			}
+		reuse_cold_zone_ctx_t rcz = { db_new, server, conf };
+		int ret = catalog_apply(&server->catalog, NULL, reuse_cold_zone_cb, &rcz, false);
+		if (ret != KNOT_EOK) {
+			log_error("catalog, failed to reload member zones (%s)", knot_strerror(ret));
 		}
 	}
 
 	catalog_commit_cleanup(&server->catalog);
 
-	catalog_it_t *it = catalog_it_begin(&server->catalog_upd);
-	int catret = 1;
-	if (!catalog_it_finished(it)) {
-		catret = catalog_begin(&server->catalog);
-	}
-	while (!catalog_it_finished(it) && catret == KNOT_EOK) {
-		catalog_upd_val_t *val = catalog_it_val(it);
-		if (val->type == MEMB_UPD_UNIQ || val->type == MEMB_UPD_MINOR ||
-		    knot_zonedb_find(db_new, val->member) == NULL) {
-			// ^ warning for existing zone later in add_member_zone()
-			catret = catalog_add2(&server->catalog, val);
-		}
-		catalog_it_next(it);
-	}
-	catalog_it_free(it);
-	if (catret == KNOT_EOK) {
-		catret = catalog_commit(&server->catalog);
-	}
-
-	it = catalog_it_begin(&server->catalog_upd);
-	while (!catalog_it_finished(it) && catret == KNOT_EOK) {
-		catalog_upd_val_t *val = catalog_it_val(it);
-		zone_t *zone = add_member_zone(val, db_new, server, conf);
-		if (zone != NULL) {
-			knot_zonedb_insert(db_new, zone);
+	int ret = catalog_update_commit(&server->catalog_upd, &server->catalog);
+	if (ret == KNOT_EOK) {
+		catalog_it_t *it = catalog_it_begin(&server->catalog_upd);
+		while (!catalog_it_finished(it)) {
+			catalog_upd_val_t *val = catalog_it_val(it);
+			zone_t *zone = add_member_zone(val, db_new, server, conf);
+			if (zone != NULL) {
+				knot_zonedb_insert(db_new, zone);
+			}
+			catalog_it_next(it);
 		}
-		catalog_it_next(it);
-	}
-	catalog_it_free(it);
-	if (catret < 0) {
-		log_error("failed to process zone catalog (%s)", knot_strerror(catret));
+		catalog_it_free(it);
+	} else {
+		log_error("catalog, failed to apply changes (%s)", knot_strerror(ret));
 	}
 
 	return db_new;
@@ -566,14 +495,9 @@ static void remove_old_zonedb(conf_t *conf, knot_zonedb_t *db_old,
 catalog_only:
 	; /* Remove deleted cataloged zones from conf. */
 	catalog_it_t *cat_it = catalog_it_begin(&server->catalog_upd);
-	int catret = 1;
-	if (!catalog_it_finished(cat_it)) {
-		catret = catalog_begin(&server->catalog);
-	}
 	while (!catalog_it_finished(cat_it)) {
 		catalog_upd_val_t *upd = catalog_it_val(cat_it);
-		if (upd->type == MEMB_UPD_REM) {
-			catalog_del(&server->catalog, upd->member);
+		if (upd->type == CAT_UPD_REM) {
 			zone_t *zone = knot_zonedb_find(db_old, upd->member);
 			if (zone != NULL) {
 				zone_purge(conf, zone, server);
@@ -582,12 +506,6 @@ catalog_only:
 		catalog_it_next(cat_it);
 	}
 	catalog_it_free(cat_it);
-	if (catret == KNOT_EOK) {
-		catret = catalog_commit(&server->catalog);
-	}
-	if (catret < 0) {
-		log_error("failed to process zone catalog (%s)", knot_strerror(catret));
-	}
 
 	/* Clear catalog changes. No need to use mutex as this is done from main
 	 * thread while all zone events are paused. */
@@ -609,6 +527,8 @@ void zonedb_reload(conf_t *conf, server_t *server)
 	list_t contents_tofree;
 	init_list(&contents_tofree);
 
+	catalog_update_finalize(&server->catalog_upd, &server->catalog);
+
 	/* Insert all required zones to the new zone DB. */
 	knot_zonedb_t *db_new = create_zonedb(conf, server, &contents_tofree);
 	if (db_new == NULL) {
diff --git a/src/knot/zone/zonedb.c b/src/knot/zone/zonedb.c
index b6c751381beb8f755ae3472d1f13fb6ea2b4b843..98cade558de218b4427a2fbec299b682f0e03599 100644
--- a/src/knot/zone/zonedb.c
+++ b/src/knot/zone/zonedb.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published by
@@ -26,11 +26,9 @@
 /*! \brief Discard zone in zone database. */
 static void discard_zone(zone_t *zone, bool abort_txn)
 {
-	const knot_dname_t *unused = NULL;
-
 	// Don't flush if removed zone (no previous configuration available).
 	if (conf_rawid_exists(conf(), C_ZONE, zone->name, knot_dname_size(zone->name)) ||
-	    catalog_get_zone(conf()->catalog, zone->name, &unused) == KNOT_EOK) {
+	    catalog_has_member(conf()->catalog, zone->name)) {
 		uint32_t journal_serial, zone_serial = zone_contents_serial(zone->contents);
 		bool exists;
 
diff --git a/src/utils/kcatalogprint/main.c b/src/utils/kcatalogprint/main.c
index 76f4d61724f7a49252332b942befe56f1b6af026..b46813c4a00ffe27ccec8f2db8eb83fde4ff9eeb 100644
--- a/src/utils/kcatalogprint/main.c
+++ b/src/utils/kcatalogprint/main.c
@@ -18,7 +18,7 @@
 #include <stdlib.h>
 #include <string.h>
 
-#include "knot/zone/catalog.h"
+#include "knot/catalog/catalog_db.h"
 #include "utils/common/params.h"
 
 #define PROGRAM_NAME	"kcatalogprint"
diff --git a/tests-extra/tests/zone/catalog/data/catalog1.zone b/tests-extra/tests/catalog/basic/data/catalog1.zone
similarity index 100%
rename from tests-extra/tests/zone/catalog/data/catalog1.zone
rename to tests-extra/tests/catalog/basic/data/catalog1.zone
diff --git a/tests-extra/tests/zone/catalog/data/cataloged1.zone b/tests-extra/tests/catalog/basic/data/cataloged1.zone
similarity index 100%
rename from tests-extra/tests/zone/catalog/data/cataloged1.zone
rename to tests-extra/tests/catalog/basic/data/cataloged1.zone
diff --git a/tests-extra/tests/zone/catalog/data/cataloged2.zone b/tests-extra/tests/catalog/basic/data/cataloged2.zone
similarity index 100%
rename from tests-extra/tests/zone/catalog/data/cataloged2.zone
rename to tests-extra/tests/catalog/basic/data/cataloged2.zone
diff --git a/tests-extra/tests/zone/catalog/test.py b/tests-extra/tests/catalog/basic/test.py
similarity index 100%
rename from tests-extra/tests/zone/catalog/test.py
rename to tests-extra/tests/catalog/basic/test.py
diff --git a/tests-extra/tests/zone/catalog_generate/test.py b/tests-extra/tests/catalog/generate/test.py
similarity index 100%
rename from tests-extra/tests/zone/catalog_generate/test.py
rename to tests-extra/tests/catalog/generate/test.py
diff --git a/tests-extra/tests/catalog/many_zones/test.py b/tests-extra/tests/catalog/many_zones/test.py
new file mode 100644
index 0000000000000000000000000000000000000000..69fa5dcecc36d5db61879cb236024ca84cca318e
--- /dev/null
+++ b/tests-extra/tests/catalog/many_zones/test.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+'''Test of handling large catalog with changes.'''
+
+from dnstest.test import Test
+from dnstest.utils import set_err, detail_log
+import os
+import random
+import time
+
+UPDATES = 5
+ADD_ZONES = 11
+REM_ZONES = 7
+DNSSEC = True
+
+t = Test(stress=False)
+
+master = t.server("knot")
+slave = t.server("knot")
+
+catz = t.zone("example.")
+
+t.link(catz, master, slave)
+
+cz = master.zones[catz[0].name]
+cz.catalog_gen_link(cz) # empty catz with "generate" role
+
+slave.zones[catz[0].name].catalog = True
+slave.dnssec(catz[0]).enable = DNSSEC
+slave.dnssec(catz[0]).alg = "ECDSAP256SHA256"
+slave.zones[catz[0].name].journal_content = "all"
+slave.journal_db_size = 200 * 1024 * 1024
+
+t.start()
+
+slave.zone_wait(catz, udp=False, tsig=True)
+
+for i in range(UPDATES):
+    zone_add = t.zone_rnd(ADD_ZONES, records=5, dnssec=False)
+    t.link(zone_add, master)
+    for z in zone_add:
+        master.zones[z.name].catalog_gen_link(master.zones[catz[0].name])
+    master.gen_confile()
+    master.reload()
+    slave.zones_wait(zone_add)
+
+    zone_rem = []
+    REM_PERCENT = REM_ZONES * 100 / len(master.zones) + 1
+    for z in master.zones:
+        if z != catz[0].name and random.random() * 100  < REM_PERCENT:
+            zone_rem.append(z)
+    serial_bef_rem = slave.zone_wait(catz, udp=False, tsig=True)
+    for z in zone_rem:
+        master.zones.pop(z)
+    master.gen_confile()
+    master.reload()
+    slave.zone_wait(catz, serial_bef_rem, udp=False, tsig=True)
+    t.sleep(5)
+    for z in zone_rem:
+        resp = slave.dig(z, "SOA")
+        if resp.count("SOA") > 0:
+            # allowed: REFUSED (zone not exists)
+            #          NXDOMAIN (in bailiwick of another existing zone)
+            #          NODATA (ditto)
+            # not allowed: NOERROR+data (zone exists with this name)
+            resp.check(rcode="REFUSED")
+
+t.end()
diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py
index 674bf475c3a6a965650840de1063ed8c9416f65e..1b858825db934cc9059b2e3c3c8932147f6bfb33 100644
--- a/tests-extra/tools/dnstest/server.py
+++ b/tests-extra/tools/dnstest/server.py
@@ -1404,6 +1404,7 @@ class Knot(Server):
             s.id_item("id", "catemplate")
             s.item_str("file", self.dir + "/master/%s.zone")
             s.item_str("zonefile-load", "difference")
+            s.item_str("journal-content", z.journal_content)
 
             # this is weird but for the sake of testing, the cataloged zones inherit dnssec policy from catalog zone
             if z.dnssec.enable: