diff --git a/lib/wp/client.c b/lib/wp/client.c
index 0d8a52d23bc53e4d1f6071ca2cb16b4eb623f835..10b08739f845b5190128738af84e5984d3e254b3 100644
--- a/lib/wp/client.c
+++ b/lib/wp/client.c
@@ -7,55 +7,60 @@
  */
 
 /**
- * SECTION: WpClient
- *
- * The #WpClient class allows accessing the properties and methods of a PipeWire
- * client object (`struct pw_client`). A #WpClient is constructed internally
- * when a new client connects to PipeWire and it is made available through the
- * #WpObjectManager API.
+ * SECTION: client
+ * @title: PipeWire Client
  */
 
-#include "client.h"
-#include "private.h"
+#define G_LOG_DOMAIN "wp-client"
 
-#include <pipewire/pipewire.h>
+#include "client.h"
+#include "private/pipewire-object-mixin.h"
 
 struct _WpClient
 {
-  WpProxy parent;
+  WpGlobalProxy parent;
   struct pw_client_info *info;
-
-  /* The client proxy listener */
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE (WpClient, wp_client, WP_TYPE_PROXY)
+static void wp_client_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
 
-static void
-wp_client_init (WpClient * self)
-{
-}
+/**
+ * WpClient:
+ *
+ * The #WpClient class allows accessing the properties and methods of a PipeWire
+ * client object (`struct pw_client`). A #WpClient is constructed internally
+ * when a new client connects to PipeWire and it is made available through the
+ * #WpObjectManager API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpClient, wp_client, WP_TYPE_GLOBAL_PROXY,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_client_pipewire_object_interface_init));
 
 static void
-wp_client_finalize (GObject * object)
+wp_client_init (WpClient * self)
 {
-  WpClient *self = WP_CLIENT (object);
-
-  g_clear_pointer (&self->info, pw_client_info_free);
-
-  G_OBJECT_CLASS (wp_client_parent_class)->finalize (object);
 }
 
-static gconstpointer
-wp_client_get_info (WpProxy * self)
+static WpObjectFeatures
+wp_client_get_supported_features (WpObject * object)
 {
-  return WP_CLIENT (self)->info;
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;
 }
 
-static WpProperties *
-wp_client_get_properties (WpProxy * self)
+static void
+wp_client_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  return wp_properties_new_wrap_dict (WP_CLIENT (self)->info->props);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_client_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
 static void
@@ -64,12 +69,11 @@ client_event_info(void *data, const struct pw_client_info *info)
   WpClient *self = WP_CLIENT (data);
 
   self->info = pw_client_info_update (self->info, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-
-  g_object_notify (G_OBJECT (self), "info");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  if (info->change_mask & PW_CLIENT_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_CLIENT_CHANGE_MASK_PROPS, 0);
 }
 
 static const struct pw_client_events client_events = {
@@ -81,25 +85,68 @@ static void
 wp_client_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
   WpClient *self = WP_CLIENT (proxy);
-  pw_client_add_listener ((struct pw_client *) pw_proxy,
+  pw_client_add_listener ((struct pw_port *) pw_proxy,
       &self->listener, &client_events, self);
 }
 
+static void
+wp_client_pw_proxy_destroyed (WpProxy * proxy)
+{
+  g_clear_pointer (&WP_CLIENT (proxy)->info, pw_client_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+}
+
 static void
 wp_client_class_init (WpClientClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_client_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_client_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_client_activate_execute_step;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Client;
   proxy_class->pw_iface_version = PW_VERSION_CLIENT;
+  proxy_class->pw_proxy_created = wp_client_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_client_pw_proxy_destroyed;
 
-  proxy_class->get_info = wp_client_get_info;
-  proxy_class->get_properties = wp_client_get_properties;
+  wp_pipewire_object_mixin_class_override_properties (object_class);
+}
 
-  proxy_class->pw_proxy_created = wp_client_pw_proxy_created;
+static gconstpointer
+wp_client_get_native_info (WpPipewireObject * obj)
+{
+  return WP_CLIENT (obj)->info;
+}
+
+static WpProperties *
+wp_client_get_properties (WpPipewireObject * obj)
+{
+  return wp_properties_new_wrap_dict (WP_CLIENT (obj)->info->props);
+}
+
+static GVariant *
+wp_client_get_param_info (WpPipewireObject * obj)
+{
+  return NULL;
+}
+
+static void
+wp_client_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_client_get_native_info;
+  iface->get_properties = wp_client_get_properties;
+  iface->get_param_info = wp_client_get_param_info;
+  iface->enum_params = wp_pipewire_object_mixin_enum_params_unimplemented;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
 }
 
 /**
diff --git a/lib/wp/client.h b/lib/wp/client.h
index 89eef4f0c9b4b235588960a67bb6a2c0a72fa789..4cd9a2bd98514cb331a342f36f7192a8253ab501 100644
--- a/lib/wp/client.h
+++ b/lib/wp/client.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_CLIENT_H__
 #define __WIREPLUMBER_CLIENT_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -22,7 +22,7 @@ struct pw_permission;
  */
 #define WP_TYPE_CLIENT (wp_client_get_type ())
 WP_API
-G_DECLARE_FINAL_TYPE (WpClient, wp_client, WP, CLIENT, WpProxy)
+G_DECLARE_FINAL_TYPE (WpClient, wp_client, WP, CLIENT, WpGlobalProxy)
 
 WP_API
 void wp_client_update_permissions (WpClient * self, guint n_perm, ...);
diff --git a/lib/wp/debug.c b/lib/wp/debug.c
index d58c03f2912727be5d9e2cc2028cf7d25c589de4..edc568b7381be2075825c688f018c00847053c9f 100644
--- a/lib/wp/debug.c
+++ b/lib/wp/debug.c
@@ -218,7 +218,7 @@ format_message (struct common_fields *cf)
     spa_dbg_str = NULL;
   }
   else if (cf->object && g_type_is_a (cf->object_type, WP_TYPE_PROXY) &&
-      (wp_proxy_get_features ((WpProxy *) cf->object) & WP_PROXY_FEATURE_BOUND)) {
+      (wp_object_get_active_features ((WpObject *) cf->object) & WP_PROXY_FEATURE_BOUND)) {
     extra_object = g_strdup_printf (":%u:",
         wp_proxy_get_bound_id ((WpProxy *) cf->object));
   }
diff --git a/lib/wp/device.c b/lib/wp/device.c
index b70013bbdedd212a28c31d884a7f7c01c1a8555f..2a5ab8de15fb34cad5efefd9de909e4c679e5134 100644
--- a/lib/wp/device.c
+++ b/lib/wp/device.c
@@ -7,164 +7,190 @@
  */
 
 /**
- * SECTION: WpDevice
- *
- * The #WpDevice class allows accessing the properties and methods of a
- * PipeWire device object (`struct pw_device`).
- *
- * A #WpDevice is constructed internally when a new device appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
- * Alternatively, a #WpDevice can also be constructed using
- * wp_device_new_from_factory(), which creates a new device object
- * on the remote PipeWire server by calling into a factory.
- *
- * A #WpSpaDevice allows running a `spa_device` object locally,
- * loading the implementation from a SPA factory. This is useful to run device
- * monitors inside the session manager and have control over creating the
- * actual nodes that the `spa_device` requests to create.
+ * SECTION: device
+ * @title: PipeWire Device
  */
 
 #define G_LOG_DOMAIN "wp-device"
 
 #include "device.h"
-#include "debug.h"
 #include "node.h"
-#include "error.h"
-#include "private.h"
+#include "core.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 #include <spa/monitor/device.h>
-#include <spa/utils/result.h>
 
 struct _WpDevice
 {
-  WpProxy parent;
-};
-
-typedef struct _WpDevicePrivate WpDevicePrivate;
-struct _WpDevicePrivate
-{
+  WpGlobalProxy parent;
   struct pw_device_info *info;
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpDevice, wp_device, WP_TYPE_PROXY)
+static void wp_device_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+
+/**
+ * WpDevice:
+ *
+ * The #WpDevice class allows accessing the properties and methods of a
+ * PipeWire device object (`struct pw_device`).
+ *
+ * A #WpDevice is constructed internally when a new device appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ * Alternatively, a #WpDevice can also be constructed using
+ * wp_device_new_from_factory(), which creates a new device object
+ * on the remote PipeWire server by calling into a factory.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpDevice, wp_device, WP_TYPE_GLOBAL_PROXY,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_device_pipewire_object_interface_init));
 
 static void
 wp_device_init (WpDevice * self)
 {
 }
 
-static void
-wp_device_finalize (GObject * object)
+static WpObjectFeatures
+wp_device_get_supported_features (WpObject * object)
 {
   WpDevice *self = WP_DEVICE (object);
-  WpDevicePrivate *priv = wp_device_get_instance_private (self);
-
-  g_clear_pointer (&priv->info, pw_device_info_free);
-
-  G_OBJECT_CLASS (wp_device_parent_class)->finalize (object);
-}
-
-static gconstpointer
-wp_device_get_info (WpProxy * self)
-{
-  WpDevicePrivate *priv = wp_device_get_instance_private (WP_DEVICE (self));
-  return priv->info;
-}
 
-static WpProperties *
-wp_device_get_properties (WpProxy * self)
-{
-  WpDevicePrivate *priv = wp_device_get_instance_private (WP_DEVICE (self));
-  return wp_properties_new_wrap_dict (priv->info->props);
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          self->info ? self->info->params : NULL,
+          self->info ? self->info->n_params : 0);
 }
 
-struct spa_param_info *
-wp_device_get_param_info (WpProxy * self, guint * n_params)
-{
-  WpDevicePrivate *priv = wp_device_get_instance_private (WP_DEVICE (self));
-  *n_params = priv->info->n_params;
-  return priv->info->params;
-}
-
-static gint
-wp_device_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, WpSpaPod * filter)
+static void
+wp_device_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  struct pw_device *pwp = (struct pw_device *) wp_proxy_get_pw_proxy (self);
-  return pw_device_enum_params (pwp, 0, id, start, num,
-      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_device_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_device_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
+static void
+wp_device_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  struct pw_device *pwp = (struct pw_device *) wp_proxy_get_pw_proxy (self);
-  return pw_device_subscribe_params (pwp, ids, n_ids);
-}
+  wp_pipewire_object_mixin_deactivate (object, features);
 
-static gint
-wp_device_set_param (WpProxy * self, guint32 id, guint32 flags, WpSpaPod *param)
-{
-  struct pw_device *pwp = (struct pw_device *) wp_proxy_get_pw_proxy (self);
-  return pw_device_set_param (pwp, id, flags,
-      wp_spa_pod_get_spa_pod (param));
+  WP_OBJECT_CLASS (wp_device_parent_class)->deactivate (object, features);
 }
 
 static void
 device_event_info(void *data, const struct pw_device_info *info)
 {
   WpDevice *self = WP_DEVICE (data);
-  WpDevicePrivate *priv = wp_device_get_instance_private (self);
-
-  priv->info = pw_device_info_update (priv->info, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
 
-  g_object_notify (G_OBJECT (self), "info");
+  self->info = pw_device_info_update (self->info, info);
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  if (info->change_mask & PW_DEVICE_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_DEVICE_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_DEVICE_CHANGE_MASK_PROPS, PW_DEVICE_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_device_events device_events = {
   PW_VERSION_DEVICE_EVENTS,
   .info = device_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
 wp_device_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
   WpDevice *self = WP_DEVICE (proxy);
-  WpDevicePrivate *priv = wp_device_get_instance_private (self);
-  pw_device_add_listener ((struct pw_device *) pw_proxy,
-      &priv->listener, &device_events, self);
+  pw_device_add_listener ((struct pw_port *) pw_proxy,
+      &self->listener, &device_events, self);
+}
+
+static void
+wp_device_pw_proxy_destroyed (WpProxy * proxy)
+{
+  g_clear_pointer (&WP_DEVICE (proxy)->info, pw_device_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
 }
 
 static void
 wp_device_class_init (WpDeviceClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_device_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_device_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_device_activate_execute_step;
+  wpobject_class->deactivate = wp_device_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Device;
   proxy_class->pw_iface_version = PW_VERSION_DEVICE;
+  proxy_class->pw_proxy_created = wp_device_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_device_pw_proxy_destroyed;
 
-  proxy_class->get_info = wp_device_get_info;
-  proxy_class->get_properties = wp_device_get_properties;
-  proxy_class->get_param_info = wp_device_get_param_info;
-  proxy_class->enum_params = wp_device_enum_params;
-  proxy_class->subscribe_params = wp_device_subscribe_params;
-  proxy_class->set_param = wp_device_set_param;
+  wp_pipewire_object_mixin_class_override_properties (object_class);
+}
 
-  proxy_class->pw_proxy_created = wp_device_pw_proxy_created;
+static gconstpointer
+wp_device_get_native_info (WpPipewireObject * obj)
+{
+  return WP_DEVICE (obj)->info;
+}
+
+static WpProperties *
+wp_device_get_properties (WpPipewireObject * obj)
+{
+  return wp_properties_new_wrap_dict (WP_DEVICE (obj)->info->props);
+}
+
+static GVariant *
+wp_device_get_param_info (WpPipewireObject * obj)
+{
+  WpDevice *self = WP_DEVICE (obj);
+  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
+      self->info->n_params);
+}
+
+static void
+wp_device_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_device, obj, id, filter, cancellable,
+      callback, user_data);
+}
+
+static void
+wp_device_set_param (WpPipewireObject * obj, const gchar * id, WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_device, obj, id, param);
+}
+
+static void
+wp_device_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_device_get_native_info;
+  iface->get_properties = wp_device_get_properties;
+  iface->get_param_info = wp_device_get_param_info;
+  iface->enum_params = wp_device_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_device_set_param;
 }
 
 /**
@@ -178,9 +204,9 @@ wp_device_class_init (WpDeviceClass * klass)
  *
  * Because of the nature of the PipeWire protocol, this operation completes
  * asynchronously at some point in the future. In order to find out when
- * this is done, you should call wp_proxy_augment(), requesting at least
+ * this is done, you should call wp_object_activate(), requesting at least
  * %WP_PROXY_FEATURE_BOUND. When this feature is ready, the device is ready for
- * use on the server. If the device cannot be created, this augment operation
+ * use on the server. If the device cannot be created, this activation operation
  * will fail.
  *
  * Returns: (nullable) (transfer full): the new device or %NULL if the core
@@ -235,6 +261,14 @@ enum
 
 static guint spa_device_signals[SPA_DEVICE_LAST_SIGNAL] = { 0 };
 
+/**
+ * WpSpaDevice:
+ *
+ * A #WpSpaDevice allows running a `spa_device` object locally,
+ * loading the implementation from a SPA factory. This is useful to run device
+ * monitors inside the session manager and have control over creating the
+ * actual nodes that the `spa_device` requests to create.
+ */
 G_DEFINE_TYPE (WpSpaDevice, wp_spa_device, G_TYPE_OBJECT)
 
 static void
diff --git a/lib/wp/device.h b/lib/wp/device.h
index ebcc95ffe67a81078c3b843d24e2dda796cf6d23..10592eac8fe27c4b1f22b731fdcc62aafa8a09ad 100644
--- a/lib/wp/device.h
+++ b/lib/wp/device.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_DEVICE_H__
 #define __WIREPLUMBER_DEVICE_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -22,7 +22,7 @@ G_BEGIN_DECLS
  */
 #define WP_TYPE_DEVICE (wp_device_get_type ())
 WP_API
