diff --git a/coccinelle/namedata.cocci b/coccinelle/namedata.cocci
index f3188ae525e..e1469be14c0 100644
--- a/coccinelle/namedata.cocci
+++ b/coccinelle/namedata.cocci
@@ -16,12 +16,13 @@ struct I1
 }
 
 @rule_namedata_strlcpy@
-expression E1, E2;
+identifier I1;
+expression E1;
 symbol NAMEDATALEN;
 @@
-- strlcpy(E1, E2, NAMEDATALEN);
+- strlcpy(I1, E1, NAMEDATALEN);
 + /* You are using strlcpy with NAMEDATALEN, please consider using NameData and namestrcpy instead. */
-+ namestrcpy(E1, E2);
++ namestrcpy(I1, E1);
 
 @rule_namedata_memcpy@
 expression E1, E2;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 650780bab42..8c3d72e3e80 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -21,6 +21,7 @@ set(SOURCES
     extension.c
     extension_constants.c
     expression_utils.c
+    foreign_key.c
     gapfill.c
     guc.c
     histogram.c
diff --git a/src/chunk_constraint.c b/src/chunk_constraint.c
index d6d9219b086..87bbeb60058 100644
--- a/src/chunk_constraint.c
+++ b/src/chunk_constraint.c
@@ -33,6 +33,7 @@
 #include "dimension_vector.h"
 #include "errors.h"
 #include "export.h"
+#include "foreign_key.h"
 #include "hypercube.h"
 #include "hypertable.h"
 #include "partitioning.h"
@@ -512,6 +513,9 @@ ts_chunk_constraints_create(const Hypertable *ht, const Chunk *chunk)
 		Assert(list_length(cookedconstrs) == list_length(newconstrs));
 		CommandCounterIncrement();
 	}
+
+	/* Copy FK triggers to this chunk */
+	ts_chunk_copy_referencing_fk(ht, chunk);
 }
 
 ScanIterator
