diff --git a/ibis/ibisfeatures.h b/ibis/ibisfeatures.h
--- a/ibis/ibisfeatures.h
+++ b/ibis/ibisfeatures.h
@@ -48,6 +48,18 @@
 G_DECLARE_FINAL_TYPE(IbisFeatures, ibis_features, IBIS, FEATURES, GObject)
 
 /**
+ * ibis_features_clear:
+ * @features: The instance.
+ *
+ * Clears all features from @features and sets the default values per
+ * https://modern.ircdocs.horse/#rplisupport-parameters.
+ *
+ * Since: 0.4
+ */
+IBIS_AVAILABLE_IN_ALL
+void ibis_features_clear(IbisFeatures *features);
+
+/**
  * ibis_features_new:
  *
  * Create a new features object.
# HG changeset patch
# Node ID d7f7cf99e66b4c2a510ed667ccff6878b04732e8
# Parent  d9905faa5d3f833eaa9e921794a9e72839d787f3
diff --git a/ibis/ibisfeatures.c b/ibis/ibisfeatures.c
--- a/ibis/ibisfeatures.c
+++ b/ibis/ibisfeatures.c
@@ -24,35 +24,10 @@
 #include "ibisstring.h"
 
 enum {
-	PROP_0,
-	PROP_AWAYLEN,
-	PROP_CASEMAPPING,
-	PROP_CHANLIMIT,
-	PROP_CHANMODES,
-	PROP_CHANNELLEN,
-	PROP_CHANTYPES,
-	PROP_ELIST,
-	PROP_EXCEPTS,
-	PROP_EXTBAN,
-	PROP_HOSTLEN,
-	PROP_INVEX,
-	PROP_KICKLEN,
-	PROP_MAXLIST,
-	PROP_MAXTARGETS,
-	PROP_MODES,
-	PROP_NETWORK,
-	PROP_NICKLEN,
-	PROP_PREFIX,
-	PROP_SAFELIST,
-	PROP_SILENCE,
-	PROP_STATUSMSG,
-	PROP_TARGMAX,
-	PROP_TOPICLEN,
-	PROP_USERLEN,
-	N_PROPERTIES,
+	SIG_CHANGED,
+	N_SIGNALS,
 };
-
-static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+static guint signals[N_SIGNALS] = {};
 
 struct _IbisFeatures {
 	GObject parent;
@@ -63,7 +38,7 @@
 /******************************************************************************
  * Helpers
  *****************************************************************************/
-void
+static void
 ibis_features_free_gvalue(gpointer data) {
 	GValue* value = data;
 	g_value_unset(value);
@@ -71,15 +46,18 @@
 }
 
 static void
-ibis_features_set_uint(IbisFeatures *features, const char *name,
-                       guint uvalue, GParamSpec *pspec)
+ibis_features_set_uint(IbisFeatures *features, const char *feature,
+                       guint uvalue)
 {
+	GQuark quark = g_quark_from_string(feature);
 	GValue *existing_value = NULL;
 	GValue *value = NULL;
 
-	existing_value = g_hash_table_lookup(features->features, name);
+	existing_value = g_hash_table_lookup(features->features,
+	                                     GINT_TO_POINTER(quark));
 	if(existing_value != NULL &&
-	   g_value_get_uint(existing_value) == uvalue) {
+	   g_value_get_uint(existing_value) == uvalue)
+	{
 		return;
 	}
 
@@ -87,21 +65,21 @@
 	g_value_init(value, G_TYPE_UINT);
 	g_value_set_uint(value, uvalue);
 
-	g_hash_table_insert(features->features, g_strdup(name), value);
+	g_hash_table_insert(features->features, GINT_TO_POINTER(quark), value);
 
-	if(pspec != NULL) {
-		g_object_notify_by_pspec(G_OBJECT(features), pspec);
-	}
+	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
 }
 
 static void