-G_DECLARE_FINAL_TYPE (WpDevice, wp_device, WP, DEVICE, WpProxy)
+G_DECLARE_FINAL_TYPE (WpDevice, wp_device, WP, DEVICE, WpGlobalProxy)
 
 WP_API
 WpDevice * wp_device_new_from_factory (WpCore * core,
diff --git a/lib/wp/endpoint-link.c b/lib/wp/endpoint-link.c
index 4e8be1fab0e2ae239907b67dac64b13408658084..5c872e7462ff04c6142e08eeab8df846cfe0a1c7 100644
--- a/lib/wp/endpoint-link.c
+++ b/lib/wp/endpoint-link.c
@@ -7,26 +7,18 @@
  */
 
 /**
- * SECTION: WpEndpointLink
- *
- * The #WpEndpointLink class allows accessing the properties and methods of a
- * PipeWire endpoint link object (`struct pw_endpoint_link` from the
- * session-manager extension).
- *
- * A #WpEndpointLink is constructed internally when a new endpoint link appears
- * on the PipeWire registry and it is made available through the
- * #WpObjectManager API.
+ * SECTION: endpoint-link
+ * @title: PipeWire Endpoint Link
  */
 
 #define G_LOG_DOMAIN "wp-endpoint-link"
 
 #include "endpoint-link.h"
-#include "debug.h"
-#include "private.h"
 #include "error.h"
 #include "wpenums.h"
+#include "private.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/extensions/session-manager.h>
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
 
@@ -37,8 +29,6 @@ enum {
 
 static guint32 signals[N_SIGNALS] = {0};
 
-/* WpEndpointLink */
-
 typedef struct _WpEndpointLinkPrivate WpEndpointLinkPrivate;
 struct _WpEndpointLinkPrivate
 {
@@ -48,79 +38,62 @@ struct _WpEndpointLinkPrivate
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpEndpointLink, wp_endpoint_link, WP_TYPE_PROXY)
+static void wp_endpoint_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+
+/**
+ * WpEndpointLink:
+ *
+ * The #WpEndpointLink class allows accessing the properties and methods of a
+ * PipeWire endpoint link object (`struct pw_endpoint_link` from the
+ * session-manager extension).
+ *
+ * A #WpEndpointLink is constructed internally when a new endpoint link appears
+ * on the PipeWire registry and it is made available through the
+ * #WpObjectManager API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpEndpointLink, wp_endpoint_link, WP_TYPE_GLOBAL_PROXY,
+    G_ADD_PRIVATE (WpEndpointLink)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_link_pipewire_object_interface_init));
 
 static void
 wp_endpoint_link_init (WpEndpointLink * self)
 {
 }
 
-static void
-wp_endpoint_link_finalize (GObject * object)
+static WpObjectFeatures
+wp_endpoint_link_get_supported_features (WpObject * object)
 {
   WpEndpointLink *self = WP_ENDPOINT_LINK (object);
   WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
 
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_link_info_free);
-
-  G_OBJECT_CLASS (wp_endpoint_link_parent_class)->finalize (object);
-}
-
-static gconstpointer
-wp_endpoint_link_get_info (WpProxy * proxy)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  return priv->info;
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          priv->info ? priv->info->params : NULL,
+          priv->info ? priv->info->n_params : 0);
 }
 
-static WpProperties *
-wp_endpoint_link_get_properties (WpProxy * proxy)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
-
-static struct spa_param_info *
-wp_endpoint_link_get_param_info (WpProxy * proxy, guint * n_params)
-{
-  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
-  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-
-  *n_params = priv->info->n_params;
-  return priv->info->params;
-}
-
-static gint
-wp_endpoint_link_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, WpSpaPod * filter)
+static void
+wp_endpoint_link_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-  return pw_endpoint_link_enum_params (priv->iface, 0, id, start, num,
-      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_endpoint_link_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_endpoint_link_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
+static void
+wp_endpoint_link_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-  return pw_endpoint_link_subscribe_params (priv->iface, ids, n_ids);
-}
+  wp_pipewire_object_mixin_deactivate (object, features);
 
-static gint
-wp_endpoint_link_set_param (WpProxy * self, guint32 id, guint32 flags,
-    WpSpaPod *param)
-{
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-  return pw_endpoint_link_set_param (priv->iface, id, flags,
-      wp_spa_pod_get_spa_pod (param));
+  WP_OBJECT_CLASS (wp_endpoint_link_parent_class)->deactivate (object, features);
 }
 
 static void
@@ -128,8 +101,8 @@ endpoint_link_event_info (void *data, const struct pw_endpoint_link_info *info)
 {
   WpEndpointLink *self = WP_ENDPOINT_LINK (data);
   WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
-  WpEndpointLinkState old_state =
-      priv->info ? priv->info->state : PW_ENDPOINT_LINK_STATE_ERROR;
+  WpEndpointLinkState old_state = priv->info ?
+      (WpEndpointLinkState) priv->info->state : WP_ENDPOINT_LINK_STATE_ERROR;
 
   priv->info = pw_endpoint_link_info_update (priv->info, info);
 
@@ -138,25 +111,22 @@ endpoint_link_event_info (void *data, const struct pw_endpoint_link_info *info)
     priv->properties = wp_properties_new_wrap_dict (priv->info->props);
   }
 
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
-
-  if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
   if (info->change_mask & PW_ENDPOINT_LINK_CHANGE_MASK_STATE) {
     g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0,
         old_state, info->state, info->error);
   }
+
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_ENDPOINT_LINK_CHANGE_MASK_PROPS, PW_ENDPOINT_LINK_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_endpoint_link_events endpoint_link_events = {
   PW_VERSION_ENDPOINT_LINK_EVENTS,
   .info = endpoint_link_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
@@ -170,24 +140,44 @@ wp_endpoint_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
       &endpoint_link_events, self);
 }
 
+static void
+wp_endpoint_link_pw_proxy_destroyed (WpProxy * proxy)
+{
+  WpEndpointLink *self = WP_ENDPOINT_LINK (proxy);
+  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
+
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&priv->info, pw_endpoint_link_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
+}
+
 static void
 wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_endpoint_link_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features =
+      wp_endpoint_link_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step =
+      wp_endpoint_link_activate_execute_step;
+  wpobject_class->deactivate = wp_endpoint_link_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_EndpointLink;
   proxy_class->pw_iface_version = PW_VERSION_ENDPOINT_LINK;
-
-  proxy_class->get_info = wp_endpoint_link_get_info;
-  proxy_class->get_properties = wp_endpoint_link_get_properties;
-  proxy_class->get_param_info = wp_endpoint_link_get_param_info;
-  proxy_class->enum_params = wp_endpoint_link_enum_params;
-  proxy_class->subscribe_params = wp_endpoint_link_subscribe_params;
-  proxy_class->set_param = wp_endpoint_link_set_param;
   proxy_class->pw_proxy_created = wp_endpoint_link_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_endpoint_link_pw_proxy_destroyed;
+
+  wp_pipewire_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpointLink::state-changed:
@@ -205,6 +195,63 @@ wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
       WP_TYPE_ENDPOINT_LINK_STATE, WP_TYPE_ENDPOINT_LINK_STATE, G_TYPE_STRING);
 }
 
+static gconstpointer
+wp_endpoint_link_get_native_info (WpPipewireObject * obj)
+{
+  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
+  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
+
+  return priv->info;
+}
+
+static WpProperties *
+wp_endpoint_link_get_properties (WpPipewireObject * obj)
+{
+  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
+  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
+
+  return wp_properties_ref (priv->properties);
+}
+
+static GVariant *
+wp_endpoint_link_get_param_info (WpPipewireObject * obj)
+{
+  WpEndpointLink *self = WP_ENDPOINT_LINK (obj);
+  WpEndpointLinkPrivate *priv = wp_endpoint_link_get_instance_private (self);
+
+  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
+      priv->info->n_params);
+}
+
+static void
+wp_endpoint_link_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_endpoint_link, obj, id, filter,
+      cancellable, callback, user_data);
+}
+
+static void
+wp_endpoint_link_set_param (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_endpoint_link, obj, id, param);
+}
+
+static void
+wp_endpoint_link_pipewire_object_interface_init (
+    WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_endpoint_link_get_native_info;
+  iface->get_properties = wp_endpoint_link_get_properties;
+  iface->get_param_info = wp_endpoint_link_get_param_info;
+  iface->enum_params = wp_endpoint_link_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_endpoint_link_set_param;
+}
+
 /**
  * wp_endpoint_link_get_linked_object_ids:
  * @self: the endpoint link
@@ -219,7 +266,7 @@ wp_endpoint_link_class_init (WpEndpointLinkClass * klass)
  *
  * Retrieves the ids of the objects that are linked by this endpoint link
  *
- * Note: Using this method requires %WP_PROXY_FEATURE_INFO
+ * Note: Using this method requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
  */
 void
 wp_endpoint_link_get_linked_object_ids (WpEndpointLink * self,
@@ -249,7 +296,7 @@ wp_endpoint_link_get_linked_object_ids (WpEndpointLink * self,
  *
  * Retrieves the current state of the link
  *
- * Note: Using this method requires %WP_PROXY_FEATURE_INFO
+ * Note: Using this method requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
  * Returns: the current state of the link
  */
 WpEndpointLinkState
@@ -262,7 +309,7 @@ wp_endpoint_link_get_state (WpEndpointLink * self, const gchar ** error)
 
   if (error)
     *error = priv->info->error;
-  return priv->info->state;
+  return (WpEndpointLinkState) priv->info->state;
 }
 
 /**
@@ -272,7 +319,7 @@ wp_endpoint_link_get_state (WpEndpointLink * self, const gchar ** error)
  *
  * Requests a state change on the link
  *
- * Note: Using this method requires %WP_PROXY_FEATURE_PW_PROXY
+ * Note: Using this method requires %WP_PROXY_FEATURE_BOUND
  */
 void
 wp_endpoint_link_request_state (WpEndpointLink * self,
@@ -419,14 +466,14 @@ populate_properties (WpImplEndpointLink * self, WpProperties *global_props)
 
   self->info.props = priv->properties ?
       (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
-
-  g_object_notify (G_OBJECT (self), "properties");
 }
 
 static void
 on_si_link_properties_changed (WpSiLink * item, WpImplEndpointLink * self)
 {
-  populate_properties (self, wp_proxy_get_global_properties (WP_PROXY (self)));
+  populate_properties (self,
+      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
+  g_object_notify (G_OBJECT (self), "properties");
 
   self->info.change_mask = PW_ENDPOINT_LINK_CHANGE_MASK_PROPS;
   pw_endpoint_link_emit_info (&self->hooks, &self->info);
@@ -478,19 +525,6 @@ wp_impl_endpoint_link_init (WpImplEndpointLink * self)
   priv->iface = (struct pw_endpoint_link *) &self->iface;
 }
 
-static void
-wp_impl_endpoint_link_finalize (GObject * object)
-{
-  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
-  WpEndpointLinkPrivate *priv =
-      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-
-  g_free (self->info.error);
-  priv->info = NULL;
-
-  G_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->finalize (object);
-}
-
 static void
 wp_impl_endpoint_link_set_property (GObject * object, guint property_id,
     const GValue * value, GParamSpec * pspec)
@@ -524,26 +558,32 @@ wp_impl_endpoint_link_get_property (GObject * object, guint property_id,
 }
 
 static void
-wp_impl_endpoint_link_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_impl_endpoint_link_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (proxy);
+  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (object);
   WpEndpointLinkPrivate *priv =
       wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
-  g_autoptr (GVariant) info = NULL;
-  g_autoptr (GVariantIter) immutable_props = NULL;
-  g_autoptr (WpProperties) props = NULL;
-
-  /* PW_PROXY depends on BOUND */
-  if (features & WP_PROXY_FEATURE_PW_PROXY)
-    features |= WP_PROXY_FEATURE_BOUND;
-
-  /* BOUND depends on INFO */
-  if (features & WP_PROXY_FEATURE_BOUND)
-    features |= WP_PROXY_FEATURE_INFO;
 
-  if (features & WP_PROXY_FEATURE_INFO) {
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
+    g_autoptr (GVariant) info = NULL;
+    g_autoptr (GVariantIter) immutable_props = NULL;
+    g_autoptr (WpProperties) props = NULL;
     const gchar *key, *value;
     WpSiStream *stream;
+    g_autoptr (WpCore) core = wp_object_get_core (object);
+    struct pw_core *pw_core = wp_core_get_pw_core (core);
+
+    /* no pw_core -> we are not connected */
+    if (!pw_core) {
+      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+              "The WirePlumber core is not connected; "
+              "object cannot be exported to PipeWire"));
+      return;
+    }
 
     /* get info from the interface */
     info = wp_si_link_get_registration_info (self->item);
@@ -553,8 +593,8 @@ wp_impl_endpoint_link_augment (WpProxy * proxy, WpProxyFeatures features)
     self->info.state =
         (wp_session_item_get_flags (WP_SESSION_ITEM (self->item))
             & WP_SI_FLAG_ACTIVE)
-        ? WP_ENDPOINT_LINK_STATE_ACTIVE
-        : WP_ENDPOINT_LINK_STATE_INACTIVE;
+        ? PW_ENDPOINT_LINK_STATE_ACTIVE
+        : PW_ENDPOINT_LINK_STATE_INACTIVE;
 
     /* associate with the session, the endpoints and the streams */
     self->info.session_id = wp_session_item_get_associated_proxy_id (
@@ -604,46 +644,57 @@ wp_impl_endpoint_link_augment (WpProxy * proxy, WpProxyFeatures features)
     self->info.params = NULL;
     self->info.n_params = 0;
     priv->info = &self->info;
-    g_object_notify (G_OBJECT (self), "info");
-    g_object_notify (G_OBJECT (self), "param-info");
-
-    wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  }
-
-  if (features & WP_PROXY_FEATURE_BOUND) {
-    g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
-    struct pw_core *pw_core = wp_core_get_pw_core (core);
-
-    /* no pw_core -> we are not connected */
-    if (!pw_core) {
-      wp_proxy_augment_error (proxy, g_error_new (WP_DOMAIN_LIBRARY,
-            WP_LIBRARY_ERROR_OPERATION_FAILED,
-            "The WirePlumber core is not connected; "
-            "object cannot be exported to PipeWire"));
-      return;
-    }
 
-    wp_proxy_set_pw_proxy (proxy, pw_core_export (pw_core,
+    /* bind */
+    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_EndpointLink,
             wp_properties_peek_dict (props),
             priv->iface, 0));
+
+    /* notify */
+    wp_object_update_features (object, WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+    g_object_notify (G_OBJECT (self), "properties");
+    g_object_notify (G_OBJECT (self), "param-info");
+
+    break;
   }
+  default:
+    WP_OBJECT_CLASS (wp_impl_endpoint_link_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
+}
+
+static void
+wp_impl_endpoint_link_pw_proxy_destroyed (WpProxy * proxy)
+{
+  WpImplEndpointLink *self = WP_IMPL_ENDPOINT_LINK (proxy);
+  WpEndpointLinkPrivate *priv =
+      wp_endpoint_link_get_instance_private (WP_ENDPOINT_LINK (self));
+
+  g_signal_handlers_disconnect_by_data (self->item, self);
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&self->info.error, g_free);
+  priv->info = NULL;
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
 }
 
 static void
 wp_impl_endpoint_link_class_init (WpImplEndpointLinkClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_impl_endpoint_link_finalize;
   object_class->set_property = wp_impl_endpoint_link_set_property;
   object_class->get_property = wp_impl_endpoint_link_get_property;
 
-  proxy_class->augment = wp_impl_endpoint_link_augment;
-  proxy_class->enum_params = NULL;
-  proxy_class->subscribe_params = NULL;
+  wpobject_class->activate_execute_step =
+      wp_impl_endpoint_link_activate_execute_step;
+
   proxy_class->pw_proxy_created = NULL;
+  proxy_class->pw_proxy_destroyed = wp_impl_endpoint_link_pw_proxy_destroyed;
 
   g_object_class_install_property (object_class, IMPL_PROP_ITEM,
       g_param_spec_object ("item", "item", "item", WP_TYPE_SI_LINK,
diff --git a/lib/wp/endpoint-link.h b/lib/wp/endpoint-link.h
index ca3f0ef1bb81a1b1d8561c59aaacd40845a03b23..1ee5d1c47851f505f19526458e2619dcdead2d8d 100644
--- a/lib/wp/endpoint-link.h
+++ b/lib/wp/endpoint-link.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_ENDPOINT_LINK_H__
 #define __WIREPLUMBER_ENDPOINT_LINK_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -35,11 +35,11 @@ typedef enum {
 #define WP_TYPE_ENDPOINT_LINK (wp_endpoint_link_get_type ())
 WP_API
 G_DECLARE_DERIVABLE_TYPE (WpEndpointLink, wp_endpoint_link,
-                          WP, ENDPOINT_LINK, WpProxy)
+                          WP, ENDPOINT_LINK, WpGlobalProxy)
 
 struct _WpEndpointLinkClass
 {
-  WpProxyClass parent_class;
+  WpGlobalProxyClass parent_class;
 };
 
 WP_API
diff --git a/lib/wp/endpoint-stream.c b/lib/wp/endpoint-stream.c
index a6cc5c3870c3f3400aa36b3f432375daa3dbcb6b..1fca995854315f2880d3366bd30832644d11a916 100644
--- a/lib/wp/endpoint-stream.c
+++ b/lib/wp/endpoint-stream.c
@@ -7,39 +7,23 @@
  */
 
 /**
- * SECTION: WpEndpointStream
- *
- * The #WpEndpointStream class allows accessing the properties and methods of a
- * PipeWire endpoint stream object (`struct pw_endpoint_stream` from the
- * session-manager extension).
- *
- * A #WpEndpointStream is constructed internally when a new endpoint appears on
- * the PipeWire registry and it is made available through the #WpObjectManager
- * API.
+ * SECTION: endpoint-stream
+ * @title: PipeWire Endpoint Stream
  */
 
 #define G_LOG_DOMAIN "wp-endpoint-stream"
 
 #include "endpoint-stream.h"
-#include "debug.h"
 #include "node.h"
-#include "private.h"
 #include "error.h"
+#include "private.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/extensions/session-manager.h>
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
 
-#include <spa/pod/builder.h>
-#include <spa/pod/parser.h>
-#include <spa/pod/filter.h>
-
-
-/* WpEndpointStream */
-
 enum {
-  PROP_0,
-  PROP_NAME,
+  PROP_NAME = WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
 };
 
 typedef struct _WpEndpointStreamPrivate WpEndpointStreamPrivate;
@@ -51,27 +35,30 @@ struct _WpEndpointStreamPrivate
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpEndpointStream, wp_endpoint_stream, WP_TYPE_PROXY)
+static void wp_endpoint_stream_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
 
-static void
-wp_endpoint_stream_init (WpEndpointStream * self)
-{
-}
+/**
+ * WpEndpointStream:
+ *
+ * The #WpEndpointStream class allows accessing the properties and methods of a
+ * PipeWire endpoint stream object (`struct pw_endpoint_stream` from the
+ * session-manager extension).
+ *
+ * A #WpEndpointStream is constructed internally when a new endpoint appears on
+ * the PipeWire registry and it is made available through the #WpObjectManager
+ * API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpEndpointStream, wp_endpoint_stream, WP_TYPE_GLOBAL_PROXY,
+    G_ADD_PRIVATE (WpEndpointStream)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_stream_pipewire_object_interface_init));
 
 static void
-wp_endpoint_stream_finalize (GObject * object)
+wp_endpoint_stream_init (WpEndpointStream * self)
 {
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (object);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_stream_info_free);
-
-  G_OBJECT_CLASS (wp_endpoint_stream_parent_class)->finalize (object);
 }
 
 static void
-wp_endpoint_stream_get_gobj_property (GObject * object, guint property_id,
+wp_endpoint_stream_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec)
 {
   WpEndpointStream *self = WP_ENDPOINT_STREAM (object);
@@ -82,65 +69,45 @@ wp_endpoint_stream_get_gobj_property (GObject * object, guint property_id,
     g_value_set_string (value, priv->info ? priv->info->name : NULL);
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    wp_pipewire_object_mixin_get_property (object, property_id, value, pspec);
     break;
   }
 }
 
-static gconstpointer
-wp_endpoint_stream_get_info (WpProxy * proxy)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return priv->info;
-}
-
-static WpProperties *
-wp_endpoint_stream_get_properties (WpProxy * proxy)
-{
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
-  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
-
-static struct spa_param_info *
-wp_endpoint_stream_get_param_info (WpProxy * proxy, guint * n_params)
+static WpObjectFeatures
+wp_endpoint_stream_get_supported_features (WpObject * object)
 {
-  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
+  WpEndpointStream *self = WP_ENDPOINT_STREAM (object);
   WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
 
-  *n_params = priv->info->n_params;
-  return priv->info->params;
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          priv->info ? priv->info->params : NULL,
+          priv->info ? priv->info->n_params : 0);
 }
 
-static gint
-wp_endpoint_stream_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, WpSpaPod * filter)
+static void
+wp_endpoint_stream_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-  return pw_endpoint_stream_enum_params (priv->iface, 0, id, start, num,
-      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_endpoint_stream_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_endpoint_stream_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
+static void
+wp_endpoint_stream_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-  return pw_endpoint_stream_subscribe_params (priv->iface, ids, n_ids);
-}
+  wp_pipewire_object_mixin_deactivate (object, features);
 
-static gint
-wp_endpoint_stream_set_param (WpProxy * self, guint32 id, guint32 flags,
-    WpSpaPod *param)
-{
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-  return pw_endpoint_stream_set_param (priv->iface, id, flags,
-      wp_spa_pod_get_spa_pod (param));
+  WP_OBJECT_CLASS (wp_endpoint_stream_parent_class)->deactivate (object, features);
 }
 
 static void
@@ -156,20 +123,18 @@ endpoint_stream_event_info (void *data, const struct pw_endpoint_stream_info *in
     priv->properties = wp_properties_new_wrap_dict (priv->info->props);
   }
 
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS,
+      PW_ENDPOINT_STREAM_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_endpoint_stream_events endpoint_stream_events = {
   PW_VERSION_ENDPOINT_STREAM_EVENTS,
   .info = endpoint_stream_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
@@ -183,26 +148,44 @@ wp_endpoint_stream_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy
       &endpoint_stream_events, self);
 }
 
+static void
+wp_endpoint_stream_pw_proxy_destroyed (WpProxy * proxy)
+{
+  WpEndpointStream *self = WP_ENDPOINT_STREAM (proxy);
+  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
+
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&priv->info, pw_endpoint_stream_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
+}
+
 static void
 wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_endpoint_stream_finalize;
-  object_class->get_property = wp_endpoint_stream_get_gobj_property;
+  object_class->get_property = wp_endpoint_stream_get_property;
+
+  wpobject_class->get_supported_features =
+      wp_endpoint_stream_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step =
+      wp_endpoint_stream_activate_execute_step;
+  wpobject_class->deactivate = wp_endpoint_stream_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_EndpointStream;
   proxy_class->pw_iface_version = PW_VERSION_ENDPOINT_STREAM;
-
-  proxy_class->get_info = wp_endpoint_stream_get_info;
-  proxy_class->get_properties = wp_endpoint_stream_get_properties;
-  proxy_class->get_param_info = wp_endpoint_stream_get_param_info;
-  proxy_class->enum_params = wp_endpoint_stream_enum_params;
-  proxy_class->subscribe_params = wp_endpoint_stream_subscribe_params;
-  proxy_class->set_param = wp_endpoint_stream_set_param;
-
   proxy_class->pw_proxy_created = wp_endpoint_stream_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_endpoint_stream_pw_proxy_destroyed;
+
+  wp_pipewire_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpointStream:name:
@@ -214,6 +197,63 @@ wp_endpoint_stream_class_init (WpEndpointStreamClass * klass)
           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 }
 
+static gconstpointer
+wp_endpoint_stream_get_native_info (WpPipewireObject * obj)
+{
+  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
+  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
+
+  return priv->info;
+}
+
+static WpProperties *
+wp_endpoint_stream_get_properties (WpPipewireObject * obj)
+{
+  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
+  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
+
+  return wp_properties_ref (priv->properties);
+}
+
+static GVariant *
+wp_endpoint_stream_get_param_info (WpPipewireObject * obj)
+{
+  WpEndpointStream *self = WP_ENDPOINT_STREAM (obj);
+  WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
+
+  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
+      priv->info->n_params);
+}
+
+static void
+wp_endpoint_stream_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_endpoint_stream, obj, id, filter,
+      cancellable, callback, user_data);
+}
+
+static void
+wp_endpoint_stream_set_param (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_endpoint_stream, obj, id, param);
+}
+
+static void
+wp_endpoint_stream_pipewire_object_interface_init (
+    WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_endpoint_stream_get_native_info;
+  iface->get_properties = wp_endpoint_stream_get_properties;
+  iface->get_param_info = wp_endpoint_stream_get_param_info;
+  iface->enum_params = wp_endpoint_stream_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_endpoint_stream_set_param;
+}
+
 /**
  * wp_endpoint_stream_get_name:
  * @self: the endpoint stream
@@ -224,8 +264,8 @@ const gchar *
 wp_endpoint_stream_get_name (WpEndpointStream * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT_STREAM (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, NULL);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
   WpEndpointStreamPrivate *priv = wp_endpoint_stream_get_instance_private (self);
   return priv->info->name;
@@ -246,7 +286,6 @@ struct _WpImplEndpointStream
   struct spa_interface iface;
   struct spa_hook_list hooks;
   struct pw_endpoint_stream_info info;
-  gboolean subscribed;
 
   WpSiStream *item;
 };
@@ -262,10 +301,10 @@ G_DEFINE_TYPE (WpImplEndpointStream, wp_impl_endpoint_stream, WP_TYPE_ENDPOINT_S
 #define pw_endpoint_stream_emit_param(hooks,...) \
     pw_endpoint_stream_emit(hooks, param, 0, ##__VA_ARGS__)
 
-static struct spa_param_info impl_param_info[] = {
-  SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
-  SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
-};
+// static struct spa_param_info impl_param_info[] = {
+//   SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+//   SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+// };
 
 static int
 impl_add_listener(void *object,
@@ -292,57 +331,12 @@ impl_enum_params (void *object, int seq,
     uint32_t id, uint32_t start, uint32_t num,
     const struct spa_pod *filter)
 {
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
-  struct spa_pod *result;
-  guint count = 0;
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-
-  switch (id) {
-    case SPA_PARAM_PropInfo: {
-      g_autoptr (WpIterator) params = wp_props_iterate_prop_info (props);
-      g_auto (GValue) item = G_VALUE_INIT;
-      guint i = 0;
-
-      for (; wp_iterator_next (params, &item); g_value_unset (&item), i++) {
-        WpSpaPod *pod = g_value_get_boxed (&item);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_endpoint_stream_emit_param (&self->hooks, seq, id, i, i+1, result);
-          if (++count == num)
-            break;
-        }
-      }
-      break;
-    }
-    case SPA_PARAM_Props: {
-      if (start == 0) {
-        g_autoptr (WpSpaPod) pod = wp_props_get_all (props);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_endpoint_stream_emit_param (&self->hooks, seq, id, 0, 1, result);
-        }
-      }
-      break;
-    }
-    default:
-      return -ENOENT;
-  }
-
-  return 0;
+  return -ENOENT;
 }
 
 static int
 impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
 {
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-
-  for (guint i = 0; i < n_ids; i++) {
-    if (ids[i] == SPA_PARAM_Props)
-      self->subscribed = TRUE;
-    impl_enum_params (self, 1, ids[i], 0, UINT32_MAX, NULL);
-  }
   return 0;
 }
 
@@ -350,14 +344,7 @@ static int
 impl_set_param (void *object, uint32_t id, uint32_t flags,
     const struct spa_pod *param)
 {
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-
-  if (id != SPA_PARAM_Props)
-    return -ENOENT;
-
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-  wp_props_set (props, NULL, wp_spa_pod_new_wrap_const (param));
-  return 0;
+  return -ENOENT;
 }
 
 static const struct pw_endpoint_stream_methods impl_endpoint_stream = {
@@ -383,14 +370,14 @@ populate_properties (WpImplEndpointStream * self, WpProperties *global_props)
 
   self->info.props = priv->properties ?
       (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
-
-  g_object_notify (G_OBJECT (self), "properties");
 }
 
 static void
 on_si_stream_properties_changed (WpSiStream * item, WpImplEndpointStream * self)
 {
-  populate_properties (self, wp_proxy_get_global_properties (WP_PROXY (self)));
+  populate_properties (self,
+      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
+  g_object_notify (G_OBJECT (self), "properties");
 
   self->info.change_mask = PW_ENDPOINT_STREAM_CHANGE_MASK_PROPS;
   pw_endpoint_stream_emit_info (&self->hooks, &self->info);
@@ -414,19 +401,6 @@ wp_impl_endpoint_stream_init (WpImplEndpointStream * self)
   priv->iface = (struct pw_endpoint_stream *) &self->iface;
 }
 
-static void
-wp_impl_endpoint_stream_finalize (GObject * object)
-{
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-
-  g_free (self->info.name);
-  priv->info = NULL;
-
-  G_OBJECT_CLASS (wp_impl_endpoint_stream_parent_class)->finalize (object);
-}
-
 static void
 wp_impl_endpoint_stream_set_property (GObject * object, guint property_id,
     const GValue * value, GParamSpec * pspec)
@@ -459,146 +433,177 @@ wp_impl_endpoint_stream_get_property (GObject * object, guint property_id,
   }
 }
 
-static void
-wp_impl_endpoint_stream_export (WpImplEndpointStream * self)
-{
-  WpEndpointStreamPrivate *priv =
-      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
-  g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self));
-  struct pw_core *pw_core = wp_core_get_pw_core (core);
-
-  g_autoptr (GVariantIter) immutable_properties = NULL;
-  g_autoptr (WpProperties) properties = NULL;
-  const gchar *key, *value;
-
-  /* no pw_core -> we are not connected */
-  if (!pw_core) {
-    wp_proxy_augment_error (WP_PROXY (self), g_error_new (WP_DOMAIN_LIBRARY,
-          WP_LIBRARY_ERROR_OPERATION_FAILED,
-          "The WirePlumber core is not connected; "
-          "object cannot be exported to PipeWire"));
-    return;
-  }
+enum {
+  STEP_ACTIVATE_NODE = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+};
 
-  wp_debug_object (self, "exporting");
+static guint
+wp_impl_endpoint_stream_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
 
-  /* get info from the interface */
-  {
-    g_autoptr (GVariant) info = NULL;
-    info = wp_si_stream_get_registration_info (self->item);
-    g_variant_get (info, "(sa{ss})", &self->info.name, &immutable_properties);
+  /* bind if not already bound */
+  if (missing & WP_PROXY_FEATURE_BOUND) {
+    g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-    /* associate with the endpoint */
-    self->info.endpoint_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (self->item), WP_TYPE_ENDPOINT);
+    /* if the item has a node, cache its props so that enum_params works */
+    if (node && !(wp_object_get_active_features (node) &
+                      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
+      return STEP_ACTIVATE_NODE;
+    else
+      return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
   }
-
-  /* construct export properties (these will come back through
-      the registry and appear in wp_proxy_get_global_properties) */
-  properties = wp_properties_new (
-      PW_KEY_ENDPOINT_STREAM_NAME, self->info.name,
-      NULL);
-  wp_properties_setf (properties, PW_KEY_ENDPOINT_ID,
-      "%d", self->info.endpoint_id);
-
-  /* populate immutable (global) properties */
-  while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
-    wp_properties_set (properties, key, value);
-
-  /* populate standard properties */
-  populate_properties (self, properties);
-
-  /* subscribe to changes */
-  g_signal_connect_object (self->item, "stream-properties-changed",
-      G_CALLBACK (on_si_stream_properties_changed), self, 0);
-
-  /* finalize info struct */
-  self->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
-  self->info.params = impl_param_info;
-  self->info.n_params = SPA_N_ELEMENTS (impl_param_info);
-  priv->info = &self->info;
-
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
-  g_object_notify (G_OBJECT (self), "param-info");
-
-  wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
-          PW_TYPE_INTERFACE_EndpointStream,
-          wp_properties_peek_dict (properties),
-          priv->iface, 0));
+  /* cache info if supported */
+  else
+    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
 }
 
 static void
-wp_impl_endpoint_stream_continue_feature_props (WpProxy * node,
-    GAsyncResult * res, WpImplEndpointStream * self)
+wp_impl_endpoint_stream_node_activated (WpObject * node,
+    GAsyncResult * res, WpTransition * transition)
 {
   g_autoptr (GError) error = NULL;
 
-  if (!wp_proxy_augment_finish (node, res, &error)) {
-    wp_proxy_augment_error (WP_PROXY (self), g_steal_pointer (&error));
+  if (!wp_object_activate_finish (node, res, &error)) {
+    wp_transition_return_error (transition, g_steal_pointer (&error));
     return;
   }
 
-  WpProps *props = wp_proxy_get_props (node);
-  wp_proxy_set_props (WP_PROXY (self), g_object_ref (props));
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_PROPS);
-  wp_impl_endpoint_stream_export (self);
+  wp_transition_advance (transition);
 }
 
 static void
-wp_impl_endpoint_stream_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_impl_endpoint_stream_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (proxy);
-  g_autoptr (GVariant) info = NULL;
-  g_autoptr (GVariantIter) immutable_props = NULL;
-  g_autoptr (WpProperties) props = NULL;
-
-  /* if any of these features are requested, export,
-     after ensuring that we have a WpProps so that enum_params works */
-  if (features & (WP_PROXY_FEATURES_STANDARD | WP_PROXY_FEATURE_PROPS)) {
-    g_autoptr (WpProxy) node = wp_session_item_get_associated_proxy (
+  WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (object);
+  WpEndpointStreamPrivate *priv =
+      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
+
+  switch (step) {
+  case STEP_ACTIVATE_NODE: {
+    g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-    if (node) {
-      /* if the item has a node, use the props of that node */
-      wp_proxy_augment (node, WP_PROXY_FEATURE_PROPS, NULL,
-          (GAsyncReadyCallback) wp_impl_endpoint_stream_continue_feature_props,
-          self);
-    } else {
-      /* else install an empty WpProps */
-      WpProps *props = wp_props_new (WP_PROPS_MODE_STORE, proxy);
-      wp_proxy_set_props (proxy, props);
-      wp_proxy_set_feature_ready (proxy, WP_PROXY_FEATURE_PROPS);
-      wp_impl_endpoint_stream_export (self);
+    wp_object_activate (node,
+        WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS,
+        NULL, (GAsyncReadyCallback) wp_impl_endpoint_stream_node_activated,
+        transition);
+    break;
+  }
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
+    g_autoptr (GVariantIter) immutable_properties = NULL;
+    g_autoptr (WpProperties) properties = NULL;
+    const gchar *key, *value;
+    g_autoptr (WpCore) core = wp_object_get_core (object);
+    struct pw_core *pw_core = wp_core_get_pw_core (core);
+
+    /* no pw_core -> we are not connected */
+    if (!pw_core) {
+      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+              "The WirePlumber core is not connected; "
+              "object cannot be exported to PipeWire"));
+      return;
     }
+
+    wp_debug_object (self, "exporting");
+
+    /* get info from the interface */
+    {
+      g_autoptr (GVariant) info = NULL;
+      info = wp_si_stream_get_registration_info (self->item);
+      g_variant_get (info, "(sa{ss})", &self->info.name, &immutable_properties);
+
+      /* associate with the endpoint */
+      self->info.endpoint_id = wp_session_item_get_associated_proxy_id (
+          WP_SESSION_ITEM (self->item), WP_TYPE_ENDPOINT);
+    }
+
+    /* construct export properties (these will come back through
+        the registry and appear in wp_proxy_get_global_properties) */
+    properties = wp_properties_new (
+        PW_KEY_ENDPOINT_STREAM_NAME, self->info.name,
+        NULL);
+    wp_properties_setf (properties, PW_KEY_ENDPOINT_ID,
+        "%d", self->info.endpoint_id);
+
+    /* populate immutable (global) properties */
+    while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
+      wp_properties_set (properties, key, value);
+
+    /* populate standard properties */
+    populate_properties (self, properties);
+
+    /* subscribe to changes */
+    g_signal_connect_object (self->item, "stream-properties-changed",
+        G_CALLBACK (on_si_stream_properties_changed), self, 0);
+
+    /* finalize info struct */
+    self->info.version = PW_VERSION_ENDPOINT_STREAM_INFO;
+    self->info.params = NULL;
+    self->info.n_params = 0;
+    priv->info = &self->info;
+
+    /* bind */
+    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
+            PW_TYPE_INTERFACE_EndpointStream,
+            wp_properties_peek_dict (properties),
+            priv->iface, 0));
+
+    /* notify */
+    wp_object_update_features (object,
+        WP_PIPEWIRE_OBJECT_FEATURE_INFO
+        /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */, 0);
+    g_object_notify (G_OBJECT (self), "properties");
+    g_object_notify (G_OBJECT (self), "param-info");
+
+    break;
+  }
+  default:
+    WP_OBJECT_CLASS (wp_impl_endpoint_stream_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
   }
 }
 
 static void
-wp_impl_endpoint_stream_prop_changed (WpProxy * proxy, const gchar * prop_name)
+wp_impl_endpoint_stream_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpImplEndpointStream *self = WP_IMPL_ENDPOINT_STREAM (proxy);
+  WpEndpointStreamPrivate *priv =
+      wp_endpoint_stream_get_instance_private (WP_ENDPOINT_STREAM (self));
 
-  /* notify subscribers */
-  if (self->subscribed)
-    impl_enum_params (self, 1, SPA_PARAM_Props, 0, UINT32_MAX, NULL);
+  g_signal_handlers_disconnect_by_data (self->item, self);
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&self->info.name, g_free);
+  priv->info = NULL;
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO
+      /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */);
 }
 
 static void
 wp_impl_endpoint_stream_class_init (WpImplEndpointStreamClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_impl_endpoint_stream_finalize;
   object_class->set_property = wp_impl_endpoint_stream_set_property;
   object_class->get_property = wp_impl_endpoint_stream_get_property;
 
-  proxy_class->augment = wp_impl_endpoint_stream_augment;
-  proxy_class->enum_params = NULL;
-  proxy_class->subscribe_params = NULL;
+  wpobject_class->activate_get_next_step =
+      wp_impl_endpoint_stream_activate_get_next_step;
+  wpobject_class->activate_execute_step =
+      wp_impl_endpoint_stream_activate_execute_step;
+
   proxy_class->pw_proxy_created = NULL;
-  proxy_class->prop_changed = wp_impl_endpoint_stream_prop_changed;
+  proxy_class->pw_proxy_destroyed = wp_impl_endpoint_stream_pw_proxy_destroyed;
 
   g_object_class_install_property (object_class, IMPL_PROP_ITEM,
       g_param_spec_object ("item", "item", "item", WP_TYPE_SI_STREAM,
diff --git a/lib/wp/endpoint-stream.h b/lib/wp/endpoint-stream.h
index c6b21bb04ecc801c5f6e2835be45500ac3c9988f..665e4b540de3cda332e0992f24c1229e26385834 100644
--- a/lib/wp/endpoint-stream.h
+++ b/lib/wp/endpoint-stream.h
@@ -9,8 +9,7 @@
 #ifndef __WIREPLUMBER_ENDPOINT_STREAM_H__
 #define __WIREPLUMBER_ENDPOINT_STREAM_H__
 
-#include "proxy.h"
-#include "spa-pod.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -22,11 +21,11 @@ G_BEGIN_DECLS
 #define WP_TYPE_ENDPOINT_STREAM (wp_endpoint_stream_get_type ())
 WP_API
 G_DECLARE_DERIVABLE_TYPE (WpEndpointStream, wp_endpoint_stream,
-                          WP, ENDPOINT_STREAM, WpProxy)
+                          WP, ENDPOINT_STREAM, WpGlobalProxy)
 
 struct _WpEndpointStreamClass
 {
-  WpProxyClass parent_class;
+  WpGlobalProxyClass parent_class;
 };
 
 WP_API
diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index cdf5d593b4f89dc1eceb16ecf559768a9be57df6..667a42798747cddc965b02855c0c632c85c8176a 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -7,41 +7,26 @@
  */
 
 /**
- * SECTION: WpEndpoint
- *
- * The #WpEndpoint class allows accessing the properties and methods of a
- * PipeWire endpoint object (`struct pw_endpoint` from the session-manager
- * extension).
- *
- * A #WpEndpoint is constructed internally when a new endpoint appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
+ * SECTION: endpoint
+ * @title: PIpeWire Endpoint
  */
 
 #define G_LOG_DOMAIN "wp-endpoint"
 
 #include "endpoint.h"
-#include "debug.h"
 #include "node.h"
 #include "session.h"
-#include "private.h"
 #include "error.h"
 #include "wpenums.h"
 #include "si-factory.h"
+#include "private.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/extensions/session-manager.h>
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
 
-#include <spa/pod/builder.h>
-#include <spa/pod/parser.h>
-#include <spa/pod/filter.h>
-#include <spa/utils/result.h>
-
-/* WpEndpoint */
-
 enum {
-  PROP_0,
-  PROP_NAME,
+  PROP_NAME = WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
   PROP_MEDIA_CLASS,
   PROP_DIRECTION,
 };
@@ -61,31 +46,31 @@ struct _WpEndpointPrivate
   struct pw_endpoint *iface;
   struct spa_hook listener;
   WpObjectManager *streams_om;
-  gboolean ft_streams_requested;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpEndpoint, wp_endpoint, WP_TYPE_PROXY)
+static void wp_endpoint_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
 
-static void
-wp_endpoint_init (WpEndpoint * self)
-{
-}
+/**
+ * WpEndpoint:
+ *
+ * The #WpEndpoint class allows accessing the properties and methods of a
+ * PipeWire endpoint object (`struct pw_endpoint` from the session-manager
+ * extension).
+ *
+ * A #WpEndpoint is constructed internally when a new endpoint appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpEndpoint, wp_endpoint, WP_TYPE_GLOBAL_PROXY,
+    G_ADD_PRIVATE (WpEndpoint)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_endpoint_pipewire_object_interface_init));
 
 static void
-wp_endpoint_finalize (GObject * object)
+wp_endpoint_init (WpEndpoint * self)
 {
-  WpEndpoint *self = WP_ENDPOINT (object);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  g_clear_object (&priv->streams_om);
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-  g_clear_pointer (&priv->info, pw_endpoint_info_free);
-
-  G_OBJECT_CLASS (wp_endpoint_parent_class)->finalize (object);
 }
 
 static void
-wp_endpoint_get_gobj_property (GObject * object, guint property_id,
+wp_endpoint_get_property (GObject * object, guint property_id,
     GValue * value, GParamSpec * pspec)
 {
   WpEndpoint *self = WP_ENDPOINT (object);
@@ -102,7 +87,7 @@ wp_endpoint_get_gobj_property (GObject * object, guint property_id,
     g_value_set_enum (value, priv->info ? priv->info->direction : 0);
     break;
   default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    wp_pipewire_object_mixin_get_property (object, property_id, value, pspec);
     break;
   }
 }
@@ -111,7 +96,7 @@ static void
 wp_endpoint_on_streams_om_installed (WpObjectManager *streams_om,
     WpEndpoint * self)
 {
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_ENDPOINT_FEATURE_STREAMS);
+  wp_object_update_features (WP_OBJECT (self), WP_ENDPOINT_FEATURE_STREAMS, 0);
 }
 
 static void
@@ -119,129 +104,120 @@ wp_endpoint_emit_streams_changed (WpObjectManager *streams_om,
     WpEndpoint * self)
 {
   g_signal_emit (self, signals[SIGNAL_STREAMS_CHANGED], 0);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_ENDPOINT_FEATURE_STREAMS);
+  wp_object_update_features (WP_OBJECT (self), WP_ENDPOINT_FEATURE_STREAMS, 0);
 }
 
 static void
-wp_endpoint_ensure_feature_streams (WpEndpoint * self, guint32 bound_id)
+wp_endpoint_enable_feature_streams (WpEndpoint * self)
 {
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-  WpProxyFeatures ft = wp_proxy_get_features (WP_PROXY (self));
-
-  if (priv->ft_streams_requested && !priv->streams_om &&
-      (ft & WP_PROXY_FEATURES_STANDARD) == WP_PROXY_FEATURES_STANDARD)
-  {
-    g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self));
-
-    if (!bound_id)
-      bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+  guint32 bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
 
-    wp_debug_object (self, "enabling WP_ENDPOINT_FEATURE_STREAMS, bound_id:%u, "
-        "n_streams:%u", bound_id, priv->info->n_streams);
+  wp_debug_object (self, "enabling WP_ENDPOINT_FEATURE_STREAMS, bound_id:%u, "
+      "n_streams:%u", bound_id, priv->info->n_streams);
 
-    priv->streams_om = wp_object_manager_new ();
-    /* proxy endpoint stream -> check for endpoint.id in global properties */
-    wp_object_manager_add_interest (priv->streams_om,
-        WP_TYPE_ENDPOINT_STREAM,
-        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_ENDPOINT_ID, "=u", bound_id,
-        NULL);
-    /* impl endpoint stream -> check for endpoint.id in standard properties */
-    wp_object_manager_add_interest (priv->streams_om,
-        WP_TYPE_IMPL_ENDPOINT_STREAM,
-        WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_ENDPOINT_ID, "=u", bound_id,
-        NULL);
-    wp_object_manager_request_proxy_features (priv->streams_om,
-        WP_TYPE_ENDPOINT_STREAM,
-        WP_PROXY_FEATURES_STANDARD | WP_PROXY_FEATURE_PROPS);
-
-    /* endpoints, under normal circumstances, always have streams.
-       When we export (self is a WpImplEndpoint), we have to export first
-       the endpoint and afterwards the streams (so that the streams can be
-       associated with the endpoint's bound id), but then the issue is that
-       the "installed" signal gets fired here without any streams being ready
-       and we get an endpoint with 0 streams in the WpSession's endpoints
-       object manager... so, unless the endpoint really has no streams,
-       wait for them to be prepared by waiting for the "objects-changed" only */
-    if (G_UNLIKELY (priv->info->n_streams == 0)) {
-      g_signal_connect_object (priv->streams_om, "installed",
-          G_CALLBACK (wp_endpoint_on_streams_om_installed), self, 0);
-    }
-    g_signal_connect_object (priv->streams_om, "objects-changed",
-        G_CALLBACK (wp_endpoint_emit_streams_changed), self, 0);
-
-    wp_core_install_object_manager (core, priv->streams_om);
+  priv->streams_om = wp_object_manager_new ();
+  /* proxy endpoint stream -> check for endpoint.id in global properties */
+  wp_object_manager_add_interest (priv->streams_om,
+      WP_TYPE_ENDPOINT_STREAM,
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_ENDPOINT_ID, "=u", bound_id,
+      NULL);
+  /* impl endpoint stream -> check for endpoint.id in standard properties */
+  wp_object_manager_add_interest (priv->streams_om,
+      WP_TYPE_IMPL_ENDPOINT_STREAM,
+      WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_ENDPOINT_ID, "=u", bound_id,
+      NULL);
+  wp_object_manager_request_object_features (priv->streams_om,
+      WP_TYPE_ENDPOINT_STREAM, WP_OBJECT_FEATURES_ALL);
+
+  /* endpoints, under normal circumstances, always have streams.
+     When we export (self is a WpImplEndpoint), we have to export first
+     the endpoint and afterwards the streams (so that the streams can be
+     associated with the endpoint's bound id), but then the issue is that
+     the "installed" signal gets fired here without any streams being ready
+     and we get an endpoint with 0 streams in the WpSession's endpoints
+     object manager... so, unless the endpoint really has no streams,
+     wait for them to be prepared by waiting for the "objects-changed" only */
+  if (G_UNLIKELY (priv->info->n_streams == 0)) {
+    g_signal_connect_object (priv->streams_om, "installed",
+        G_CALLBACK (wp_endpoint_on_streams_om_installed), self, 0);
   }
+  g_signal_connect_object (priv->streams_om, "objects-changed",
+      G_CALLBACK (wp_endpoint_emit_streams_changed), self, 0);
+
+  wp_core_install_object_manager (core, priv->streams_om);
 }
 
-static void
-wp_endpoint_augment (WpProxy * proxy, WpProxyFeatures features)
+static WpObjectFeatures
+wp_endpoint_get_supported_features (WpObject * object)
 {
-  WpEndpoint *self = WP_ENDPOINT (proxy);
+  WpEndpoint *self = WP_ENDPOINT (object);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  /* call the parent impl first to ensure we have a pw proxy if necessary */
-  WP_PROXY_CLASS (wp_endpoint_parent_class)->augment (proxy, features);
-
-  if (features & WP_ENDPOINT_FEATURE_STREAMS) {
-    priv->ft_streams_requested = TRUE;
-    wp_endpoint_ensure_feature_streams (self, 0);
-  }
+  return
+      WP_PROXY_FEATURE_BOUND |
+      WP_ENDPOINT_FEATURE_STREAMS |
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          priv->info ? priv->info->params : NULL,
+          priv->info ? priv->info->n_params : 0);
 }
 
-static gconstpointer
-wp_endpoint_get_info (WpProxy * proxy)
+enum {
+  STEP_STREAMS = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+};
+
+static guint
+wp_endpoint_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpEndpoint *self = WP_ENDPOINT (proxy);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
+      step, missing);
 
-  return priv->info;
+  /* extend the mixin's state machine; when the only remaining feature to
+     enable is FEATURE_STREAMS, advance to STEP_STREAMS */
+  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
+      missing == WP_ENDPOINT_FEATURE_STREAMS)
+    return STEP_STREAMS;
+
+  return step;
 }
 
-static WpProperties *
-wp_endpoint_get_properties (WpProxy * proxy)
+static void
+wp_endpoint_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpEndpoint *self = WP_ENDPOINT (proxy);
-  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  case STEP_STREAMS:
+    wp_endpoint_enable_feature_streams (WP_ENDPOINT (object));
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_endpoint_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static struct spa_param_info *
-wp_endpoint_get_param_info (WpProxy * proxy, guint * n_params)
+static void
+wp_endpoint_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpEndpoint *self = WP_ENDPOINT (proxy);
+  WpEndpoint *self = WP_ENDPOINT (object);
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
 
-  *n_params = priv->info->n_params;
-  return priv->info->params;
-}
+  wp_pipewire_object_mixin_deactivate (object, features);
 
-static gint
-wp_endpoint_enum_params (WpProxy * self, guint32 id, guint32 start, guint32 num,
-    WpSpaPod * filter)
-{
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-  return pw_endpoint_enum_params (priv->iface, 0, id,
-      start, num, filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
-}
-
-static gint
-wp_endpoint_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
-{
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-  return pw_endpoint_subscribe_params (priv->iface, ids, n_ids);
-}
+  if (features & WP_ENDPOINT_FEATURE_STREAMS) {
+    g_clear_object (&priv->streams_om);
+    wp_object_update_features (object, 0, WP_ENDPOINT_FEATURE_STREAMS);
+  }
 
-static gint
-wp_endpoint_set_param (WpProxy * self, guint32 id, guint32 flags,
-    WpSpaPod *param)
-{
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-  return pw_endpoint_set_param (priv->iface, id, flags,
-      wp_spa_pod_get_spa_pod (param));
+  WP_OBJECT_CLASS (wp_endpoint_parent_class)->deactivate (object, features);
 }
 
 static void
@@ -257,22 +233,17 @@ endpoint_event_info (void *data, const struct pw_endpoint_info *info)
     priv->properties = wp_properties_new_wrap_dict (priv->info->props);
   }
 
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
-
-  if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_ENDPOINT_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  wp_endpoint_ensure_feature_streams (self, 0);
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_ENDPOINT_CHANGE_MASK_PROPS, PW_ENDPOINT_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_endpoint_events endpoint_events = {
   PW_VERSION_ENDPOINT_EVENTS,
   .info = endpoint_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
@@ -287,34 +258,41 @@ wp_endpoint_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 }
 
 static void
