diff --git a/Knot.files b/Knot.files
index bb93e4d13d9abd1129ec73c4d1a68bccb3a298e4..353c15aa79c6f73974eab1f953fde137563e1408 100644
--- a/Knot.files
+++ b/Knot.files
@@ -221,6 +221,8 @@ src/knot/worker/pool.c
 src/knot/worker/pool.h
 src/knot/worker/queue.c
 src/knot/worker/queue.h
+src/knot/zone/adds_tree.c
+src/knot/zone/adds_tree.h
 src/knot/zone/adjust.c
 src/knot/zone/adjust.h
 src/knot/zone/contents.c
diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc
index b8a70a4c2f48e11750001d6e2ac06b343e9b8944..dc916af28d65de3854434e226e758e2a47933451 100644
--- a/src/knot/Makefile.inc
+++ b/src/knot/Makefile.inc
@@ -153,6 +153,8 @@ libknotd_la_SOURCES = \
 	knot/worker/pool.h			\
 	knot/worker/queue.c			\
 	knot/worker/queue.h			\
+	knot/zone/adds_tree.c			\
+	knot/zone/adds_tree.h			\
 	knot/zone/adjust.c			\
 	knot/zone/adjust.h			\
 	knot/zone/contents.c			\
diff --git a/src/knot/zone/adds_tree.c b/src/knot/zone/adds_tree.c
new file mode 100644
index 0000000000000000000000000000000000000000..4dc25a0415cd015c90aa77733c06e65ba01cca23
--- /dev/null
+++ b/src/knot/zone/adds_tree.c
@@ -0,0 +1,232 @@
+/*  Copyright (C) 2019 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 <assert.h>
+#include <stdlib.h>
+
+#include "knot/zone/adds_tree.h"
+
+#include "libknot/error.h"
+#include "libknot/rrtype/rdname.h"
+
+static bool same_node(const zone_node_t *a, const zone_node_t *b)
+{
+	return (a == b || binode_counterpart((zone_node_t *)a) == b);
+}
+
+static int free_a_t_node(trie_val_t *val, void *null)
+{
+	assert(null == NULL);
+	list_t *nodes = *(list_t **)val;
+	ptrlist_free(nodes, NULL);
+	free(nodes);
+	return 0;
+}
+
+void additionals_tree_free(additionals_tree_t *a_t)
+{
+	if (a_t != NULL) {
+		trie_apply(a_t, free_a_t_node, NULL);
+		trie_free(a_t);
+	}
+}
+
+int zone_node_additionals_foreach(const zone_node_t *node, const knot_dname_t *zone_apex,
+                                  zone_node_additionals_cb_t cb, void *ctx)
+{
+	int ret = KNOT_EOK;
+	for (int i = 0; ret == KNOT_EOK && i < node->rrset_count; i++) {
+		struct rr_data *rr_data = &node->rrs[i];
+		for (int j = 0; ret == KNOT_EOK && j < rr_data->rrs.count; j++) {
+			knot_rdata_t *rdata = knot_rdataset_at(&rr_data->rrs, j);
+			const knot_dname_t *name = knot_rdata_name(rdata, rr_data->type);
+
+			if (knot_dname_in_bailiwick(name, zone_apex) > 0) {
+				ret = cb(name, ctx);
+			}
+		}
+	}
+	return ret;
+}
+
+typedef struct {
+	additionals_tree_t *a_t;
+	zone_node_t *node;
+} a_t_node_ctx_t;
+
+static int remove_node_from_a_t(const knot_dname_t *name, void *a_ctx)
+{
+	a_t_node_ctx_t *ctx = a_ctx;
+
+	knot_dname_storage_t lf_storage;
+	uint8_t *lf = knot_dname_lf(name, lf_storage);
+
+	trie_val_t *val = trie_get_try(ctx->a_t, lf + 1, *lf);
+	if (val == NULL) {
+		return KNOT_EOK;
+	}
+
+	list_t *nodes = *(list_t **)val;
+	if (nodes == NULL) {
+		goto rem_empty;
+	}
+
+	ptrnode_t *node_in_list = NULL, *next = NULL;
+	WALK_LIST_DELSAFE(node_in_list, next, *nodes) {
+		if (same_node(node_in_list->d, ctx->node)) {
+			rem_node(&node_in_list->n);
+			free(node_in_list);
+		}
+	}
+
+	if (EMPTY_LIST(*nodes)) {
+		free(nodes);
+rem_empty:
+		trie_del(ctx->a_t, lf + 1, *lf, NULL);
+	}
+
+	return KNOT_EOK;
+}
+
+static int add_node_to_a_t(const knot_dname_t *name, void *a_ctx)
+{
+	a_t_node_ctx_t *ctx = a_ctx;
+
+	knot_dname_storage_t lf_storage;
+	uint8_t *lf = knot_dname_lf(name, lf_storage);
+
+	trie_val_t *val = trie_get_ins(ctx->a_t, lf + 1, *lf);
+	if (*val == NULL) {
+		*val = malloc(sizeof(list_t));
+		if (*val == NULL) {
+			return KNOT_ENOMEM;
+		}
+		init_list(*(list_t **)val);
+	}
+
+	list_t *nodes = *(list_t **)val;
+
+	ptrnode_t *node_in_list = NULL;
+	// TODO optimize more
+	WALK_LIST(node_in_list, *nodes) {
+		if (node_in_list->d == ctx->node) { // optimization: yes a bi-node can be stored twice, but solving it is too slow
+			return KNOT_EOK;
+		}
+	}
+
+	ptrlist_add(nodes, ctx->node, NULL);
+	return KNOT_EOK;
+}
+
+int additionals_tree_update_node(additionals_tree_t *a_t, const knot_dname_t *zone_apex,
+                                 zone_node_t *old_node, zone_node_t *new_node)
+{
+	a_t_node_ctx_t ctx = { a_t, 0 };
+	int ret = KNOT_EOK;
+
+	if (a_t == NULL || zone_apex == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	// for every additional in old_node rrsets, remove mentioning of this node in tree
+	if (old_node != NULL && !(old_node->flags & NODE_FLAGS_DELETED)) {
+		ctx.node = old_node;
+		ret = zone_node_additionals_foreach(old_node, zone_apex, remove_node_from_a_t, &ctx);
+	}
+
+	// for every additional in new_node rrsets, add reverse link into the tree
+	if (new_node != NULL && !(new_node->flags & NODE_FLAGS_DELETED) && ret == KNOT_EOK) {
+		ctx.node = new_node;
+		ret = zone_node_additionals_foreach(new_node, zone_apex, add_node_to_a_t, &ctx);
+	}
+	return ret;
+}
+
+int additionals_tree_from_zone(additionals_tree_t **a_t, const zone_contents_t *zone)
+{
+	*a_t = additionals_tree_new();
+	if (*a_t == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	zone_tree_it_t it = { 0 };
+	int ret = zone_tree_it_begin(zone->nodes, &it);
+	while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
+		ret = additionals_tree_update_node(*a_t, zone->apex->owner, NULL, zone_tree_it_val(&it));
+		zone_tree_it_next(&it);
+	}
+	zone_tree_it_free(&it);
+
+	if (ret != KNOT_EOK) {
+		additionals_tree_free(*a_t);
+		*a_t = NULL;
+	}
+	return ret;
+}
+
+int additionals_tree_update_from_binodes(additionals_tree_t *a_t, const zone_tree_t *tree,
+                                         const knot_dname_t *zone_apex)
+{
+	zone_tree_it_t it = { 0 };
+	int ret = zone_tree_it_begin((zone_tree_t *)tree, &it);
+	while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
+		zone_node_t *node = zone_tree_it_val(&it);
+		ret = additionals_tree_update_node(a_t, zone_apex, binode_counterpart(node), node);
+		zone_tree_it_next(&it);
+	}
+	zone_tree_it_free(&it);
+	return ret;
+}
+
+int additionals_reverse_apply(additionals_tree_t *a_t, const knot_dname_t *name,
+                              node_apply_cb_t cb, void *ctx)
+{
+	knot_dname_storage_t lf_storage;
+	uint8_t *lf = knot_dname_lf(name, lf_storage);
+
+	trie_val_t *val = trie_get_try(a_t, lf + 1, *lf);
+	if (val == NULL) {
+		return KNOT_EOK;
+	}
+
+	list_t *nodes = *(list_t **)val;
+	if (nodes == NULL) {
+		return KNOT_EOK;
+	}
+
+	ptrnode_t *node_in_list = NULL;
+	WALK_LIST(node_in_list, *nodes) {
+		int ret = cb(node_in_list->d, ctx);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+	}
+
+	return KNOT_EOK;
+}
+
+int additionals_reverse_apply_multi(additionals_tree_t *a_t, const zone_tree_t *tree,
+                                    node_apply_cb_t cb, void *ctx)
+{
+	zone_tree_it_t it = { 0 };
+	int ret = zone_tree_it_begin((zone_tree_t *)tree, &it);
+	while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) {
+		ret = additionals_reverse_apply(a_t, zone_tree_it_val(&it)->owner, cb, ctx);
+		zone_tree_it_next(&it);
+	}
+	zone_tree_it_free(&it);
+	return ret;
+}
diff --git a/src/knot/zone/adds_tree.h b/src/knot/zone/adds_tree.h
new file mode 100644
index 0000000000000000000000000000000000000000..7ec4252472714301be732201b58fa785445402a8
--- /dev/null
+++ b/src/knot/zone/adds_tree.h
@@ -0,0 +1,107 @@
+/*  Copyright (C) 2019 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 "contrib/ucw/lists.h"
+#include "knot/zone/contents.h"
+
+typedef trie_t additionals_tree_t;
+
+inline static additionals_tree_t *additionals_tree_new(void) { return trie_create(NULL); }
+void additionals_tree_free(additionals_tree_t *a_t);
+
+/*!
+ * \brief Foreach additional in all node RRSets, do sth.
+ *
+ * \note This is not too related to additionals_tree, might be moved.
+ *
+ * \param node        Zone node with possibly NS, MX, etc rrsets.
+ * \param zone_apex   Name of the zone apex.
+ * \param cb          Callback to be performed.
+ * \param ctx         Arbitrary context for the callback.
+ *
+ * \return KNOT_E*
+ */
+typedef int (*zone_node_additionals_cb_t)(const knot_dname_t *additional, void *ctx);
+int zone_node_additionals_foreach(const zone_node_t *node, const knot_dname_t *zone_apex,
+                                  zone_node_additionals_cb_t cb, void *ctx);
+
+/*!
+ * \brief Update additionals tree according to changed RRsets in a zone node.
+ *
+ * \param a_t         Additionals tree to be updated.
+ * \param zone_apex   Name of the zone.
+ * \param old_node    Old state of the node (additionals will be removed).
+ * \param new_node    New state of the node (additionals will be added)d
+ *
+ * \return KNOT_E*
+ */
+int additionals_tree_update_node(additionals_tree_t *a_t, const knot_dname_t *zone_apex,
+                                 zone_node_t *old_node, zone_node_t *new_node);
+
+/*!
+ * \brief Create additionals tree from a zone (by scanning all additionals in zone RRsets).
+ *
+ * \param a_t    Out: additionals tree to be created (NULL if error).
+ * \param zone   Zone contents.
+ *
+ * \return KNOT_E*
+ */
+int additionals_tree_from_zone(additionals_tree_t **a_t, const zone_contents_t *zone);
+
+/*!
+ * \brief Update attionals tree according to changed RRsets in all nodes in a zone tree.
+ *
+ * \param a_t          Additionals tree to be updated.
+ * \param tree         Zone tree containing updated nodes as bi-nodes.
+ * \param zone_apex    Name of the zone.
+ *
+ * \return KNOT_E*
+ */
+int additionals_tree_update_from_binodes(additionals_tree_t *a_t, const zone_tree_t *tree,
+                                         const knot_dname_t *zone_apex);
+
+/*!
+ * \brief Foreach node that has specified name in its additionals, do sth.
+ *
+ * \note The node passed to the callback might not be correct part of bi-node!
+ *
+ * \param a_t    Additionals reverse tree.
+ * \param name   Name to be looked up in the additionals.
+ * \param cb     Callback to be called.
+ * \param ctx    Arbitrary context for the callback.
+ *
+ * \return KNOT_E*
+ */
+typedef int (*node_apply_cb_t)(zone_node_t *node, void *ctx);
+int additionals_reverse_apply(additionals_tree_t *a_t, const knot_dname_t *name,
+                              node_apply_cb_t cb, void *ctx);
+
+/*!
+ * \brief Call additionals_reverse_apply() for every name in specified tree.
+ *
+ * \param a_t    Additionals reverse tree.
+ * \param tree   Zone tree with names to be looked up in additionals.
+ * \param cb     Callback to be called for each affected node.
+ * \param ctx    Arbitrary context for the callback.
+ *
+ * \return KNOT_E*
+ */
+int additionals_reverse_apply_multi(additionals_tree_t *a_t, const zone_tree_t *tree,
+                                    node_apply_cb_t cb, void *ctx);
+
diff --git a/src/knot/zone/adjust.c b/src/knot/zone/adjust.c
index 581c1cba61c8029a1ea65ba8bc9957b754654775..a504c8c2869e106d8dbbd89f4825667000429861 100644
--- a/src/knot/zone/adjust.c
+++ b/src/knot/zone/adjust.c
@@ -20,6 +20,7 @@
 #include "contrib/macros.h"
 #include "knot/common/log.h"
 #include "knot/dnssec/zone-nsec.h"