diff --git a/src/foreign_key.c b/src/foreign_key.c
new file mode 100644
index 00000000000..817c996371a
--- /dev/null
+++ b/src/foreign_key.c
@@ -0,0 +1,518 @@
+/*
+ * This file and its contents are licensed under the Apache License 2.0.
+ * Please see the included NOTICE for copyright information and
+ * LICENSE-APACHE for a copy of the license.
+ */
+
+#include <postgres.h>
+#include "access/attmap.h"
+#include "catalog/pg_trigger.h"
+#include "commands/trigger.h"
+#include "parser/parser.h"
+
+#include "compat/compat.h"
+#include "chunk.h"
+#include "foreign_key.h"
+#include "hypertable.h"
+
+static List *relation_get_referencing_fk(Oid reloid);
+static void constraint_get_trigger(Oid conoid, Oid *updtrigoid, Oid *deltrigoid);
+static char *ChooseForeignKeyConstraintNameAddition(int numkeys, AttrNumber *keys, Oid relid);
+static void createForeignKeyActionTriggers(Form_pg_constraint fk, Oid relid, Oid refRelOid,
+										   Oid constraintOid, Oid indexOid, Oid parentDelTrigger,
+										   Oid parentUpdTrigger);
+static void clone_constraint_on_chunk(const Chunk *chunk, Relation parentRel, Form_pg_constraint fk,
+									  int numfks, AttrNumber *conkey, AttrNumber *confkey,
+									  Oid *conpfeqop, Oid *conppeqop, Oid *conffeqop,
+#if PG15_GE
+									  int numfkdelsetcols, AttrNumber *confdelsetcols,
+#endif
+									  Oid parentDelTrigger, Oid parentUpdTrigger);
+
+static void
+propagate_fk(Relation ht_rel, HeapTuple fk_tuple, List *chunks)
+{
+	Form_pg_constraint fk = (Form_pg_constraint) GETSTRUCT(fk_tuple);
+
+	int numfks;
+	AttrNumber conkey[INDEX_MAX_KEYS];
+	AttrNumber confkey[INDEX_MAX_KEYS];
+	Oid conpfeqop[INDEX_MAX_KEYS];
+	Oid conppeqop[INDEX_MAX_KEYS];
+	Oid conffeqop[INDEX_MAX_KEYS];
+#if PG15_GE
+	int numfkdelsetcols;
+	AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+#endif
+
+	DeconstructFkConstraintRow(fk_tuple,
+							   &numfks,
+							   conkey,
+							   confkey,
+							   conpfeqop,
+							   conppeqop,
+							   conffeqop
+#if PG15_GE
+							   ,
+							   &numfkdelsetcols,
+							   confdelsetcols
+#endif
+	);
+
+	Oid parentDelTrigger, parentUpdTrigger;
+	constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger);
+
+	ListCell *lc;
+	foreach (lc, chunks)
+	{
+		Chunk *chunk = lfirst(lc);
+		clone_constraint_on_chunk(chunk,
+								  ht_rel,
+								  fk,
+								  numfks,
+								  conkey,
+								  confkey,
+								  conpfeqop,
+								  conppeqop,
+								  conffeqop,
+#if PG15_GE
+								  numfkdelsetcols,
+								  confdelsetcols,
+#endif
+								  parentDelTrigger,
+								  parentUpdTrigger);
+	}
+}
+
+/*
+ * Copy foreign key constraints from the main table to a chunk.
+ */
+void
+ts_chunk_copy_referencing_fk(const Hypertable *ht, const Chunk *chunk)
+{
+	ListCell *lc;
+	List *chunks = list_make1((Chunk *) chunk);
+	List *fks = relation_get_referencing_fk(ht->main_table_relid);
+
+	Relation ht_rel = table_open(ht->main_table_relid, AccessShareLock);
+	foreach (lc, fks)
+	{
+		HeapTuple fk_tuple = lfirst(lc);
+		propagate_fk(ht_rel, fk_tuple, chunks);
+	}
+	table_close(ht_rel, NoLock);
+}
+
+void
+ts_fk_propagate(Constraint *c, Hypertable *ht)
+{
+	ListCell *lc1;
+	ListCell *lc2;
+
+	List *fks = relation_get_referencing_fk(ht->main_table_relid);
+	List *chunks = ts_chunk_get_by_hypertable_id(ht->fd.id);
+
+	Relation parentRel = table_open(ht->main_table_relid, AccessShareLock);
+
+	foreach (lc1, fks)
+	{
+		HeapTuple tuple = lfirst(lc1);
+		Form_pg_constraint fk = (Form_pg_constraint) GETSTRUCT(tuple);
+
+		int numfks;
+		AttrNumber conkey[INDEX_MAX_KEYS];
+		AttrNumber confkey[INDEX_MAX_KEYS];
+		Oid conpfeqop[INDEX_MAX_KEYS];
+		Oid conppeqop[INDEX_MAX_KEYS];
+		Oid conffeqop[INDEX_MAX_KEYS];
+#if PG15_GE
+		int numfkdelsetcols;
+		AttrNumber confdelsetcols[INDEX_MAX_KEYS];
+#endif
+
+		DeconstructFkConstraintRow(tuple,
+								   &numfks,
+								   conkey,
+								   confkey,
+								   conpfeqop,
+								   conppeqop,
+								   conffeqop
+#if PG15_GE
+								   ,
+								   &numfkdelsetcols,
+								   confdelsetcols
+#endif
+		);
+
+		Oid parentDelTrigger = InvalidOid;
+		Oid parentUpdTrigger = InvalidOid;
+
+		constraint_get_trigger(fk->oid, &parentUpdTrigger, &parentDelTrigger);
+
+		foreach (lc2, chunks)
+		{
+			Chunk *chunk = lfirst(lc2);
+
+			/*
+			 * We can skip the OSM chunks here because those
+			 * chunks are immutable and therefore don't need
+			 * the update and delete triggers.
+			 */
+			if (chunk->fd.osm_chunk)
+				continue;
+
+			clone_constraint_on_chunk(chunk,
+									  parentRel,
+									  fk,
+									  numfks,
+									  conkey,
+									  confkey,
+									  conpfeqop,
+									  conppeqop,
+									  conffeqop,
+#if PG15_GE
+									  numfkdelsetcols,
+									  confdelsetcols,
+#endif
+									  parentDelTrigger,
+									  parentUpdTrigger);
+		}
+	}
+
+	table_close(parentRel, AccessShareLock);
+}
+
+static void
+clone_constraint_on_chunk(const Chunk *chunk, Relation parentRel, Form_pg_constraint fk, int numfks,
+						  AttrNumber *conkey, AttrNumber *confkey, Oid *conpfeqop, Oid *conppeqop,
+						  Oid *conffeqop,
+#if PG15_GE
+						  int numfkdelsetcols, AttrNumber *confdelsetcols,
+#endif
+						  Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+	AttrNumber mapped_confkey[INDEX_MAX_KEYS];
+	Relation pkrel = table_open(chunk->table_id, AccessShareLock);
+
+	// XXX FIXME allow non-primary as well and check if it's unique
+	// XXX FIXME bail when index is missing
+	Oid indexoid = RelationGetPrimaryKeyIndex(pkrel);
+
+	/* Map the foreign key columns on the hypertable side to the chunk columns */
+#if PG16_GE
+	AttrMap *attmap =
+		build_attrmap_by_name(RelationGetDescr(pkrel), RelationGetDescr(parentRel), false);
+#else
+	AttrMap *attmap = build_attrmap_by_name(RelationGetDescr(pkrel), RelationGetDescr(parentRel));
+#endif
+	for (int i = 0; i < numfks; i++)
+		mapped_confkey[i] = attmap->attnums[confkey[i] - 1];
+
+	table_close(pkrel, NoLock);
+
+	char *conname_addition =
+		ChooseForeignKeyConstraintNameAddition(numfks, confkey, parentRel->rd_id);
+	char *conname = ChooseConstraintName(get_rel_name(fk->conrelid),
+										 conname_addition,
+										 "fkey",
+										 fk->connamespace,
+										 NIL);
+	Oid conoid = CreateConstraintEntry(conname,
+									   fk->connamespace,
+									   CONSTRAINT_FOREIGN,
+									   fk->condeferrable,
+									   fk->condeferred,
+									   fk->convalidated,
+									   fk->oid,
+									   fk->conrelid,
+									   conkey,
+									   numfks,
+									   numfks,
+									   InvalidOid,
+									   indexoid,
+									   chunk->table_id,
+									   mapped_confkey,
+									   conpfeqop,
+									   conppeqop,
+									   conffeqop,
+									   numfks,
+									   fk->confupdtype,
+									   fk->confdeltype,
+#if PG15_GE
+									   confdelsetcols,
+									   numfkdelsetcols,
+#endif
+									   fk->confmatchtype,
+									   NULL,
+									   NULL,
+									   NULL,
+									   false,
+									   1,
+									   false,
+									   false);
+
+	ObjectAddress address, referenced;
+	ObjectAddressSet(address, ConstraintRelationId, conoid);
+	ObjectAddressSet(referenced, ConstraintRelationId, fk->oid);
+	recordDependencyOn(&address, &referenced, DEPENDENCY_INTERNAL);
+
+	CommandCounterIncrement();
+
+	createForeignKeyActionTriggers(fk,
+								   fk->conrelid,
+								   chunk->table_id,
+								   conoid,
+								   indexoid,
+								   parentDelTrigger,
+								   parentUpdTrigger);
+}
+
+/*
+ * Generate the column-name portion of the constraint name for a new foreign
+ * key given the list of column names that reference the referenced
+ * table.  This will be passed to ChooseConstraintName along with the parent
+ * table name and the "fkey" suffix.
+ *
+ * We know that less than NAMEDATALEN characters will actually be used, so we
+ * can truncate the result once we've generated that many.
+ *
+ * This function is based on a function in tablecmds.c in PostgreSQL.
+ */
+static char *
+ChooseForeignKeyConstraintNameAddition(int numkeys, AttrNumber *keys, Oid relid)
+{
+	char buf[NAMEDATALEN * 2];
+	int buflen = 0;
+
+	buf[0] = '\0';
+	for (int i = 0; i < numkeys; i++)
+	{
+		char *name = get_attname(relid, keys[i], false);
+		if (buflen > 0)
+			buf[buflen++] = '_'; /* insert _ between names */
+
+		/*
+		 * At this point we have buflen <= NAMEDATALEN.  name should be less
+		 * than NAMEDATALEN already, but use strlcpy for paranoia.
+		 */
+		strlcpy(buf + buflen, name, NAMEDATALEN);
+		buflen += strlen(buf + buflen);
+		if (buflen >= NAMEDATALEN)
+			break;
+	}
+	return pstrdup(buf);
+}
+
+/*
+ * createForeignKeyActionTriggers
+ *		Create the referenced-side "action" triggers that implement a foreign
+ *		key.
+ */
+static void
+createForeignKeyActionTriggers(Form_pg_constraint fk, Oid relid, Oid refRelOid, Oid constraintOid,
+							   Oid indexOid, Oid parentDelTrigger, Oid parentUpdTrigger)
+{
+	CreateTrigStmt *fk_trigger;
+
+	/*
+	 * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
+	 * DELETE action on the referenced table.
+	 */
+	fk_trigger = makeNode(CreateTrigStmt);
+	fk_trigger->replace = false;
+	fk_trigger->isconstraint = true;
+	fk_trigger->trigname = "RI_ConstraintTrigger_a";
+	fk_trigger->relation = NULL;
+	fk_trigger->args = NIL;
+	fk_trigger->row = true;
+	fk_trigger->timing = TRIGGER_TYPE_AFTER;
+	fk_trigger->events = TRIGGER_TYPE_DELETE;
+	fk_trigger->columns = NIL;
+	fk_trigger->whenClause = NULL;
+	fk_trigger->transitionRels = NIL;
+	fk_trigger->constrrel = NULL;
+	switch (fk->confdeltype)
+	{
+		case FKCONSTR_ACTION_NOACTION:
+			fk_trigger->deferrable = fk->condeferrable;
+			fk_trigger->initdeferred = fk->condeferred;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_del");
+			break;
+		case FKCONSTR_ACTION_RESTRICT:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_del");
+			break;
+		case FKCONSTR_ACTION_CASCADE:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_del");
+			break;
+		case FKCONSTR_ACTION_SETNULL:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_del");
+			break;
+		case FKCONSTR_ACTION_SETDEFAULT:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_del");
+			break;
+		default:
+			elog(ERROR, "unrecognized FK action type: %d", (int) fk->confdeltype);
+			break;
+	}
+
+	CreateTrigger(fk_trigger,
+				  NULL,
+				  refRelOid,
+				  relid,
+				  constraintOid,
+				  indexOid,
+				  InvalidOid,
+				  parentDelTrigger,
+				  NULL,
+				  true,
+				  false);
+
+	/* Make changes-so-far visible */
+	CommandCounterIncrement();
+
+	/*
+	 * Build and execute a CREATE CONSTRAINT TRIGGER statement for the ON
+	 * UPDATE action on the referenced table.
+	 */
+	fk_trigger = makeNode(CreateTrigStmt);
+	fk_trigger->replace = false;
+	fk_trigger->isconstraint = true;
+	fk_trigger->trigname = "RI_ConstraintTrigger_a";
+	fk_trigger->relation = NULL;
+	fk_trigger->args = NIL;
+	fk_trigger->row = true;
+	fk_trigger->timing = TRIGGER_TYPE_AFTER;
+	fk_trigger->events = TRIGGER_TYPE_UPDATE;
+	fk_trigger->columns = NIL;
+	fk_trigger->whenClause = NULL;
+	fk_trigger->transitionRels = NIL;
+	fk_trigger->constrrel = NULL;
+	switch (fk->confupdtype)
+	{
+		case FKCONSTR_ACTION_NOACTION:
+			fk_trigger->deferrable = fk->condeferrable;
+			fk_trigger->initdeferred = fk->condeferred;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_noaction_upd");
+			break;
+		case FKCONSTR_ACTION_RESTRICT:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_restrict_upd");
+			break;
+		case FKCONSTR_ACTION_CASCADE:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_cascade_upd");
+			break;
+		case FKCONSTR_ACTION_SETNULL:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_setnull_upd");
+			break;
+		case FKCONSTR_ACTION_SETDEFAULT:
+			fk_trigger->deferrable = false;
+			fk_trigger->initdeferred = false;
+			fk_trigger->funcname = SystemFuncName("RI_FKey_setdefault_upd");
+			break;
+		default:
+			elog(ERROR, "unrecognized FK action type: %d", (int) fk->confupdtype);
+			break;
+	}
+
+	CreateTrigger(fk_trigger,
+				  NULL,
+				  refRelOid,
+				  relid,
+				  constraintOid,
+				  indexOid,
+				  InvalidOid,
+				  parentUpdTrigger,
+				  NULL,
+				  true,
+				  false);
+
+	/* Make changes-so-far visible */
+	CommandCounterIncrement();
+}
+
+/*
+ * Return a list of foreign key pg_constraint heap tuples referencing reloid.
+ */
+static List *
+relation_get_referencing_fk(Oid reloid)
+{
+	List *result = NIL;
+	Relation conrel;
+	SysScanDesc conscan;
+	ScanKeyData skey[2];
+	HeapTuple htup;
+
+	/* Prepare to scan pg_constraint for entries having confrelid = this rel. */
+	ScanKeyInit(&skey[0],
+				Anum_pg_constraint_confrelid,
+				BTEqualStrategyNumber,
+				F_OIDEQ,
+				ObjectIdGetDatum(reloid));
+
+	ScanKeyInit(&skey[1],
+				Anum_pg_constraint_contype,
+				BTEqualStrategyNumber,
+				F_CHAREQ,
+				CharGetDatum(CONSTRAINT_FOREIGN));
+
+	conrel = table_open(ConstraintRelationId, AccessShareLock);
+	conscan = systable_beginscan(conrel, InvalidOid, false, NULL, 2, skey);
+
+	while (HeapTupleIsValid(htup = systable_getnext(conscan)))
+	{
+		result = lappend(result, heap_copytuple(htup));
+	}
+
+	systable_endscan(conscan);
+	table_close(conrel, AccessShareLock);
+
+	return result;
+}
+
+/* Get the UPDATE and DELETE trigger OIDs for the given constraint OID */
+static void
+constraint_get_trigger(Oid conoid, Oid *updtrigoid, Oid *deltrigoid)
+{
+	Relation rel;
+	SysScanDesc scan;
+	ScanKeyData skey[1];
+	HeapTuple htup;
+
+	*updtrigoid = InvalidOid;
+	*deltrigoid = InvalidOid;
+
+	ScanKeyInit(&skey[0],
+				Anum_pg_trigger_tgconstraint,
+				BTEqualStrategyNumber,
+				F_OIDEQ,
+				ObjectIdGetDatum(conoid));
+
+	rel = table_open(TriggerRelationId, AccessShareLock);
+	scan = systable_beginscan(rel, TriggerConstraintIndexId, true, NULL, 1, skey);
+
+	while (HeapTupleIsValid(htup = systable_getnext(scan)))
+	{
+		Form_pg_trigger trigform = (Form_pg_trigger) GETSTRUCT(htup);
+
+		if ((trigform->tgtype & TRIGGER_TYPE_UPDATE) == TRIGGER_TYPE_UPDATE)
+			*updtrigoid = trigform->oid;
+		if ((trigform->tgtype & TRIGGER_TYPE_DELETE) == TRIGGER_TYPE_DELETE)
+			*deltrigoid = trigform->oid;
+	}
+
+	systable_endscan(scan);
+	table_close(rel, AccessShareLock);
+}
diff --git a/src/foreign_key.h b/src/foreign_key.h
new file mode 100644
index 00000000000..3f78f183508
--- /dev/null
+++ b/src/foreign_key.h
@@ -0,0 +1,16 @@
+/*
+ * This file and its contents are licensed under the Apache License 2.0.
+ * Please see the included NOTICE for copyright information and
+ * LICENSE-APACHE for a copy of the license.
+ */
+
+#include <postgres.h>
+#include <catalog/pg_constraint.h>
+#include <nodes/parsenodes.h>
+
+#include "chunk.h"
+#include "export.h"
+#include "hypertable.h"
+
+extern TSDLLEXPORT void ts_fk_propagate(Constraint *c, Hypertable *ht);
+extern TSDLLEXPORT void ts_chunk_copy_referencing_fk(const Hypertable *ht, const Chunk *chunk);
diff --git a/src/planner/planner.c b/src/planner/planner.c
index 2ed58cfcaf6..c2e520cf2a1 100644
--- a/src/planner/planner.c
+++ b/src/planner/planner.c
@@ -406,6 +406,20 @@ preprocess_query(Node *node, PreprocessQueryContext *context)
 
 					if (ht)
 					{
+						/* SELECT 1 FROM ONLY "foo"."bar" x WHERE "time" OPERATOR(pg_catalog.=) $1
+						 * FOR KEY SHARE OF x */
+						if (query->commandType == CMD_SELECT && query->hasForUpdate &&
+							list_length(query->rtable) == 1 && context->root->glob->boundParams)
+						{
+							RangeTblEntry *rte = linitial_node(RangeTblEntry, query->rtable);
+							if (!rte->inh && rte->rellockmode == RowShareLock &&
+								list_length(query->jointree->fromlist) == 1 &&
+								query->jointree->quals && strcmp(rte->eref->aliasname, "x") == 0)
+							{
+								rte->inh = true;
+							}
+						}
+
 						/* Mark hypertable RTEs we'd like to expand ourselves */
 						if (ts_guc_enable_optimizations && ts_guc_enable_constraint_exclusion &&
 							!IS_UPDL_CMD(context->rootquery) && query->resultRelation == 0 &&
diff --git a/src/process_utility.c b/src/process_utility.c
index 6855daea337..ed8087ca665 100644
--- a/src/process_utility.c
+++ b/src/process_utility.c
@@ -3,8 +3,8 @@
  * Please see the included NOTICE for copyright information and
  * LICENSE-APACHE for a copy of the license.
  */
-#include <postgres.h>
 
+#include <postgres.h>
 #include <access/htup_details.h>
 #include <access/xact.h>
 #include <catalog/index.h>
@@ -56,6 +56,7 @@
 #include "export.h"
 #include "extension.h"
 #include "extension_constants.h"
+#include "foreign_key.h"
 #include "hypercube.h"
 #include "hypertable.h"
 #include "hypertable_cache.h"
@@ -2228,35 +2229,6 @@ process_altertable_drop_column(Hypertable *ht, AlterTableCmd *cmd)
 	}
 }
 