-wp_endpoint_bound (WpProxy * proxy, guint32 id)
+wp_endpoint_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpEndpoint *self = WP_ENDPOINT (proxy);
-  wp_endpoint_ensure_feature_streams (self, id);
+  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&priv->info, pw_endpoint_info_free);
+  g_clear_object (&priv->streams_om);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO | WP_ENDPOINT_FEATURE_STREAMS);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
 }
 
 static void
 wp_endpoint_class_init (WpEndpointClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_endpoint_finalize;
-  object_class->get_property = wp_endpoint_get_gobj_property;
+  object_class->get_property = wp_endpoint_get_property;
+
+  wpobject_class->get_supported_features = wp_endpoint_get_supported_features;
+  wpobject_class->activate_get_next_step = wp_endpoint_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_endpoint_activate_execute_step;
+  wpobject_class->deactivate = wp_endpoint_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Endpoint;
   proxy_class->pw_iface_version = PW_VERSION_ENDPOINT;
-
-  proxy_class->augment = wp_endpoint_augment;
-  proxy_class->get_info = wp_endpoint_get_info;
-  proxy_class->get_properties = wp_endpoint_get_properties;
-  proxy_class->get_param_info = wp_endpoint_get_param_info;
-  proxy_class->enum_params = wp_endpoint_enum_params;
-  proxy_class->subscribe_params = wp_endpoint_subscribe_params;
-  proxy_class->set_param = wp_endpoint_set_param;
-
   proxy_class->pw_proxy_created = wp_endpoint_pw_proxy_created;
-  proxy_class->bound = wp_endpoint_bound;
+  proxy_class->pw_proxy_destroyed = wp_endpoint_pw_proxy_destroyed;
+
+  wp_pipewire_object_mixin_class_override_properties (object_class);
 
   /**
    * WpEndpoint::streams-changed:
@@ -355,18 +333,77 @@ wp_endpoint_class_init (WpEndpointClass * klass)
           WP_TYPE_DIRECTION, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 }
 
+static gconstpointer
+wp_endpoint_get_native_info (WpPipewireObject * obj)
+{
+  WpEndpoint *self = WP_ENDPOINT (obj);
+  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+
+  return priv->info;
+}
+
+static WpProperties *
+wp_endpoint_get_properties (WpPipewireObject * obj)
+{
+  WpEndpoint *self = WP_ENDPOINT (obj);
+  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+
+  return wp_properties_ref (priv->properties);
+}
+
+static GVariant *
+wp_endpoint_get_param_info (WpPipewireObject * obj)
+{
+  WpEndpoint *self = WP_ENDPOINT (obj);
+  WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
+
+  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
+      priv->info->n_params);
+}
+
+static void
+wp_endpoint_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_endpoint, obj, id, filter,
+      cancellable, callback, user_data);
+}
+
+static void
+wp_endpoint_set_param (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_endpoint, obj, id, param);
+}
+
+static void
+wp_endpoint_pipewire_object_interface_init (
+    WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_endpoint_get_native_info;
+  iface->get_properties = wp_endpoint_get_properties;
+  iface->get_param_info = wp_endpoint_get_param_info;
+  iface->enum_params = wp_endpoint_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_endpoint_set_param;
+}
+
 /**
  * wp_endpoint_get_name:
  * @self: the endpoint
  *
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
+ *
  * Returns: the name of the endpoint
  */
 const gchar *
 wp_endpoint_get_name (WpEndpoint * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, NULL);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
   return priv->info->name;
@@ -376,14 +413,16 @@ wp_endpoint_get_name (WpEndpoint * self)
  * wp_endpoint_get_media_class:
  * @self: the endpoint
  *
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
+ *
  * Returns: the media class of the endpoint (ex. "Audio/Sink")
  */
 const gchar *
 wp_endpoint_get_media_class (WpEndpoint * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, NULL);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
   return priv->info->media_class;
@@ -393,14 +432,16 @@ wp_endpoint_get_media_class (WpEndpoint * self)
  * wp_endpoint_get_direction:
  * @self: the endpoint
  *
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
+ *
  * Returns: the direction of this endpoint
  */
 WpDirection
 wp_endpoint_get_direction (WpEndpoint * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, 0);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
   return (WpDirection) priv->info->direction;
@@ -418,7 +459,7 @@ guint
 wp_endpoint_get_n_streams (WpEndpoint * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_ENDPOINT_FEATURE_STREAMS, 0);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
@@ -438,7 +479,7 @@ WpIterator *
 wp_endpoint_iterate_streams (WpEndpoint * self)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_ENDPOINT_FEATURE_STREAMS, NULL);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
@@ -484,7 +525,7 @@ wp_endpoint_iterate_streams_filtered_full (WpEndpoint * self,
     WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_ENDPOINT_FEATURE_STREAMS, NULL);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
@@ -529,7 +570,7 @@ WpEndpointStream *
 wp_endpoint_lookup_stream_full (WpEndpoint * self, WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_ENDPOINT (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_ENDPOINT_FEATURE_STREAMS, NULL);
 
   WpEndpointPrivate *priv = wp_endpoint_get_instance_private (self);
@@ -589,7 +630,6 @@ struct _WpImplEndpoint
   struct spa_interface iface;
   struct spa_hook_list hooks;
   struct pw_endpoint_info info;
-  gboolean subscribed;
 
   WpSiEndpoint *item;
 };
@@ -603,10 +643,10 @@ G_DEFINE_TYPE (WpImplEndpoint, wp_impl_endpoint, WP_TYPE_ENDPOINT)
 #define pw_endpoint_emit_info(hooks,...)  pw_endpoint_emit(hooks, info, 0, ##__VA_ARGS__)
 #define pw_endpoint_emit_param(hooks,...) pw_endpoint_emit(hooks, param, 0, ##__VA_ARGS__)
 
-static struct spa_param_info impl_param_info[] = {
-  SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
-  SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
-};
+// static struct spa_param_info impl_param_info[] = {
+//   SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
+//   SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
+// };
 
 static int
 impl_add_listener(void *object,
@@ -632,57 +672,12 @@ impl_enum_params (void *object, int seq,
     uint32_t id, uint32_t start, uint32_t num,
     const struct spa_pod *filter)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
-  struct spa_pod *result;
-  guint count = 0;
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-
-  switch (id) {
-    case SPA_PARAM_PropInfo: {
-      g_autoptr (WpIterator) params = wp_props_iterate_prop_info (props);
-      g_auto (GValue) item = G_VALUE_INIT;
-      guint i = 0;
-
-      for (; wp_iterator_next (params, &item); g_value_unset (&item), i++) {
-        WpSpaPod *pod = g_value_get_boxed (&item);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_endpoint_emit_param (&self->hooks, seq, id, i, i+1, result);
-          if (++count == num)
-            break;
-        }
-      }
-      break;
-    }
-    case SPA_PARAM_Props: {
-      if (start == 0) {
-        g_autoptr (WpSpaPod) pod = wp_props_get_all (props);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_endpoint_emit_param (&self->hooks, seq, id, 0, 1, result);
-        }
-      }
-      break;
-    }
-    default:
-      return -ENOENT;
-  }
-
-  return 0;
+  return -ENOENT;
 }
 
 static int
 impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-
-  for (guint i = 0; i < n_ids; i++) {
-    if (ids[i] == SPA_PARAM_Props)
-      self->subscribed = TRUE;
-    impl_enum_params (self, 1, ids[i], 0, UINT32_MAX, NULL);
-  }
   return 0;
 }
 
@@ -690,14 +685,7 @@ static int
 impl_set_param (void *object, uint32_t id, uint32_t flags,
     const struct spa_pod *param)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-
-  if (id != SPA_PARAM_Props)
-    return -ENOENT;
-
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-  wp_props_set (props, NULL, wp_spa_pod_new_wrap_const (param));
-  return 0;
+  return -ENOENT;
 }
 
 static void
@@ -839,7 +827,7 @@ impl_create_link (void *object, const struct spa_dict *props)
     GVariantBuilder b;
     guint64 out_stream_i, in_stream_i;
 
-    core = wp_proxy_get_core (WP_PROXY (self));
+    core = wp_object_get_core (WP_OBJECT (self));
     link = wp_session_item_make (core, "si-standard-link");
     if (!link) {
       wp_warning_object (self, "si-standard-link factory is not available");
@@ -898,14 +886,14 @@ populate_properties (WpImplEndpoint * self, WpProperties *global_props)
 
   self->info.props = priv->properties ?
       (struct spa_dict *) wp_properties_peek_dict (priv->properties) : NULL;
-
-  g_object_notify (G_OBJECT (self), "properties");
 }
 
 static void
 on_si_endpoint_properties_changed (WpSiEndpoint * item, WpImplEndpoint * self)
 {
-  populate_properties (self, wp_proxy_get_global_properties (WP_PROXY (self)));
+  populate_properties (self,
+      wp_global_proxy_get_global_properties (WP_GLOBAL_PROXY (self)));
+  g_object_notify (G_OBJECT (self), "properties");
 
   self->info.change_mask = PW_ENDPOINT_CHANGE_MASK_PROPS;
   pw_endpoint_emit_info (&self->hooks, &self->info);
@@ -929,20 +917,6 @@ wp_impl_endpoint_init (WpImplEndpoint * self)
   priv->iface = (struct pw_endpoint *) &self->iface;
 }
 
-static void
-wp_impl_endpoint_finalize (GObject * object)
-{
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-
-  g_free (self->info.name);
-  g_free (self->info.media_class);
-  priv->info = NULL;
-
-  G_OBJECT_CLASS (wp_impl_endpoint_parent_class)->finalize (object);
-}
-
 static void
 wp_impl_endpoint_set_property (GObject * object, guint property_id,
     const GValue * value, GParamSpec * pspec)
@@ -975,157 +949,189 @@ wp_impl_endpoint_get_property (GObject * object, guint property_id,
   }
 }
 
-static void
-wp_impl_endpoint_export (WpImplEndpoint *self)
-{
-  WpEndpointPrivate *priv =
-      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
-  g_autoptr (GVariantIter) immutable_properties = NULL;
-  g_autoptr (WpProperties) properties = NULL;
-  g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self));
-  struct pw_core *pw_core = wp_core_get_pw_core (core);
-
-  /* no pw_core -> we are not connected */
-  if (!pw_core) {
-    wp_proxy_augment_error (WP_PROXY (self), g_error_new (WP_DOMAIN_LIBRARY,
-          WP_LIBRARY_ERROR_OPERATION_FAILED,
-          "The WirePlumber core is not connected; "
-          "object cannot be exported to PipeWire"));
-    return;
-  }
-
-  wp_debug_object (self, "exporting");
-
-  /* get info from the interface */
-  {
-    g_autoptr (GVariant) info = NULL;
-    guchar direction;
-
-    info = wp_si_endpoint_get_registration_info (self->item);
-    g_variant_get (info, "(ssya{ss})", &self->info.name,
-        &self->info.media_class, &direction, &immutable_properties);
-
-    self->info.direction = (enum pw_direction) direction;
-    self->info.n_streams = wp_si_endpoint_get_n_streams (self->item);
+enum {
+  STEP_ACTIVATE_NODE = STEP_STREAMS + 1,
+};
 
-    /* associate with the session */
-    self->info.session_id = wp_session_item_get_associated_proxy_id (
-        WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
-  }
+static guint
+wp_impl_endpoint_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
 
-  /* construct export properties (these will come back through
-    the registry and appear in wp_proxy_get_global_properties) */
-  properties = wp_properties_new (
-      PW_KEY_ENDPOINT_NAME, self->info.name,
-      PW_KEY_MEDIA_CLASS, self->info.media_class,
-      NULL);
-  wp_properties_setf (properties, PW_KEY_SESSION_ID,
-      "%d", self->info.session_id);
+  /* bind if not already bound */
+  if (missing & WP_PROXY_FEATURE_BOUND) {
+    g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
+        WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-  /* populate immutable (global) properties */
-  {
-    const gchar *key, *value;
-    while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
-      wp_properties_set (properties, key, value);
+    /* if the item has a node, cache its props so that enum_params works */
+    if (node && !(wp_object_get_active_features (node) &
+                      WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS))
+      return STEP_ACTIVATE_NODE;
+    else
+      return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
   }
-
-  /* populate standard properties */
-  populate_properties (self, properties);
-
-  /* subscribe to changes */
-  g_signal_connect_object (self->item, "endpoint-properties-changed",
-      G_CALLBACK (on_si_endpoint_properties_changed), self, 0);
-
-  /* finalize info struct */
-  self->info.version = PW_VERSION_ENDPOINT_INFO;
-  self->info.params = impl_param_info;
-  self->info.n_params = SPA_N_ELEMENTS (impl_param_info);
-  priv->info = &self->info;
-
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
-  g_object_notify (G_OBJECT (self), "param-info");
-
-  wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
-          PW_TYPE_INTERFACE_Endpoint,
-          wp_properties_peek_dict (properties),
-          priv->iface, 0));
+  /* enable FEATURE_STREAMS when there is nothing else left to activate */
+  else if (missing == WP_ENDPOINT_FEATURE_STREAMS)
+    return STEP_STREAMS;
+  else
+    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
 }
 
 static void
-wp_impl_endpoint_continue_feature_props (WpProxy * node, GAsyncResult * res,
-    WpImplEndpoint * self)
+wp_impl_endpoint_node_activated (WpObject * node,
+    GAsyncResult * res, WpTransition * transition)
 {
   g_autoptr (GError) error = NULL;
 
-  if (!wp_proxy_augment_finish (node, res, &error)) {
-    wp_proxy_augment_error (WP_PROXY (self), g_steal_pointer (&error));
+  if (!wp_object_activate_finish (node, res, &error)) {
+    wp_transition_return_error (transition, g_steal_pointer (&error));
     return;
   }
 
-  WpProps *props = wp_proxy_get_props (node);
-  wp_proxy_set_props (WP_PROXY (self), g_object_ref (props));
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_PROPS);
-  wp_impl_endpoint_export (self);
+  wp_transition_advance (transition);
 }
 
 static void
-wp_impl_endpoint_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_impl_endpoint_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpImplEndpoint *self = WP_IMPL_ENDPOINT (proxy);
+  WpImplEndpoint *self = WP_IMPL_ENDPOINT (object);
   WpEndpointPrivate *priv =
       wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
-  /* if any of these features are requested, export,
-     after ensuring that we have a WpProps so that enum_params works */
-  if (features & (WP_PROXY_FEATURES_STANDARD | WP_PROXY_FEATURE_PROPS)) {
-    g_autoptr (WpProxy) node = wp_session_item_get_associated_proxy (
+  switch (step) {
+  case STEP_ACTIVATE_NODE: {
+    g_autoptr (WpObject) node = wp_session_item_get_associated_proxy (
         WP_SESSION_ITEM (self->item), WP_TYPE_NODE);
 
-    if (node) {
-      /* if the item has a node, use the props of that node */
-      wp_proxy_augment (node, WP_PROXY_FEATURE_PROPS, NULL,
-          (GAsyncReadyCallback) wp_impl_endpoint_continue_feature_props, self);
-    } else {
-      /* else install an empty WpProps */
-      WpProps *props = wp_props_new (WP_PROPS_MODE_STORE, proxy);
-      wp_proxy_set_props (proxy, props);
-      wp_proxy_set_feature_ready (proxy, WP_PROXY_FEATURE_PROPS);
-      wp_impl_endpoint_export (self);
-    }
+    wp_object_activate (node,
+        WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS,
+        NULL, (GAsyncReadyCallback) wp_impl_endpoint_node_activated,
+        transition);
+    break;
   }
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
+    g_autoptr (GVariantIter) immutable_properties = NULL;
+    g_autoptr (WpProperties) properties = NULL;
+    g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+    struct pw_core *pw_core = wp_core_get_pw_core (core);
+
+    /* no pw_core -> we are not connected */
+    if (!pw_core) {
+      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+              "The WirePlumber core is not connected; "
+              "object cannot be exported to PipeWire"));
+      return;
+    }
 
-  if (features & WP_ENDPOINT_FEATURE_STREAMS) {
-    priv->ft_streams_requested = TRUE;
-    wp_endpoint_ensure_feature_streams (WP_ENDPOINT (self), 0);
+    wp_debug_object (self, "exporting");
+
+    /* get info from the interface */
+    {
+      g_autoptr (GVariant) info = NULL;
+      guchar direction;
+
+      info = wp_si_endpoint_get_registration_info (self->item);
+      g_variant_get (info, "(ssya{ss})", &self->info.name,
+          &self->info.media_class, &direction, &immutable_properties);
+
+      self->info.direction = (enum pw_direction) direction;
+      self->info.n_streams = wp_si_endpoint_get_n_streams (self->item);
+
+      /* associate with the session */
+      self->info.session_id = wp_session_item_get_associated_proxy_id (
+          WP_SESSION_ITEM (self->item), WP_TYPE_SESSION);
+    }
+
+    /* construct export properties (these will come back through
+      the registry and appear in wp_proxy_get_global_properties) */
+    properties = wp_properties_new (
+        PW_KEY_ENDPOINT_NAME, self->info.name,
+        PW_KEY_MEDIA_CLASS, self->info.media_class,
+        NULL);
+    wp_properties_setf (properties, PW_KEY_SESSION_ID,
+        "%d", self->info.session_id);
+
+    /* populate immutable (global) properties */
+    {
+      const gchar *key, *value;
+      while (g_variant_iter_next (immutable_properties, "{&s&s}", &key, &value))
+        wp_properties_set (properties, key, value);
+    }
+
+    /* populate standard properties */
+    populate_properties (self, properties);
+
+    /* subscribe to changes */
+    g_signal_connect_object (self->item, "endpoint-properties-changed",
+        G_CALLBACK (on_si_endpoint_properties_changed), self, 0);
+
+    /* finalize info struct */
+    self->info.version = PW_VERSION_ENDPOINT_INFO;
+    self->info.params = NULL;
+    self->info.n_params = 0;
+    priv->info = &self->info;
+
+    /* bind */
+    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
+            PW_TYPE_INTERFACE_Endpoint,
+            wp_properties_peek_dict (properties),
+            priv->iface, 0));
+
+    /* notify */
+    wp_object_update_features (object,
+        WP_PIPEWIRE_OBJECT_FEATURE_INFO
+        /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */, 0);
+    g_object_notify (G_OBJECT (self), "properties");
+    g_object_notify (G_OBJECT (self), "param-info");
+
+    break;
+  }
+  default:
+    WP_OBJECT_CLASS (wp_impl_endpoint_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
   }
 }
 
 static void
-wp_impl_endpoint_prop_changed (WpProxy * proxy, const gchar * prop_name)
+wp_impl_endpoint_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpImplEndpoint *self = WP_IMPL_ENDPOINT (proxy);
+  WpEndpointPrivate *priv =
+      wp_endpoint_get_instance_private (WP_ENDPOINT (self));
 