-ibis_features_set_string(IbisFeatures *features, const char *name,
-                         const char *svalue, GParamSpec *pspec)
+ibis_features_set_string(IbisFeatures *features, const char *feature,
+                         const char *svalue)
 {
+	GQuark quark = g_quark_from_string(feature);
 	GValue *existing_value = NULL;
 	GValue *value = NULL;
 
-	existing_value = g_hash_table_lookup(features->features, name);
+	existing_value = g_hash_table_lookup(features->features,
+	                                     GINT_TO_POINTER(quark));
 	if(existing_value != NULL &&
 	   ibis_str_equal(g_value_get_string(existing_value), svalue)) {
 		return;
@@ -111,23 +89,24 @@
 	g_value_init(value, G_TYPE_STRING);
 	g_value_set_string(value, svalue);
 
-	g_hash_table_insert(features->features, g_strdup(name), value);
+	g_hash_table_insert(features->features, GINT_TO_POINTER(quark), value);
 
-	if(pspec != NULL) {
-		g_object_notify_by_pspec(G_OBJECT(features), pspec);
-	}
+	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
 }
 
 static void
-ibis_features_set_char(IbisFeatures *features, const char *name,
-                       char cvalue, GParamSpec *pspec)
+ibis_features_set_char(IbisFeatures *features, const char *feature,
+                       char cvalue)
 {
+	GQuark quark = g_quark_from_string(feature);
 	GValue *existing_value = NULL;
 	GValue *value = NULL;
 
-	existing_value = g_hash_table_lookup(features->features, name);
+	existing_value = g_hash_table_lookup(features->features,
+	                                     GINT_TO_POINTER(quark));
 	if(existing_value != NULL &&
-	   g_value_get_schar(existing_value) == cvalue) {
+	   g_value_get_schar(existing_value) == cvalue)
+	{
 		return;
 	}
 
@@ -135,23 +114,24 @@
 	g_value_init(value, G_TYPE_CHAR);
 	g_value_set_schar(value, cvalue);
 
-	g_hash_table_insert(features->features, g_strdup(name), value);
+	g_hash_table_insert(features->features, GINT_TO_POINTER(quark), value);
 
-	if(pspec != NULL) {
-		g_object_notify_by_pspec(G_OBJECT(features), pspec);
-	}
+	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
 }
 
 static void
-ibis_features_set_boolean(IbisFeatures *features, const char *name,
-                          gboolean bvalue, GParamSpec *pspec)
+ibis_features_set_boolean(IbisFeatures *features, const char *feature,
+                          gboolean bvalue)
 {
+	GQuark quark = g_quark_from_string(feature);
 	GValue *existing_value = NULL;
 	GValue *value = NULL;
 
-	existing_value = g_hash_table_lookup(features->features, name);
+	existing_value = g_hash_table_lookup(features->features,
+	                                     GINT_TO_POINTER(quark));
 	if(existing_value != NULL &&
-	   g_value_get_boolean(existing_value) == bvalue) {
+	   g_value_get_boolean(existing_value) == bvalue)
+	{
 		return;
 	}
 
@@ -159,11 +139,9 @@
 	g_value_init(value, G_TYPE_BOOLEAN);
 	g_value_set_boolean(value, bvalue);
 
-	g_hash_table_insert(features->features, g_strdup(name), value);
+	g_hash_table_insert(features->features, GINT_TO_POINTER(quark), value);
 
-	if(pspec != NULL) {
-		g_object_notify_by_pspec(G_OBJECT(features), pspec);
-	}
+	g_signal_emit(features, signals[SIG_CHANGED], quark, feature);
 }
 
 static void
@@ -174,58 +152,45 @@
 
 	if(ibis_str_equal(feature, IBIS_FEATURE_AWAYLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_AWAYLEN,
-		                       ibis_str_to_uint(value), properties[PROP_AWAYLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CASEMAPPING)) {
-		ibis_features_set_string(features, IBIS_FEATURE_CASEMAPPING, value,
-		                         properties[PROP_CASEMAPPING]);
+		ibis_features_set_string(features, IBIS_FEATURE_CASEMAPPING, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANLIMIT)) {
-		ibis_features_set_string(features, IBIS_FEATURE_CHANLIMIT, value,
-		                         properties[PROP_CHANLIMIT]);
+		ibis_features_set_string(features, IBIS_FEATURE_CHANLIMIT, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANMODES)) {
-		ibis_features_set_string(features, IBIS_FEATURE_CHANMODES, value,
-		                         properties[PROP_CHANMODES]);
+		ibis_features_set_string(features, IBIS_FEATURE_CHANMODES, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANNELLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_CHANNELLEN,
-		                       ibis_str_to_uint(value),
-		                       properties[PROP_CHANNELLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_CHANTYPES)) {
-		ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, value,
-		                         properties[PROP_CHANTYPES]);
+		ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_ELIST)) {
-		ibis_features_set_string(features, IBIS_FEATURE_ELIST, value,
-		                         properties[PROP_ELIST]);
+		ibis_features_set_string(features, IBIS_FEATURE_ELIST, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXCEPTS)) {
 		if(strlen(value) == 1) {
-			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, value[0],
-			                       properties[PROP_EXCEPTS]);
+			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, value[0]);
 		} else {
-			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, 'e',
-			                       properties[PROP_EXCEPTS]);
+			ibis_features_set_char(features, IBIS_FEATURE_EXCEPTS, 'e');
 		}
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_EXTBAN)) {
-		ibis_features_set_string(features, IBIS_FEATURE_EXTBAN, value,
-		                         properties[PROP_EXTBAN]);
+		ibis_features_set_string(features, IBIS_FEATURE_EXTBAN, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_HOSTLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_HOSTLEN,
-		                       ibis_str_to_uint(value), properties[PROP_HOSTLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_INVEX)) {
 		if(strlen(value) == 1) {
-			ibis_features_set_char(features, IBIS_FEATURE_INVEX, value[0],
-			                       properties[PROP_INVEX]);
+			ibis_features_set_char(features, IBIS_FEATURE_INVEX, value[0]);
 		} else {
-			ibis_features_set_char(features, IBIS_FEATURE_INVEX, 'I',
-			                       properties[PROP_INVEX]);
+			ibis_features_set_char(features, IBIS_FEATURE_INVEX, 'I');
 		}
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_KICKLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_KICKLEN,
-		                       ibis_str_to_uint(value), properties[PROP_KICKLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXLIST)) {
-		ibis_features_set_string(features, IBIS_FEATURE_MAXLIST, value,
-		                         properties[PROP_MAXLIST]);
+		ibis_features_set_string(features, IBIS_FEATURE_MAXLIST, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_MAXTARGETS)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_MAXTARGETS,
-		                       ibis_str_to_uint(value),
-		                       properties[PROP_MAXTARGETS]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_MODES)) {
 		guint uvalue;
 		if(strlen(value) > 0) {
@@ -233,56 +198,45 @@
 		} else {
 			uvalue = G_MAXUINT32;
 		}
-		ibis_features_set_uint(features, IBIS_FEATURE_MODES,
-		                       uvalue, properties[PROP_MODES]);
+		ibis_features_set_uint(features, IBIS_FEATURE_MODES, uvalue);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_NETWORK)) {
-		ibis_features_set_string(features, IBIS_FEATURE_NETWORK, value,
-		                         properties[PROP_NETWORK]);
+		ibis_features_set_string(features, IBIS_FEATURE_NETWORK, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_NICKLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_NICKLEN,
-		                       ibis_str_to_uint(value), properties[PROP_NICKLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_PREFIX)) {
-		ibis_features_set_string(features, IBIS_FEATURE_PREFIX, value,
-		                         properties[PROP_PREFIX]);
+		ibis_features_set_string(features, IBIS_FEATURE_PREFIX, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_SAFELIST)) {
-		ibis_features_set_boolean(features, IBIS_FEATURE_SAFELIST, TRUE,
-		                          properties[PROP_SAFELIST]);
+		ibis_features_set_boolean(features, IBIS_FEATURE_SAFELIST, TRUE);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_SILENCE)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_SILENCE,
-		                       ibis_str_to_uint(value), properties[PROP_SILENCE]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_STATUSMSG)) {
-		ibis_features_set_string(features, IBIS_FEATURE_STATUSMSG, value,
-		                         properties[PROP_STATUSMSG]);
+		ibis_features_set_string(features, IBIS_FEATURE_STATUSMSG, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_TARGMAX)) {
-		ibis_features_set_string(features, IBIS_FEATURE_TARGMAX, value,
-		                         properties[PROP_TARGMAX]);
+		ibis_features_set_string(features, IBIS_FEATURE_TARGMAX, value);
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_TOPICLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_TOPICLEN,
-		                       ibis_str_to_uint(value),
-		                       properties[PROP_TOPICLEN]);
+		                       ibis_str_to_uint(value));
 	} else if(ibis_str_equal(feature, IBIS_FEATURE_USERLEN)) {
 		ibis_features_set_uint(features, IBIS_FEATURE_USERLEN,
-		                       ibis_str_to_uint(value), properties[PROP_USERLEN]);
+		                       ibis_str_to_uint(value));
 	} else {
 		GRegex *regex = NULL;
-		GValue *variant_value = g_new0(GValue, 1);
 
-		/* we're assuming we only get positive integers when receiving numeric
-		 * values
+		/* We're assuming we only get positive integers when receiving numeric
+		 * values.
 		 */
 		regex = g_regex_new("^[0-9]+$", 0, 0, NULL);
 
 		if(g_regex_match(regex, value, 0, NULL)) {
-			g_value_init(variant_value, G_TYPE_UINT);
-			g_value_set_uint(variant_value, ibis_str_to_uint(value));
+			ibis_features_set_uint(features, feature, ibis_str_to_uint(value));
 		} else if(!ibis_str_is_empty(value)) {
-			g_value_init(variant_value, G_TYPE_STRING);
-			g_value_set_string(variant_value, value);
+			ibis_features_set_string(features, feature, value);
 		} else {
-			g_value_init(variant_value, G_TYPE_BOOLEAN);
-			g_value_set_boolean(variant_value, TRUE);
+			ibis_features_set_boolean(features, feature, TRUE);
 		}
-		g_hash_table_insert(features->features, g_strdup(feature), variant_value);
+
 		g_regex_unref(regex);
 	}
 }