-/* process all regular-table alter commands to make sure they aren't adding
- * foreign-key constraints to hypertables */
-static void
-verify_constraint_plaintable(RangeVar *relation, Constraint *constr)
-{
-	Cache *hcache;
-	Hypertable *ht;
-
-	Assert(IsA(constr, Constraint));
-
-	hcache = ts_hypertable_cache_pin();
-
-	switch (constr->contype)
-	{
-		case CONSTR_FOREIGN:
-			ht = ts_hypertable_cache_get_entry_rv(hcache, constr->pktable);
-
-			if (NULL != ht)
-				ereport(ERROR,
-						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-						 errmsg("foreign keys to hypertables are not supported")));
-			break;
-		default:
-			break;
-	}
-
-	ts_cache_release(hcache);
-}
-
 /*
  * Verify that a constraint is supported on a hypertable.
  */
@@ -2326,9 +2298,7 @@ verify_constraint(RangeVar *relation, Constraint *constr)
 	Cache *hcache = ts_hypertable_cache_pin();
 	Hypertable *ht = ts_hypertable_cache_get_entry_rv(hcache, relation);
 
-	if (NULL == ht)
-		verify_constraint_plaintable(relation, constr);
-	else
+	if (ht)
 		verify_constraint_hypertable(ht, (Node *) constr);
 
 	ts_cache_release(hcache);