-  /* notify subscribers */
-  if (self->subscribed)
-    impl_enum_params (self, 1, SPA_PARAM_Props, 0, UINT32_MAX, NULL);
+  g_signal_handlers_disconnect_by_data (self->item, self);
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&self->info.name, g_free);
+  g_clear_pointer (&self->info.media_class, g_free);
+  priv->info = NULL;
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO
+      /*| WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS */);
 }
 
 static void
 wp_impl_endpoint_class_init (WpImplEndpointClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_impl_endpoint_finalize;
   object_class->set_property = wp_impl_endpoint_set_property;
   object_class->get_property = wp_impl_endpoint_get_property;
 
-  proxy_class->augment = wp_impl_endpoint_augment;
-  proxy_class->enum_params = NULL;
-  proxy_class->subscribe_params = NULL;
+  wpobject_class->activate_get_next_step =
+      wp_impl_endpoint_activate_get_next_step;
+  wpobject_class->activate_execute_step =
+      wp_impl_endpoint_activate_execute_step;
+
   proxy_class->pw_proxy_created = NULL;
-  proxy_class->prop_changed = wp_impl_endpoint_prop_changed;
+  proxy_class->pw_proxy_destroyed = wp_impl_endpoint_pw_proxy_destroyed;
 
   g_object_class_install_property (object_class, IMPL_PROP_ITEM,
       g_param_spec_object ("item", "item", "item", WP_TYPE_SI_ENDPOINT,
diff --git a/lib/wp/endpoint.h b/lib/wp/endpoint.h
index 2aefdff819b86f5979d73f590a916c889868a842..d7bce89c5b644c618fe0f621e65cb224de2ca7b4 100644
--- a/lib/wp/endpoint.h
+++ b/lib/wp/endpoint.h
@@ -9,8 +9,7 @@
 #ifndef __WIREPLUMBER_ENDPOINT_H__
 #define __WIREPLUMBER_ENDPOINT_H__
 
-#include "spa-pod.h"
-#include "proxy.h"
+#include "global-proxy.h"
 #include "port.h"
 #include "endpoint-stream.h"
 #include "iterator.h"
@@ -27,20 +26,9 @@ G_BEGIN_DECLS
  * An extension of #WpProxyFeatures
  */
 typedef enum { /*< flags >*/
-  WP_ENDPOINT_FEATURE_STREAMS = (WP_PROXY_FEATURE_LAST << 0),
+  WP_ENDPOINT_FEATURE_STREAMS = (WP_PROXY_FEATURE_CUSTOM_START << 0),
 } WpEndpointFeatures;
 
-/**
- * WP_ENDPOINT_FEATURES_STANDARD:
- *
- * A constant set of features that contains the standard features that are
- * available in the #WpEndpoint class.
- */
-#define WP_ENDPOINT_FEATURES_STANDARD \
-    (WP_PROXY_FEATURES_STANDARD | \
-     WP_PROXY_FEATURE_PROPS | \
-     WP_ENDPOINT_FEATURE_STREAMS)
-
 /**
  * WP_TYPE_ENDPOINT:
  *
@@ -48,11 +36,11 @@ typedef enum { /*< flags >*/
  */
 #define WP_TYPE_ENDPOINT (wp_endpoint_get_type ())
 WP_API
-G_DECLARE_DERIVABLE_TYPE (WpEndpoint, wp_endpoint, WP, ENDPOINT, WpProxy)
+G_DECLARE_DERIVABLE_TYPE (WpEndpoint, wp_endpoint, WP, ENDPOINT, WpGlobalProxy)
 
 struct _WpEndpointClass
 {
-  WpProxyClass parent_class;
+  WpGlobalProxyClass parent_class;
 };
 
 WP_API
diff --git a/lib/wp/global-proxy.c b/lib/wp/global-proxy.c
new file mode 100644
index 0000000000000000000000000000000000000000..7de94a45bf1dc5541bbccd7319427c5212b29893
--- /dev/null
+++ b/lib/wp/global-proxy.c
@@ -0,0 +1,293 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+/**
+ * SECTION: global-proxy
+ * @title: PipeWire Global Object Proxy
+ */
+
+#define G_LOG_DOMAIN "wp-global-proxy"
+
+#include "global-proxy.h"
+#include "private/registry.h"
+#include "core.h"
+#include "error.h"
+
+typedef struct _WpGlobalProxyPrivate WpGlobalProxyPrivate;
+struct _WpGlobalProxyPrivate
+{
+  WpGlobal *global;
+};
+
+enum {
+  PROP_0,
+  PROP_GLOBAL,
+  PROP_PERMISSIONS,
+  PROP_GLOBAL_PROPERTIES,
+};
+
+/**
+ * WpGlobalProxy:
+ *
+ * A proxy that represents a PipeWire global object, i.e. an object that is
+ * made available through the PipeWire registry.
+ */
+G_DEFINE_TYPE_WITH_PRIVATE (WpGlobalProxy, wp_global_proxy, WP_TYPE_PROXY)
+
+static void
+wp_global_proxy_init (WpGlobalProxy * self)
+{
+}
+
+static void
+wp_global_proxy_dispose (GObject * object)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  if (priv->global)
+    wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
+
+  G_OBJECT_CLASS (wp_global_proxy_parent_class)->dispose (object);
+}
+
+static void
+wp_global_proxy_finalize (GObject * object)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  g_clear_pointer (&priv->global, wp_global_unref);
+
+  G_OBJECT_CLASS (wp_global_proxy_parent_class)->finalize (object);
+}
+
+static void
+wp_global_proxy_set_property (GObject * object, guint property_id,
+    const GValue * value, GParamSpec * pspec)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  switch (property_id) {
+  case PROP_GLOBAL:
+    priv->global = g_value_dup_boxed (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static void
+wp_global_proxy_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
+
+  switch (property_id) {
+  case PROP_PERMISSIONS:
+    g_value_set_uint (value, wp_global_proxy_get_permissions (self));
+    break;
+  case PROP_GLOBAL_PROPERTIES:
+    g_value_set_boxed (value, wp_global_proxy_get_global_properties (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+static WpObjectFeatures
+wp_global_proxy_get_supported_features (WpObject * object)
+{
+  return WP_PROXY_FEATURE_BOUND;
+}
+
+enum {
+  STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
+};
+
+static guint
+wp_global_proxy_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  g_return_val_if_fail (missing == WP_PROXY_FEATURE_BOUND,
+      WP_TRANSITION_STEP_ERROR);
+
+  return STEP_BIND;
+}
+
+static void
+wp_global_proxy_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (object);
+
+  switch (step) {
+  case STEP_BIND:
+    if (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL) {
+      if (!wp_global_proxy_bind (self)) {
+        wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+                WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVALID_ARGUMENT,
+                "No global specified; cannot bind proxy"));
+      }
+    }
+    break;
+  case WP_TRANSITION_STEP_ERROR:
+    break;
+  default:
+    g_assert_not_reached ();
+  }
+}
+
+static void
+wp_global_proxy_bound (WpProxy * proxy, guint32 global_id)
+{
+  WpGlobalProxy *self = WP_GLOBAL_PROXY (proxy);
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+
+  if (!priv->global) {
+    wp_registry_prepare_new_global (wp_core_get_registry (core),
+        global_id, PW_PERM_ALL, WP_GLOBAL_FLAG_OWNED_BY_PROXY,
+        G_TYPE_FROM_INSTANCE (self), self, NULL, &priv->global);
+  }
+}
+
+static void
+wp_global_proxy_class_init (WpGlobalProxyClass * klass)
+{
+  GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
+  WpProxyClass *proxy_class = (WpProxyClass *) klass;
+
+  object_class->finalize = wp_global_proxy_finalize;
+  object_class->dispose = wp_global_proxy_dispose;
+  object_class->set_property = wp_global_proxy_set_property;
+  object_class->get_property = wp_global_proxy_get_property;
+
+  wpobject_class->get_supported_features =
+      wp_global_proxy_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_global_proxy_activate_get_next_step;
+  wpobject_class->activate_execute_step =
+      wp_global_proxy_activate_execute_step;
+
+  proxy_class->bound = wp_global_proxy_bound;
+
+  g_object_class_install_property (object_class, PROP_GLOBAL,
+      g_param_spec_boxed ("global", "global", "Internal WpGlobal object",
+          wp_global_get_type (),
+          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_PERMISSIONS,
+      g_param_spec_uint ("permissions", "permissions",
+          "The pipewire global permissions", 0, G_MAXUINT, 0,
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  g_object_class_install_property (object_class, PROP_GLOBAL_PROPERTIES,
+      g_param_spec_boxed ("global-properties", "global-properties",
+          "The pipewire global properties", WP_TYPE_PROPERTIES,
+          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+}
+
+/**
+ * wp_global_proxy_request_destroy:
+ * @self: the pipewire global
+ *
+ * Requests the PipeWire server to destroy the object represented by this proxy.
+ * If the server allows it, the object will be destroyed and the
+ * WpProxy::pw-proxy-destroyed signal will be emitted. If the server does
+ * not allow it, nothing will happen.
+ *
+ * This is mostly useful for destroying #WpLink and #WpEndpointLink objects.
+ */
+void
+wp_global_proxy_request_destroy (WpGlobalProxy * self)
+{
+  g_return_if_fail (WP_IS_GLOBAL_PROXY (self));
+
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+
+  if (priv->global && core) {
+    WpRegistry *reg = wp_core_get_registry (core);
+    pw_registry_destroy (reg->pw_registry, priv->global->id);
+  }
+}
+
+/**
+ * wp_global_proxy_get_permissions:
+ * @self: the pipewire global
+ *
+ * Returns: the permissions that wireplumber has on this object
+ */
+guint32
+wp_global_proxy_get_permissions (WpGlobalProxy * self)
+{
+  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), 0);
+
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  return priv->global ? priv->global->permissions : PW_PERM_ALL;
+}
+
+/**
+ * wp_global_proxy_get_global_properties:
+ * @self: the pipewire global
+ *
+ * Returns: (transfer full): the global (immutable) properties of this
+ *   pipewire object
+ */
+WpProperties *
+wp_global_proxy_get_global_properties (WpGlobalProxy * self)
+{
+  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), NULL);
+
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  if (priv->global && priv->global->properties)
+    return wp_properties_ref (priv->global->properties);
+  return NULL;
+}
+
+/**
+ * wp_global_proxy_bind:
+ * @self: the pipewire global
+ *
+ * Binds to the global and creates the underlying `pw_proxy`. This may only
+ * be called if there is no `pw_proxy` associated with this object yet.
+ *
+ * This is mostly meant to be called internally. It will create the `pw_proxy`
+ * and will activate the %WP_PROXY_FEATURE_BOUND feature.
+ *
+ * Returns: %TRUE on success, %FALSE if there is no global to bind to
+ */
+gboolean
+wp_global_proxy_bind (WpGlobalProxy * self)
+{
+  g_return_val_if_fail (WP_IS_GLOBAL_PROXY (self), FALSE);
+  g_return_val_if_fail (wp_proxy_get_pw_proxy (WP_PROXY (self)) == NULL, FALSE);
+
+  WpGlobalProxyPrivate *priv =
+      wp_global_proxy_get_instance_private (self);
+
+  if (priv->global)
+    wp_proxy_set_pw_proxy (WP_PROXY (self), wp_global_bind (priv->global));
+  return !!priv->global;
+}
diff --git a/lib/wp/global-proxy.h b/lib/wp/global-proxy.h
new file mode 100644
index 0000000000000000000000000000000000000000..2a674353c8854e65f2b91b48040ae3f9d4ec47ec
--- /dev/null
+++ b/lib/wp/global-proxy.h
@@ -0,0 +1,47 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef __WIREPLUMBER_GLOBAL_PROXY_H__
+#define __WIREPLUMBER_GLOBAL_PROXY_H__
+
+#include "proxy.h"
+#include "properties.h"
+
+G_BEGIN_DECLS
+
+/**
+ * WP_TYPE_GLOBAL_PROXY:
+ *
+ * The #WpGlobalProxy #GType
+ */
+#define WP_TYPE_GLOBAL_PROXY (wp_global_proxy_get_type ())
+WP_API
+G_DECLARE_DERIVABLE_TYPE (WpGlobalProxy, wp_global_proxy,
+                          WP, GLOBAL_PROXY, WpObject)
+
+struct _WpGlobalProxyClass
+{
+  WpProxyClass parent_class;
+};
+
+WP_API
+void wp_global_proxy_request_destroy (WpGlobalProxy * self);
+
+WP_API
+guint32 wp_global_proxy_get_permissions (WpGlobalProxy * self);
+
+WP_API
+WpProperties * wp_global_proxy_get_global_properties (
+    WpGlobalProxy * self);
+
+WP_API
+gboolean wp_global_proxy_bind (WpGlobalProxy * self);
+
+G_END_DECLS
+
+#endif
diff --git a/lib/wp/link.c b/lib/wp/link.c
index 157cb155cc985e0318e45d4f52d2f6e9b2fdb288..0a030b1125364df33bfa89d0db9c27958f644c8b 100644
--- a/lib/wp/link.c
+++ b/lib/wp/link.c
@@ -1,67 +1,70 @@
 /* WirePlumber
  *
- * Copyright © 2019 Collabora Ltd.
+ * Copyright © 2019-2020 Collabora Ltd.
  *    @author Julian Bouzas <julian.bouzas@collabora.com>
  *
  * SPDX-License-Identifier: MIT
  */
 
 /**
- * SECTION: WpLink
- *
- * The #WpLink class allows accessing the properties and methods of a
- * PipeWire link object (`struct pw_link`).
- *
- * A #WpLink is constructed internally when a new link appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
- * Alternatively, a #WpLink can also be constructed using
- * wp_link_new_from_factory(), which creates a new link object
- * on the remote PipeWire server by calling into a factory.
+ * SECTION: link
+ * @title: PipeWire Link
  */
 
 #define G_LOG_DOMAIN "wp-link"
 
 #include "link.h"
-#include "private.h"
-
-#include <pipewire/pipewire.h>
+#include "private/pipewire-object-mixin.h"
 
 struct _WpLink
 {
-  WpProxy parent;
+  WpGlobalProxy parent;
   struct pw_link_info *info;
-
-  /* The link proxy listener */
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE (WpLink, wp_link, WP_TYPE_PROXY)
+static void wp_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+
+/**
+ * WpLink:
+ *
+ * The #WpLink class allows accessing the properties and methods of a
+ * PipeWire link object (`struct pw_link`).
+ *
+ * A #WpLink is constructed internally when a new link appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ * Alternatively, a #WpLink can also be constructed using
+ * wp_link_new_from_factory(), which creates a new link object
+ * on the remote PipeWire server by calling into a factory.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpLink, wp_link, WP_TYPE_GLOBAL_PROXY,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_link_pipewire_object_interface_init));
 
 static void
 wp_link_init (WpLink * self)
 {
 }
 
-static void
-wp_link_finalize (GObject * object)
+static WpObjectFeatures
+wp_link_get_supported_features (WpObject * object)
 {
-  WpLink *self = WP_LINK (object);
-
-  g_clear_pointer (&self->info, pw_link_info_free);
-
-  G_OBJECT_CLASS (wp_link_parent_class)->finalize (object);
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO;
 }
 
-static gconstpointer
-wp_link_get_info (WpProxy * self)
-{
-  return WP_LINK (self)->info;
-}
-
-static WpProperties *
-wp_link_get_properties (WpProxy * self)
+static void
+wp_link_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  return wp_properties_new_wrap_dict (WP_LINK (self)->info->props);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    /* just wait, info will be emitted anyway after binding */
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_link_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
 static void
@@ -70,12 +73,11 @@ link_event_info(void *data, const struct pw_link_info *info)
   WpLink *self = WP_LINK (data);
 
   self->info = pw_link_info_update (self->info, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-
-  g_object_notify (G_OBJECT (self), "info");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  if (info->change_mask & PW_LINK_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_LINK_CHANGE_MASK_PROPS, 0);
 }
 
 static const struct pw_link_events link_events = {
@@ -91,21 +93,64 @@ wp_link_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
       &self->listener, &link_events, self);
 }
 
+static void
+wp_link_pw_proxy_destroyed (WpProxy * proxy)
+{
+  g_clear_pointer (&WP_LINK (proxy)->info, pw_link_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+}
+
 static void
 wp_link_class_init (WpLinkClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_link_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_link_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_link_activate_execute_step;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Link;
   proxy_class->pw_iface_version = PW_VERSION_LINK;
+  proxy_class->pw_proxy_created = wp_link_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_link_pw_proxy_destroyed;
 
-  proxy_class->get_info = wp_link_get_info;
-  proxy_class->get_properties = wp_link_get_properties;
+  wp_pipewire_object_mixin_class_override_properties (object_class);
+}
 
-  proxy_class->pw_proxy_created = wp_link_pw_proxy_created;
+static gconstpointer
+wp_link_get_native_info (WpPipewireObject * obj)
+{
+  return WP_LINK (obj)->info;
+}
+
+static WpProperties *
+wp_link_get_properties (WpPipewireObject * obj)
+{
+  return wp_properties_new_wrap_dict (WP_LINK (obj)->info->props);
+}
+
+static GVariant *
+wp_link_get_param_info (WpPipewireObject * obj)
+{
+  return NULL;
+}
+
+static void
+wp_link_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_link_get_native_info;
+  iface->get_properties = wp_link_get_properties;
+  iface->get_param_info = wp_link_get_param_info;
+  iface->enum_params = wp_pipewire_object_mixin_enum_params_unimplemented;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
 }
 
 /**
@@ -119,9 +164,9 @@ wp_link_class_init (WpLinkClass * klass)
  *
  * Because of the nature of the PipeWire protocol, this operation completes
  * asynchronously at some point in the future. In order to find out when
- * this is done, you should call wp_proxy_augment(), requesting at least
+ * this is done, you should call wp_object_activate(), requesting at least
  * %WP_PROXY_FEATURE_BOUND. When this feature is ready, the link is ready for
- * use on the server. If the link cannot be created, this augment operation
+ * use on the server. If the link cannot be created, this activation operation
  * will fail.
  *
  * Returns: (nullable) (transfer full): the new link or %NULL if the core
diff --git a/lib/wp/link.h b/lib/wp/link.h
index a9b294d09b490b0cf7062c681d47338e40a7a9ba..f0fba7cbcb2caf81251d3f0dbaf331631c17b1c1 100644
--- a/lib/wp/link.h
+++ b/lib/wp/link.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_LINK_H__
 #define __WIREPLUMBER_LINK_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -20,7 +20,7 @@ G_BEGIN_DECLS
  */
 #define WP_TYPE_LINK (wp_link_get_type ())
 WP_API
-G_DECLARE_FINAL_TYPE (WpLink, wp_link, WP, LINK, WpProxy)
+G_DECLARE_FINAL_TYPE (WpLink, wp_link, WP, LINK, WpGlobalProxy)
 
 WP_API
 WpLink * wp_link_new_from_factory (WpCore * core,
diff --git a/lib/wp/meson.build b/lib/wp/meson.build
index 3fa8be4c354febd3b17a3b9ea931b903fc025f9b..d8292a082077a4ec7af912ea81394edc2f894cba 100644
--- a/lib/wp/meson.build
+++ b/lib/wp/meson.build
@@ -1,4 +1,5 @@
 wp_lib_sources = files(
+  'private/pipewire-object-mixin.c',
   'client.c',
   'configuration.c',
   'core.c',
@@ -8,6 +9,7 @@ wp_lib_sources = files(
   'endpoint-link.c',
   'endpoint-stream.c',
   'error.c',
+  'global-proxy.c',
   'iterator.c',
   'link.c',
   'metadata.c',
@@ -19,7 +21,7 @@ wp_lib_sources = files(
   'plugin.c',
   'port.c',
   'properties.c',
-  'props.c',
+  #'props.c',
   'proxy.c',
   'proxy-interfaces.c',
   'session.c',
@@ -44,6 +46,7 @@ wp_lib_headers = files(
   'endpoint-link.h',
   'endpoint-stream.h',
   'error.h',
+  'global-proxy.h',
   'iterator.h',
   'link.h',
   'metadata.h',
@@ -55,7 +58,7 @@ wp_lib_headers = files(
   'plugin.h',
   'port.h',
   'properties.h',
-  'props.h',
+  #'props.h',
   'proxy.h',
   'proxy-interfaces.h',
   'session.h',
diff --git a/lib/wp/metadata.c b/lib/wp/metadata.c
index 4729cbca3c3dc1c47dcd50584ff8235fb35a7593..c578c8c3489c9c397095097ed97739f16290dc04 100644
--- a/lib/wp/metadata.c
+++ b/lib/wp/metadata.c
@@ -7,20 +7,18 @@
  */
 
 /**
- * SECTION: WpMetadata
- *
- * The #WpMetadata class allows accessing the properties and methods of
- * Pipewire metadata object (`struct pw_metadata`).
- *
+ * SECTION: metadata
+ * @title: PipeWire Metadata
  */
 
 #define G_LOG_DOMAIN "wp-metadata"
 
 #include "metadata.h"
+#include "core.h"
 #include "debug.h"
-#include "private.h"
 #include "error.h"
 #include "wpenums.h"
+#include "private.h"
 
 #include <pipewire/pipewire.h>
 #include <pipewire/extensions/metadata.h>
@@ -114,7 +112,13 @@ struct _WpMetadataPrivate
   struct pw_array metadata;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpMetadata, wp_metadata, WP_TYPE_PROXY)
+/**
+ * WpMetadata:
+ *
+ * The #WpMetadata class allows accessing the properties and methods of
+ * PipeWire metadata object (`struct pw_metadata`).
+ */
+G_DEFINE_TYPE_WITH_PRIVATE (WpMetadata, wp_metadata, WP_TYPE_GLOBAL_PROXY)
 
 static void
 wp_metadata_init (WpMetadata * self)
@@ -129,12 +133,54 @@ wp_metadata_finalize (GObject * object)
   WpMetadataPrivate *priv =
       wp_metadata_get_instance_private (WP_METADATA (object));
 
-  clear_items (&priv->metadata);
   pw_array_clear (&priv->metadata);
 
   G_OBJECT_CLASS (wp_metadata_parent_class)->finalize (object);
 }
 
+static WpObjectFeatures
+wp_metadata_get_supported_features (WpObject * object)
+{
+  return WP_PROXY_FEATURE_BOUND | WP_METADATA_FEATURE_DATA;
+}
+
+enum {
+  STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
+  STEP_CACHE
+};
+
+static guint
+wp_metadata_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  g_return_val_if_fail (
+      missing & (WP_PROXY_FEATURE_BOUND | WP_METADATA_FEATURE_DATA),
+      WP_TRANSITION_STEP_ERROR);
+
+  /* bind if not already bound */
+  if (missing & WP_PROXY_FEATURE_BOUND)
+    return STEP_BIND;
+  else
+    return STEP_CACHE;
+}
+
+static void
+wp_metadata_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  switch (step) {
+  case STEP_CACHE:
+    /* just wait for initial_sync_done() */
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_metadata_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
+}
+
 static int
 metadata_event_property (void *object, uint32_t subject, const char *key,
     const char *type, const char *value)
@@ -194,7 +240,7 @@ initial_sync_done (WpCore * core, GAsyncResult * res, WpMetadata * self)
     return;
   }
 
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  wp_object_update_features (WP_OBJECT (self), WP_METADATA_FEATURE_DATA, 0);
 }
 
 static void
@@ -202,7 +248,7 @@ wp_metadata_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
   WpMetadata *self = WP_METADATA (proxy);
   WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
-  g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
 
   priv->iface = (struct pw_metadata *) pw_proxy;
   pw_metadata_add_listener (priv->iface, &priv->listener,
@@ -210,18 +256,33 @@ wp_metadata_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
   wp_core_sync (core, NULL, (GAsyncReadyCallback) initial_sync_done, self);
 }
 
+static void
+wp_metadata_pw_proxy_destroyed (WpProxy * proxy)
+{
+  WpMetadata *self = WP_METADATA (proxy);
+  WpMetadataPrivate *priv = wp_metadata_get_instance_private (self);
+
+  clear_items (&priv->metadata);
+  wp_object_update_features (WP_OBJECT (self), 0, WP_METADATA_FEATURE_DATA);
+}
+
 static void
 wp_metadata_class_init (WpMetadataClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
   object_class->finalize = wp_metadata_finalize;
 
+  wpobject_class->get_supported_features = wp_metadata_get_supported_features;
+  wpobject_class->activate_get_next_step = wp_metadata_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_metadata_activate_execute_step;
+
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Metadata;
   proxy_class->pw_iface_version = PW_VERSION_METADATA;
-
   proxy_class->pw_proxy_created = wp_metadata_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_metadata_pw_proxy_destroyed;
 
   signals[SIGNAL_CHANGED] = g_signal_new ("changed", G_TYPE_FROM_CLASS (klass),
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 4,
@@ -421,6 +482,13 @@ struct _WpImplMetadata
   struct spa_hook_list hooks;
 };
 
+/**
+ * WpImplMetadata:
+ *
+ * The #WpImplMetadata class implements a PipeWire metadata object. It can
+ * be exported and made available by requesting the %WP_PROXY_FEATURE_BOUND
+ * feature.
+ */
 G_DEFINE_TYPE (WpImplMetadata, wp_impl_metadata, WP_TYPE_METADATA)
 
 #define pw_metadata_emit(hooks,method,version,...) \
@@ -500,7 +568,19 @@ wp_impl_metadata_init (WpImplMetadata * self)
   spa_hook_list_init (&self->hooks);
 
   priv->iface = (struct pw_metadata *) &self->iface;
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  wp_object_update_features (WP_OBJECT (self), WP_METADATA_FEATURE_DATA, 0);
+}
+
+static void
+wp_impl_metadata_dispose (GObject * object)
+{
+  WpMetadataPrivate *priv =
+      wp_metadata_get_instance_private (WP_METADATA (object));
+
+  clear_items (&priv->metadata);
+  wp_object_update_features (WP_OBJECT (object), 0, WP_METADATA_FEATURE_DATA);
+
+  G_OBJECT_CLASS (wp_impl_metadata_parent_class)->dispose (object);
 }
 
 static void
@@ -513,44 +593,68 @@ wp_impl_metadata_on_changed (WpImplMetadata * self, guint32 subject,
 }
 
 static void
-wp_impl_metadata_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_impl_metadata_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpImplMetadata *self = WP_IMPL_METADATA (proxy);
+  WpImplMetadata *self = WP_IMPL_METADATA (object);
   WpMetadataPrivate *priv =
       wp_metadata_get_instance_private (WP_METADATA (self));
 
-  /* PW_PROXY depends on BOUND */
-  if (features & WP_PROXY_FEATURE_PW_PROXY)
-    features |= WP_PROXY_FEATURE_BOUND;
-
-  if (features & WP_PROXY_FEATURE_BOUND) {
-    g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+  switch (step) {
+  case STEP_BIND: {
+    g_autoptr (WpCore) core = wp_object_get_core (object);
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
     /* no pw_core -> we are not connected */
     if (!pw_core) {
-      wp_proxy_augment_error (proxy, g_error_new (WP_DOMAIN_LIBRARY,
-              WP_LIBRARY_ERROR_OPERATION_FAILED,
+      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
               "The WirePlumber core is not connected; "
               "object cannot be exported to PipeWire"));
       return;
     }
 
-    wp_proxy_set_pw_proxy (proxy, pw_core_export (pw_core,
+    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_Metadata,
             NULL, priv->iface, 0));
     g_signal_connect (self, "changed",
         (GCallback) wp_impl_metadata_on_changed, NULL);
+    break;
+  }
+  case STEP_CACHE:
+    /* never reached because WP_METADATA_FEATURE_DATA is always enabled */
+    g_assert_not_reached ();
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_impl_metadata_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
   }
 }
 
+static void
+wp_impl_metadata_pw_proxy_destroyed (WpProxy * proxy)
+{
+  g_signal_handlers_disconnect_by_func (proxy,
+      (GCallback) wp_impl_metadata_on_changed, NULL);
+}
+
 static void
 wp_impl_metadata_class_init (WpImplMetadataClass * klass)
 {
+  GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  proxy_class->augment = wp_impl_metadata_augment;
+  object_class->dispose = wp_impl_metadata_dispose;
+
+  wpobject_class->activate_execute_step =
+      wp_impl_metadata_activate_execute_step;
+
+  /* disable adding a listener for events */
   proxy_class->pw_proxy_created = NULL;
+  proxy_class->pw_proxy_destroyed = wp_impl_metadata_pw_proxy_destroyed;
 }
 
 WpImplMetadata *
diff --git a/lib/wp/metadata.h b/lib/wp/metadata.h
index c501339d526e75a964660fb10561be79dfb8fefc..5008eebd6ed7c3c17e5a07575c153608310da522 100644
--- a/lib/wp/metadata.h
+++ b/lib/wp/metadata.h
@@ -9,10 +9,20 @@
 #ifndef __WIREPLUMBER_METADATA_H__
 #define __WIREPLUMBER_METADATA_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
+/**
+ * WpMetadataFeatures:
+ * @WP_METADATA_FEATURE_DATA: caches metadata locally
+ *
+ * An extension of #WpProxyFeatures
+ */
+typedef enum { /*< flags >*/
+  WP_METADATA_FEATURE_DATA = (WP_PROXY_FEATURE_CUSTOM_START << 0),
+} WpMetadataFeatures;
+
 /**
  * WP_TYPE_METADATA:
  *
@@ -21,11 +31,11 @@ G_BEGIN_DECLS
 #define WP_TYPE_METADATA (wp_metadata_get_type ())
 
 WP_API
-G_DECLARE_DERIVABLE_TYPE (WpMetadata, wp_metadata, WP, METADATA, WpProxy)
+G_DECLARE_DERIVABLE_TYPE (WpMetadata, wp_metadata, WP, METADATA, WpGlobalProxy)
 
 struct _WpMetadataClass
 {
-  WpProxyClass parent_class;
+  WpGlobalProxyClass parent_class;
 };
 
 WP_API
diff --git a/lib/wp/node.c b/lib/wp/node.c
index 4196cbe3613bdb874983a0638109edf58019365f..fbd850537c67fd13759ac8aa6731e97290e22213 100644
--- a/lib/wp/node.c
+++ b/lib/wp/node.c
@@ -7,33 +7,18 @@
  */
 
 /**
- * SECTION: WpNode
- *
- * The #WpNode class allows accessing the properties and methods of a
- * PipeWire node object (`struct pw_node`).
- *
- * A #WpNode is constructed internally when a new node appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
- * Alternatively, a #WpNode can also be constructed using
- * wp_node_new_from_factory(), which creates a new node object
- * on the remote PipeWire server by calling into a factory.
- *
- * A #WpImplNode allows running a node implementation (`struct pw_impl_node`)
- * locally, loading the implementation from factory or wrapping a manually
- * constructed `pw_impl_node`. This object can then be exported to PipeWire
- * by requesting %WP_PROXY_FEATURE_BOUND and be used as if it was a #WpNode
- * proxy to a remote object.
+ * SECTION: node
+ * @title: PipeWire Node
  */
 
 #define G_LOG_DOMAIN "wp-node"
 
 #include "node.h"
-#include "debug.h"
-#include "error.h"
-#include "private.h"
+#include "core.h"
+#include "object-manager.h"
 #include "wpenums.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/impl.h>
 
 enum {
@@ -44,38 +29,40 @@ enum {
 
 static guint32 signals[N_SIGNALS] = {0};
 
-typedef struct _WpNodePrivate WpNodePrivate;
-struct _WpNodePrivate
+struct _WpNode
 {
+  WpGlobalProxy parent;
   struct pw_node_info *info;
   struct spa_hook listener;
   WpObjectManager *ports_om;
-  gboolean ft_ports_requested;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpNode, wp_node, WP_TYPE_PROXY)
+static void wp_node_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
 
-static void
-wp_node_init (WpNode * self)
-{
-}
+/**
+ * WpNode:
+ *
+ * The #WpNode class allows accessing the properties and methods of a
+ * PipeWire node object (`struct pw_node`).
+ *
+ * A #WpNode is constructed internally when a new node appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ * Alternatively, a #WpNode can also be constructed using
+ * wp_node_new_from_factory(), which creates a new node object
+ * on the remote PipeWire server by calling into a factory.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpNode, wp_node, WP_TYPE_GLOBAL_PROXY,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_node_pipewire_object_interface_init));
 
 static void
-wp_node_finalize (GObject * object)
+wp_node_init (WpNode * self)
 {
-  WpNode *self = WP_NODE (object);
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-
-  g_clear_pointer (&priv->info, pw_node_info_free);
-  g_clear_object (&priv->ports_om);
-
-  G_OBJECT_CLASS (wp_node_parent_class)->finalize (object);
 }
 
 static void
 wp_node_on_ports_om_installed (WpObjectManager *ports_om, WpNode * self)
 {
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_NODE_FEATURE_PORTS);
+  wp_object_update_features (WP_OBJECT (self), WP_NODE_FEATURE_PORTS, 0);
 }
 
 static void
@@ -85,169 +72,165 @@ wp_node_emit_ports_changed (WpObjectManager *ports_om, WpNode * self)
 }
 
 static void
-wp_node_ensure_feature_ports (WpNode * self, guint32 bound_id)
+wp_node_enable_feature_ports (WpNode * self)
 {
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  WpProxyFeatures ft = wp_proxy_get_features (WP_PROXY (self));
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+  guint32 bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
 
-  if (priv->ft_ports_requested && !priv->ports_om &&
-      (ft & WP_PROXY_FEATURES_STANDARD) == WP_PROXY_FEATURES_STANDARD)
-  {
-    g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (self));
+  wp_debug_object (self, "enabling WP_NODE_FEATURE_PORTS, bound_id:%u",
+      bound_id);
 
-    if (!bound_id)
-      bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
-
-    wp_debug_object (self, "enabling WP_NODE_FEATURE_PORTS, bound_id:%u",
-        bound_id);
-
-    priv->ports_om = wp_object_manager_new ();
-    wp_object_manager_add_interest (priv->ports_om,
-        WP_TYPE_PORT,
-        WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_NODE_ID, "=u", bound_id,
-        NULL);
-    wp_object_manager_request_proxy_features (priv->ports_om,
-        WP_TYPE_PORT, WP_PROXY_FEATURES_STANDARD);
+  self->ports_om = wp_object_manager_new ();
+  wp_object_manager_add_interest (self->ports_om,
+      WP_TYPE_PORT,
+      WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_NODE_ID, "=u", bound_id,
+      NULL);
+  wp_object_manager_request_object_features (self->ports_om,
+      WP_TYPE_PORT, WP_OBJECT_FEATURES_ALL);
 
-    g_signal_connect_object (priv->ports_om, "installed",
-        G_CALLBACK (wp_node_on_ports_om_installed), self, 0);
-    g_signal_connect_object (priv->ports_om, "objects-changed",
-        G_CALLBACK (wp_node_emit_ports_changed), self, 0);
+  g_signal_connect_object (self->ports_om, "installed",
+      G_CALLBACK (wp_node_on_ports_om_installed), self, 0);
+  g_signal_connect_object (self->ports_om, "objects-changed",
+      G_CALLBACK (wp_node_emit_ports_changed), self, 0);
 
-    wp_core_install_object_manager (core, priv->ports_om);
-  }
+  wp_core_install_object_manager (core, self->ports_om);
 }
 
-static void
-wp_node_augment (WpProxy * proxy, WpProxyFeatures features)
+static WpObjectFeatures
+wp_node_get_supported_features (WpObject * object)
 {
-  WpNode *self = WP_NODE (proxy);
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-
-  /* call the parent impl first to ensure we have a pw proxy if necessary */
-  WP_PROXY_CLASS (wp_node_parent_class)->augment (proxy, features);
-
-  if (features & WP_NODE_FEATURE_PORTS) {
-    priv->ft_ports_requested = TRUE;
-    wp_node_ensure_feature_ports (self, 0);
-  }
+  WpNode *self = WP_NODE (object);
+  return
+      WP_PROXY_FEATURE_BOUND |
+      WP_NODE_FEATURE_PORTS |
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          self->info ? self->info->params : NULL,
+          self->info ? self->info->n_params : 0);
 }
 
-static gconstpointer
-wp_node_get_info (WpProxy * self)
-{
-  WpNodePrivate *priv = wp_node_get_instance_private (WP_NODE (self));
-  return priv->info;
-}
+enum {
+  STEP_PORTS = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+};
 
-static WpProperties *
-wp_node_get_properties (WpProxy * self)
+static guint
+wp_node_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpNodePrivate *priv = wp_node_get_instance_private (WP_NODE (self));
-  return wp_properties_new_wrap_dict (priv->info->props);
-}
+  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
+      step, missing);
 
-static struct spa_param_info *
-wp_node_get_param_info (WpProxy * proxy, guint * n_params)
-{
-  WpNodePrivate *priv = wp_node_get_instance_private (WP_NODE (proxy));
+  /* extend the mixin's state machine; when the only remaining feature to
+     enable is FEATURE_PORTS, advance to STEP_PORTS */
+  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
+      missing == WP_NODE_FEATURE_PORTS)
+    return STEP_PORTS;
 
-  *n_params = priv->info->n_params;
-  return priv->info->params;
+  return step;
 }
 
-static gint
-wp_node_enum_params (WpProxy * self, guint32 id, guint32 start, guint32 num,
-    WpSpaPod * filter)
+static void
+wp_node_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  struct pw_node *pwp = (struct pw_node *) wp_proxy_get_pw_proxy (self);
-  return pw_node_enum_params (pwp, 0, id, start, num,
-      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  case STEP_PORTS:
+    wp_node_enable_feature_ports (WP_NODE (object));
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_node_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_node_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
+static void
+wp_node_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  struct pw_node *pwp = (struct pw_node *) wp_proxy_get_pw_proxy (self);
-  return pw_node_subscribe_params (pwp, ids, n_ids);
-}
+  WpNode *self = WP_NODE (object);
 
-static gint
-wp_node_set_param (WpProxy * self, guint32 id, guint32 flags, WpSpaPod *param)
-{
-  struct pw_node *pwp = (struct pw_node *) wp_proxy_get_pw_proxy (self);
-  return pw_node_set_param (pwp, id, flags, wp_spa_pod_get_spa_pod (param));
+  wp_pipewire_object_mixin_deactivate (object, features);
+
+  if (features & WP_NODE_FEATURE_PORTS) {
+    g_clear_object (&self->ports_om);
+    wp_object_update_features (object, 0, WP_NODE_FEATURE_PORTS);
+  }
+
+  WP_OBJECT_CLASS (wp_node_parent_class)->deactivate (object, features);
 }
 
 static void
 node_event_info(void *data, const struct pw_node_info *info)
 {
   WpNode *self = WP_NODE (data);
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  enum pw_node_state old_state = priv->info ?
-      priv->info->state : PW_NODE_STATE_CREATING;
-
-  priv->info = pw_node_info_update (priv->info, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  enum pw_node_state old_state = self->info ?
+      self->info->state : PW_NODE_STATE_CREATING;
 
-  g_object_notify (G_OBJECT (self), "info");
-
-  if (info->change_mask & PW_NODE_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_NODE_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  self->info = pw_node_info_update (self->info, info);
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
   if (info->change_mask & PW_NODE_CHANGE_MASK_STATE)
     g_signal_emit (self, signals[SIGNAL_STATE_CHANGED], 0, old_state,
-        priv->info->state);
+        self->info->state);
 
-  wp_node_ensure_feature_ports (self, 0);
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_NODE_CHANGE_MASK_PROPS, PW_NODE_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_node_events node_events = {
   PW_VERSION_NODE_EVENTS,
   .info = node_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
 wp_node_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 {
   WpNode *self = WP_NODE (proxy);
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  pw_node_add_listener ((struct pw_node *) pw_proxy,
-      &priv->listener, &node_events, self);
+  pw_node_add_listener ((struct pw_port *) pw_proxy,
+      &self->listener, &node_events, self);
 }
 
 static void
-wp_node_bound (WpProxy * proxy, guint32 id)
+wp_node_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpNode *self = WP_NODE (proxy);
-  wp_node_ensure_feature_ports (self, id);
+
+  g_clear_pointer (&self->info, pw_node_info_free);
+  g_clear_object (&self->ports_om);
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO | WP_NODE_FEATURE_PORTS);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (self),
+      WP_OBJECT_FEATURES_ALL);
 }
 
 static void
 wp_node_class_init (WpNodeClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_node_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_node_get_supported_features;
+  wpobject_class->activate_get_next_step = wp_node_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_node_activate_execute_step;
+  wpobject_class->deactivate = wp_node_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Node;
   proxy_class->pw_iface_version = PW_VERSION_NODE;
-
-  proxy_class->augment = wp_node_augment;
-  proxy_class->get_info = wp_node_get_info;
-  proxy_class->get_properties = wp_node_get_properties;
-  proxy_class->get_param_info = wp_node_get_param_info;
-  proxy_class->enum_params = wp_node_enum_params;
-  proxy_class->subscribe_params = wp_node_subscribe_params;
-  proxy_class->set_param = wp_node_set_param;
-
   proxy_class->pw_proxy_created = wp_node_pw_proxy_created;
-  proxy_class->bound = wp_node_bound;
+  proxy_class->pw_proxy_destroyed = wp_node_pw_proxy_destroyed;
+
+  wp_pipewire_object_mixin_class_override_properties (object_class);
 
   /**
    * WpNode::state-changed:
@@ -275,6 +258,53 @@ wp_node_class_init (WpNodeClass * klass)
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
+static gconstpointer
+wp_node_get_native_info (WpPipewireObject * obj)
+{
+  return WP_NODE (obj)->info;
+}
+
+static WpProperties *
+wp_node_get_properties (WpPipewireObject * obj)
+{
+  return wp_properties_new_wrap_dict (WP_NODE (obj)->info->props);
+}
+
+static GVariant *
+wp_node_get_param_info (WpPipewireObject * obj)
+{
+  WpNode *self = WP_NODE (obj);
+  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
+      self->info->n_params);
+}
+
+static void
+wp_node_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_node, obj, id, filter, cancellable,
+      callback, user_data);
+}
+
+static void
+wp_node_set_param (WpPipewireObject * obj, const gchar * id, WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_node, obj, id, param);
+}
+
+static void
+wp_node_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_node_get_native_info;
+  iface->get_properties = wp_node_get_properties;
+  iface->get_param_info = wp_node_get_param_info;
+  iface->enum_params = wp_node_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_node_set_param;
+}
+
 /**
  * wp_node_new_from_factory:
  * @core: the wireplumber core
@@ -286,9 +316,9 @@ wp_node_class_init (WpNodeClass * klass)
  *
  * Because of the nature of the PipeWire protocol, this operation completes
  * asynchronously at some point in the future. In order to find out when
- * this is done, you should call wp_proxy_augment(), requesting at least
+ * this is done, you should call wp_object_activate(), requesting at least
  * %WP_PROXY_FEATURE_BOUND. When this feature is ready, the node is ready for
- * use on the server. If the node cannot be created, this augment operation
+ * use on the server. If the node cannot be created, this activation operation
  * will fail.
  *
  * Returns: (nullable) (transfer full): the new node or %NULL if the core
@@ -318,13 +348,12 @@ WpNodeState
 wp_node_get_state (WpNode * self, const gchar ** error)
 {
   g_return_val_if_fail (WP_IS_NODE (self), WP_NODE_STATE_ERROR);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, WP_NODE_STATE_ERROR);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, WP_NODE_STATE_ERROR);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
   if (error)
-    *error = priv->info->error;
-  return (WpNodeState) priv->info->state;
+    *error = self->info->error;
+  return (WpNodeState) self->info->state;
 }
 
 /**
@@ -332,7 +361,7 @@ wp_node_get_state (WpNode * self, const gchar ** error)
  * @self: the node
  * @max: (out) (optional): the maximum supported number of input ports
  *
- * Requires %WP_PROXY_FEATURE_INFO
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
  *
  * Returns: the number of input ports of this node, as reported by the node info
  */
@@ -340,13 +369,12 @@ guint
 wp_node_get_n_input_ports (WpNode * self, guint * max)
 {
   g_return_val_if_fail (WP_IS_NODE (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, 0);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
   if (max)
-    *max = priv->info->max_input_ports;
-  return priv->info->n_input_ports;
+    *max = self->info->max_input_ports;
+  return self->info->n_input_ports;
 }
 
 /**
@@ -354,7 +382,7 @@ wp_node_get_n_input_ports (WpNode * self, guint * max)
  * @self: the node
  * @max: (out) (optional): the maximum supported number of output ports
  *
- * Requires %WP_PROXY_FEATURE_INFO
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
  *
  * Returns: the number of output ports of this node, as reported by the node info
  */
@@ -362,13 +390,12 @@ guint
 wp_node_get_n_output_ports (WpNode * self, guint * max)
 {
   g_return_val_if_fail (WP_IS_NODE (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, 0);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
   if (max)
-    *max = priv->info->max_output_ports;
-  return priv->info->n_output_ports;
+    *max = self->info->max_output_ports;
+  return self->info->n_output_ports;
 }
 
 /**
@@ -387,11 +414,10 @@ guint
 wp_node_get_n_ports (WpNode * self)
 {
   g_return_val_if_fail (WP_IS_NODE (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_NODE_FEATURE_PORTS, 0);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  return wp_object_manager_get_n_objects (priv->ports_om);
+  return wp_object_manager_get_n_objects (self->ports_om);
 }
 
 /**
@@ -407,11 +433,10 @@ WpIterator *
 wp_node_iterate_ports (WpNode * self)
 {
   g_return_val_if_fail (WP_IS_NODE (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_NODE_FEATURE_PORTS, NULL);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  return wp_object_manager_iterate (priv->ports_om);
+  return wp_object_manager_iterate (self->ports_om);
 }
 
 /**
@@ -452,11 +477,10 @@ WpIterator *
 wp_node_iterate_ports_filtered_full (WpNode * self, WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_NODE (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_NODE_FEATURE_PORTS, NULL);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
-  return wp_object_manager_iterate_filtered_full (priv->ports_om, interest);
+  return wp_object_manager_iterate_filtered_full (self->ports_om, interest);
 }
 
 /**
@@ -497,12 +521,11 @@ WpPort *
 wp_node_lookup_port_full (WpNode * self, WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_NODE (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_NODE_FEATURE_PORTS, NULL);
 
-  WpNodePrivate *priv = wp_node_get_instance_private (self);
   return (WpPort *)
-      wp_object_manager_lookup_full (priv->ports_om, interest);
+      wp_object_manager_lookup_full (self->ports_om, interest);
 }
 
 /**
@@ -539,6 +562,15 @@ struct _WpImplNode
   struct pw_proxy *proxy;
 };
 
+/**
+ * WpImplNode:
+ *
+ * A #WpImplNode allows running a node implementation (`struct pw_impl_node`)
+ * locally, loading the implementation from factory or wrapping a manually
+ * constructed `pw_impl_node`. This object can then be exported to PipeWire
+ * by requesting %WP_PROXY_FEATURE_BOUND and be used as if it was a #WpNode
+ * proxy to a remote object.
+ */
 G_DEFINE_TYPE (WpImplNode, wp_impl_node, G_TYPE_OBJECT)
 
 static void
diff --git a/lib/wp/node.h b/lib/wp/node.h
index 2b6ae2e22012f07d232c991606bfc6a8729b3584..55d2dcb54cb493d93b81e8e5d9a7169210805be3 100644
--- a/lib/wp/node.h
+++ b/lib/wp/node.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_NODE_H__
 #define __WIREPLUMBER_NODE_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 #include "port.h"
 #include "iterator.h"
 #include "object-interest.h"
@@ -59,19 +59,9 @@ typedef enum {
  * An extension of #WpProxyFeatures
  */
 typedef enum { /*< flags >*/
-  WP_NODE_FEATURE_PORTS = (WP_PROXY_FEATURE_LAST << 0),
+  WP_NODE_FEATURE_PORTS = (WP_PROXY_FEATURE_CUSTOM_START << 0),
 } WpNodeFeatures;
 
-/**
- * WP_NODE_FEATURES_STANDARD:
- *
- * A constant set of features that contains the standard features that are
- * available in the #WpNode class.
- */
-#define WP_NODE_FEATURES_STANDARD \
-    (WP_PROXY_FEATURES_STANDARD | \
-     WP_NODE_FEATURE_PORTS)
-
 /**
  * WP_TYPE_NODE:
  *
@@ -79,12 +69,7 @@ typedef enum { /*< flags >*/
  */
 #define WP_TYPE_NODE (wp_node_get_type ())
 WP_API
-G_DECLARE_DERIVABLE_TYPE (WpNode, wp_node, WP, NODE, WpProxy)
-
-struct _WpNodeClass
-{
-  WpProxyClass parent_class;
-};
+G_DECLARE_FINAL_TYPE (WpNode, wp_node, WP, NODE, WpGlobalProxy)
 
 WP_API
 WpNode * wp_node_new_from_factory (WpCore * core,
diff --git a/lib/wp/object-interest.c b/lib/wp/object-interest.c
index 72694ae39d66592f0d5729ffe3875cda40ed1296..b9f7a70ac8792b34d3fb8afa4586deeb4893e96c 100644
--- a/lib/wp/object-interest.c
+++ b/lib/wp/object-interest.c
@@ -14,6 +14,8 @@
 #define G_LOG_DOMAIN "wp-object-interest"
 
 #include "object-interest.h"
+#include "global-proxy.h"
+#include "proxy-interfaces.h"
 #include "debug.h"
 #include "error.h"
 #include "private.h"
@@ -308,7 +310,8 @@ gboolean
 wp_object_interest_validate (WpObjectInterest * self, GError ** error)
 {
   struct constraint *c;
-  gboolean is_proxy;
+  gboolean is_pwobj;
+  gboolean is_global;
 
   g_return_val_if_fail (self != NULL, FALSE);
 
@@ -322,7 +325,8 @@ wp_object_interest_validate (WpObjectInterest * self, GError ** error)
     return FALSE;
   }
 
-  is_proxy = g_type_is_a (self->gtype, WP_TYPE_PROXY);
+  is_pwobj = g_type_is_a (self->gtype, WP_TYPE_PIPEWIRE_OBJECT);
+  is_global = g_type_is_a (self->gtype, WP_TYPE_GLOBAL_PROXY);
 
   pw_array_for_each (c, &self->constraints) {
     const GVariantType *value_type = NULL;
@@ -334,10 +338,16 @@ wp_object_interest_validate (WpObjectInterest * self, GError ** error)
       return FALSE;
     }
 
-    if (!is_proxy && (c->type == WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY ||
-            c->type == WP_CONSTRAINT_TYPE_PW_PROPERTY)) {
+    if (!is_pwobj && c->type == WP_CONSTRAINT_TYPE_PW_PROPERTY) {
       g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
-          "constraint type %d cannot apply to non-WpProxy type '%s'",
+          "constraint type %d cannot apply to non-WpPipewireObject type '%s'",
+          c->type, g_type_name (self->gtype));
+      return FALSE;
+    }
+
+    if (!is_global && c->type == WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY) {
+      g_set_error (error, WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_INVARIANT,
+          "constraint type %d cannot apply to non-WpGlobalProxy type '%s'",
           c->type, g_type_name (self->gtype));
       return FALSE;
     }
@@ -698,8 +708,8 @@ wp_object_interest_matches (WpObjectInterest * self, gpointer object)
  * are any constraints that require them, the match will fail.
  * As a special case, if @object is not %NULL and is a subclass of #WpProxy,
  * then @pw_props and @pw_global_props, if required, will be internally
- * retrieved from @object by calling wp_proxy_get_properties() and
- * wp_proxy_get_global_properties() respectively.
+ * retrieved from @object by calling wp_pipewire_object_get_properties() and
+ * wp_global_proxy_get_global_properties() respectively.
  *
  * Returns: %TRUE if the the type matches this interest and the properties
  *   match the constraints, %FALSE otherwise
@@ -727,14 +737,20 @@ wp_object_interest_matches_full (WpObjectInterest * self,
     return FALSE;
 
   /* prepare for constraint lookups on proxy properties */
-  if (object && g_type_is_a (object_type, WP_TYPE_PROXY)) {
-    WpProxy *p = WP_PROXY (object);
+  if (object) {
+    if (!pw_global_props && WP_IS_GLOBAL_PROXY (object)) {
+      WpGlobalProxy *pwg = (WpGlobalProxy *) object;
+      pw_global_props = global_props =
+          wp_global_proxy_get_global_properties (pwg);
+    }
 
-    if (!pw_global_props)
-      pw_global_props = global_props = wp_proxy_get_global_properties (p);
+    if (!pw_props && WP_IS_PIPEWIRE_OBJECT (object)) {
+      WpObject *oo = (WpObject *) object;
+      WpPipewireObject *pwo = (WpPipewireObject *) object;
 
-    if (!pw_props && wp_proxy_get_features (p) & WP_PROXY_FEATURE_INFO)
-      pw_props = props = wp_proxy_get_properties (p);
+      if (wp_object_get_active_features (oo) & WP_PIPEWIRE_OBJECT_FEATURE_INFO)
+        pw_props = props = wp_pipewire_object_get_properties (pwo);
+    }
   }
 
   /* check all constraints; if any of them fails at any point, fail the match */
diff --git a/lib/wp/object-interest.h b/lib/wp/object-interest.h
index b52853c71f2f14884280c66d6d952d020f2232dc..983eeea79b95217924460ca7623229de9a979df6 100644
--- a/lib/wp/object-interest.h
+++ b/lib/wp/object-interest.h
@@ -20,10 +20,10 @@ G_BEGIN_DECLS
  * @WP_CONSTRAINT_TYPE_NONE: invalid constraint type
  * @WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY: constraint applies
  *   to a PipeWire global property of the object (the ones returned by
- *   wp_proxy_get_global_properties())
+ *   wp_global_proxy_get_global_properties())
  * @WP_CONSTRAINT_TYPE_PW_PROPERTY: constraint applies
  *   to a PipeWire property of the object (the ones returned by
- *   wp_proxy_get_properties())
+ *   wp_pipewire_object_get_properties())
  * @WP_CONSTRAINT_TYPE_G_PROPERTY: constraint applies to a #GObject
  *   property of the object
  */
diff --git a/lib/wp/object-manager.c b/lib/wp/object-manager.c
index 60289b5493df9fa5425302fd4512d51dfaede53d..13a8cede242c3d10165163fe1049afcad6cb7e2e 100644
--- a/lib/wp/object-manager.c
+++ b/lib/wp/object-manager.c
@@ -15,14 +15,14 @@
  *
  * There are 4 kinds of objects that can be managed by a #WpObjectManager:
  *   * remote PipeWire global objects that are advertised on the registry;
- *     these are bound locally to subclasses of #WpProxy
+ *     these are bound locally to subclasses of #WpGlobalProxy
  *   * remote PipeWire global objects that are created by calling a remote
  *     factory through the WirePlumber API; these are very similar to other
- *     global objects but it should be noted that the same #WpProxy instance
- *     that created them appears in the #WpObjectManager (as soon as its
- *     %WP_PROXY_FEATURE_BOUND is enabled)
+ *     global objects but it should be noted that the same #WpGlobalProxy
+ *     instance that created them appears in the #WpObjectManager (as soon as
+ *     its %WP_PROXY_FEATURE_BOUND is enabled)
  *   * local PipeWire objects that are being exported to PipeWire
- *     (#WpImplNode, WpImplEndpoint [private], etc); these appear in the
+ *     (#WpImplSession, WpImplEndpoint [private], etc); these appear in the
  *     #WpObjectManager as soon as they are exported (so, when their
  *     %WP_PROXY_FEATURE_BOUND is enabled)
  *   * WirePlumber-specific objects, such as WirePlumber factories
@@ -278,29 +278,29 @@ wp_object_manager_add_interest_full (WpObjectManager *self,
 }
 
 /**
- * wp_object_manager_request_proxy_features:
+ * wp_object_manager_request_object_features:
  * @self: the object manager
- * @proxy_type: the #WpProxy descendant type
- * @wanted_features: the features to enable on this kind of proxy
+ * @object_type: the #WpProxy descendant type
+ * @wanted_features: the features to enable on this kind of object
  *
  * Requests the object manager to automatically prepare the @wanted_features
- * on any managed object that is of the specified @proxy_type. These features
+ * on any managed object that is of the specified @object_type. These features
  * will always be prepared before the object appears on the object manager.
  */
 void
-wp_object_manager_request_proxy_features (WpObjectManager *self,
-    GType proxy_type, WpProxyFeatures wanted_features)
+wp_object_manager_request_object_features (WpObjectManager *self,
+    GType object_type, WpProxyFeatures wanted_features)
 {
   g_autofree GType *children = NULL;
   GType *child;
 
   g_return_if_fail (WP_IS_OBJECT_MANAGER (self));
-  g_return_if_fail (g_type_is_a (proxy_type, WP_TYPE_PROXY));
+  g_return_if_fail (g_type_is_a (object_type, WP_TYPE_OBJECT));
 
-  g_hash_table_insert (self->features, GSIZE_TO_POINTER (proxy_type),
+  g_hash_table_insert (self->features, GSIZE_TO_POINTER (object_type),
       GUINT_TO_POINTER (wanted_features));
 
-  child = children = g_type_children (proxy_type, NULL);
+  child = children = g_type_children (object_type, NULL);
   while (*child) {
     WpProxyFeatures existing_ft = (WpProxyFeatures) GPOINTER_TO_UINT (
         g_hash_table_lookup (self->features, GSIZE_TO_POINTER (*child)));
@@ -565,7 +565,7 @@ wp_object_manager_is_interested_in_object (WpObjectManager * self,
 
 static gboolean
 wp_object_manager_is_interested_in_global (WpObjectManager * self,
-    WpGlobal * global, WpProxyFeatures * wanted_features)
+    WpGlobal * global, WpObjectFeatures * wanted_features)
 {
   gint i;
   WpObjectInterest *interest = NULL;
@@ -576,7 +576,7 @@ wp_object_manager_is_interested_in_global (WpObjectManager * self,
             global->proxy, NULL, global->properties)) {
       gpointer ft = g_hash_table_lookup (self->features,
           GSIZE_TO_POINTER (global->type));
-      *wanted_features = (WpProxyFeatures) GPOINTER_TO_UINT (ft);
+      *wanted_features = (WpObjectFeatures) GPOINTER_TO_UINT (ft);
       return TRUE;
     }
   }
@@ -658,8 +658,8 @@ on_proxy_ready (GObject * proxy, GAsyncResult * res, gpointer data)
 
   self->pending_objects--;
 
-  if (!wp_proxy_augment_finish (WP_PROXY (proxy), res, &error)) {
-    wp_message_object (self, "proxy augment failed: %s", error->message);
+  if (!wp_object_activate_finish (WP_OBJECT (proxy), res, &error)) {
+    wp_message_object (self, "proxy activation failed: %s", error->message);
   } else {
     wp_trace_object (self, "added: " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (proxy));
     g_ptr_array_add (self->objects, proxy);
@@ -678,7 +678,7 @@ wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
 
   /* do not allow proxies that don't have a defined subclass;
      bind will fail because proxy_class->pw_iface_type is NULL */
-  if (global->type == WP_TYPE_PROXY)
+  if (global->type == WP_TYPE_GLOBAL_PROXY)
     return;
 
   if (wp_object_manager_is_interested_in_global (self, global, &features)) {
@@ -695,8 +695,8 @@ wp_object_manager_add_global (WpObjectManager * self, WpGlobal * global)
     wp_trace_object (self, "adding global:%u -> " WP_OBJECT_FORMAT,
         global->id, WP_OBJECT_ARGS (global->proxy));
 
-    wp_proxy_augment (global->proxy, features, NULL, on_proxy_ready,
-        g_object_ref (self));
+    wp_object_activate (WP_OBJECT (global->proxy), features, NULL,
+        on_proxy_ready, g_object_ref (self));
   }
 }
 
@@ -742,7 +742,7 @@ wp_object_manager_rm_object (WpObjectManager * self, gpointer object)
  *
  * 2) PipeWire global objects, which were constructed by this process, either
  *    by calling into a remove factory (see wp_node_new_from_factory()) or
- *    by exporting a local object (WpImplNode etc...).
+ *    by exporting a local object (WpImplSession etc...).
  *
  *    These objects are also represented by a WpGlobal, which may however be
  *    constructed before they appear on the registry. The associated WpProxy
@@ -805,7 +805,7 @@ object_manager_destroyed (gpointer data, GObject * om)
   g_ptr_array_remove_fast (self->object_managers, om);
 }
 
-/* find the subclass of WpProxy that can handle
+/* find the subclass of WpPipewireGloabl that can handle
    the given pipewire interface type of the given version */
 static inline GType
 find_proxy_instance_type (const char * type, guint32 version)
@@ -813,7 +813,7 @@ find_proxy_instance_type (const char * type, guint32 version)
   g_autofree GType *children;
   guint n_children;
 
-  children = g_type_children (WP_TYPE_PROXY, &n_children);
+  children = g_type_children (WP_TYPE_GLOBAL_PROXY, &n_children);
 
   for (gint i = 0; i < n_children; i++) {
     WpProxyClass *klass = (WpProxyClass *) g_type_class_ref (children[i]);
@@ -826,7 +826,7 @@ find_proxy_instance_type (const char * type, guint32 version)
     g_type_class_unref (klass);
   }
 
-  return WP_TYPE_PROXY;
+  return WP_TYPE_GLOBAL_PROXY;
 }
 
 /* called by the registry when a global appears */
@@ -1043,7 +1043,7 @@ expose_tmp_globals (WpCore *core, GAsyncResult *res, WpRegistry *self)
 void
 wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
     guint32 permissions, guint32 flag, GType type,
-    WpProxy *proxy, const struct spa_dict *props,
+    WpGlobalProxy *proxy, const struct spa_dict *props,
     WpGlobal ** new_global)
 {
   g_autoptr (WpGlobal) global = NULL;
@@ -1260,7 +1260,7 @@ wp_global_rm_flag (WpGlobal *global, guint rm_flag)
     if (global->proxy) {
       if (reg)
         wp_registry_notify_rm_object (reg, global->proxy);
-      wp_proxy_destroy (global->proxy);
+      wp_object_deactivate (WP_OBJECT (global->proxy), WP_PROXY_FEATURE_BOUND);
 
       /* if the proxy is not owning the global, unref it */
       if (global->flags == 0)
diff --git a/lib/wp/object-manager.h b/lib/wp/object-manager.h
index e36037d2894beb1f203e75f8b75e8c5ed4f55cb2..dd9dadcdedc360432b331170e018121e6833ede4 100644
--- a/lib/wp/object-manager.h
+++ b/lib/wp/object-manager.h
@@ -10,7 +10,7 @@
 #define __WIREPLUMBER_OBJECT_MANAGER_H__
 
 #include <glib-object.h>
-#include "proxy.h"
+#include "object.h"
 #include "iterator.h"
 #include "object-interest.h"
 
@@ -43,11 +43,11 @@ WP_API
 void wp_object_manager_add_interest_full (WpObjectManager * self,
     WpObjectInterest * interest);
 
-/* proxy features */
+/* object features */
 
 WP_API
-void wp_object_manager_request_proxy_features (WpObjectManager *self,
-    GType proxy_type, WpProxyFeatures wanted_features);
+void wp_object_manager_request_object_features (WpObjectManager *self,
+    GType object_type, WpObjectFeatures wanted_features);
 
 /* object inspection */
 
diff --git a/lib/wp/port.c b/lib/wp/port.c
index 490edd7f36ac959d56cd85bbed7f49c265a23ff8..5d501140588e4f31a862dfd9d915b42f8aab0783 100644
--- a/lib/wp/port.c
+++ b/lib/wp/port.c
@@ -1,91 +1,80 @@
 /* WirePlumber
  *
- * Copyright © 2019 Collabora Ltd.
+ * Copyright © 2019-2020 Collabora Ltd.
  *    @author Julian Bouzas <julian.bouzas@collabora.com>
  *
  * SPDX-License-Identifier: MIT
  */
 
 /**
- * SECTION: WpPort
- *
- * The #WpPort class allows accessing the properties and methods of a
- * PipeWire port object (`struct pw_port`).
- *
- * A #WpPort is constructed internally when a new port appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
+ * SECTION: port
+ * @title: PipeWire Port
  */
 
 #define G_LOG_DOMAIN "wp-port"
 
 #include "port.h"
-#include "private.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
-
-/**
- * WpPort:
- */
 struct _WpPort
 {
-  WpProxy parent;
+  WpGlobalProxy parent;
   struct pw_port_info *info;
-
-  /* The port proxy listener */
   struct spa_hook listener;
 };
 
-G_DEFINE_TYPE (WpPort, wp_port, WP_TYPE_PROXY)
+static void wp_port_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
+
+/**
+ * WpPort:
+ *
+ * The #WpPort class allows accessing the properties and methods of a
+ * PipeWire port object (`struct pw_port`).
+ *
+ * A #WpPort is constructed internally when a new port appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpPort, wp_port, WP_TYPE_GLOBAL_PROXY,
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_port_pipewire_object_interface_init));
 
 static void
 wp_port_init (WpPort * self)
 {
 }
 
-static void
-wp_port_finalize (GObject * object)
+static WpObjectFeatures
+wp_port_get_supported_features (WpObject * object)
 {
   WpPort *self = WP_PORT (object);
 
-  g_clear_pointer (&self->info, pw_port_info_free);
-
-  G_OBJECT_CLASS (wp_port_parent_class)->finalize (object);
-}
-
-static gconstpointer
-wp_port_get_info (WpProxy * self)
-{
-  return WP_PORT (self)->info;
-}
-
-static WpProperties *
-wp_port_get_properties (WpProxy * self)
-{
-  return wp_properties_new_wrap_dict (WP_PORT (self)->info->props);
+  return WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          self->info ? self->info->params : NULL,
+          self->info ? self->info->n_params : 0);
 }
 
-static struct spa_param_info *
-wp_port_get_param_info (WpProxy * proxy, guint * n_params)
+static void
+wp_port_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpPort *self = WP_PORT (proxy);
-  *n_params = self->info->n_params;
-  return self->info->params;
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_port_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_port_enum_params (WpProxy * self, guint32 id, guint32 start, guint32 num,
-    WpSpaPod * filter)
+static void
+wp_port_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  struct pw_port *pwp = (struct pw_port *) wp_proxy_get_pw_proxy (self);
-  return pw_port_enum_params (pwp, 0, id, start, num,
-      filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
-}
+  wp_pipewire_object_mixin_deactivate (object, features);
 
-static gint
-wp_port_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
-{
-  struct pw_port *pwp = (struct pw_port *) wp_proxy_get_pw_proxy (self);
-  return pw_port_subscribe_params (pwp, ids, n_ids);
+  WP_OBJECT_CLASS (wp_port_parent_class)->deactivate (object, features);
 }
 
 static void
@@ -94,21 +83,17 @@ port_event_info(void *data, const struct pw_port_info *info)
   WpPort *self = WP_PORT (data);
 
   self->info = pw_port_info_update (self->info, info);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  g_object_notify (G_OBJECT (self), "info");
-
-  if (info->change_mask & PW_PORT_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
-
-  if (info->change_mask & PW_PORT_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_PORT_CHANGE_MASK_PROPS, PW_PORT_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_port_events port_events = {
   PW_VERSION_PORT_EVENTS,
   .info = port_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
@@ -119,32 +104,87 @@ wp_port_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
       &self->listener, &port_events, self);
 }
 
+static void
+wp_port_pw_proxy_destroyed (WpProxy * proxy)
+{
+  g_clear_pointer (&WP_PORT (proxy)->info, pw_port_info_free);
+  wp_object_update_features (WP_OBJECT (proxy), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
+}
+
 static void
 wp_port_class_init (WpPortClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_port_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_port_get_supported_features;
+  wpobject_class->activate_get_next_step =
+      wp_pipewire_object_mixin_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_port_activate_execute_step;
+  wpobject_class->deactivate = wp_port_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Port;
   proxy_class->pw_iface_version = PW_VERSION_PORT;
+  proxy_class->pw_proxy_created = wp_port_pw_proxy_created;
+  proxy_class->pw_proxy_destroyed = wp_port_pw_proxy_destroyed;
 
-  proxy_class->get_info = wp_port_get_info;
-  proxy_class->get_properties = wp_port_get_properties;
-  proxy_class->get_param_info = wp_port_get_param_info;
-  proxy_class->enum_params = wp_port_enum_params;
-  proxy_class->subscribe_params = wp_port_subscribe_params;
+  wp_pipewire_object_mixin_class_override_properties (object_class);
+}
 
-  proxy_class->pw_proxy_created = wp_port_pw_proxy_created;
+static gconstpointer
+wp_port_get_native_info (WpPipewireObject * obj)
+{
+  return WP_PORT (obj)->info;
+}
+
+static WpProperties *
+wp_port_get_properties (WpPipewireObject * obj)
+{
+  return wp_properties_new_wrap_dict (WP_PORT (obj)->info->props);
+}
+
+static GVariant *
+wp_port_get_param_info (WpPipewireObject * obj)
+{
+  WpPort *self = WP_PORT (obj);
+  return wp_pipewire_object_mixin_param_info_to_gvariant (self->info->params,
+      self->info->n_params);
+}
+
+static void
+wp_port_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_port, obj, id, filter, cancellable,
+      callback, user_data);
+}
+
+static void
+wp_port_pipewire_object_interface_init (WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_port_get_native_info;
+  iface->get_properties = wp_port_get_properties;
+  iface->get_param_info = wp_port_get_param_info;
+  iface->enum_params = wp_port_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_pipewire_object_mixin_set_param_unimplemented;
 }
 
 WpDirection
 wp_port_get_direction (WpPort * self)
 {
   g_return_val_if_fail (WP_IS_PORT (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, 0);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
   return (WpDirection) self->info->direction;
 }
diff --git a/lib/wp/port.h b/lib/wp/port.h
index e9de692df6d237d769efeda70cde23dccf2314e6..bba7eecff3eb060ad3f1461d000c583fca463e5e 100644
--- a/lib/wp/port.h
+++ b/lib/wp/port.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_PORT_H__
 #define __WIREPLUMBER_PORT_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 
 G_BEGIN_DECLS
 
@@ -32,7 +32,7 @@ typedef enum {
  */
 #define WP_TYPE_PORT (wp_port_get_type ())
 WP_API
-G_DECLARE_FINAL_TYPE (WpPort, wp_port, WP, PORT, WpProxy)
+G_DECLARE_FINAL_TYPE (WpPort, wp_port, WP, PORT, WpGlobalProxy)
 
 WP_API
 WpDirection wp_port_get_direction (WpPort * self);
diff --git a/lib/wp/private.h b/lib/wp/private.h
index b913ea90f273d5bff86b98d8f228fbcd81418c10..2734d0991739e58a5cf90f4189bdabaddcbbaf28 100644
--- a/lib/wp/private.h
+++ b/lib/wp/private.h
@@ -33,20 +33,6 @@ struct spa_pod_builder;
 void wp_props_handle_proxy_param_event (WpProps * self, guint32 id,
     WpSpaPod * pod);
 
-/* proxy */
-
-void wp_proxy_destroy (WpProxy *self);
-void wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy);
-
-void wp_proxy_set_feature_ready (WpProxy * self, WpProxyFeatures feature);
-void wp_proxy_augment_error (WpProxy * self, GError * error);
-
-void wp_proxy_handle_event_param (void * proxy, int seq, uint32_t id,
-    uint32_t index, uint32_t next, const struct spa_pod *param);
-
-WpProps * wp_proxy_get_props (WpProxy * self);
-void wp_proxy_set_props (WpProxy * self, WpProps * props);
-
 /* iterator */
 
 struct _WpIteratorMethods {
diff --git a/lib/wp/private/pipewire-object-mixin.c b/lib/wp/private/pipewire-object-mixin.c
new file mode 100644
index 0000000000000000000000000000000000000000..49b316d6f2f12f4ee0c38b41b6361cfe6301be67
--- /dev/null
+++ b/lib/wp/private/pipewire-object-mixin.c
@@ -0,0 +1,258 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#define G_LOG_DOMAIN "wp-pw-obj-mixin"
+
+#include "private/pipewire-object-mixin.h"
+#include "core.h"
+#include "error.h"
+
+G_DEFINE_QUARK (WpPipewireObjectMixinEnumParamsTasks, enum_params_tasks)
+
+void
+wp_pipewire_object_mixin_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec)
+{
+  switch (property_id) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO:
+    g_value_set_pointer (value, (gpointer)
+        wp_pipewire_object_get_native_info (WP_PIPEWIRE_OBJECT (object)));
+    break;
+  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES:
+    g_value_set_boxed (value,
+        wp_pipewire_object_get_properties (WP_PIPEWIRE_OBJECT (object)));
+    break;
+  case WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO:
+    g_value_set_variant (value,
+        wp_pipewire_object_get_param_info (WP_PIPEWIRE_OBJECT (object)));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+    break;
+  }
+}
+
+void
+wp_pipewire_object_mixin_class_override_properties (GObjectClass * klass)
+{
+  g_object_class_override_property (klass,
+      WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO, "native-info");
+  g_object_class_override_property (klass,
+      WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES, "properties");
+  g_object_class_override_property (klass,
+      WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO, "param-info");
+}
+
+static const struct {
+  gint param_id;
+  WpObjectFeatures feature;
+} feature_mappings[] = {
+  { SPA_PARAM_Props, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS },
+  { SPA_PARAM_Format, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT },
+  { SPA_PARAM_Profile, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE },
+  { SPA_PARAM_PortConfig, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG },
+  { SPA_PARAM_Route, WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE },
+};
+
+WpObjectFeatures
+wp_pipewire_object_mixin_param_info_to_features (struct spa_param_info * info,
+    guint n_params)
+{
+  WpObjectFeatures ft = 0;
+
+  for (gint i = 0; i < n_params; i++) {
+    for (gint j = 0; j < G_N_ELEMENTS (feature_mappings); j++) {
+      if (info[i].id == feature_mappings[j].param_id &&
+          info[i].flags & SPA_PARAM_INFO_READ)
+        ft |= feature_mappings[j].feature;
+    }
+  }
+  return ft;
+}
+
+guint
+wp_pipewire_object_mixin_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  /* bind if not already bound */
+  if (missing & WP_PROXY_FEATURE_BOUND)
+    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND;
+  /* then cache info */
+  else
+    return WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO;
+
+  /* returning to STEP_NONE is handled by WpFeatureActivationTransition */
+}
+
+void
+wp_pipewire_object_mixin_cache_info (WpObject * object,
+    WpFeatureActivationTransition * transition)
+{
+  /* TODO */
+}
+
+void
+wp_pipewire_object_mixin_deactivate (WpObject * object,
+    WpObjectFeatures features)
+{
+  /* TODO */
+}
+
+static gint
+task_has_seq (gconstpointer task, gconstpointer seq)
+{
+  gpointer t_seq = g_task_get_source_tag (G_TASK (task));
+  return (GPOINTER_TO_INT (t_seq) == GPOINTER_TO_INT (seq)) ? 0 : 1;
+}
+
+void
+wp_pipewire_object_mixin_handle_event_param (gpointer instance, int seq,
+    uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param)
+{
+  g_autoptr (WpSpaPod) w_param = wp_spa_pod_new_wrap_const (param);
+  GList *list;
+  GTask *task;
+
+  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
+  list = g_list_find_custom (list, GINT_TO_POINTER (seq), task_has_seq);
+  task = list ? G_TASK (list->data) : NULL;
+
+  if (task) {
+    GPtrArray *array = g_task_get_task_data (task);
+    g_ptr_array_add (array, wp_spa_pod_copy (w_param));
+  }
+}
+
+GVariant *
+wp_pipewire_object_mixin_param_info_to_gvariant (struct spa_param_info * info,
+    guint n_params)
+{
+  g_auto (GVariantBuilder) b =
+      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_DICTIONARY);
+
+  if (!info || n_params == 0)
+    return NULL;
+
+  for (guint i = 0; i < n_params; i++) {
+    const gchar *nick = NULL;
+    gchar flags[3];
+    guint flags_idx = 0;
+
+    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, info[i].id, NULL, &nick,
+        NULL);
+    g_return_val_if_fail (nick != NULL, NULL);
+
+    if (info[i].flags & SPA_PARAM_INFO_READ)
+      flags[flags_idx++] = 'r';
+    if (info[i].flags & SPA_PARAM_INFO_WRITE)
+      flags[flags_idx++] = 'w';
+    flags[flags_idx] = '\0';
+
+    g_variant_builder_add (&b, "{ss}", nick, flags);
+  }
+
+  return g_variant_builder_end (&b);
+}
+
+WpIterator *
+wp_pipewire_object_mixin_enum_params_finish (WpPipewireObject * obj,
+    GAsyncResult * res, GError ** error)
+{
+  g_return_val_if_fail (g_task_is_valid (res, obj), NULL);
+
+  GPtrArray *array = g_task_propagate_pointer (G_TASK (res), error);
+  if (!array)
+    return NULL;
+
+  return wp_iterator_new_ptr_array (array, WP_TYPE_SPA_POD);
+}
+
+WpIterator *
+wp_pipewire_object_mixin_enum_cached_params (WpPipewireObject * obj,
+    const gchar * id)
+{
+  return NULL; //TODO
+}
+
+void
+wp_pipewire_object_mixin_enum_params_unimplemented (WpPipewireObject * obj,
+    const gchar * id, WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_create_enum_params_task (obj, 0, cancellable,
+      callback, user_data);
+}
+
+void
+wp_pipewire_object_mixin_set_param_unimplemented (WpPipewireObject * obj,
+    const gchar * id, WpSpaPod * param)
+{
+  wp_warning_object (obj,
+      "setting params is not implemented on this object type");
+}
+
+static void
+enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
+{
+  g_autoptr (GTask) task = G_TASK (data);
+  g_autoptr (GError) error = NULL;
+  gpointer instance = g_task_get_source_object (G_TASK (data));
+  GList *list;
+
+  /* finish the sync task */
+  wp_core_sync_finish (core, res, &error);
+
+  /* remove the task from the stored list; ref is held by the g_autoptr */
+  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
+  list = g_list_remove (list, instance);
+  g_object_set_qdata (G_OBJECT (instance), enum_params_tasks_quark (), list);
+
+  if (error)
+    g_task_return_error (task, g_steal_pointer (&error));
+  else {
+    GPtrArray *params = g_task_get_task_data (task);
+    g_task_return_pointer (task, g_ptr_array_ref (params),
+        (GDestroyNotify) g_ptr_array_unref);
+  }
+}
+
+void
+wp_pipewire_object_mixin_create_enum_params_task (gpointer instance,
+    gint seq, GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data)
+{
+  g_autoptr (GTask) task = NULL;
+  GPtrArray *params;
+  GList *list;
+
+  /* create task */
+  task = g_task_new (instance, cancellable, callback, user_data);
+  params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
+  g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
+  g_task_set_source_tag (task, GINT_TO_POINTER (seq));
+
+  /* return early if seq contains an error */
+  if (G_UNLIKELY (SPA_RESULT_IS_ERROR (seq))) {
+    wp_message_object (instance, "enum_params failed: %s", spa_strerror (seq));
+    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
+        WP_LIBRARY_ERROR_OPERATION_FAILED, "enum_params failed: %s",
+        spa_strerror (seq));
+    return;
+  }
+
+  /* store */
+  list = g_object_get_qdata (G_OBJECT (instance), enum_params_tasks_quark ());
+  list = g_list_append (list, g_object_ref (task));
+  g_object_set_qdata (G_OBJECT (instance), enum_params_tasks_quark (), list);
+
+  /* call sync */
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (instance));
+  wp_core_sync (core, cancellable, (GAsyncReadyCallback) enum_params_done,
+      task);
+}
diff --git a/lib/wp/private/pipewire-object-mixin.h b/lib/wp/private/pipewire-object-mixin.h
new file mode 100644
index 0000000000000000000000000000000000000000..a329f8d87ef51ef60d5ebd86b712e5dc468b91aa
--- /dev/null
+++ b/lib/wp/private/pipewire-object-mixin.h
@@ -0,0 +1,162 @@
+/* WirePlumber
+ *
+ * Copyright © 2020 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#ifndef __WIREPLUMBER_PIPEWIRE_OBJECT_MIXIN_H__
+#define __WIREPLUMBER_PIPEWIRE_OBJECT_MIXIN_H__
+
+#include "proxy-interfaces.h"
+#include "spa-type.h"
+#include "spa-pod.h"
+#include "debug.h"
+#include "private.h"
+
+#include <pipewire/pipewire.h>
+#include <spa/utils/result.h>
+
+G_BEGIN_DECLS
+
+enum {
+  /* this is the same STEP_BIND as in WpGlobalProxy */
+  WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND = WP_TRANSITION_STEP_CUSTOM_START,
+  WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO,
+
+  WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+};
+
+enum {
+  WP_PIPEWIRE_OBJECT_MIXIN_PROP_0,
+
+  // WpPipewireObject
+  WP_PIPEWIRE_OBJECT_MIXIN_PROP_NATIVE_INFO,
+  WP_PIPEWIRE_OBJECT_MIXIN_PROP_PROPERTIES,
+  WP_PIPEWIRE_OBJECT_MIXIN_PROP_PARAM_INFO,
+
+  WP_PIPEWIRE_OBJECT_MIXIN_PROP_CUSTOM_START,
+};
+
+/******************/
+/* PROPERTIES API */
+
+/* assign to get_property or chain it from there */
+void wp_pipewire_object_mixin_get_property (GObject * object, guint property_id,
+    GValue * value, GParamSpec * pspec);
+
+/* call from class_init */
+void wp_pipewire_object_mixin_class_override_properties (GObjectClass * klass);
+
+/****************/
+/* FEATURES API */
+
+/* call from get_supported_features */
+WpObjectFeatures wp_pipewire_object_mixin_param_info_to_features (
+    struct spa_param_info * info, guint n_params);
+
+/* assign directly to activate_get_next_step */
+guint wp_pipewire_object_mixin_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing);
+
+/* call from activate_execute_step when
+   step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO */
+void wp_pipewire_object_mixin_cache_info (WpObject * object,
+    WpFeatureActivationTransition * transition);
+
+/* handle deactivation of PARAM_* caching features */
+void wp_pipewire_object_mixin_deactivate (WpObject * object,
+    WpObjectFeatures features);
+
+/***************************/
+/* PIPEWIRE EVENT HANDLERS */
+
+/* call at the end of the info event callback */
+#define wp_pipewire_object_mixin_handle_event_info(instance, info, CM_PROPS, CM_PARAMS) \
+({ \
+  if (info->change_mask & CM_PROPS) \
+    g_object_notify (G_OBJECT (instance), "properties"); \
+  \
+  if (info->change_mask & CM_PARAMS) \
+    g_object_notify (G_OBJECT (instance), "param-info"); \
+})
+
+/* assign as the param event callback */
+void wp_pipewire_object_mixin_handle_event_param (gpointer instance, int seq,
+    uint32_t id, uint32_t index, uint32_t next, const struct spa_pod *param);
+
+/***********************/
+/* PIPEWIRE OBJECT API */
+
+/* call from get_param_info */
+GVariant * wp_pipewire_object_mixin_param_info_to_gvariant (
+    struct spa_param_info * info, guint n_params);
+
+/* assign directly to enum_params_finish */
+WpIterator * wp_pipewire_object_mixin_enum_params_finish (WpPipewireObject * obj,
+    GAsyncResult * res, GError ** error);
+
+/* assign directly to enum_cached_params */
+WpIterator * wp_pipewire_object_mixin_enum_cached_params (WpPipewireObject * obj,
+    const gchar * id);
+
+/* assign to enum_params on objects that don't support params (like pw_link) */
+void wp_pipewire_object_mixin_enum_params_unimplemented (WpPipewireObject * obj,
+    const gchar * id, WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data);
+
+/* assign to set_param on objects that don't support params (like pw_link) */
+void wp_pipewire_object_mixin_set_param_unimplemented (WpPipewireObject * obj,
+    const gchar * id, WpSpaPod * param);
+
+/* call from enum_params */
+#define wp_pipewire_object_mixin_enum_params(type, instance, id, filter, cancellable, callback, user_data) \
+({ \
+  struct type *pwp; \
+  gint seq; \
+  guint32 id_num = 0; \
+  \
+  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num, \
+          NULL, NULL)) { \
+    wp_critical_object (instance, "invalid param id: %s", id); \
+  } else { \
+    pwp = (struct type *) wp_proxy_get_pw_proxy (WP_PROXY (instance)); \
+    seq = type ## _enum_params (pwp, 0, id_num, 0, -1, \
+        filter ? wp_spa_pod_get_spa_pod (filter) : NULL); \
+    \
+    wp_pipewire_object_mixin_create_enum_params_task (instance, seq, cancellable, \
+        callback, user_data); \
+  } \
+})
+
+/* call from set_param */
+#define wp_pipewire_object_mixin_set_param(type, instance, id, param) \
+({ \
+  struct type *pwp; \
+  gint ret; \
+  guint32 id_num = 0; \
+  \
+  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num, \
+          NULL, NULL)) { \
+    wp_critical_object (instance, "invalid param id: %s", id); \
+  } else { \
+    pwp = (struct type *) wp_proxy_get_pw_proxy (WP_PROXY (instance)); \
+    ret = type ## _set_param (pwp, id_num, 0, wp_spa_pod_get_spa_pod (param)); \
+    if (G_UNLIKELY (SPA_RESULT_IS_ERROR (ret))) { \
+      wp_message_object (instance, "set_param failed: %s", spa_strerror (ret)); \
+    } \
+  } \
+})
+
+/************/
+/* INTERNAL */
+
+void wp_pipewire_object_mixin_create_enum_params_task (gpointer instance,
+    gint seq, GCancellable * cancellable, GAsyncReadyCallback callback,
+    gpointer user_data);
+
+G_END_DECLS
+
+#endif
diff --git a/lib/wp/private/registry.h b/lib/wp/private/registry.h
index cb62664fc9f8bac1ca26412e2e1f292ffb1f3da2..9078e9aa853f4b3363e76d645cd44ce3c3de2498 100644
--- a/lib/wp/private/registry.h
+++ b/lib/wp/private/registry.h
@@ -10,6 +10,7 @@
 #define __WIREPLUMBER_REGISTRY_H__
 
 #include "core.h"