@@ -300,405 +254,77 @@
 
 	G_OBJECT_CLASS(ibis_features_parent_class)->finalize(obj);
 }
-static void
-ibis_features_get_property(GObject *obj, guint param_id, GValue *value,
-                           GParamSpec *pspec)
-{
-	IbisFeatures *features = IBIS_FEATURES(obj);
-
-	switch(param_id) {
-	case PROP_AWAYLEN:
-		g_value_set_uint(value, ibis_features_get_awaylen(features));
-		break;
-	case PROP_CASEMAPPING:
-		g_value_set_string(value, ibis_features_get_casemapping(features));
-		break;
-	case PROP_CHANLIMIT:
-		g_value_set_string(value, ibis_features_get_chanlimit(features));
-		break;
-	case PROP_CHANMODES:
-		g_value_set_string(value, ibis_features_get_chanmodes(features));
-		break;
-	case PROP_CHANNELLEN:
-		g_value_set_uint(value, ibis_features_get_channellen(features));
-		break;
-	case PROP_CHANTYPES:
-		g_value_set_string(value, ibis_features_get_chantypes(features));
-		break;
-	case PROP_ELIST:
-		g_value_set_string(value, ibis_features_get_elist(features));
-		break;
-	case PROP_EXCEPTS:
-		g_value_set_schar(value, ibis_features_get_excepts(features));
-		break;
-	case PROP_EXTBAN:
-		g_value_set_string(value, ibis_features_get_extban(features));
-		break;
-	case PROP_HOSTLEN:
-		g_value_set_uint(value, ibis_features_get_hostlen(features));
-		break;
-	case PROP_INVEX:
-		g_value_set_schar(value, ibis_features_get_invex(features));
-		break;
-	case PROP_KICKLEN:
-		g_value_set_uint(value, ibis_features_get_kicklen(features));
-		break;
-	case PROP_MAXLIST:
-		g_value_set_string(value, ibis_features_get_maxlist(features));
-		break;
-	case PROP_MAXTARGETS:
-		g_value_set_uint(value, ibis_features_get_maxtargets(features));
-		break;
-	case PROP_MODES:
-		g_value_set_uint(value, ibis_features_get_modes(features));
-		break;
-	case PROP_NETWORK:
-		g_value_set_string(value, ibis_features_get_network(features));
-		break;
-	case PROP_NICKLEN:
-		g_value_set_uint(value, ibis_features_get_nicklen(features));
-		break;
-	case PROP_PREFIX:
-		g_value_set_string(value, ibis_features_get_prefix(features));
-		break;
-	case PROP_SAFELIST:
-		g_value_set_boolean(value, ibis_features_get_safelist(features));
-		break;
-	case PROP_SILENCE:
-		g_value_set_uint(value, ibis_features_get_silence(features));
-		break;
-	case PROP_STATUSMSG:
-		g_value_set_string(value, ibis_features_get_statusmsg(features));
-		break;
-	case PROP_TARGMAX:
-		g_value_set_string(value, ibis_features_get_targmax(features));
-		break;
-	case PROP_TOPICLEN:
-		g_value_set_uint(value, ibis_features_get_topiclen(features));
-		break;
-	case PROP_USERLEN:
-		g_value_set_uint(value, ibis_features_get_userlen(features));
-		break;
-	default:
-		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
-		break;
-	}
-}
 
 static void
 ibis_features_init(IbisFeatures *features) {
-	features->features = g_hash_table_new_full(g_str_hash, g_str_equal,
-	                                                 g_free,
-	                                                 ibis_features_free_gvalue);
-	/* set the default values specified at
-	 * https://modern.ircdocs.horse/#rplisupport-parameters
-	 */
-	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#", NULL);
-	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, "(ov)@+", NULL);
+	features->features = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+	                                           NULL,
+	                                           ibis_features_free_gvalue);
+
+	ibis_features_clear(features);
 }
 
 static void
 ibis_features_class_init(IbisFeaturesClass *klass) {
 	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
 
-	obj_class->get_property = ibis_features_get_property;
 	obj_class->finalize = ibis_features_finalize;
 
 	/**
-	 * IbisFeatures:awaylen:
-	 *
-	 * The [const@FEATURE_AWAYLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_AWAYLEN] = g_param_spec_uint(
-		"awaylen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:casemapping:
-	 *
-	 * The [const@FEATURE_CASEMAPPING] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_CASEMAPPING] = g_param_spec_string(
-		"casemapping", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:chanlimit:
-	 *
-	 * The [const@FEATURE_CHANLIMIT] parameter of the features object.
+	 * IbisFeatures::changed:
+	 * @features: The instance.
+	 * @feature: The name of the feature that changed.
 	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_CHANLIMIT] = g_param_spec_string(
-		"chanlimit", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:chanmodes:
-	 *
-	 * The [const@FEATURE_CHANMODES] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_CHANMODES] = g_param_spec_string(
-		"chanmodes", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:channellen:
-	 *
-	 * The [const@FEATURE_CHANNELLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_CHANNELLEN] = g_param_spec_uint(
-		"channellen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:chantypes:
-	 *
-	 * The [const@FEATURE_CHANTYPES] parameter of the features object.
+	 * Emitted when a feature changes.
 	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_CHANTYPES] = g_param_spec_string(
-		"chantypes", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:elist:
-	 *
-	 * The [const@FEATURE_ELIST] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_ELIST] = g_param_spec_string(
-		"elist", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:excepts:
-	 *
-	 * The [const@FEATURE_EXCEPTS] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_EXCEPTS] = g_param_spec_char(
-		"excepts", NULL, NULL,
-		0, G_MAXINT8, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:extban:
-	 *
-	 * The [const@FEATURE_EXTBAN] parameter of the features object.
+	 * This signal supports *details* so you can connect to it with something
+	 * like `changed::chantypes` so your callback is only called if the
+	 * `chantypes` feature changed.
 	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_EXTBAN] = g_param_spec_string(
-		"extban", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:hostlen:
-	 *
-	 * The [const@FEATURE_HOSTLEN] parameter of the features object.
+	 * Note: This will also be called when a feature is removed.
 	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_HOSTLEN] = g_param_spec_uint(
-		"hostlen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:invex:
-	 *
-	 * The [const@FEATURE_INVEX] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_INVEX] = g_param_spec_char(
-		"invex", NULL, NULL,
-		0, G_MAXINT8, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:kicklen:
-	 *
-	 * The [const@FEATURE_KICKLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
+	 * Since: 0.4
 	 */
-	properties[PROP_KICKLEN] = g_param_spec_uint(
-		"kicklen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:maxlist:
-	 *
-	 * The [const@FEATURE_MAXLIST] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_MAXLIST] = g_param_spec_string(
-		"maxlist", NULL, NULL,
+	signals[SIG_CHANGED] = g_signal_new_class_handler(
+		"changed",
+		G_OBJECT_CLASS_TYPE(klass),
+		G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
 		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:maxtargets:
-	 *
-	 * The [const@FEATURE_MAXTARGETS] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_MAXTARGETS] = g_param_spec_uint(
-		"maxtargets", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:modes:
-	 *
-	 * The [const@FEATURE_MODES] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_MODES] = g_param_spec_uint(
-		"modes", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:network:
-	 *
-	 * The [const@FEATURE_NETWORK] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_NETWORK] = g_param_spec_string(
-		"network", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:nicklen:
-	 *
-	 * The [const@FEATURE_NICKLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_NICKLEN] = g_param_spec_uint(
-		"nicklen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:prefix:
-	 *
-	 * The [const@FEATURE_PREFIX] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_PREFIX] = g_param_spec_string(
-		"prefix", NULL, NULL,
 		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:safelist:
-	 *
-	 * The [const@FEATURE_SAFELIST] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_SAFELIST] = g_param_spec_boolean(
-		"safelist", NULL, NULL,
-		FALSE,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:silence:
-	 *
-	 * The [const@FEATURE_SILENCE] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_SILENCE] = g_param_spec_uint(
-		"silence", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:statusmsg:
-	 *
-	 * The [const@FEATURE_STATUSMSG] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_STATUSMSG] = g_param_spec_string(
-		"statusmsg", NULL, NULL,
+		NULL,
 		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:targmax:
-	 *
-	 * The [const@FEATURE_TARGMAX] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_TARGMAX] = g_param_spec_string(
-		"targmax", NULL, NULL,
-		NULL,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:topiclen:
-	 *
-	 * The [const@FEATURE_TOPICLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_TOPICLEN] = g_param_spec_uint(
-		"topiclen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	/**
-	 * IbisFeatures:userlen:
-	 *
-	 * The [const@FEATURE_USERLEN] parameter of the features object.
-	 *
-	 * Since: 0.3
-	 */
-	properties[PROP_USERLEN] = g_param_spec_uint(
-		"userlen", NULL, NULL,
-		0, G_MAXUINT32, 0,
-		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
-
-	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+		G_TYPE_NONE,
+		1,
+		G_TYPE_STRING);
 }
 
-
 /******************************************************************************
  * Public API
  *****************************************************************************/
+void
+ibis_features_clear(IbisFeatures *features) {
+	GHashTableIter iter;
+	gpointer key = NULL;
+
+	g_return_if_fail(IBIS_IS_FEATURES(features));
+
+	g_hash_table_iter_init(&iter, features->features);
+	while(g_hash_table_iter_next(&iter, &key, NULL)) {
+		GQuark detail = GPOINTER_TO_INT(key);
+
+		g_hash_table_iter_remove(&iter);
+
+		g_signal_emit(features, signals[SIG_CHANGED], detail,
+		              g_quark_to_string(detail));
+	}
+
+	/* set the default values specified at
+	 * https://modern.ircdocs.horse/#rplisupport-parameters
+	 */
+	ibis_features_set_string(features, IBIS_FEATURE_CHANTYPES, "#");
+	ibis_features_set_string(features, IBIS_FEATURE_PREFIX, "(ov)@+");
+}
+
 IbisFeatures *
 ibis_features_new(void) {
 	return g_object_new(IBIS_TYPE_FEATURES, NULL);
@@ -719,14 +345,9 @@
 		return FALSE;
 	}
 
-	/* Freeze notification since ibis_features_set_feature will notify for changed
-	 * properties.
-	 */
-	g_object_freeze_notify(G_OBJECT(features));
-
 	params = ibis_message_get_params(message);
 	param_count = g_strv_length(params);
-	regex = g_regex_new("(?<key>[A-Z0-9]+)(?:=(?<value>\\S+))?", 0, 0, NULL);
+	regex = g_regex_new("(?<key>[A-Z0-9-]+)(?:=(?<value>\\S+))?", 0, 0, NULL);
 
 	for(i = 0; i < param_count; ++i) {
 		matches = g_regex_match_full(regex, params[i], -1, 0, 0, &info, NULL);
@@ -736,6 +357,7 @@
 
 			key = g_match_info_fetch_named(info, "key");
 			value = g_match_info_fetch_named(info, "value");
+
 			ibis_features_set_feature(features, key, value);
 			g_free(key);
 			g_free(value);
@@ -744,8 +366,6 @@
 	}
 	g_regex_unref(regex);
 
-	g_object_thaw_notify(G_OBJECT(features));
-
 	return TRUE;
 }
 
@@ -919,10 +539,13 @@
 
 guint
 ibis_features_get_uint(IbisFeatures *features, const char *parameter) {
+	GQuark quark = 0;
 	GValue *value = NULL;
+
 	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);
 
-	value = g_hash_table_lookup(features->features, parameter);
+	quark = g_quark_from_string(parameter);
+	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));
 
 	if(value == NULL || !G_VALUE_HOLDS_UINT(value)) {
 		return 0;
@@ -933,10 +556,13 @@
 
 const char *
 ibis_features_get_string(IbisFeatures *features, const char *parameter) {
+	GQuark quark = 0;
 	GValue *value = NULL;
+
 	g_return_val_if_fail(IBIS_IS_FEATURES(features), NULL);
 
-	value = g_hash_table_lookup(features->features, parameter);
+	quark = g_quark_from_string(parameter);
+	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));
 
 	if(value == NULL || !G_VALUE_HOLDS_STRING(value)) {
 		return NULL;
@@ -947,10 +573,13 @@
 
 gboolean
 ibis_features_get_boolean(IbisFeatures *features, const char *parameter) {
+	GQuark quark = 0;
 	GValue *value = NULL;
+
 	g_return_val_if_fail(IBIS_IS_FEATURES(features), FALSE);
 
-	value = g_hash_table_lookup(features->features, parameter);
+	quark = g_quark_from_string(parameter);
+	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));
 
 	if(value == NULL || !G_VALUE_HOLDS_BOOLEAN(value)) {
 		return FALSE;
@@ -961,10 +590,13 @@
 
 char
 ibis_features_get_char(IbisFeatures *features, const char *parameter) {
+	GQuark quark = 0;
 	GValue *value = NULL;
+
 	g_return_val_if_fail(IBIS_IS_FEATURES(features), 0);
 
-	value = g_hash_table_lookup(features->features, parameter);
+	quark = g_quark_from_string(parameter);
+	value = g_hash_table_lookup(features->features, GINT_TO_POINTER(quark));
 
 	if(value == NULL || !G_VALUE_HOLDS_CHAR(value)) {
 		return 0;
diff --git a/ibis/tests/test_features.c b/ibis/tests/test_features.c
--- a/ibis/tests/test_features.c
+++ b/ibis/tests/test_features.c
@@ -28,11 +28,12 @@
  * Callbacks
  *****************************************************************************/
 static void
-test_ibis_features_property_notify_counter(G_GNUC_UNUSED GObject *self,
-                                           G_GNUC_UNUSED GParamSpec *pspec,
-                                           gpointer data)
+test_ibis_features_changed_counter(G_GNUC_UNUSED GObject *self,
+                                   G_GNUC_UNUSED GParamSpec *pspec,
+                                   gpointer data)
 {
 	guint *counter = data;
+
 	*counter = *counter + 1;
 }
 
@@ -106,8 +107,8 @@
 	ibis_message_set_paramsv(message, msg_params);
 
 	features = ibis_features_new();
-	g_signal_connect(features, "notify",
-	                 G_CALLBACK(test_ibis_features_property_notify_counter),
+	g_signal_connect(features, "changed",
+	                 G_CALLBACK(test_ibis_features_changed_counter),
 	                 &notify_counter);
 	g_assert_cmpuint(notify_counter, ==, 0);
 
@@ -197,6 +198,7 @@
 	IbisFeatures *features = NULL;
 
 	features = ibis_features_new();
+
 	g_assert_cmpuint(ibis_features_get_awaylen(features), ==, 0);
 	g_assert_cmpstr(ibis_features_get_casemapping(features), ==, NULL);
 	g_assert_cmpstr(ibis_features_get_chanlimit(features), ==, NULL);
@@ -221,18 +223,60 @@
 	g_assert_cmpstr(ibis_features_get_targmax(features), ==, NULL);
 	g_assert_cmpuint(ibis_features_get_topiclen(features), ==, 0);
 	g_assert_cmpuint(ibis_features_get_userlen(features), ==, 0);
+
 	g_assert_finalize_object(features);
 }
 
+static void
+test_ibis_features_clear(void) {
+	IbisMessage *message = NULL;
+	IbisFeatures *features = NULL;
+	guint counter = 0;
+
+	features = ibis_features_new();
+	g_signal_connect(features, "changed",
+	                 G_CALLBACK(test_ibis_features_changed_counter), &counter);
+
+	/* There are 2 default features, they should get removed and reset on a
+	 * clear.
+	 */
+	ibis_features_clear(features);
+	g_assert_cmpuint(counter, ==, 4);
+
+	/* Build a simple RPL_ISUPPORT message and parse it. */
+	message = ibis_message_new(IBIS_RPL_ISUPPORT);
+	ibis_message_set_params(message, "CASEMAPPING=rfc1459", "TOPICLEN=307",
+	                        NULL);
+
+	counter = 0;
+	ibis_features_parse(features, message);
+	g_assert_cmpuint(counter, ==, 2);
+
+	/* Clear again, and make sure we got called 6 times, 4 removals, 2 adds. */
+	counter = 0;
+	ibis_features_clear(features);
+	g_assert_cmpuint(counter, ==, 6);
+
+	/* Cleanup. */
+	g_assert_finalize_object(message);
+	g_assert_finalize_object(features);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
 int
 main(int argc, char *argv[]) {
 	g_test_init(&argc, &argv, NULL);
 
+	g_test_set_nonfatal_assertions();
+
 	g_test_add_func("/ibis/features/new", test_ibis_features_new);
 	g_test_add_func("/ibis/features/parse_fail", test_ibis_features_parse_fail);
 	g_test_add_func("/ibis/features/parse_all", test_ibis_features_parse_all);
 	g_test_add_func("/ibis/features/parse_unknown", test_ibis_features_parse_unknown);
 	g_test_add_func("/ibis/features/no_parse", test_ibis_features_no_parse);
+	g_test_add_func("/ibis/features/clear", test_ibis_features_clear);
 
 	return g_test_run();
 }