@@ -3349,10 +3319,7 @@ process_altertable_start_table(ProcessUtilityArgs *args)
 				col = (ColumnDef *) cmd->def;
 				if (ht && TS_HYPERTABLE_HAS_COMPRESSION_ENABLED(ht))
 					check_altertable_add_column_for_compressed(ht, col);
-				if (NULL == ht)
-					foreach (constraint_lc, col->constraints)
-						verify_constraint_plaintable(stmt->relation, lfirst(constraint_lc));
-				else
+				if (ht)
 					foreach (constraint_lc, col->constraints)
 						verify_constraint_hypertable(ht, lfirst(constraint_lc));
 				break;
@@ -3361,7 +3328,7 @@ process_altertable_start_table(ProcessUtilityArgs *args)
 #if PG16_LT
 			case AT_DropColumnRecurse:
 #endif
-				if (NULL != ht)
+				if (ht)
 					process_altertable_drop_column(ht, cmd);
 				break;
 			case AT_AddConstraint:
@@ -3370,15 +3337,13 @@ process_altertable_start_table(ProcessUtilityArgs *args)
 #endif
 				Assert(IsA(cmd->def, Constraint));
 
-				if (NULL == ht)
-					verify_constraint_plaintable(stmt->relation, (Constraint *) cmd->def);
-				else
+				if (ht)
 					verify_constraint_hypertable(ht, cmd->def);
 				break;
 			case AT_AlterColumnType:
 				Assert(IsA(cmd->def, ColumnDef));
 