+#include "global-proxy.h"
 
 #include <pipewire/pipewire.h>
 
@@ -38,7 +39,7 @@ void wp_registry_detach (WpRegistry *self);
 
 void wp_registry_prepare_new_global (WpRegistry * self, guint32 id,
     guint32 permissions, guint32 flag, GType type,
-    WpProxy *proxy, const struct spa_dict *props,
+    WpGlobalProxy *proxy, const struct spa_dict *props,
     WpGlobal ** new_global);
 
 gpointer wp_registry_find_object (WpRegistry *reg, GEqualFunc func,
@@ -66,7 +67,7 @@ struct _WpGlobal
   GType type;
   guint32 permissions;
   WpProperties *properties;
-  WpProxy *proxy;
+  WpGlobalProxy *proxy;
   WpRegistry *registry;
 };
 
diff --git a/lib/wp/proxy.c b/lib/wp/proxy.c
index b4dd8d66f7c34018945d90bb67f2e4f57c954a04..bed50720ca3889b3f19f950df10631e14a952aa8 100644
--- a/lib/wp/proxy.c
+++ b/lib/wp/proxy.c
@@ -1,69 +1,35 @@
 /* WirePlumber
  *
- * Copyright © 2019 Collabora Ltd.
- *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ * Copyright © 2020 Collabora Ltd.
  *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
  *
  * SPDX-License-Identifier: MIT
  */
 
 /**
- * SECTION: WpProxy
- *
+ * SECTION: proxy
+ * @title: PipeWire Object Proxy
  */
 
 #define G_LOG_DOMAIN "wp-proxy"
 
 #include "proxy.h"
 #include "debug.h"