+#include "knot/zone/adds_tree.h"
 
 int adjust_cb_flags(zone_node_t *node, const zone_contents_t *zone)
 {
@@ -256,15 +257,6 @@ int adjust_cb_nsec3_and_additionals(zone_node_t *node, const zone_contents_t *zo
 	return ret;
 }
 
-static int adjust_cb_nsec3_and_additionals2(zone_node_t *node, const zone_contents_t *zone)
-{
-	int ret = adjust_cb_point_to_nsec3(node, zone);
-	if (ret == KNOT_EOK) {
-		ret = adjust_cb_additionals(node, zone);
-	}
-	return ret;
-}
-
 int adjust_cb_void(zone_node_t *node, const zone_contents_t *zone)
 {
 	UNUSED(node);
@@ -394,17 +386,43 @@ int zone_adjust_full(zone_contents_t *zone)
 	if (ret == KNOT_EOK) {
 		ret = zone_adjust_contents(zone, adjust_cb_nsec3_and_additionals, NULL, false);
 	}
+	if (ret == KNOT_EOK) {
+		additionals_tree_free(zone->adds_tree);
+		ret = additionals_tree_from_zone(&zone->adds_tree, zone);
+	}
 	return ret;
 }
 
+static int adjust_additionals_cb(zone_node_t *node, void *ctx)
+{
+	const zone_contents_t *zone = ctx;
+	zone_node_t *real_node = binode_node(node, (zone->nodes->flags & ZONE_TREE_BINO_SECOND));
+	return adjust_cb_additionals(real_node, zone);
+}
+
 int zone_adjust_incremental_update(zone_update_t *update)
 {
 	int ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, adjust_cb_nsec3_flags, true);
 	if (ret == KNOT_EOK) {
-		ret = zone_adjust_contents(update->new_cont, adjust_cb_nsec3_and_additionals2, NULL, false);
+		ret = zone_adjust_contents(update->new_cont, adjust_cb_point_to_nsec3, NULL, false);
 	}
 	if (ret == KNOT_EOK) {
 		ret = zone_adjust_update(update, adjust_cb_wildcard_nsec3, NULL);
 	}
+	if (ret == KNOT_EOK) {
+		ret = additionals_tree_update_from_binodes(
+			update->new_cont->adds_tree,
+			update->a_ctx->node_ptrs,
+			update->new_cont->apex->owner
+		);
+	}
+	if (ret == KNOT_EOK) {
+		ret = additionals_reverse_apply_multi(
+			update->new_cont->adds_tree,
+			update->a_ctx->node_ptrs,
+			adjust_additionals_cb,
+			update->new_cont
+		);
+	}
 	return ret;
 }
diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c
index 78cf779e688ac7bd9c0e6da7155dbae037b0a1b6..915110da58c768322bb97b10bd4ca6c739f35334 100644
--- a/src/knot/zone/contents.c
+++ b/src/knot/zone/contents.c
@@ -17,6 +17,7 @@
 #include <assert.h>
 
 #include "libdnssec/error.h"
+#include "knot/zone/adds_tree.h"
 #include "knot/zone/adjust.h"
 #include "knot/zone/contents.h"
 #include "knot/common/log.h"
@@ -495,6 +496,7 @@ int zone_contents_shallow_copy(const zone_contents_t *from, zone_contents_t **to
 	} else {
 		contents->nsec3_nodes = NULL;
 	}
+	contents->adds_tree = from->adds_tree;
 
 	*to = contents;
 	return KNOT_EOK;
@@ -511,6 +513,7 @@ void zone_contents_free(zone_contents_t *contents)
 	zone_tree_free(&contents->nsec3_nodes);
 
 	dnssec_nsec3_params_free(&contents->nsec3_params);
+	additionals_tree_free(contents->adds_tree);
 
 	free(contents);
 }
diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h
index 6ec3fed86f10cf2c35ca80be7f926e77cdcc6100..c1ed7b3724cf1ecb3d332e07a448125219f4836b 100644
--- a/src/knot/zone/contents.h
+++ b/src/knot/zone/contents.h
@@ -32,6 +32,8 @@ typedef struct zone_contents {
 	zone_tree_t *nodes;
 	zone_tree_t *nsec3_nodes;
 
+	trie_t *adds_tree; // "additionals tree" for reverse lookup of nodes affected by additionals
+
 	dnssec_nsec3_params_t nsec3_params;
 	size_t size;
 	uint32_t max_ttl;
diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c
index b90fd9d2663eb7614ce8569c5c2eb5f5c6fc0940..474c6b75aa9d1520400286401df967ab3092ddbc 100644
--- a/src/knot/zone/node.c
+++ b/src/knot/zone/node.c
@@ -130,10 +130,7 @@ zone_node_t *node_new(const knot_dname_t *owner, bool binode, bool second, knot_
 	return ret;
 }
 
-/*!
- * \brief Return the other part of a bi-node.
- */
-static zone_node_t *binode_counterpart(zone_node_t *node)
+zone_node_t *binode_counterpart(zone_node_t *node)
 {
 	zone_node_t *counterpart = NULL;
 
diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h
index e278a27f2d74f698ca43447956721f3b3ba9e9dd..4ca4f1933cd7b294d63001ecaf75c34d4c3b6a13 100644
--- a/src/knot/zone/node.h
+++ b/src/knot/zone/node.h
@@ -150,6 +150,15 @@ inline static zone_node_t *binode_node(zone_node_t *node, bool second)
 	return node + (second - (int)((node->flags & NODE_FLAGS_SECOND) >> 9));
 }
 
+/*!
+ * \brief Return the other node from a bi-node.
+ *
+ * \param node   A node in a bi-node.
+ *
+ * \return The counterpart node in the smae bi-node.
+ */
+zone_node_t *binode_counterpart(zone_node_t *node);
+
 /*!
  * \brief Return true if the rdataset of specified type is shared (shallow-copied) among both parts of bi-node.
  */
diff --git a/tests-extra/tests/ddns/basic/test.py b/tests-extra/tests/ddns/basic/test.py
index 7b8123998db160f75e82b07681f8a68b24a15a21..f6b53148d3f7bcccd5ccde7c82e6454635c6dcce 100644
--- a/tests-extra/tests/ddns/basic/test.py
+++ b/tests-extra/tests/ddns/basic/test.py
@@ -124,6 +124,26 @@ def do_normal_tests(master, zone, dnssec=False):
     resp.check_record(section="additional", rtype="A", rdata="1.2.3.4")
     verify(master, zone, dnssec)
 
+    # add delegation w/o glue
+    check_log("Delegation w/o glue")
+    up = master.update(zone)
+    up.add("deleglue.ddns.", 3600, "NS", "a.deleglue.ddns.")
+    up.send("NOERROR")
+    resp = master.dig("deleglue.ddns.", "NS")
+    resp.check_record(section="authority", rtype="NS", rdata="a.deleglue.ddns.")
+    resp.check_no_rr(section="additional", rname="a.deleglue.ddns.", rtype="A")
+    verify(master, zone, dnssec)
+
+    # add glue to delegation
+    check_log("Glue for existing delegation")
+    up = master.update(zone)
+    up.add("a.deleglue.ddns.", 3600, "A", "10.20.30.40")
+    up.send("NOERROR")
+    resp = master.dig("deleglue.ddns.", "NS")
+    resp.check_record(section="authority", rtype="NS", rdata="a.deleglue.ddns.")
+    resp.check_record(section="additional", rtype="A", rdata="10.20.30.40")
+    verify(master, zone, dnssec)
+
     # add CNAME to node with A records, should be ignored
     check_log("Add CNAME to A node")
     up = master.update(zone)