-				if (ht != NULL)
+				if (ht)
 					process_alter_column_type_start(ht, cmd);
 				break;
 			case AT_AttachPartition:
@@ -3388,7 +3353,7 @@ process_altertable_start_table(ProcessUtilityArgs *args)
 
 				partstmt = (PartitionCmd *) cmd->def;
 				relation = partstmt->name;
-				Assert(NULL != relation);
+				Assert(relation);
 
 				if (OidIsValid(ts_hypertable_relid(relation)))
 				{
@@ -3856,7 +3821,7 @@ process_altertable_end_table(Node *parsetree, CollectedCommand *cmd)
 
 	ht = ts_hypertable_cache_get_cache_and_entry(relid, CACHE_FLAG_MISSING_OK, &hcache);
 
-	if (NULL != ht)
+	if (ht)
 	{
 		switch (cmd->type)
 		{
@@ -3870,6 +3835,35 @@ process_altertable_end_table(Node *parsetree, CollectedCommand *cmd)
 				break;
 		}
 	}
+	else
+	{
+		/*
+		 * Check any ALTER TABLE command is adding a FOREIGN KEY constraint
+		 * referencing a hypertable.
+		 */
+		if (cmd->type == SCT_AlterTable)
+		{
+			AlterTableStmt *stmt = castNode(AlterTableStmt, parsetree);
+			ListCell *lc;
+
+			foreach (lc, stmt->cmds)
+			{
+				AlterTableCmd *subcmd = (AlterTableCmd *) lfirst(lc);
+
+				if (subcmd->subtype == AT_AddConstraint)
+				{
+					Constraint *c = castNode(Constraint, subcmd->def);
+					if (c->contype == CONSTR_FOREIGN)
+					{
+						Oid relid = RangeVarGetRelid(c->pktable, AccessShareLock, true);
+						ht = ts_hypertable_cache_get_entry(hcache, relid, CACHE_FLAG_MISSING_OK);
+						if (ht)
+							ts_fk_propagate(c, ht);
+					}
+				}
+			}
+		}
+	}
 	ts_cache_release(hcache);
 }
 