-#include "core.h"
-#include "error.h"
-#include "wpenums.h"
-#include "private.h"
 
 #include <pipewire/pipewire.h>
-#include <pipewire/impl.h>
-#include <pipewire/extensions/metadata.h>
-#include <pipewire/extensions/client-node.h>
-#include <pipewire/extensions/session-manager.h>
-
-#include <spa/debug/types.h>
-#include <spa/pod/builder.h>
-#include <spa/utils/result.h>
+#include <spa/utils/hook.h>
 
 typedef struct _WpProxyPrivate WpProxyPrivate;
 struct _WpProxyPrivate
 {
-  /* properties */
-  GWeakRef core;
-  WpGlobal *global;
   struct pw_proxy *pw_proxy;
-
-  /* The proxy listener */
   struct spa_hook listener;
-
-  /* augment state */
-  WpProxyFeatures ft_ready;
-  GPtrArray *augment_tasks; // element-type: GTask*
-
-  GHashTable *async_tasks; // <int seq, GTask*>
-
-  /* props cache */
-  WpProps *props;
 };
 
 enum {
   PROP_0,
-  PROP_CORE,
-  PROP_GLOBAL,
-  PROP_GLOBAL_PERMISSIONS,
-  PROP_GLOBAL_PROPERTIES,
-  PROP_FEATURES,
-  PROP_PW_PROXY,
-  PROP_INFO,
-  PROP_PROPERTIES,
-  PROP_PARAM_INFO,
   PROP_BOUND_ID,
+  PROP_PW_PROXY,
 };
 
 enum
@@ -71,74 +37,46 @@ enum
   SIGNAL_PW_PROXY_CREATED,
   SIGNAL_PW_PROXY_DESTROYED,
   SIGNAL_BOUND,
-  SIGNAL_PARAM,
-  SIGNAL_PROP_CHANGED,
   LAST_SIGNAL,
 };
 
-static guint wp_proxy_signals[LAST_SIGNAL] = { 0 };
+static guint signals[LAST_SIGNAL] = { 0 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpProxy, wp_proxy, G_TYPE_OBJECT)
+/**
+ * WpProxy:
+ *
+ * Base class for all objects that expose PipeWire objects using `pw_proxy`
+ * underneath.
+ *
+ * This base class cannot be instantiated. It provides handling of
+ * pw_proxy's events and exposes common functionality.
+ */
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (WpProxy, wp_proxy, WP_TYPE_OBJECT)
 
 static void
 proxy_event_destroy (void *data)
 {
-  /* hold a reference to the proxy because unref-ing the tasks might
-    destroy the proxy, in case the registry is no longer holding a reference */
-  g_autoptr (WpProxy) self = g_object_ref (WP_PROXY (data));
+  WpProxy *self = WP_PROXY (data);
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  GHashTableIter iter;
-  GTask *task;
 
   wp_trace_object (self, "destroyed pw_proxy %p (%u)", priv->pw_proxy,
-      priv->global ? priv->global->id : pw_proxy_get_bound_id (priv->pw_proxy));
+      pw_proxy_get_bound_id (priv->pw_proxy));
 
   spa_hook_remove (&priv->listener);
   priv->pw_proxy = NULL;
-  g_signal_emit (self, wp_proxy_signals[SIGNAL_PW_PROXY_DESTROYED], 0);
-
-  /* Return error if the pw_proxy destruction happened while the async
-   * init or augment of this proxy object was in progress */
-  if (priv->augment_tasks->len > 0) {
-    GError *err = g_error_new (WP_DOMAIN_LIBRARY,
-        WP_LIBRARY_ERROR_OPERATION_FAILED,
-        "pipewire proxy destroyed before finishing");
-    wp_proxy_augment_error (self, err);
-  }
-
-  g_hash_table_iter_init (&iter, priv->async_tasks);
-  while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &task)) {
-    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
-        WP_LIBRARY_ERROR_OPERATION_FAILED,
-        "pipewire proxy destroyed before finishing");
-    g_hash_table_iter_remove (&iter);
-  }
+  wp_object_update_features (WP_OBJECT (self), 0, WP_PROXY_FEATURE_BOUND);
+  g_signal_emit (self, signals[SIGNAL_PW_PROXY_DESTROYED], 0);
 }
 
 static void
 proxy_event_bound (void *data, uint32_t global_id)
 {
   WpProxy *self = WP_PROXY (data);
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  /* we generally make the assumption here that the bound id is the
-     same as the global id, but while this **is** it's intended use,
-     the truth is that the bound id **can** be changed anytime with
-     pw_proxy_set_bound_id() and this can be very bad... */
-  g_warn_if_fail (!priv->global || priv->global->id == global_id);
-
-  wp_proxy_set_feature_ready (self, WP_PROXY_FEATURE_BOUND);
 
-  /* construct a WpGlobal if it was not already there */
-  if (!priv->global) {
-    g_autoptr (WpCore) core = g_weak_ref_get (&priv->core);
+  wp_trace_object (self, "bound to %u", global_id);
 
-    wp_registry_prepare_new_global (wp_core_get_registry (core),
-        global_id, PW_PERM_RWX, WP_GLOBAL_FLAG_OWNED_BY_PROXY,
-        G_TYPE_FROM_INSTANCE (self), self, NULL, &priv->global);
-  }
-
-  g_signal_emit (self, wp_proxy_signals[SIGNAL_BOUND], 0, global_id);
+  wp_object_update_features (WP_OBJECT (self), WP_PROXY_FEATURE_BOUND, 0);
+  g_signal_emit (self, signals[SIGNAL_BOUND], 0, global_id);
 }
 
 static void
@@ -154,130 +92,24 @@ static const struct pw_proxy_events proxy_events = {
   .removed = proxy_event_removed,
 };
 
-void
-wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy)
-{
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  if (!proxy)
-    return;
-
-  g_return_if_fail (priv->pw_proxy == NULL);
-  priv->pw_proxy = proxy;
-
-  pw_proxy_add_listener (priv->pw_proxy, &priv->listener, &proxy_events,
-      self);
-
-  /* inform subclasses and listeners */
-  g_signal_emit (self, wp_proxy_signals[SIGNAL_PW_PROXY_CREATED], 0,
-      priv->pw_proxy);
-
-  /* declare the feature as ready */
-  wp_proxy_set_feature_ready (self, WP_PROXY_FEATURE_PW_PROXY);
-}
-
 static void
 wp_proxy_init (WpProxy * self)
 {
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  g_weak_ref_init (&priv->core, NULL);
-  priv->augment_tasks = g_ptr_array_new_with_free_func (g_object_unref);
-  priv->async_tasks = g_hash_table_new_full (g_direct_hash, g_direct_equal,
-      NULL, g_object_unref);
 }
 
 static void
