From 2f3f5f8e66c577d0568b43340162a2dfbfeaf4c9 Mon Sep 17 00:00:00 2001
From: George Kiagiadakis <george.kiagiadakis@collabora.com>
Date: Tue, 10 Nov 2020 19:17:02 +0200
Subject: [PATCH] lib: refactor WpProxy

This is an attempt to unclutter the API of WpProxy and
split functionality into smaller pieces, making it easier
to work with.

In this new class layout, we have the following classes:

- WpObject: base class for everything; handles activating
|           and deactivating "features"
|- WpProxy: base class for anything that wraps a pw_proxy;
 |          handles events from pw_proxy and nothing more
 |- WpGlobalProxy: handles integration with the registry

All the other classes derive from WpGlobalProxy. The reason
for separating WpGlobalProxy from WpProxy, though, is that
classes such as WpImplNode / WpSpaDevice can also derive from
WpProxy now, without interfacing with the registry.

All objects that come with an "info" structure and have properties
and/or params also implement the WpPipewireObject interface. This
provides the API to query properties and get/set params. Essentially,
this is implemented by all classes except WpMetadata (pw_metadata
does not have info)

This interface is implemented on each object separately, using
a private "mixin", which is a set of vfunc implementations and helper
functions (and macros) to facilitate the implementation of this interface.

A notable difference to the old WpProxy is that now features can be
deactivated, so it is possible to enable something and later disable
it again.

This commit disables modules, tests, tools, etc, to avoid growing the
patch more, while ensuring that the project compiles.
---
 lib/wp/client.c                        |  125 ++-
 lib/wp/client.h                        |    4 +-
 lib/wp/debug.c                         |    2 +-
 lib/wp/device.c                        |  226 +++---
 lib/wp/device.h                        |    4 +-
 lib/wp/endpoint-link.c                 |  347 ++++----
 lib/wp/endpoint-link.h                 |    6 +-
 lib/wp/endpoint-stream.c               |  557 ++++++-------
 lib/wp/endpoint-stream.h               |    7 +-
 lib/wp/endpoint.c                      |  712 ++++++++--------
 lib/wp/endpoint.h                      |   20 +-
 lib/wp/global-proxy.c                  |  293 +++++++
 lib/wp/global-proxy.h                  |   47 ++
 lib/wp/link.c                          |  135 ++--
 lib/wp/link.h                          |    4 +-
 lib/wp/meson.build                     |    7 +-
 lib/wp/metadata.c                      |  152 +++-
 lib/wp/metadata.h                      |   16 +-
 lib/wp/node.c                          |  374 +++++----
 lib/wp/node.h                          |   21 +-
 lib/wp/object-interest.c               |   42 +-
 lib/wp/object-interest.h               |    4 +-
 lib/wp/object-manager.c                |   54 +-
 lib/wp/object-manager.h                |    8 +-
 lib/wp/port.c                          |  180 +++--
 lib/wp/port.h                          |    4 +-
 lib/wp/private.h                       |   14 -
 lib/wp/private/pipewire-object-mixin.c |  258 ++++++
 lib/wp/private/pipewire-object-mixin.h |  162 ++++
 lib/wp/private/registry.h              |    5 +-
 lib/wp/proxy.c                         | 1033 ++----------------------
 lib/wp/proxy.h                         |  151 +---
 lib/wp/session-item.c                  |   40 +-
 lib/wp/session.c                       |  556 ++++++-------
 lib/wp/session.h                       |   30 +-
 lib/wp/transition.c                    |    9 +
 lib/wp/wp.h                            |    2 +-
 meson.build                            |    8 +-
 38 files changed, 2818 insertions(+), 2801 deletions(-)
 create mode 100644 lib/wp/global-proxy.c
 create mode 100644 lib/wp/global-proxy.h
 create mode 100644 lib/wp/private/pipewire-object-mixin.c
 create mode 100644 lib/wp/private/pipewire-object-mixin.h

diff --git a/lib/wp/client.c b/lib/wp/client.c
index 0d8a52d2..10b08739 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 89eef4f0..4cd9a2bd 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 d58c03f2..edc568b7 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 b70013bb..2a5ab8de 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 ebcc95ff..10592eac 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 4e8be1fa..5c872e74 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 ca3f0ef1..1ee5d1c4 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 a6cc5c38..1fca9958 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 c6b21bb0..665e4b54 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 cdf5d593..667a4279 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 2aefdff8..d7bce89c 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 00000000..7de94a45
--- /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 00000000..2a674353
--- /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 157cb155..0a030b11 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 a9b294d0..f0fba7cb 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 3fa8be4c..d8292a08 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 4729cbca..c578c8c3 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 c501339d..5008eebd 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 4196cbe3..fbd85053 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 2b6ae2e2..55d2dcb5 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 72694ae3..b9f7a70a 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 b52853c7..983eeea7 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 60289b54..13a8cede 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 e36037d2..dd9dadcd 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 490edd7f..5d501140 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 e9de692d..bba7eecf 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 b913ea90..2734d099 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 00000000..49b316d6
--- /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 00000000..a329f8d8
--- /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 cb62664f..9078e9aa 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 b4dd8d66..bed50720 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 2b5d51d4..e9ed203b 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 c8636b44..ca215442 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 a2d0f74c..a7dda9aa 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 00db81be..a5eb590a 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 0f69f516..5ee1ea28 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 167d4397..2c6625f5 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 9f71356f..4c87f82e 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')
-- 
GitLab