diff --git a/tsl/test/expected/foreign_keys.out b/tsl/test/expected/foreign_keys.out
new file mode 100644
index 00000000000..cf692055585
--- /dev/null
+++ b/tsl/test/expected/foreign_keys.out
@@ -0,0 +1,114 @@
+-- This file and its contents are licensed under the Timescale License.
+-- Please see the included NOTICE for copyright information and
+-- LICENSE-TIMESCALE for a copy of the license.
+-- test single column fk constraint from plain table to hypertable during hypertable creation
+CREATE TABLE metrics(time timestamptz primary key, device text, value float);
+SELECT table_name FROM create_hypertable('metrics', 'time');
+ table_name 
+------------
+ metrics
+(1 row)
+
+INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0);
+CREATE TABLE event(time timestamptz references metrics(time) ON DELETE RESTRICT, info text);
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1');
+ERROR:  insert or update on table "event" violates foreign key constraint "event_time_fkey"
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2');
+-- should fail
+\set ON_ERROR_STOP 0
+DELETE FROM metrics WHERE time = '2020-01-01';
+ERROR:  update or delete on table "_hyper_1_1_chunk" violates foreign key constraint "event_time_fkey1" on table "event"
+\set ON_ERROR_STOP 1
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+     conname      | conrelid |               confrelid                | parent 
+------------------+----------+----------------------------------------+--------
+ event_time_fkey  | event    | metrics                                | f
+ event_time_fkey1 | event    | _timescaledb_internal._hyper_1_1_chunk | t
+(2 rows)
+
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+         tgfoid         | parent | tgisinternal | tgconstrrelid 
+------------------------+--------+--------------+---------------
+ "RI_FKey_restrict_del" | f      | t            | event
+ "RI_FKey_noaction_upd" | f      | t            | event
+ "RI_FKey_restrict_del" | t      | t            | event
+ "RI_FKey_noaction_upd" | t      | t            | event
+(4 rows)
+
+-- create new chunk and repeat the test
+INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0);
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1');
+ERROR:  insert or update on table "event" violates foreign key constraint "event_time_fkey"
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2');
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+     conname      | conrelid |               confrelid                | parent 
+------------------+----------+----------------------------------------+--------
+ event_time_fkey  | event    | metrics                                | f
+ event_time_fkey1 | event    | _timescaledb_internal._hyper_1_1_chunk | t
+ event_time_fkey2 | event    | _timescaledb_internal._hyper_1_2_chunk | t
+(3 rows)
+
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+         tgfoid         | parent | tgisinternal | tgconstrrelid 
+------------------------+--------+--------------+---------------
+ "RI_FKey_restrict_del" | f      | t            | event
+ "RI_FKey_noaction_upd" | f      | t            | event
+ "RI_FKey_restrict_del" | t      | t            | event
+ "RI_FKey_noaction_upd" | t      | t            | event
+ "RI_FKey_restrict_del" | t      | t            | event
+ "RI_FKey_noaction_upd" | t      | t            | event
+(6 rows)
+
+DROP TABLE event;
+DROP TABLE metrics;
+-- test single column fk constraint from plain table to hypertable with constraint being added separately
+CREATE TABLE metrics(time timestamptz primary key, device text, value float);
+SELECT table_name FROM create_hypertable('metrics', 'time');
+ table_name 
+------------
+ metrics
+(1 row)
+
+INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0);
+CREATE TABLE event(time timestamptz, info text);
+ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE RESTRICT;
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1');
+ERROR:  insert or update on table "event" violates foreign key constraint "event_time_fkey"
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2');
+-- should fail
+\set ON_ERROR_STOP 0
+DELETE FROM metrics WHERE time = '2020-01-01';
+ERROR:  update or delete on table "_hyper_2_3_chunk" violates foreign key constraint "event_time_fkey1" on table "event"
+\set ON_ERROR_STOP 1
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+     conname      | conrelid |               confrelid                | parent 
+------------------+----------+----------------------------------------+--------
+ event_time_fkey  | event    | metrics                                | f
+ event_time_fkey1 | event    | _timescaledb_internal._hyper_2_3_chunk | t
+(2 rows)
+
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+         tgfoid         | parent | tgisinternal | tgconstrrelid 
+------------------------+--------+--------------+---------------
+ "RI_FKey_restrict_del" | f      | t            | event
+ "RI_FKey_noaction_upd" | f      | t            | event
+ "RI_FKey_restrict_del" | t      | t            | event
+ "RI_FKey_noaction_upd" | t      | t            | event
+(4 rows)
+
+DROP TABLE event;
+DROP TABLE metrics;
+-- test single column fk constraint from hypertable to hypertable
+-- test multi-column fk constraint from plain table to hypertable
diff --git a/tsl/test/sql/CMakeLists.txt b/tsl/test/sql/CMakeLists.txt
index 3320af7fddd..2841c98ff10 100644
--- a/tsl/test/sql/CMakeLists.txt
+++ b/tsl/test/sql/CMakeLists.txt
@@ -31,6 +31,7 @@ set(TEST_FILES
     compression_sorted_merge_distinct.sql
     compression_sorted_merge_columns.sql
     decompress_index.sql
+    foreign_keys.sql
     move.sql
     partialize_finalize.sql
     policy_generalization.sql
diff --git a/tsl/test/sql/foreign_keys.sql b/tsl/test/sql/foreign_keys.sql
new file mode 100644
index 00000000000..459a6aecaa2
--- /dev/null
+++ b/tsl/test/sql/foreign_keys.sql
@@ -0,0 +1,73 @@
+-- This file and its contents are licensed under the Timescale License.
+-- Please see the included NOTICE for copyright information and
+-- LICENSE-TIMESCALE for a copy of the license.
+
+-- test single column fk constraint from plain table to hypertable during hypertable creation
+CREATE TABLE metrics(time timestamptz primary key, device text, value float);
+SELECT table_name FROM create_hypertable('metrics', 'time');
+
+INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0);
+
+CREATE TABLE event(time timestamptz references metrics(time) ON DELETE RESTRICT, info text);
+
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1');
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2');
+
+-- should fail
+\set ON_ERROR_STOP 0
+DELETE FROM metrics WHERE time = '2020-01-01';
+\set ON_ERROR_STOP 1
+
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+
+-- create new chunk and repeat the test
+INSERT INTO metrics(time, device, value) VALUES ('2021-01-01', 'd1', 1.0);
+
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2021-01-02', 'info1');
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2021-01-01', 'info2');
+
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+
+DROP TABLE event;
+DROP TABLE metrics;
+
+-- test single column fk constraint from plain table to hypertable with constraint being added separately
+CREATE TABLE metrics(time timestamptz primary key, device text, value float);
+SELECT table_name FROM create_hypertable('metrics', 'time');
+
+INSERT INTO metrics(time, device, value) VALUES ('2020-01-01', 'd1', 1.0);
+
+CREATE TABLE event(time timestamptz, info text);
+
+ALTER TABLE event ADD CONSTRAINT event_time_fkey FOREIGN KEY (time) REFERENCES metrics(time) ON DELETE RESTRICT;
+
+-- should fail
+\set ON_ERROR_STOP 0
+INSERT INTO event(time, info) VALUES ('2020-01-02', 'info1');
+\set ON_ERROR_STOP 1
+-- should succeed
+INSERT INTO event(time, info) VALUES ('2020-01-01', 'info2');
+
+-- should fail
+\set ON_ERROR_STOP 0
+DELETE FROM metrics WHERE time = '2020-01-01';
+\set ON_ERROR_STOP 1
+
+SELECT conname, conrelid::regclass, confrelid::regclass, conparentid <> 0 AS parent FROM pg_constraint WHERE conrelid='event'::regclass ORDER BY oid;
+SELECT tgfoid::regproc, tgparentid <> 0 AS parent, tgisinternal, tgconstrrelid::regclass FROM pg_trigger WHERE tgconstrrelid='event'::regclass ORDER BY oid;
+
+DROP TABLE event;
+DROP TABLE metrics;
+
+-- test single column fk constraint from hypertable to hypertable
+-- test multi-column fk constraint from plain table to hypertable