-wp_proxy_dispose (GObject * object)
-{
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY(object));
-
-  wp_trace_object (object, "dispose (global %u; pw_proxy %p)",
-      priv->global ? priv->global->id : 0, priv->pw_proxy);
-
-  if (priv->global)
-    wp_global_rm_flag (priv->global, WP_GLOBAL_FLAG_OWNED_BY_PROXY);
-
-  /* this will trigger proxy_event_destroy() if the pw_proxy exists */
-  if (priv->pw_proxy)
-    pw_proxy_destroy (priv->pw_proxy);
-
-  G_OBJECT_CLASS (wp_proxy_parent_class)->dispose (object);
-}
-
-static void
-wp_proxy_finalize (GObject * object)
-{
-  WpProxy *self = WP_PROXY (object);
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  g_clear_object (&priv->props);
-  g_clear_pointer (&priv->augment_tasks, g_ptr_array_unref);
-  g_clear_pointer (&priv->global, wp_global_unref);
-  g_weak_ref_clear (&priv->core);
-  g_clear_pointer (&priv->async_tasks, g_hash_table_unref);
-
-  G_OBJECT_CLASS (wp_proxy_parent_class)->finalize (object);
-}
-
-static void
-wp_proxy_set_gobj_property (GObject * object, guint property_id,
-    const GValue * value, GParamSpec * pspec)
-{
-  WpProxy *self = WP_PROXY (object);
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  switch (property_id) {
-  case PROP_CORE:
-    g_weak_ref_set (&priv->core, g_value_get_object (value));
-    break;
-  case PROP_GLOBAL:
-    priv->global = g_value_dup_boxed (value);
-    break;
-  case PROP_PW_PROXY:
-    wp_proxy_set_pw_proxy (self, g_value_get_pointer (value));
-    break;
-  default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
-    break;
-  }
-}
-
-static void
-wp_proxy_get_gobj_property (GObject * object, guint property_id, GValue * value,
+wp_proxy_get_property (GObject * object, guint property_id, GValue * value,
     GParamSpec * pspec)
 {
   WpProxy *self = WP_PROXY (object);
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
 
   switch (property_id) {
-  case PROP_CORE:
-    g_value_take_object (value, g_weak_ref_get (&priv->core));
-    break;
-  case PROP_GLOBAL_PERMISSIONS:
-    g_value_set_uint (value, priv->global ? priv->global->permissions : 0);
-    break;
-  case PROP_GLOBAL_PROPERTIES:
-    g_value_set_boxed (value, priv->global ? priv->global->properties : NULL);
-    break;
-  case PROP_FEATURES:
-    g_value_set_flags (value, priv->ft_ready);
-    break;
-  case PROP_PW_PROXY:
-    g_value_set_pointer (value, priv->pw_proxy);
-    break;
-  case PROP_INFO:
-    g_value_set_pointer (value, (gpointer) wp_proxy_get_info (self));
-    break;
-  case PROP_PROPERTIES:
-    g_value_take_boxed (value, wp_proxy_get_properties (self));
-    break;
-  case PROP_PARAM_INFO:
-    g_value_take_variant (value, wp_proxy_get_param_info (self));
-    break;
   case PROP_BOUND_ID:
     g_value_set_uint (value, wp_proxy_get_bound_id (self));
     break;
+  case PROP_PW_PROXY:
+    g_value_set_pointer (value, wp_proxy_get_pw_proxy (self));
+    break;
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
     break;
@@ -285,91 +117,13 @@ wp_proxy_get_gobj_property (GObject * object, guint property_id, GValue * value,
 }
 
 static void
-wp_proxy_enum_props_done (WpCore * core, GAsyncResult * res, WpProxy * self)
-{
-  g_autoptr (GError) error = NULL;
-  if (!wp_core_sync_finish (core, res, &error)) {
-    wp_warning_object (self, "core sync failed: %s", error->message);
-  }
-
-  wp_trace_object (self, "enum props done");
-  wp_proxy_set_feature_ready (self, WP_PROXY_FEATURE_PROPS);
-}
-
-static void
-wp_proxy_enable_feature_props (WpProxy * self)
-{
-  WpProxyClass *klass = WP_PROXY_GET_CLASS (self);
-  struct spa_param_info *param_info;
-  guint n_params;
-  guint have_propinfo = FALSE, have_props = FALSE;
-  uint32_t ids[] = { SPA_PARAM_Props };
-
-  /* check if we actually have props */
-  param_info = klass->get_param_info (self, &n_params);
-  for (guint i = 0; i < n_params; i++) {
-    if (param_info[i].id == SPA_PARAM_PropInfo)
-      have_propinfo = TRUE;
-    else if (param_info[i].id == SPA_PARAM_Props)
-      have_props = TRUE;
-  }
-
-  if (have_propinfo && have_props) {
-    g_autoptr (WpCore) core = wp_proxy_get_core (self);
-
-    if (!klass->enum_params || !klass->subscribe_params) {
-      wp_proxy_augment_error (self, g_error_new (WP_DOMAIN_LIBRARY,
-            WP_LIBRARY_ERROR_INVARIANT,
-            "Proxy does not support enum/subscribe params API"));
-      return;
-    }
-
-    klass->enum_params (self, SPA_PARAM_PropInfo, 0, -1, NULL);
-    klass->subscribe_params (self, ids, SPA_N_ELEMENTS (ids));
-    wp_core_sync (core, NULL, (GAsyncReadyCallback) wp_proxy_enum_props_done,
-        self);
-  } else {
-    /* declare as ready with no props */
-    wp_proxy_set_feature_ready (self, WP_PROXY_FEATURE_PROPS);
-  }
-
-  g_signal_handlers_disconnect_by_func (self,
-      wp_proxy_enable_feature_props, self);
-}
-
-static void
-wp_proxy_default_augment (WpProxy * self, WpProxyFeatures features)
+wp_proxy_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-
-  /* ensure we have a pw_proxy, as we can't have
-   * any other feature without first having that */
-  if (!priv->pw_proxy && features != 0)
-    features |= WP_PROXY_FEATURE_PW_PROXY;
-
-  /* if we don't have a pw_proxy, we have to assume that this WpProxy
-   * represents a global object from the registry; we have no other way
-   * to get a pw_proxy */
-  if (features & WP_PROXY_FEATURE_PW_PROXY) {
-    if (priv->global == NULL) {
-      wp_proxy_augment_error (self, g_error_new (WP_DOMAIN_LIBRARY,
-            WP_LIBRARY_ERROR_INVALID_ARGUMENT,
-            "No global specified; cannot bind pw_proxy"));
-      return;
-    }
-
-    /* bind */
-    wp_proxy_set_pw_proxy (self, wp_global_bind (priv->global));
-  }
-
-  if (features & WP_PROXY_FEATURE_PROPS && !priv->props) {
-    wp_proxy_set_props (self, wp_props_new (WP_PROPS_MODE_CACHE, self));
-
-    if (priv->ft_ready & WP_PROXY_FEATURE_INFO)
-      wp_proxy_enable_feature_props (self);
-    else
-      g_signal_connect (self, "notify::param-info",
-          G_CALLBACK (wp_proxy_enable_feature_props), self);
+  if (features & WP_PROXY_FEATURE_BOUND) {
+    WpProxyPrivate *priv = wp_proxy_get_instance_private (WP_PROXY (object));
+    if (priv->pw_proxy)
+      pw_proxy_destroy (priv->pw_proxy);
+    wp_object_update_features (object, 0, WP_PROXY_FEATURE_BOUND);
   }
 }
 
@@ -377,451 +131,38 @@ static void
 wp_proxy_class_init (WpProxyClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
 
-  object_class->dispose = wp_proxy_dispose;
-  object_class->finalize = wp_proxy_finalize;
-  object_class->get_property = wp_proxy_get_gobj_property;
-  object_class->set_property = wp_proxy_set_gobj_property;
+  object_class->get_property = wp_proxy_get_property;
 
-  klass->augment = wp_proxy_default_augment;
+  wpobject_class->deactivate = wp_proxy_deactivate;
 
   /* Install the properties */
 
-  g_object_class_install_property (object_class, PROP_CORE,
-      g_param_spec_object ("core", "core", "The WpCore", WP_TYPE_CORE,
-          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_GLOBAL,
-      g_param_spec_boxed ("global", "global", "Internal WpGlobal object",
-          wp_global_get_type (),
-          G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_GLOBAL_PERMISSIONS,
-      g_param_spec_uint ("global-permissions", "global-permissions",
-          "The pipewire global permissions", 0, G_MAXUINT, 0,
-          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_GLOBAL_PROPERTIES,
-      g_param_spec_boxed ("global-properties", "global-properties",
-          "The pipewire global properties", WP_TYPE_PROPERTIES,
-          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_FEATURES,
-      g_param_spec_flags ("features", "features",
-          "The ready WpProxyFeatures on this proxy", WP_TYPE_PROXY_FEATURES, 0,
+  g_object_class_install_property (object_class, PROP_BOUND_ID,
+      g_param_spec_uint ("bound-id", "bound-id",
+          "The id that this object has on the registry", 0, G_MAXUINT, 0,
           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   g_object_class_install_property (object_class, PROP_PW_PROXY,
       g_param_spec_pointer ("pw-proxy", "pw-proxy", "The struct pw_proxy *",
-          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_INFO,
-      g_param_spec_pointer ("info", "info", "The native info structure",
-          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_PROPERTIES,
-      g_param_spec_boxed ("properties", "properties",
-          "The pipewire properties of the object", WP_TYPE_PROPERTIES,
-          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_PARAM_INFO,
-      g_param_spec_variant ("param-info", "param-info",
-          "The param info of the object", G_VARIANT_TYPE ("a{ss}"), NULL,
-          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
-
-  g_object_class_install_property (object_class, PROP_BOUND_ID,
-      g_param_spec_uint ("bound-id", "bound-id",
-          "The id that this object has on the registry", 0, G_MAXUINT, 0,
           G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
 
   /* Signals */
-  wp_proxy_signals[SIGNAL_PW_PROXY_CREATED] = g_signal_new (
+  signals[SIGNAL_PW_PROXY_CREATED] = g_signal_new (
       "pw-proxy-created", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
       G_STRUCT_OFFSET (WpProxyClass, pw_proxy_created), NULL, NULL, NULL,
       G_TYPE_NONE, 1, G_TYPE_POINTER);
 
-  wp_proxy_signals[SIGNAL_PW_PROXY_DESTROYED] = g_signal_new (
+  signals[SIGNAL_PW_PROXY_DESTROYED] = g_signal_new (
       "pw-proxy-destroyed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
       G_STRUCT_OFFSET (WpProxyClass, pw_proxy_destroyed), NULL, NULL, NULL,
       G_TYPE_NONE, 0);
 
-  wp_proxy_signals[SIGNAL_BOUND] = g_signal_new (
+  signals[SIGNAL_BOUND] = g_signal_new (
       "bound", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_FIRST,
       G_STRUCT_OFFSET (WpProxyClass, bound), NULL, NULL, NULL,
       G_TYPE_NONE, 1, G_TYPE_UINT);
-
-  wp_proxy_signals[SIGNAL_PROP_CHANGED] = g_signal_new (
-      "prop-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST,
-      G_STRUCT_OFFSET (WpProxyClass, prop_changed), NULL, NULL, NULL,
-      G_TYPE_NONE, 1, G_TYPE_STRING);
-}
-
-/* private */
-void
-wp_proxy_destroy (WpProxy *self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  priv = wp_proxy_get_instance_private (self);
-  if (priv->pw_proxy)
-    pw_proxy_destroy (priv->pw_proxy);
-}
-
-/**
- * wp_proxy_request_destroy:
- * @self: the proxy
- *
- * Requests the PipeWire server to destroy the object represented by this proxy.
- * If the server allows it, the object will be destroyed and the
- * WpProxy::pw-proxy-destroyed signal will be emitted. If the server does
- * not allow it, nothing will happen.
- *
- * This is mostly useful for destroying #WpLink and #WpEndpointLink objects.
- */
-void
-wp_proxy_request_destroy (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-  g_autoptr (WpCore) core = NULL;
-  WpRegistry *reg;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  priv = wp_proxy_get_instance_private (self);
-  core = wp_proxy_get_core (self);
-
-  if (priv->pw_proxy && core) {
-    reg = wp_core_get_registry (core);
-    pw_registry_destroy (reg->pw_registry,
-        pw_proxy_get_bound_id (priv->pw_proxy));
-  }
-}
-
-void
-wp_proxy_augment (WpProxy * self,
-    WpProxyFeatures ft_wanted, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data)
-{
-  WpProxyPrivate *priv;
-  WpProxyFeatures missing = 0;
-  g_autoptr (GTask) task = NULL;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-  g_return_if_fail (WP_PROXY_GET_CLASS (self)->augment);
-
-  priv = wp_proxy_get_instance_private (self);
-
-  task = g_task_new (self, cancellable, callback, user_data);
-
-  /* find which features are wanted but missing from the "ready" set */
-  missing = (priv->ft_ready ^ ft_wanted) & ft_wanted;
-
-  /* if the features are not ready, call augment(),
-   * otherwise signal the callback directly */
-  if (missing != 0) {
-    g_task_set_task_data (task, GUINT_TO_POINTER (missing), NULL);
-    g_ptr_array_add (priv->augment_tasks, g_steal_pointer (&task));
-    WP_PROXY_GET_CLASS (self)->augment (self, missing);
-  } else {
-    g_task_return_boolean (task, TRUE);
-  }
-}
-
-gboolean
-wp_proxy_augment_finish (WpProxy * self, GAsyncResult * res,
-    GError ** error)
-{
-  g_return_val_if_fail (WP_IS_PROXY (self), FALSE);
-  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
-
-  return g_task_propagate_boolean (G_TASK (res), error);
-}
-
-void
-wp_proxy_set_feature_ready (WpProxy * self, WpProxyFeatures feature)
-{
-  WpProxyPrivate *priv;
-  g_autoptr (GPtrArray) ready_tasks = NULL;
-  guint i;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  priv = wp_proxy_get_instance_private (self);
-
-  /* feature already marked as ready */
-  if (priv->ft_ready & feature)
-    return;
-
-  priv->ft_ready |= feature;
-
-  if (wp_log_level_is_enabled (WP_LOG_LEVEL_TRACE)) {
-    g_autofree gchar *str = g_flags_to_string (WP_TYPE_PROXY_FEATURES,
-        priv->ft_ready);
-    wp_trace_object (self, "features changed: %s", str);
-  }
-
-  g_object_notify (G_OBJECT (self), "features");
-
-  /* hold a reference to the proxy because unref-ing the tasks might
-    destroy the proxy, in case the registry is no longer holding a reference */
-  g_object_ref (self);
-
-  /* move the ready tasks to another array to avoid recursion issues */
-  ready_tasks = g_ptr_array_new_with_free_func (g_object_unref);
-
-  /* return from the task if all the wanted features are now ready */
-  for (i = priv->augment_tasks->len; i > 0; i--) {
-    GTask *task = g_ptr_array_index (priv->augment_tasks, i - 1);
-    WpProxyFeatures wanted = GPOINTER_TO_UINT (g_task_get_task_data (task));
-
-    if ((priv->ft_ready & wanted) == wanted) {
-      /* this is safe as long as we are traversing the array backwards */
-      g_ptr_array_add (ready_tasks,
-          g_ptr_array_steal_index_fast (priv->augment_tasks, i - 1));
-    }
-  }
-
-  for (i = 0; i < ready_tasks->len; i++) {
-    GTask *task = g_ptr_array_index (ready_tasks, i);
-    g_task_return_boolean (task, TRUE);
-  }
-
-  g_object_unref (self);
-}
-
-/**
- * wp_proxy_augment_error:
- * @self: the proxy
- * @error: (transfer full): the error
- *
- * Reports an error that occured during the augment process
- */
-void
-wp_proxy_augment_error (WpProxy * self, GError * error)
-{
-  WpProxyPrivate *priv;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  priv = wp_proxy_get_instance_private (self);
-
-  /* steal the array to avoid recursion here */
-  if (priv->augment_tasks->len > 0) {
-    guint i;
-    g_autoptr (GPtrArray) augment_tasks =
-        g_steal_pointer (&priv->augment_tasks);
-    priv->augment_tasks = g_ptr_array_new_with_free_func (g_object_unref);
-
-    for (i = 0; i < augment_tasks->len; i++) {
-      GTask *task = g_ptr_array_index (augment_tasks, i);
-      g_task_return_error (task, g_error_copy (error));
-    }
-  }
-
-  g_error_free (error);
-}
-
-WpProxyFeatures
-wp_proxy_get_features (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), 0);
-
-  priv = wp_proxy_get_instance_private (self);
-  return priv->ft_ready;
-}
-
-/**
- * wp_proxy_get_core:
- * @self: the proxy
- *
- * Returns: (transfer full): the core that created this proxy
- */
-WpCore *
-wp_proxy_get_core (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  priv = wp_proxy_get_instance_private (self);
-  return g_weak_ref_get (&priv->core);
-}
-
-guint32
-wp_proxy_get_global_permissions (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), 0);
-
-  priv = wp_proxy_get_instance_private (self);
-  return priv->global ? priv->global->permissions : 0;
-}
-
-/**
- * wp_proxy_get_global_properties:
- *
- * Returns: (transfer full): the global properties of the proxy
- */
-WpProperties *
-wp_proxy_get_global_properties (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  priv = wp_proxy_get_instance_private (self);
-  if (!priv->global || !priv->global->properties)
-    return NULL;
-  return wp_properties_ref (priv->global->properties);
-}
-
-struct pw_proxy *
-wp_proxy_get_pw_proxy (WpProxy * self)
-{
-  WpProxyPrivate *priv;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  priv = wp_proxy_get_instance_private (self);
-  return priv->pw_proxy;
-}
-
-/**
- * wp_proxy_get_info:
- * @self: the proxy
- *
- * Requires %WP_PROXY_FEATURE_INFO
- *
- * Returns: the pipewire info structure of this object
- *    (pw_node_info, pw_port_info, etc...)
- */
-gconstpointer
-wp_proxy_get_info (WpProxy * self)
-{
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_warn_if_fail (priv->ft_ready & WP_PROXY_FEATURE_INFO);
-
-  return (WP_PROXY_GET_CLASS (self)->get_info) ?
-      WP_PROXY_GET_CLASS (self)->get_info (self) : NULL;
-}
-
-/**
- * wp_proxy_get_properties:
- * @self: the proxy
- *
- * Requires %WP_PROXY_FEATURE_INFO
- *
- * Returns: (transfer full): the pipewire properties of this object;
- *   normally these are the properties that are part of the info structure
- */
-WpProperties *
-wp_proxy_get_properties (WpProxy * self)
-{
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_warn_if_fail (priv->ft_ready & WP_PROXY_FEATURE_INFO);
-
-  return (WP_PROXY_GET_CLASS (self)->get_properties) ?
-      WP_PROXY_GET_CLASS (self)->get_properties (self) : NULL;
-}
-
-/**
- * wp_proxy_get_property:
- * @self: the proxy
- * @key: the property name
- *
- * Returns the value of a single pipewire property. This is the same as getting
- * the whole properties structure with wp_proxy_get_properties() and accessing
- * a single property with wp_properties_get(), but saves one call
- * and having to clean up the #WpProperties reference count afterwards.
- *
- * The value is owned by the proxy, but it is guaranteed to stay alive
- * until execution returns back to the event loop.
- *
- * Requires %WP_PROXY_FEATURE_INFO
- *
- * Returns: (transfer none) (nullable): the value of the pipewire property @key
- *   or %NULL if the property doesn't exist
- */
-const gchar *
-wp_proxy_get_property (WpProxy * self, const gchar * key)
-{
-  /* the proxy always keeps a ref to the data, so it's safe
-     to discard the ref count of the WpProperties */
-  g_autoptr (WpProperties) props = NULL;
-  props = wp_proxy_get_properties (self);
-  return props ? wp_properties_get (props, key) : NULL;
-}
-
-/**
- * wp_proxy_get_param_info:
- * @self: the proxy
- *
- * Returns the available parameters of this proxy. The return value is
- * a variant of type `a{ss}`, where the key of each map entry is a spa param
- * type id (the same ids that you can pass in wp_proxy_enum_params())
- * and the value is a string that can contain the following letters,
- * each of them representing a flag:
- *   - `r`: the param is readable (`SPA_PARAM_INFO_READ`)
- *   - `w`: the param is writable (`SPA_PARAM_INFO_WRITE`)
- *   - `s`: the param was updated (`SPA_PARAM_INFO_SERIAL`)
- *
- * For params that are readable, you can query them with wp_proxy_enum_params()
- *
- * Params that are writable can be set with wp_proxy_set_param()
- *
- * Requires %WP_PROXY_FEATURE_INFO
- *
- * Returns: (transfer full) (nullable): a variant of type `a{ss}` or %NULL
- *   if the proxy does not support params at all
- */
-GVariant *
-wp_proxy_get_param_info (WpProxy * self)
-{
-  g_auto (GVariantBuilder) b =
-      G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_DICTIONARY);
-  guint n_params = 0;
-  struct spa_param_info *info;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_warn_if_fail (priv->ft_ready & WP_PROXY_FEATURE_INFO);
-
-  info = (WP_PROXY_GET_CLASS (self)->get_param_info) ?
-      WP_PROXY_GET_CLASS (self)->get_param_info (self, &n_params) : NULL;
-  if (!info || n_params == 0)
-    return NULL;
-
-  for (guint i = 0; i < n_params; i++) {
-    const gchar *nick = NULL;
-    gchar flags[4];
-    guint flags_idx = 0;
-
-    wp_spa_type_get_by_id (WP_SPA_TYPE_TABLE_PARAM, info[i].id, NULL, &nick,
-        NULL);
-    g_return_val_if_fail (nick != NULL, NULL);
-
-    if (info[i].flags & SPA_PARAM_INFO_READ)
-      flags[flags_idx++] = 'r';
-    if (info[i].flags & SPA_PARAM_INFO_WRITE)
-      flags[flags_idx++] = 'w';
-    if (info[i].flags & SPA_PARAM_INFO_SERIAL)
-      flags[flags_idx++] = 's';
-    flags[flags_idx] = '\0';
-
-    g_variant_builder_add (&b, "{ss}", nick, flags);
-  }
-
-  return g_variant_builder_end (&b);
 }
 
 /**
@@ -838,300 +179,50 @@ guint32
 wp_proxy_get_bound_id (WpProxy * self)
 {
   g_return_val_if_fail (WP_IS_PROXY (self), 0);
+  g_warn_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+      WP_PROXY_FEATURE_BOUND);
 
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_warn_if_fail (priv->ft_ready & WP_PROXY_FEATURE_BOUND);
-
   return priv->pw_proxy ? pw_proxy_get_bound_id (priv->pw_proxy) : SPA_ID_INVALID;
 }
 
-static void
-wp_proxy_register_async_task (WpProxy * self, int seq, GTask * task)
-{
-  WpProxyPrivate *priv;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-  g_return_if_fail (g_task_is_valid (task, self));
-
-  priv = wp_proxy_get_instance_private (self);
-  g_hash_table_insert (priv->async_tasks, GINT_TO_POINTER (seq), task);
-}
-
-static GTask *
-wp_proxy_find_async_task (WpProxy * self, int seq, gboolean steal)
-{
-  WpProxyPrivate *priv;
-  GTask *task = NULL;
-
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  priv = wp_proxy_get_instance_private (self);
-  if (steal)
-    g_hash_table_steal_extended (priv->async_tasks, GINT_TO_POINTER (seq),
-        NULL, (gpointer *) &task);
-  else
-    task = g_hash_table_lookup (priv->async_tasks, GINT_TO_POINTER (seq));
-
-  return task;
-}
-
-static void
-enum_params_done (WpCore * core, GAsyncResult * res, gpointer data)
-{
-  int seq = GPOINTER_TO_INT (g_task_get_source_tag (G_TASK (data)));
-  WpProxy *proxy = g_task_get_source_object (G_TASK (data));
-  g_autoptr (GTask) task = NULL;
-  g_autoptr (GError) error = NULL;
-
-  /* finish the sync task */
-  wp_core_sync_finish (core, res, &error);
-
-  /* find the enum params task in the hash table to steal the reference */
-  task = wp_proxy_find_async_task (proxy, seq, TRUE);
-  g_return_if_fail (task != NULL);
-
-  if (error)
-    g_task_return_error (task, g_steal_pointer (&error));
-  else {
-    GPtrArray *params = g_task_get_task_data (task);
-    g_task_return_pointer (task, g_ptr_array_ref (params),
-        (GDestroyNotify) g_ptr_array_unref);
-  }
-}
-
-/**
- * wp_proxy_enum_params:
- * @self: the proxy
- * @id: (nullable): the parameter id to enumerate or %NULL for all parameters
- * @filter: (nullable): a param filter or %NULL
- * @cancellable: (nullable): a cancellable for the async operation
- * @callback: (scope async): a callback to call with the result
- * @user_data: (closure): data to pass to @callback
- *
- * Enumerate object parameters. This will asynchronously return the result,
- * or an error, by calling the given @callback. The result is going to
- * be a #WpIterator containing #WpSpaPod objects, which can be retrieved
- * with wp_proxy_enum_params_finish().
- */
-void
-wp_proxy_enum_params (WpProxy * self, const gchar * id, WpSpaPod *filter,
-    GCancellable * cancellable, GAsyncReadyCallback callback,
-    gpointer user_data)
-{
-  g_autoptr (GTask) task = NULL;
-  guint32 id_num = 0;
-  int seq;
-  GPtrArray *params;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  /* create task for enum_params */
-  task = g_task_new (self, cancellable, callback, user_data);
-  params = g_ptr_array_new_with_free_func ((GDestroyNotify) wp_spa_pod_unref);
-  g_task_set_task_data (task, params, (GDestroyNotify) g_ptr_array_unref);
-
-  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num,
-          NULL, NULL)) {
-    wp_critical_object (self, "invalid param id: %s", id);
-    return;
-  }
-
-  /* call enum_params */
-  seq = (WP_PROXY_GET_CLASS (self)->enum_params) ?
-      WP_PROXY_GET_CLASS (self)->enum_params (self, id_num, 0, -1, filter) :
-      -ENOTSUP;
-  if (G_UNLIKELY (seq < 0)) {
-    wp_message_object (self, "enum_params failed: %s", spa_strerror (seq));
-    g_task_return_new_error (task, WP_DOMAIN_LIBRARY,
-        WP_LIBRARY_ERROR_OPERATION_FAILED, "enum_params failed: %s",
-        spa_strerror (seq));
-    return;
-  }
-  g_task_set_source_tag (task, GINT_TO_POINTER (seq));
-  wp_proxy_register_async_task (self, seq, g_object_ref (task));
-
-  /* call sync */
-  g_autoptr (WpCore) core = wp_proxy_get_core (self);
-  wp_core_sync (core, cancellable, (GAsyncReadyCallback) enum_params_done,
-      task);
-}
-
-/**
- * wp_proxy_enum_params_finish:
- * @self: the proxy
- * @res: the async result
- * @error: (out) (optional): the reported error of the operation, if any
- *
- * Returns: (transfer full) (nullable): an iterator to iterate over the
- *   collected params, or %NULL if the operation resulted in error;
- *   the items in the iterator are #WpSpaPod
- */
-WpIterator *
-wp_proxy_enum_params_finish (WpProxy * self, GAsyncResult * res,
-    GError ** error)
-{
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-  g_return_val_if_fail (g_task_is_valid (res, self), NULL);
-
-  GPtrArray *array = g_task_propagate_pointer (G_TASK (res), error);
-  if (!array)
-    return NULL;
-
-  return wp_iterator_new_ptr_array (array, WP_TYPE_SPA_POD);
-}
-
 /**
- * wp_proxy_set_param:
- * @self: the proxy
- * @id: the parameter id to set
- * @param: the parameter to set
+ * wp_proxy_get_pw_proxy:
  *
- * Sets a parameter on the object.
+ * Returns: a pointer to the underlying `pw_proxy` object
  */
-void
-wp_proxy_set_param (WpProxy * self, const gchar * id, WpSpaPod *param)
-{
-  guint32 id_num = 0;
-  gint ret;
-
-  g_return_if_fail (WP_IS_PROXY (self));
-
-  if (!wp_spa_type_get_by_nick (WP_SPA_TYPE_TABLE_PARAM, id, &id_num,
-          NULL, NULL)) {
-    wp_critical_object (self, "invalid param id: %s", id);
-    return;
-  }
-
-  ret = (WP_PROXY_GET_CLASS (self)->set_param) ?
-      WP_PROXY_GET_CLASS (self)->set_param (self, id_num, 0, param) :
-      -ENOTSUP;
-  if (G_UNLIKELY (ret < 0)) {
-    wp_message_object (self, "set_param failed: %s", spa_strerror (ret));
-  }
-}
-
-/**
- * wp_proxy_iterate_prop_info:
- * @self: the proxy
- *
- * Requires %WP_PROXY_FEATURE_PROPS
- *
- * Returns: (transfer full) (nullable): an iterator to iterate over the
- *   `SPA_PARAM_PropInfo` params, or %NULL if the object has no props;
- *   the items in the iterator are #WpSpaPod
- */
-WpIterator *
-wp_proxy_iterate_prop_info (WpProxy * self)
-{
-  g_return_val_if_fail (WP_IS_PROXY (self), NULL);
-
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_return_val_if_fail (priv->ft_ready & WP_PROXY_FEATURE_PROPS, NULL);
-
-  return wp_props_iterate_prop_info (priv->props);
-}
-
-/**
- * wp_proxy_get_prop:
- * @self: the proxy
- * @prop_name: the prop name
- *
- * Requires %WP_PROXY_FEATURE_PROPS
- *
- * Returns: (transfer full) (nullable): the spa pod containing the value
- *   of this prop, or %NULL if @prop_name does not exist on this proxy
- */
-WpSpaPod *
-wp_proxy_get_prop (WpProxy * self, const gchar * prop_name)
+struct pw_proxy *
+wp_proxy_get_pw_proxy (WpProxy * self)
 {
   g_return_val_if_fail (WP_IS_PROXY (self), NULL);
 
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_return_val_if_fail (priv->ft_ready & WP_PROXY_FEATURE_PROPS, NULL);
-
-  return wp_props_get (priv->props, prop_name);
+  return priv->pw_proxy;
 }
 
 /**
- * wp_proxy_set_prop:
- * @self: the proxy
- * @prop_name: the prop name
- * @value: (transfer full): the new value for this prop, as a spa pod
+ * wp_proxy_set_pw_proxy:
  *
- * Sets a single property in the `SPA_PARAM_Props` param of this object.
+ * Private method to be used by subclasses to set the `pw_proxy` pointer
+ * when it is available. This can be called only if there is no `pw_proxy`
+ * already set. Takes ownership of @proxy.
  */
 void
-wp_proxy_set_prop (WpProxy * self, const gchar * prop_name, WpSpaPod * value)
+wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy)
 {
   g_return_if_fail (WP_IS_PROXY (self));
-  g_return_if_fail (value != NULL);
 
   WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_return_if_fail (priv->ft_ready & WP_PROXY_FEATURE_PROPS);
-
-  wp_props_set (priv->props, prop_name, value);
-}
 
-void
-wp_proxy_handle_event_param (void * proxy, int seq, uint32_t id,
-    uint32_t index, uint32_t next, const struct spa_pod *param)
-{
-  WpProxy *self = WP_PROXY (proxy);
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  g_autoptr (WpSpaPod) w_param = wp_spa_pod_new_wrap_const (param);
-  GTask *task;
-
-  /* if this param event was emited because of enum_params(),
-   * copy the param in the result array of that API */
-  task = wp_proxy_find_async_task (self, seq, FALSE);
-  if (task) {
-    GPtrArray *array = g_task_get_task_data (task);
-    g_ptr_array_add (array, wp_spa_pod_copy (w_param));
-  }
-  /* else consider this to be a prop update, either triggered from augment()
-   * or because we are subscribed to props */
-  else if (priv->props) {
-    switch (id) {
-      case SPA_PARAM_PropInfo:
-        wp_trace_boxed (WP_TYPE_SPA_POD, w_param,
-            "storing PropInfo on " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (self));
-        wp_props_register_from_info (priv->props, g_steal_pointer (&w_param));
-        break;
-      case SPA_PARAM_Props:
-        wp_trace_boxed (WP_TYPE_SPA_POD, w_param,
-            "storing Props on " WP_OBJECT_FORMAT, WP_OBJECT_ARGS (self));
-        wp_props_store (priv->props, NULL, g_steal_pointer (&w_param));
-        break;
-      default:
-        break;
-    }
-  }
-}
-
-WpProps *
-wp_proxy_get_props (WpProxy * self)
-{
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
-  return priv->props;
-}
-
-static void
-propagate_prop_changed (WpProps * props, const gchar * name, WpProxy * self)
-{
-  /* only emit if FEATURE_PROPS is enabled, because users might call
-     wp_proxy_get_prop() in the handler and it will assert */
-  if (wp_proxy_get_features (self) & WP_PROXY_FEATURE_PROPS)
-    g_signal_emit (self, wp_proxy_signals[SIGNAL_PROP_CHANGED], 0, name);
-}
+  if (!proxy)
+    return;
 
-void
-wp_proxy_set_props (WpProxy * self, WpProps * props)
-{
-  WpProxyPrivate *priv = wp_proxy_get_instance_private (self);
+  g_return_if_fail (priv->pw_proxy == NULL);
+  priv->pw_proxy = proxy;
 
-  g_return_if_fail (priv->props == NULL);
-  priv->props = props;
+  pw_proxy_add_listener (priv->pw_proxy, &priv->listener, &proxy_events,
+      self);
 
-  g_signal_connect_object (props, "prop-changed",
-      G_CALLBACK (propagate_prop_changed), self, 0);
+  /* inform subclasses and listeners */
+  g_signal_emit (self, signals[SIGNAL_PW_PROXY_CREATED], 0, priv->pw_proxy);
 }
diff --git a/lib/wp/proxy.h b/lib/wp/proxy.h
index 2b5d51d479684f3734564f5c3ea607ed0ffa2f61..e9ed203b7718013851680c65c0a5b9b9cb5311f2 100644
--- a/lib/wp/proxy.h
+++ b/lib/wp/proxy.h
@@ -1,7 +1,6 @@
 /* WirePlumber
  *
- * Copyright © 2019 Collabora Ltd.
- *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ * Copyright © 2020 Collabora Ltd.
  *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
  *
  * SPDX-License-Identifier: MIT
@@ -10,50 +9,54 @@
 #ifndef __WIREPLUMBER_PROXY_H__
 #define __WIREPLUMBER_PROXY_H__
 
-#include <gio/gio.h>
-
-#include "spa-pod.h"
-#include "properties.h"
+#include "object.h"
 
 G_BEGIN_DECLS
 
 struct pw_proxy;
-struct spa_param_info;
-typedef struct _WpCore WpCore;
 
 /**
  * WpProxyFeatures:
  *
- * Flags that specify functionality that is available on this class.
- * Use wp_proxy_augment() to enable more features and wp_proxy_get_features()
- * to find out which features are already enabled.
- *
- * Subclasses may also specify additional features that can be ORed with these
- * ones and they can also be enabled with wp_proxy_augment().
+ * Flags to be used as #WpObjectFeatures for #WpProxy subclasses.
  */
 typedef enum { /*< flags >*/
   /* standard features */
-  WP_PROXY_FEATURE_PW_PROXY     = (1 << 0),
-  WP_PROXY_FEATURE_INFO         = (1 << 1),
-  WP_PROXY_FEATURE_BOUND        = (1 << 2),
+  WP_PROXY_FEATURE_BOUND                       = (1 << 0),
 
-  /* param caching features */
-  WP_PROXY_FEATURE_PROPS        = (1 << 3),
+  /* WpPipewireObjectInterface */
+  WP_PIPEWIRE_OBJECT_FEATURE_INFO              = (1 << 4),
+  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS       = (1 << 5),
+  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT      = (1 << 6),
+  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE     = (1 << 7),
+  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG = (1 << 8),
+  WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE       = (1 << 9),
 
-  WP_PROXY_FEATURE_LAST         = (1 << 16), /*< skip >*/
+  WP_PROXY_FEATURE_CUSTOM_START                = (1 << 16), /*< skip >*/
 } WpProxyFeatures;
 
 /**
- * WP_PROXY_FEATURES_STANDARD:
+ * WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL:
  *
- * A constant set of features that contains the standard features that are
- * available in the #WpProxy class. The standard features are usually all
- * enabled at once, even if not requested explicitly. It is a good practice,
- * though, to enable only the features that you actually need. This leaves
- * room for optimizations in the #WpProxy class.
+ * The minimal feature set for proxies implementing #WpPipewireObject.
+ * This is a subset of #WP_PIPEWIRE_OBJECT_FEATURES_ALL
  */
-#define WP_PROXY_FEATURES_STANDARD \
-    (WP_PROXY_FEATURE_PW_PROXY | WP_PROXY_FEATURE_INFO | WP_PROXY_FEATURE_BOUND)
+static const WpObjectFeatures WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL =
+    (WP_PROXY_FEATURE_BOUND | WP_PIPEWIRE_OBJECT_FEATURE_INFO);
+
+/**
+ * WP_PIPEWIRE_OBJECT_FEATURES_ALL:
+ *
+ * The complete common feature set for proxies implementing #WpPipewireObject.
+ * This is a subset of #WP_OBJECT_FEATURES_ALL
+ */
+static const WpObjectFeatures WP_PIPEWIRE_OBJECT_FEATURES_ALL =
+    (WP_PIPEWIRE_OBJECT_FEATURES_MINIMAL |
+     WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROPS |
+     WP_PIPEWIRE_OBJECT_FEATURE_PARAM_FORMAT |
+     WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PROFILE |
+     WP_PIPEWIRE_OBJECT_FEATURE_PARAM_PORT_CONFIG |
+     WP_PIPEWIRE_OBJECT_FEATURE_PARAM_ROUTE);
 
 /**
  * WP_TYPE_PROXY:
@@ -62,7 +65,7 @@ typedef enum { /*< flags >*/
  */
 #define WP_TYPE_PROXY (wp_proxy_get_type ())
 WP_API
-G_DECLARE_DERIVABLE_TYPE (WpProxy, wp_proxy, WP, PROXY, GObject)
+G_DECLARE_DERIVABLE_TYPE (WpProxy, wp_proxy, WP, PROXY, WpObject)
 
 /**
  * WpProxyClass:
@@ -73,110 +76,28 @@ G_DECLARE_DERIVABLE_TYPE (WpProxy, wp_proxy, WP, PROXY, GObject)
  */
 struct _WpProxyClass
 {
-  GObjectClass parent_class;
+  WpObjectClass parent_class;
 
   const gchar * pw_iface_type;
   guint32 pw_iface_version;
 
-  void (*augment) (WpProxy *self, WpProxyFeatures features);
-
-  gconstpointer (*get_info) (WpProxy * self);
-  WpProperties * (*get_properties) (WpProxy * self);
-  struct spa_param_info * (*get_param_info) (WpProxy * self, guint * n_params);
-
-  gint (*enum_params) (WpProxy * self, guint32 id, guint32 start, guint32 num,
-      WpSpaPod * filter);
-  gint (*subscribe_params) (WpProxy * self, guint32 *ids, guint32 n_ids);
-  gint (*set_param) (WpProxy * self, guint32 id, guint32 flags,
-      WpSpaPod * param);
-
   /* signals */
 
   void (*pw_proxy_created) (WpProxy * self, struct pw_proxy * proxy);
   void (*pw_proxy_destroyed) (WpProxy * self);
   void (*bound) (WpProxy * self, guint32 id);
-  void (*prop_changed) (WpProxy * self, const gchar * prop_name);
 };
 
-WP_API
-void wp_proxy_request_destroy (WpProxy * self);
-
-/* features API */
-
-WP_API
-void wp_proxy_augment (WpProxy *self,
-    WpProxyFeatures wanted_features, GCancellable * cancellable,
-    GAsyncReadyCallback callback, gpointer user_data);
-
-WP_API
-gboolean wp_proxy_augment_finish (WpProxy * self, GAsyncResult * res,
-    GError ** error);
-
-WP_API
-WpProxyFeatures wp_proxy_get_features (WpProxy * self);
-
-/* the owner core */
-
-WP_API
-WpCore * wp_proxy_get_core (WpProxy * self);
-
-/* global object API */
-
-WP_API
-guint32 wp_proxy_get_global_permissions (WpProxy * self);
-
-WP_API
-WpProperties * wp_proxy_get_global_properties (WpProxy * self);
-
-/* native pw_proxy object getter (requires FEATURE_PW_PROXY) */
-
-WP_API
-struct pw_proxy * wp_proxy_get_pw_proxy (WpProxy * self);
-
-/* native info structure + wrappers (requires FEATURE_INFO) */
-
-WP_API
-gconstpointer wp_proxy_get_info (WpProxy * self);
-
-WP_API
-WpProperties * wp_proxy_get_properties (WpProxy * self);
-
-WP_API
-const gchar * wp_proxy_get_property (WpProxy * self, const gchar * key);
-
-WP_API
-GVariant * wp_proxy_get_param_info (WpProxy * self);
-
-/* the bound id (aka global id, requires FEATURE_BOUND) */
-
 WP_API
 guint32 wp_proxy_get_bound_id (WpProxy * self);
 
-/* params API */
-
-WP_API
-void wp_proxy_enum_params (WpProxy * self, const gchar * id, WpSpaPod *filter,
-    GCancellable * cancellable, GAsyncReadyCallback callback,
-    gpointer user_data);
-
-WP_API
-WpIterator * wp_proxy_enum_params_finish (WpProxy * self, GAsyncResult * res,
-    GError ** error);
-
-WP_API
-void wp_proxy_set_param (WpProxy * self, const gchar * id, WpSpaPod * param);
-
-/* PARAM_PropInfo - PARAM_Props */
-
 WP_API
-WpIterator * wp_proxy_iterate_prop_info (WpProxy * self);
+struct pw_proxy * wp_proxy_get_pw_proxy (WpProxy * self);
 
-WP_API
-WpSpaPod * wp_proxy_get_prop (WpProxy * self, const gchar * prop_name);
+/* for subclasses only */
 
 WP_API
-void wp_proxy_set_prop (WpProxy * self, const gchar * prop_name,
-    WpSpaPod * value);
+void wp_proxy_set_pw_proxy (WpProxy * self, struct pw_proxy * proxy);
 
 G_END_DECLS
 
diff --git a/lib/wp/session-item.c b/lib/wp/session-item.c
index c8636b449e42053fa1436258d4e803e760c774f0..ca2154420ea3aa2bd36c4e0109283b98dafd1031 100644
--- a/lib/wp/session-item.c
+++ b/lib/wp/session-item.c
@@ -222,7 +222,7 @@ wp_session_item_default_export_get_next_step (WpSessionItem * self,
     g_return_val_if_fail (WP_IS_SI_ENDPOINT (self), WP_TRANSITION_STEP_ERROR);
     g_return_val_if_fail (priv->impl_streams, WP_TRANSITION_STEP_ERROR);
 
-    /* go to next step only when all impl proxies are augmented */
+    /* go to next step only when all impl proxies are activated */
     if (g_hash_table_size (priv->impl_streams) ==
         wp_si_endpoint_get_n_streams (WP_SI_ENDPOINT (self)))
       return EXPORT_STEP_ENDPOINT_FT_STREAMS;
@@ -245,14 +245,14 @@ wp_session_item_default_export_get_next_step (WpSessionItem * self,
 }
 
 static void
-on_export_proxy_augmented (WpProxy * proxy, GAsyncResult * res, gpointer data)
+on_export_proxy_activated (WpObject * proxy, GAsyncResult * res, gpointer data)
 {
   WpTransition *transition = WP_TRANSITION (data);
   WpSessionItem *self = wp_transition_get_source_object (transition);
   WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
   g_autoptr (GError) error = NULL;
 
-  if (!wp_proxy_augment_finish (proxy, res, &error)) {
+  if (!wp_object_activate_finish (proxy, res, &error)) {
     wp_transition_return_error (transition, g_steal_pointer (&error));
     return;
   }
@@ -266,7 +266,7 @@ on_export_proxy_augmented (WpProxy * proxy, GAsyncResult * res, gpointer data)
     g_hash_table_insert (priv->impl_streams, si_stream, g_object_ref (proxy));
   }
 
-  wp_debug_object (self, "export proxy " WP_OBJECT_FORMAT " augmented",
+  wp_debug_object (self, "export proxy " WP_OBJECT_FORMAT " activated",
       WP_OBJECT_ARGS (proxy));
 
   wp_transition_advance (transition);
@@ -293,10 +293,10 @@ on_export_proxy_destroyed_deferred (WpSessionItem * self)
 }
 
 static void
-on_export_proxy_destroyed (WpProxy * proxy, gpointer data)
+on_export_proxy_destroyed (WpObject * proxy, gpointer data)
 {
   WpSessionItem *self = WP_SESSION_ITEM (data);
-  g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+  g_autoptr (WpCore) core = wp_object_get_core (proxy);
 
   if (core)
     wp_core_idle_add_closure (core, NULL, g_cclosure_new_object (
@@ -309,15 +309,15 @@ wp_session_item_default_export_execute_step (WpSessionItem * self,
 {
   WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
   g_autoptr (WpSession) session = g_weak_ref_get (&priv->session);
-  g_autoptr (WpCore) core = wp_proxy_get_core (WP_PROXY (session));
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (session));
 
   switch (step) {
   case EXPORT_STEP_ENDPOINT:
     priv->impl_endpoint = wp_impl_endpoint_new (core, WP_SI_ENDPOINT (self));
 
-    wp_proxy_augment (WP_PROXY (priv->impl_endpoint),
-        WP_PROXY_FEATURES_STANDARD, NULL,
-        (GAsyncReadyCallback) on_export_proxy_augmented,
+    wp_object_activate (WP_OBJECT (priv->impl_endpoint),
+        WP_PIPEWIRE_OBJECT_FEATURES_ALL, NULL,
+        (GAsyncReadyCallback) on_export_proxy_activated,
         transition);
     break;
 
@@ -333,33 +333,33 @@ wp_session_item_default_export_execute_step (WpSessionItem * self,
       WpImplEndpointStream *impl_stream =
           wp_impl_endpoint_stream_new (core, stream);
 
-      wp_proxy_augment (WP_PROXY (impl_stream),
-          WP_PROXY_FEATURES_STANDARD, NULL,
-          (GAsyncReadyCallback) on_export_proxy_augmented,
+      wp_object_activate (WP_OBJECT (impl_stream),
+          WP_OBJECT_FEATURES_ALL, NULL,
+          (GAsyncReadyCallback) on_export_proxy_activated,
           transition);
 
       /* the augment task holds a ref; object will be added to
-         priv->impl_streams when augmented */
+         priv->impl_streams when activated */
       g_object_unref (impl_stream);
     }
     break;
   }
   case EXPORT_STEP_ENDPOINT_FT_STREAMS:
     /* add feature streams only after the streams are exported, otherwise
-       the endpoint will never be augmented in the first place (because it
+       the endpoint will never be activated in the first place (because it
        internally waits for the streams to be ready) */
-    wp_proxy_augment (WP_PROXY (priv->impl_endpoint),
+    wp_object_activate (WP_OBJECT (priv->impl_endpoint),
         WP_ENDPOINT_FEATURE_STREAMS, NULL,
-        (GAsyncReadyCallback) on_export_proxy_augmented,
+        (GAsyncReadyCallback) on_export_proxy_activated,
         transition);
     break;
 
   case EXPORT_STEP_LINK:
     priv->impl_link = wp_impl_endpoint_link_new (core, WP_SI_LINK (self));
 
-    wp_proxy_augment (WP_PROXY (priv->impl_link),
-        WP_PROXY_FEATURES_STANDARD, NULL,
-        (GAsyncReadyCallback) on_export_proxy_augmented,
+    wp_object_activate (WP_OBJECT (priv->impl_link),
+        WP_OBJECT_FEATURES_ALL, NULL,
+        (GAsyncReadyCallback) on_export_proxy_activated,
         transition);
     break;
 
diff --git a/lib/wp/session.c b/lib/wp/session.c
index a2d0f74cd1fadb4d687c740d6a5477d05bce00d4..a7dda9aabadb0df01ef2147d96cc3a19fd2d7527 100644
--- a/lib/wp/session.c
+++ b/lib/wp/session.c
@@ -7,37 +7,21 @@
  */
 
 /**
- * SECTION: WpSession
- *
- * The #WpSession class allows accessing the properties and methods of a
- * PipeWire session object (`struct pw_session` from the session-manager
- * extension).
- *
- * A #WpSession is constructed internally when a new session appears on the
- * PipeWire registry and it is made available through the #WpObjectManager API.
- *
- * A #WpImplSession allows implementing a session and exporting it to PipeWire,
- * which is done by augmenting the #WpImplSession with %WP_PROXY_FEATURE_BOUND.
+ * SECTION: session
+ * @title: PipeWire Session
  */
 
 #define G_LOG_DOMAIN "wp-session"
 
 #include "session.h"
-#include "spa-type.h"
-#include "spa-pod.h"
-#include "debug.h"
-#include "private.h"
 #include "error.h"
 #include "wpenums.h"
+#include "private.h"
+#include "private/pipewire-object-mixin.h"
 
-#include <pipewire/pipewire.h>
 #include <pipewire/extensions/session-manager.h>
 #include <pipewire/extensions/session-manager/introspect-funcs.h>
 
-#include <spa/pod/builder.h>
-#include <spa/pod/parser.h>
-#include <spa/pod/filter.h>
-
 enum {
   SIGNAL_DEFAULT_ENDPOINT_CHANGED,
   SIGNAL_ENDPOINTS_CHANGED,
@@ -47,8 +31,6 @@ enum {
 
 static guint32 signals[N_SIGNALS] = {0};
 
-/* WpSession */
-
 typedef struct _WpSessionPrivate WpSessionPrivate;
 struct _WpSessionPrivate
 {
@@ -58,36 +40,34 @@ struct _WpSessionPrivate
   struct spa_hook listener;
   WpObjectManager *endpoints_om;
   WpObjectManager *links_om;
-  gboolean ft_endpoints_requested;
-  gboolean ft_links_requested;
 };
 
-G_DEFINE_TYPE_WITH_PRIVATE (WpSession, wp_session, WP_TYPE_PROXY)
+static void wp_session_pipewire_object_interface_init (WpPipewireObjectInterface * iface);
 
-static void
-wp_session_init (WpSession * self)
-{
-}
+/**
+ * WpSession:
+ *
+ * The #WpSession class allows accessing the properties and methods of a
+ * PipeWire session object (`struct pw_session` from the session-manager
+ * extension).
+ *
+ * A #WpSession is constructed internally when a new session appears on the
+ * PipeWire registry and it is made available through the #WpObjectManager API.
+ */
+G_DEFINE_TYPE_WITH_CODE (WpSession, wp_session, WP_TYPE_GLOBAL_PROXY,
+    G_ADD_PRIVATE (WpSession)
+    G_IMPLEMENT_INTERFACE (WP_TYPE_PIPEWIRE_OBJECT, wp_session_pipewire_object_interface_init));
 
 static void
-wp_session_finalize (GObject * object)
+wp_session_init (WpSession * self)
 {
-  WpSession *self = WP_SESSION (object);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  g_clear_object (&priv->endpoints_om);
-  g_clear_object (&priv->links_om);
-  g_clear_pointer (&priv->info, pw_session_info_free);
-  g_clear_pointer (&priv->properties, wp_properties_unref);
-
-  G_OBJECT_CLASS (wp_session_parent_class)->finalize (object);
 }
 
 static void
 wp_session_on_endpoints_om_installed (WpObjectManager *endpoints_om,
     WpSession * self)
 {
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_SESSION_FEATURE_ENDPOINTS);
+  wp_object_update_features (WP_OBJECT (self), WP_SESSION_FEATURE_ENDPOINTS, 0);
 }
 
 static void
@@ -100,7 +80,7 @@ wp_session_emit_endpoints_changed (WpObjectManager *endpoints_om,
 static void
 wp_session_on_links_om_installed (WpObjectManager *links_om, WpSession * self)
 {
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_SESSION_FEATURE_LINKS);
+  wp_object_update_features (WP_OBJECT (self), WP_SESSION_FEATURE_LINKS, 0);
 }
 
 static void
@@ -110,20 +90,14 @@ wp_session_emit_links_changed (WpObjectManager *links_om, WpSession * self)
 }
 
 static void
-wp_session_ensure_features_endpoints_links (WpSession * self, guint32 bound_id)
+wp_session_enable_features_endpoints_links (WpSession * self,
+    WpObjectFeatures missing)
 {
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
-  WpProxyFeatures ft = wp_proxy_get_features (WP_PROXY (self));
-  g_autoptr (WpCore) core = NULL;
-
-  if (!(ft & WP_PROXY_FEATURE_BOUND))
-    return;
+  g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self));
+  guint32 bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
 
-  core = wp_proxy_get_core (WP_PROXY (self));
-  if (!bound_id)
-    bound_id = wp_proxy_get_bound_id (WP_PROXY (self));
-
-  if (priv->ft_endpoints_requested && !priv->endpoints_om) {
+  if (missing & WP_SESSION_FEATURE_ENDPOINTS) {
     wp_debug_object (self, "enabling WP_SESSION_FEATURE_ENDPOINTS, bound_id:%u",
         bound_id);
 
@@ -139,8 +113,8 @@ wp_session_ensure_features_endpoints_links (WpSession * self, guint32 bound_id)
         WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_SESSION_ID, "=u", bound_id,
         NULL);
 
-    wp_object_manager_request_proxy_features (priv->endpoints_om,
-        WP_TYPE_ENDPOINT, WP_ENDPOINT_FEATURES_STANDARD);
+    wp_object_manager_request_object_features (priv->endpoints_om,
+        WP_TYPE_ENDPOINT, WP_OBJECT_FEATURES_ALL);
 
     g_signal_connect_object (priv->endpoints_om, "installed",
         G_CALLBACK (wp_session_on_endpoints_om_installed), self, 0);
@@ -150,7 +124,7 @@ wp_session_ensure_features_endpoints_links (WpSession * self, guint32 bound_id)
     wp_core_install_object_manager (core, priv->endpoints_om);
   }
 
-  if (priv->ft_links_requested && !priv->links_om) {
+  if (missing & WP_SESSION_FEATURE_LINKS) {
     wp_debug_object (self, "enabling WP_SESSION_FEATURE_LINKS, bound_id:%u",
         bound_id);
 
@@ -166,8 +140,8 @@ wp_session_ensure_features_endpoints_links (WpSession * self, guint32 bound_id)
         WP_CONSTRAINT_TYPE_PW_PROPERTY, PW_KEY_SESSION_ID, "=u", bound_id,
         NULL);
 
-    wp_object_manager_request_proxy_features (priv->links_om,
-        WP_TYPE_ENDPOINT_LINK, WP_PROXY_FEATURES_STANDARD);
+    wp_object_manager_request_object_features (priv->links_om,
+        WP_TYPE_ENDPOINT_LINK, WP_OBJECT_FEATURES_ALL);
 
     g_signal_connect_object (priv->links_om, "installed",
         G_CALLBACK (wp_session_on_links_om_installed), self, 0);
@@ -178,57 +152,81 @@ wp_session_ensure_features_endpoints_links (WpSession * self, guint32 bound_id)
   }
 }
 
-static gconstpointer
-wp_session_get_info (WpProxy * proxy)
+static WpObjectFeatures
+wp_session_get_supported_features (WpObject * object)
 {
-  WpSession *self = WP_SESSION (proxy);
+  WpSession *self = WP_SESSION (object);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
 
-  return priv->info;
+  return
+      WP_PROXY_FEATURE_BOUND |
+      WP_SESSION_FEATURE_ENDPOINTS |
+      WP_SESSION_FEATURE_LINKS |
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      wp_pipewire_object_mixin_param_info_to_features (
+          priv->info ? priv->info->params : NULL,
+          priv->info ? priv->info->n_params : 0);
 }
 
-static WpProperties *
-wp_session_get_properties (WpProxy * proxy)
-{
-  WpSession *self = WP_SESSION (proxy);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
-
-  return wp_properties_ref (priv->properties);
-}
+enum {
+  STEP_CHILDREN = WP_PIPEWIRE_OBJECT_MIXIN_STEP_CUSTOM_START,
+};
 
-static struct spa_param_info *
-wp_session_get_param_info (WpProxy * proxy, guint * n_params)
+static guint
+wp_session_activate_get_next_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpSession *self = WP_SESSION (proxy);
-  WpSessionPrivate *priv = wp_session_get_instance_private (self);
+  step = wp_pipewire_object_mixin_activate_get_next_step (object, transition,
+      step, missing);
 
-  *n_params = priv->info->n_params;
-  return priv->info->params;
-}
+  /* extend the mixin's state machine; when the only remaining feature(s) to
+     enable are FEATURE_ENDPOINTS or FEATURE_LINKS, advance to STEP_CHILDREN */
+  if (step == WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO &&
+      (missing & ~(WP_SESSION_FEATURE_ENDPOINTS | WP_SESSION_FEATURE_LINKS)) == 0)
+    return STEP_CHILDREN;
 
-static gint
-wp_session_enum_params (WpProxy * self, guint32 id, guint32 start,
-    guint32 num, WpSpaPod * filter)
-{
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
-  return pw_session_enum_params (priv->iface, 0, id,
-      start, num, filter ? wp_spa_pod_get_spa_pod (filter) : NULL);
+  return step;
 }
 
-static gint
-wp_session_subscribe_params (WpProxy * self, guint32 *ids, guint32 n_ids)
-{
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
-  return pw_session_subscribe_params (priv->iface, ids, n_ids);
+static void
+wp_session_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
+{
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_CACHE_INFO:
+    wp_pipewire_object_mixin_cache_info (object, transition);
+    break;
+  case STEP_CHILDREN:
+    wp_session_enable_features_endpoints_links (WP_SESSION (object),
+        missing);
+    break;
+  default:
+    WP_OBJECT_CLASS (wp_session_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
+  }
 }
 
-static gint
-wp_session_set_param (WpProxy * self, guint32 id, guint32 flags,
-    WpSpaPod *param)
+static void
+wp_session_deactivate (WpObject * object, WpObjectFeatures features)
 {
-  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
-  return pw_session_set_param (priv->iface, id, flags,
-      wp_spa_pod_get_spa_pod (param));
+  WpSession *self = WP_SESSION (object);
+  WpSessionPrivate *priv = wp_session_get_instance_private (self);
+
+  wp_pipewire_object_mixin_deactivate (object, features);
+
+  if (features & WP_SESSION_FEATURE_ENDPOINTS) {
+    g_clear_object (&priv->endpoints_om);
+    wp_object_update_features (object, 0, WP_SESSION_FEATURE_ENDPOINTS);
+  }
+  if (features & WP_SESSION_FEATURE_LINKS) {
+    g_clear_object (&priv->links_om);
+    wp_object_update_features (object, 0, WP_SESSION_FEATURE_LINKS);
+  }
+
+  WP_OBJECT_CLASS (wp_session_parent_class)->deactivate (object, features);
 }
 
 static void
@@ -244,20 +242,17 @@ session_event_info (void *data, const struct pw_session_info *info)
     priv->properties = wp_properties_new_wrap_dict (priv->info->props);
   }
 
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-  g_object_notify (G_OBJECT (self), "info");
-
-  if (info->change_mask & PW_SESSION_CHANGE_MASK_PROPS)
-    g_object_notify (G_OBJECT (self), "properties");
+  wp_object_update_features (WP_OBJECT (self),
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
 
-  if (info->change_mask & PW_SESSION_CHANGE_MASK_PARAMS)
-    g_object_notify (G_OBJECT (self), "param-info");
+  wp_pipewire_object_mixin_handle_event_info (self, info,
+      PW_SESSION_CHANGE_MASK_PROPS, PW_SESSION_CHANGE_MASK_PARAMS);
 }
 
 static const struct pw_session_events session_events = {
   PW_VERSION_SESSION_EVENTS,
   .info = session_event_info,
-  .param = wp_proxy_handle_event_param,
+  .param = wp_pipewire_object_mixin_handle_event_param,
 };
 
 static void
@@ -271,67 +266,44 @@ wp_session_pw_proxy_created (WpProxy * proxy, struct pw_proxy * pw_proxy)
 }
 
 static void
-wp_session_bound (WpProxy * proxy, guint32 id)
-{
-  WpSession *self = WP_SESSION (proxy);
-  wp_session_ensure_features_endpoints_links (self, id);
-}
-
-static void
-wp_session_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_session_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpSession *self = WP_SESSION (proxy);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
 
-  /* call the parent impl first to ensure we have a pw proxy if necessary */
-  WP_PROXY_CLASS (wp_session_parent_class)->augment (proxy, features);
-
-  if (features & (WP_SESSION_FEATURE_ENDPOINTS | WP_SESSION_FEATURE_LINKS)) {
-    priv->ft_endpoints_requested = (features & WP_SESSION_FEATURE_ENDPOINTS);
-    priv->ft_links_requested = (features & WP_SESSION_FEATURE_LINKS);
-    wp_session_ensure_features_endpoints_links (self, 0);
-  }
-}
-
-static void
-wp_session_prop_changed (WpProxy * proxy, const gchar * prop)
-{
-  WpSession *self = WP_SESSION (proxy);
+  g_clear_pointer (&priv->properties, wp_properties_unref);
+  g_clear_pointer (&priv->info, pw_session_info_free);
+  g_clear_object (&priv->endpoints_om);
+  g_clear_object (&priv->links_om);
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      WP_SESSION_FEATURE_ENDPOINTS |
+      WP_SESSION_FEATURE_LINKS);
 
-  if (g_strcmp0 (prop, "Wp:defaultSink") == 0) {
-    g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0,
-        WP_DIRECTION_INPUT,
-        wp_session_get_default_endpoint (self, WP_DIRECTION_INPUT));
-  }
-  else if (g_strcmp0 (prop, "Wp:defaultSource") == 0) {
-    g_signal_emit (self, signals[SIGNAL_DEFAULT_ENDPOINT_CHANGED], 0,
-        WP_DIRECTION_OUTPUT,
-        wp_session_get_default_endpoint (self, WP_DIRECTION_OUTPUT));
-  }
+  wp_pipewire_object_mixin_deactivate (WP_OBJECT (proxy),
+      WP_OBJECT_FEATURES_ALL);
 }
 
 static void
 wp_session_class_init (WpSessionClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
-  object_class->finalize = wp_session_finalize;
+  object_class->get_property = wp_pipewire_object_mixin_get_property;
+
+  wpobject_class->get_supported_features = wp_session_get_supported_features;
+  wpobject_class->activate_get_next_step = wp_session_activate_get_next_step;
+  wpobject_class->activate_execute_step = wp_session_activate_execute_step;
+  wpobject_class->deactivate = wp_session_deactivate;
 
   proxy_class->pw_iface_type = PW_TYPE_INTERFACE_Session;
   proxy_class->pw_iface_version = PW_VERSION_SESSION;
-
-  proxy_class->augment = wp_session_augment;
-  proxy_class->get_info = wp_session_get_info;
-  proxy_class->get_properties = wp_session_get_properties;
-  proxy_class->get_param_info = wp_session_get_param_info;
-  proxy_class->enum_params = wp_session_enum_params;
-  proxy_class->subscribe_params = wp_session_subscribe_params;
-  proxy_class->set_param = wp_session_set_param;
-
   proxy_class->pw_proxy_created = wp_session_pw_proxy_created;
-  proxy_class->bound = wp_session_bound;
-  proxy_class->prop_changed = wp_session_prop_changed;
+  proxy_class->pw_proxy_destroyed = wp_session_pw_proxy_destroyed;
+
+  wp_pipewire_object_mixin_class_override_properties (object_class);
 
   /**
    * WpSession::default-endpoint-changed:
@@ -371,87 +343,80 @@ wp_session_class_init (WpSessionClass * klass)
       G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0);
 }
 
-/**
- * wp_session_get_name:
- * @self: the session
- *
- * Returns: (transfer none): the (unique) name of the session
- */
-const gchar *
-wp_session_get_name (WpSession * self)
+static gconstpointer
+wp_session_get_native_info (WpPipewireObject * obj)
 {
-  g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
-          WP_PROXY_FEATURE_INFO, NULL);
+  WpSession *self = WP_SESSION (obj);
+  WpSessionPrivate *priv = wp_session_get_instance_private (self);
+
+  return priv->info;
+}
 
+static WpProperties *
+wp_session_get_properties (WpPipewireObject * obj)
+{
+  WpSession *self = WP_SESSION (obj);
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
-  return wp_properties_get (priv->properties, "session.name");
+
+  return wp_properties_ref (priv->properties);
 }
 
-/**
- * wp_session_get_default_endpoint:
- * @self: the session
- * @direction: the endpoint direction
- *
- * Returns: the bound id of the default endpoint of this @direction
- */
-guint32
-wp_session_get_default_endpoint (WpSession * self, WpDirection direction)
+static GVariant *
+wp_session_get_param_info (WpPipewireObject * obj)
 {
-  g_autoptr (WpSpaPod) pod = NULL;
-  const gchar *id_name = NULL;
-  gint32 value;
-
-  g_return_val_if_fail (WP_IS_SESSION (self), SPA_ID_INVALID);
-
-  switch (direction) {
-    case WP_DIRECTION_INPUT:
-      id_name = "Wp:defaultSink";
-      break;
-    case WP_DIRECTION_OUTPUT:
-      id_name = "Wp:defaultSource";
-      break;
-    default:
-      g_return_val_if_reached (SPA_ID_INVALID);
-      break;
-  }
+  WpSession *self = WP_SESSION (obj);
+  WpSessionPrivate *priv = wp_session_get_instance_private (self);
 
-  pod = wp_proxy_get_prop (WP_PROXY (self), id_name);
-  if (pod && wp_spa_pod_get_int (pod, &value))
-    return (guint32) value;
-  return 0;
+  return wp_pipewire_object_mixin_param_info_to_gvariant (priv->info->params,
+      priv->info->n_params);
+}
+
+static void
+wp_session_enum_params (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod *filter, GCancellable * cancellable,
+    GAsyncReadyCallback callback, gpointer user_data)
+{
+  wp_pipewire_object_mixin_enum_params (pw_session, obj, id, filter,
+      cancellable, callback, user_data);
+}
+
+static void
+wp_session_set_param (WpPipewireObject * obj, const gchar * id,
+    WpSpaPod * param)
+{
+  wp_pipewire_object_mixin_set_param (pw_session, obj, id, param);
+}
+
+static void
+wp_session_pipewire_object_interface_init (
+    WpPipewireObjectInterface * iface)
+{
+  iface->get_native_info = wp_session_get_native_info;
+  iface->get_properties = wp_session_get_properties;
+  iface->get_param_info = wp_session_get_param_info;
+  iface->enum_params = wp_session_enum_params;
+  iface->enum_params_finish = wp_pipewire_object_mixin_enum_params_finish;
+  iface->enum_cached_params = wp_pipewire_object_mixin_enum_cached_params;
+  iface->set_param = wp_session_set_param;
 }
 
 /**
- * wp_session_set_default_endpoint:
+ * wp_session_get_name:
  * @self: the session
- * @direction: the endpoint direction
- * @id: the bound id of the endpoint to set as the default for this @direction
  *
- * Sets the default endpoint for this @direction to be the one identified
- * with @id
+ * Requires %WP_PIPEWIRE_OBJECT_FEATURE_INFO
+ *
+ * Returns: (transfer none): the (unique) name of the session
  */
-void
-wp_session_set_default_endpoint (WpSession * self, WpDirection direction,
-    guint32 id)
+const gchar *
+wp_session_get_name (WpSession * self)
 {
-  const gchar *id_name = NULL;
-
-  g_return_if_fail (WP_IS_SESSION (self));
-
-  switch (direction) {
-    case WP_DIRECTION_INPUT:
-      id_name = "Wp:defaultSink";
-      break;
-    case WP_DIRECTION_OUTPUT:
-      id_name = "Wp:defaultSource";
-      break;
-    default:
-      g_return_if_reached ();
-      break;
-  }
+  g_return_val_if_fail (WP_IS_SESSION (self), NULL);
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
+          WP_PIPEWIRE_OBJECT_FEATURE_INFO, NULL);
 
-  wp_proxy_set_prop (WP_PROXY (self), id_name, wp_spa_pod_new_int (id));
+  WpSessionPrivate *priv = wp_session_get_instance_private (self);
+  return wp_properties_get (priv->properties, "session.name");
 }
 
 /**
@@ -466,7 +431,7 @@ guint
 wp_session_get_n_endpoints (WpSession * self)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_ENDPOINTS, 0);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -486,7 +451,7 @@ WpIterator *
 wp_session_iterate_endpoints (WpSession * self)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_ENDPOINTS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -532,7 +497,7 @@ wp_session_iterate_endpoints_filtered_full (WpSession * self,
     WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_ENDPOINTS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -577,7 +542,7 @@ WpEndpoint *
 wp_session_lookup_endpoint_full (WpSession * self, WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_ENDPOINTS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -597,7 +562,7 @@ guint
 wp_session_get_n_links (WpSession * self)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), 0);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_LINKS, 0);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -617,7 +582,7 @@ WpIterator *
 wp_session_iterate_links (WpSession * self)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_LINKS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -663,7 +628,7 @@ wp_session_iterate_links_filtered_full (WpSession * self,
     WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_LINKS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -708,7 +673,7 @@ WpEndpointLink *
 wp_session_lookup_link_full (WpSession * self, WpObjectInterest * interest)
 {
   g_return_val_if_fail (WP_IS_SESSION (self), NULL);
-  g_return_val_if_fail (wp_proxy_get_features (WP_PROXY (self)) &
+  g_return_val_if_fail (wp_object_get_active_features (WP_OBJECT (self)) &
           WP_SESSION_FEATURE_LINKS, NULL);
 
   WpSessionPrivate *priv = wp_session_get_instance_private (self);
@@ -726,9 +691,14 @@ struct _WpImplSession
   struct spa_interface iface;
   struct spa_hook_list hooks;
   struct pw_session_info info;
-  gboolean subscribed;
 };
 
+/**
+ * WpImplSession:
+ *
+ * A #WpImplSession allows implementing a session and exporting it to PipeWire.
+ * To export a #WpImplSession, activate %WP_PROXY_FEATURE_BOUND.
+ */
 G_DEFINE_TYPE (WpImplSession, wp_impl_session, WP_TYPE_SESSION)
 
 #define pw_session_emit(hooks,method,version,...) \
@@ -738,11 +708,6 @@ G_DEFINE_TYPE (WpImplSession, wp_impl_session, WP_TYPE_SESSION)
 #define pw_session_emit_info(hooks,...)  pw_session_emit(hooks, info, 0, ##__VA_ARGS__)
 #define pw_session_emit_param(hooks,...) pw_session_emit(hooks, param, 0, ##__VA_ARGS__)
 
-static struct spa_param_info impl_param_info[] = {
-  SPA_PARAM_INFO (SPA_PARAM_Props, SPA_PARAM_INFO_READWRITE),
-  SPA_PARAM_INFO (SPA_PARAM_PropInfo, SPA_PARAM_INFO_READ)
-};
-
 static int
 impl_add_listener(void *object,
     struct spa_hook *listener,
@@ -767,56 +732,12 @@ impl_enum_params (void *object, int seq,
     uint32_t id, uint32_t start, uint32_t num,
     const struct spa_pod *filter)
 {
-  WpImplSession *self = WP_IMPL_SESSION (object);
-  char buf[1024];
-  struct spa_pod_builder b = SPA_POD_BUILDER_INIT (buf, sizeof (buf));
-  struct spa_pod *result;
-  guint count = 0;
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-
-  switch (id) {
-    case SPA_PARAM_PropInfo: {
-      g_autoptr (WpIterator) params = wp_props_iterate_prop_info (props);
-      g_auto (GValue) item = G_VALUE_INIT;
-      guint i = 0;
-
-      for (; wp_iterator_next (params, &item); g_value_unset (&item), i++) {
-        WpSpaPod *pod = g_value_get_boxed (&item);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_session_emit_param (&self->hooks, seq, id, i, i+1, result);
-          if (++count == num)
-            break;
-        }
-      }
-      break;
-    }
-    case SPA_PARAM_Props: {
-      if (start == 0) {
-        g_autoptr (WpSpaPod) pod = wp_props_get_all (props);
-        const struct spa_pod *param = wp_spa_pod_get_spa_pod (pod);
-        if (spa_pod_filter (&b, &result, param, filter) == 0) {
-          pw_session_emit_param (&self->hooks, seq, id, 0, 1, result);
-        }
-      }
-      break;
-    }
-    default:
-      return -ENOENT;
-  }
-
-  return 0;
+  return -ENOENT;
 }
 
 static int
 impl_subscribe_params (void *object, uint32_t *ids, uint32_t n_ids)
 {
-  WpImplSession *self = WP_IMPL_SESSION (object);
-  for (guint i = 0; i < n_ids; i++) {
-    if (ids[i] == SPA_PARAM_Props)
-      self->subscribed = TRUE;
-    impl_enum_params (self, 1, ids[i], 0, UINT32_MAX, NULL);
-  }
   return 0;
 }
 
@@ -824,14 +745,7 @@ static int
 impl_set_param (void *object, uint32_t id, uint32_t flags,
     const struct spa_pod *param)
 {
-  WpImplSession *self = WP_IMPL_SESSION (object);
-
-  if (id != SPA_PARAM_Props)
-    return -ENOENT;
-
-  WpProps *props = wp_proxy_get_props (WP_PROXY (self));
-  wp_props_set (props, NULL, wp_spa_pod_new_wrap_const (param));
-  return 0;
+  return -ENOENT;
 }
 
 static const struct pw_session_methods impl_session = {
@@ -848,7 +762,6 @@ wp_impl_session_init (WpImplSession * self)
   /* reuse the parent's private to optimize memory usage and to be able
      to re-use some of the parent's methods without reimplementing them */
   WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
-  WpProps *props;
 
   self->iface = SPA_INTERFACE_INIT (
       PW_TYPE_INTERFACE_Session,
@@ -863,21 +776,9 @@ wp_impl_session_init (WpImplSession * self)
   self->info.version = PW_VERSION_SESSION_INFO;
   self->info.props =
       (struct spa_dict *) wp_properties_peek_dict (priv->properties);
-  self->info.params = impl_param_info;
-  self->info.n_params = SPA_N_ELEMENTS (impl_param_info);
+  self->info.params = NULL;
+  self->info.n_params = 0;
   priv->info = &self->info;
-  g_object_notify (G_OBJECT (self), "info");
-
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_INFO);
-
-  /* prepare props */
-  props = wp_props_new (WP_PROPS_MODE_STORE, WP_PROXY (self));
-  wp_props_register (props,
-      "Wp:defaultSource", "Default Source", wp_spa_pod_new_int (0));
-  wp_props_register (props,
-      "Wp:defaultSink", "Default Sink", wp_spa_pod_new_int (0));
-  wp_proxy_set_props (WP_PROXY (self), props);
-  wp_proxy_set_feature_ready (WP_PROXY (self), WP_PROXY_FEATURE_PROPS);
 }
 
 static void
@@ -885,32 +786,30 @@ wp_impl_session_finalize (GObject * object)
 {
   WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (object));
 
-  /* set to NULL to prevent parent's finalize from calling free() on it */
-  priv->info = NULL;
+  g_clear_pointer (&priv->properties, wp_properties_unref);
 
   G_OBJECT_CLASS (wp_impl_session_parent_class)->finalize (object);
 }
 
 static void
-wp_impl_session_augment (WpProxy * proxy, WpProxyFeatures features)
+wp_impl_session_activate_execute_step (WpObject * object,
+    WpFeatureActivationTransition * transition, guint step,
+    WpObjectFeatures missing)
 {
-  WpImplSession *self = WP_IMPL_SESSION (proxy);
+  WpImplSession *self = WP_IMPL_SESSION (object);
   WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
 
-  /* PW_PROXY depends on BOUND */
-  if (features & WP_PROXY_FEATURE_PW_PROXY)
-    features |= WP_PROXY_FEATURE_BOUND;
-
-  if (features & WP_PROXY_FEATURE_BOUND) {
-    g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+  switch (step) {
+  case WP_PIPEWIRE_OBJECT_MIXIN_STEP_BIND: {
+    g_autoptr (WpCore) core = wp_object_get_core (object);
     struct pw_core *pw_core = wp_core_get_pw_core (core);
 
     /* no pw_core -> we are not connected */
     if (!pw_core) {
-      wp_proxy_augment_error (proxy, g_error_new (WP_DOMAIN_LIBRARY,
-            WP_LIBRARY_ERROR_OPERATION_FAILED,
-            "The WirePlumber core is not connected; "
-            "object cannot be exported to PipeWire"));
+      wp_transition_return_error (WP_TRANSITION (transition), g_error_new (
+              WP_DOMAIN_LIBRARY, WP_LIBRARY_ERROR_OPERATION_FAILED,
+              "The WirePlumber core is not connected; "
+              "object cannot be exported to PipeWire"));
       return;
     }
 
@@ -919,44 +818,49 @@ wp_impl_session_augment (WpProxy * proxy, WpProxyFeatures features)
     wp_properties_set (priv->properties, PW_KEY_CLIENT_ID, NULL);
     wp_properties_set (priv->properties, PW_KEY_FACTORY_ID, NULL);
 
-    wp_proxy_set_pw_proxy (proxy, pw_core_export (pw_core,
+    wp_proxy_set_pw_proxy (WP_PROXY (self), pw_core_export (pw_core,
             PW_TYPE_INTERFACE_Session,
             wp_properties_peek_dict (priv->properties),
             priv->iface, 0));
-  }
 
-  if (features & (WP_SESSION_FEATURE_ENDPOINTS | WP_SESSION_FEATURE_LINKS)) {
-    priv->ft_endpoints_requested = (features & WP_SESSION_FEATURE_ENDPOINTS);
-    priv->ft_links_requested = (features & WP_SESSION_FEATURE_LINKS);
-    wp_session_ensure_features_endpoints_links (WP_SESSION (self), 0);
+    wp_object_update_features (WP_OBJECT (self),
+        WP_PIPEWIRE_OBJECT_FEATURE_INFO, 0);
+    break;
+  }
+  default:
+    WP_OBJECT_CLASS (wp_impl_session_parent_class)->
+        activate_execute_step (object, transition, step, missing);
+    break;
   }
 }
 
 static void
-wp_impl_session_prop_changed (WpProxy * proxy, const gchar * prop_name)
+wp_impl_session_pw_proxy_destroyed (WpProxy * proxy)
 {
   WpImplSession *self = WP_IMPL_SESSION (proxy);
+  WpSessionPrivate *priv = wp_session_get_instance_private (WP_SESSION (self));
 
-  /* notify subscribers */
-  if (self->subscribed)
-    impl_enum_params (self, 1, SPA_PARAM_Props, 0, UINT32_MAX, NULL);
-
-  WP_PROXY_CLASS (wp_impl_session_parent_class)->prop_changed (proxy, prop_name);
+  g_clear_object (&priv->endpoints_om);
+  g_clear_object (&priv->links_om);
+  wp_object_update_features (WP_OBJECT (self), 0,
+      WP_PIPEWIRE_OBJECT_FEATURE_INFO |
+      WP_SESSION_FEATURE_ENDPOINTS |
+      WP_SESSION_FEATURE_LINKS);
 }
 
 static void
 wp_impl_session_class_init (WpImplSessionClass * klass)
 {
   GObjectClass *object_class = (GObjectClass *) klass;
+  WpObjectClass *wpobject_class = (WpObjectClass *) klass;
   WpProxyClass *proxy_class = (WpProxyClass *) klass;
 
   object_class->finalize = wp_impl_session_finalize;
 
-  proxy_class->augment = wp_impl_session_augment;
-  proxy_class->enum_params = NULL;
-  proxy_class->subscribe_params = NULL;
+  wpobject_class->activate_execute_step = wp_impl_session_activate_execute_step;
+
   proxy_class->pw_proxy_created = NULL;
-  proxy_class->prop_changed = wp_impl_session_prop_changed;
+  proxy_class->pw_proxy_destroyed = wp_impl_session_pw_proxy_destroyed;
 }
 
 /**
@@ -1001,7 +905,7 @@ wp_impl_session_set_property (WpImplSession * self,
   g_object_notify (G_OBJECT (self), "properties");
 
   /* update only after the session has been exported */
-  if (wp_proxy_get_features (WP_PROXY (self)) & WP_PROXY_FEATURE_BOUND) {
+  if (wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND) {
     self->info.change_mask = PW_SESSION_CHANGE_MASK_PROPS;
     pw_session_emit_info (&self->hooks, &self->info);
     self->info.change_mask = 0;
@@ -1034,7 +938,7 @@ wp_impl_session_update_properties (WpImplSession * self,
   g_object_notify (G_OBJECT (self), "properties");
 
   /* update only after the session has been exported */
-  if (wp_proxy_get_features (WP_PROXY (self)) & WP_PROXY_FEATURE_BOUND) {
+  if (wp_object_get_active_features (WP_OBJECT (self)) & WP_PROXY_FEATURE_BOUND) {
     self->info.change_mask = PW_SESSION_CHANGE_MASK_PROPS;
     pw_session_emit_info (&self->hooks, &self->info);
     self->info.change_mask = 0;
diff --git a/lib/wp/session.h b/lib/wp/session.h
index 00db81be286bedcd630b545ecdefbd1aa3edd81a..a5eb590ac6b1acdeeafa06f9dea085b428d31930 100644
--- a/lib/wp/session.h
+++ b/lib/wp/session.h
@@ -9,7 +9,7 @@
 #ifndef __WIREPLUMBER_SESSION_H__
 #define __WIREPLUMBER_SESSION_H__
 
-#include "proxy.h"
+#include "global-proxy.h"
 #include "endpoint.h"
 #include "endpoint-link.h"
 
@@ -27,22 +27,10 @@ G_BEGIN_DECLS
  * An extension of #WpProxyFeatures
  */
 typedef enum { /*< flags >*/
-  WP_SESSION_FEATURE_ENDPOINTS = (WP_PROXY_FEATURE_LAST << 0),
-  WP_SESSION_FEATURE_LINKS = (WP_PROXY_FEATURE_LAST << 1),
+  WP_SESSION_FEATURE_ENDPOINTS = (WP_PROXY_FEATURE_CUSTOM_START << 0),
+  WP_SESSION_FEATURE_LINKS = (WP_PROXY_FEATURE_CUSTOM_START << 1),
 } WpSessionFeatures;
 
-/**
- * WP_SESSION_FEATURES_STANDARD:
- *
- * A constant set of features that contains the standard features that are
- * available in the #WpSession class.
- */
-#define WP_SESSION_FEATURES_STANDARD \
-    (WP_PROXY_FEATURES_STANDARD | \
-     WP_PROXY_FEATURE_PROPS | \
-     WP_SESSION_FEATURE_ENDPOINTS | \
-     WP_SESSION_FEATURE_LINKS)
-
 /**
  * WP_TYPE_SESSION:
  *
@@ -50,24 +38,16 @@ typedef enum { /*< flags >*/
  */
 #define WP_TYPE_SESSION (wp_session_get_type ())
 WP_API
-G_DECLARE_DERIVABLE_TYPE (WpSession, wp_session, WP, SESSION, WpProxy)
+G_DECLARE_DERIVABLE_TYPE (WpSession, wp_session, WP, SESSION, WpGlobalProxy)
 
 struct _WpSessionClass
 {
-  WpProxyClass parent_class;
+  WpGlobalProxyClass parent_class;
 };
 
 WP_API
 const gchar * wp_session_get_name (WpSession * self);
 
-WP_API
-guint32 wp_session_get_default_endpoint (WpSession * self,
-    WpDirection direction);
-
-WP_API
-void wp_session_set_default_endpoint (WpSession * self,
-    WpDirection direction, guint32 id);
-
 /* endpoints */
 
 WP_API
diff --git a/lib/wp/transition.c b/lib/wp/transition.c
index 0f69f51640f107f2a0e3af7fcc2edfb7acd17917..5ee1ea28ed6c9cfbf2c5f3dcfe7c084ed42b3444 100644
--- a/lib/wp/transition.c
+++ b/lib/wp/transition.c
@@ -433,6 +433,9 @@ wp_transition_advance (WpTransition * self)
   /* find the next step */
   next_step = WP_TRANSITION_GET_CLASS (self)->get_next_step (self, priv->step);
 
+  wp_trace_object (priv->source_object, "transition: %d -> %d", priv->step,
+      next_step);
+
   if (next_step == WP_TRANSITION_STEP_ERROR) {
     /* return error if the callback didn't do it already */
     if (G_UNLIKELY (!priv->error)) {
@@ -454,6 +457,8 @@ wp_transition_advance (WpTransition * self)
   if (next_step == priv->step)
     return;
 
+  wp_trace_object (priv->source_object, "transition: execute %d", next_step);
+
   /* execute the next step */
   priv->step = next_step;
   WP_TRANSITION_GET_CLASS (self)->execute_step (self, priv->step);
@@ -516,5 +521,9 @@ wp_transition_finish (GAsyncResult * res, GError ** error)
     g_propagate_error (error, priv->error);
     priv->error = NULL;
   }
+
+  wp_trace_object (priv->source_object, "transition: finished %s",
+      (priv->step == WP_TRANSITION_STEP_NONE) ? "ok" : "with error");
+
   return (priv->step == WP_TRANSITION_STEP_NONE);
 }
diff --git a/lib/wp/wp.h b/lib/wp/wp.h
index 167d4397a9a68da0c7bb56d98297f00f908c6e05..2c6625f58013062e4737f16f0962596c2960b23a 100644
--- a/lib/wp/wp.h
+++ b/lib/wp/wp.h
@@ -18,6 +18,7 @@
 #include "endpoint-link.h"
 #include "endpoint-stream.h"
 #include "error.h"
+#include "global-proxy.h"
 #include "iterator.h"
 #include "link.h"
 #include "metadata.h"
@@ -29,7 +30,6 @@
 #include "plugin.h"
 #include "port.h"
 #include "properties.h"
-#include "props.h"
 #include "proxy.h"
 #include "proxy-interfaces.h"
 #include "session.h"
diff --git a/meson.build b/meson.build
index 9f71356f4ae50ef0e0a3235867b0e53273e1a6f3..4c87f82efcaa4be76ab447fd5f927c01b49eae71 100644
--- a/meson.build
+++ b/meson.build
@@ -72,7 +72,7 @@ endif
 
 subdir('lib')
 subdir('docs')
-subdir('modules')
-subdir('src')
-subdir('tests')
-subdir('tools')
+#subdir('modules')
+#subdir('src')
+#subdir('tests')
+#subdir('tools')