diff --git a/modules/meson.build b/modules/meson.build
index 606e510ef9c7c4f2b6659dc450c7e54bf48a46f5..6cccd833a88f740053f3944ff67b0a085811436e 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -41,11 +41,11 @@ shared_library(
 )
 
 shared_library(
-  'wireplumber-module-pw-simple-policy',
+  'wireplumber-module-simple-policy',
   [
-    'module-pw-simple-policy.c'
+    'module-simple-policy.c'
   ],
-  c_args : [common_c_args, '-DG_LOG_DOMAIN="m-pw-simple-policy"'],
+  c_args : [common_c_args, '-DG_LOG_DOMAIN="m-simple-policy"'],
   install : true,
   install_dir : wireplumber_module_dir,
   dependencies : [wp_dep, pipewire_dep],
diff --git a/modules/module-pw-simple-policy.c b/modules/module-pw-simple-policy.c
deleted file mode 100644
index 949824e0cefa25e297affbcdc900d1ead98e8182..0000000000000000000000000000000000000000
--- a/modules/module-pw-simple-policy.c
+++ /dev/null
@@ -1,224 +0,0 @@
-/* WirePlumber
- *
- * Copyright © 2019 Collabora Ltd.
- *    @author Julian Bouzas <julian.bouzas@collabora.com>
- *
- * SPDX-License-Identifier: MIT
- */
-
-/**
- * module-pw-simple-policy connects the first audio output client endpoint with
- * the first audio sink remote endpoint
- */
-
-#include <wp/wp.h>
-#include <pipewire/pipewire.h>
-
-typedef void (*WpDoneCallback)(gpointer);
-
-struct impl {
-  WpCore *wp_core;
-
-  /* Remote */
-  struct pw_remote *remote;
-  struct spa_hook remote_listener;
-
-  /* Core */
-  struct pw_core_proxy *core_proxy;
-  struct spa_hook core_listener;
-  int core_seq;
-  WpDoneCallback done_cb;
-  gpointer done_cb_data;
-
-  /* Endpoints */
-  WpEndpoint *ep_client;
-  WpEndpoint *ep_remote;
-};
-
-static void
-sync_core_with_callabck(struct impl* impl, WpDoneCallback callback,
-    gpointer data)
-{
-  /* Set the callback and data */
-  impl->done_cb = callback;
-  impl->done_cb_data = data;
-
-  /* Sync the core */
-  impl->core_seq = pw_core_proxy_sync(impl->core_proxy, 0, impl->core_seq);
-}
-
-static WpEndpoint *
-endpoint_get_selected (WpCore *core, const char *media_class)
-{
-  g_autoptr (GPtrArray) ptr_array = NULL;
-  int i;
-
-  /* Get all the endpoints with the specific media lcass*/
-  ptr_array = wp_endpoint_find (core, media_class);
-  if (!ptr_array)
-    return NULL;
-
-  /* Find and return the "selected" endpoint */
-  /* FIXME: fix the endpoint API, this is terrible */
-  for (i = 0; i < ptr_array->len; i++) {
-    WpEndpoint *ep = g_ptr_array_index (ptr_array, i);
-    GVariantIter iter;
-    g_autoptr (GVariant) controls = NULL;
-    g_autoptr (GVariant) value = NULL;
-    const gchar *name;
-    guint id;
-
-    controls = wp_endpoint_list_controls (ep);
-    g_variant_iter_init (&iter, controls);
-    while ((value = g_variant_iter_next_value (&iter))) {
-      if (!g_variant_lookup (value, "name", "&s", &name)
-          || !g_str_equal (name, "selected")) {
-        g_variant_unref (value);
-        continue;
-      }
-      g_variant_lookup (value, "id", "u", &id);
-      g_variant_unref (value);
-    }
-
-    value = wp_endpoint_get_control_value (ep, id);
-    if (value && g_variant_get_boolean (value))
-      return ep;
-  }
-
-  /* If not found, return the first endpoint */
-  return (ptr_array->len > 1) ? g_ptr_array_index (ptr_array, 0) : NULL;
-}
-
-static void
-link_endpoints(gpointer data)
-{
-  struct impl *impl = data;
-  WpEndpointLink *ep_link = NULL;
-
-  /* Make sure the endpoints are valid */
-  if (!impl->ep_client || !impl->ep_remote) {
-    g_warning ("Endpoints not valid to link. Skipping...\n");
-    return;
-  }
-
-  /* Link the client with the remote */
-  ep_link = wp_endpoint_link_new(impl->wp_core, impl->ep_client, 0,
-      impl->ep_remote, 0, NULL);
-  if (!ep_link) {
-    g_warning ("Could not link endpoints. Skipping...\n");
-    return;
-  }
-
-  g_info ("Endpoints linked successfully\n");
-}
-
-static void
-endpoint_added (WpCore *core, GQuark key, WpEndpoint *ep, struct impl * impl)
-{
-  const char *media_class = NULL;
-
-  /* Reset endpoints */
-  impl->ep_remote = NULL;
-  impl->ep_client = NULL;
-
-  /* Make sure an endpoint has been added */
-  g_return_if_fail (key == WP_GLOBAL_ENDPOINT);
-
-  /* Get the media class */
-  media_class = wp_endpoint_get_media_class(ep);
-
-  /* Only process client endpoints */
-  if (!g_str_has_prefix(media_class, "Stream"))
-    return;
-
-  /* TODO: For now we only accept audio output clients */
-  if (!g_str_has_prefix(media_class, "Stream/Output/Audio"))
-    return;
-  impl->ep_client = ep;
-
-  /* Get the first endpoint with media class Audio/Sink */
-  impl->ep_remote = endpoint_get_selected (core, "Audio/Sink");
-  if (!impl->ep_remote) {
-    g_warning ("Could not get an Audio/Sink remote endpoint\n");
-    return;
-  }
-
-  /* Do the linking when core is done */
-  sync_core_with_callabck (impl, link_endpoints, impl);
-}
-
-static void core_done(void *data, uint32_t id, int seq)
-{
-  struct impl * impl = data;
-
-  /* Call the done callback if it exists */
-  if (impl->done_cb)
-    impl->done_cb(impl->done_cb_data);
-
-  impl->done_cb = NULL;
-  impl->done_cb_data = NULL;
-}
-
-static const struct pw_core_proxy_events core_events = {
-  PW_VERSION_CORE_EVENTS,
-  .done = core_done
-};
-
-static void on_state_changed(void *_data, enum pw_remote_state old,
-    enum pw_remote_state state, const char *error)
-{
-  struct impl *impl = _data;
-
-  switch (state) {
-  case PW_REMOTE_STATE_ERROR:
-          break;
-
-  case PW_REMOTE_STATE_CONNECTED:
-          /* Register the core event callbacks */
-          impl->core_proxy = pw_remote_get_core_proxy(impl->remote);
-          pw_core_proxy_add_listener(impl->core_proxy, &impl->core_listener,
-              &core_events, impl);
-          break;
-
-  case PW_REMOTE_STATE_UNCONNECTED:
-          break;
-
-  default:
-          break;
-  }
-}
-
-static const struct pw_remote_events remote_events = {
-  PW_VERSION_REMOTE_EVENTS,
-  .state_changed = on_state_changed,
-};
-
-static void
-module_destroy (gpointer data)
-{
-  struct impl *impl = data;
-  g_slice_free (struct impl, impl);
-}
-
-void
-wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
-{
-  struct impl *impl = g_new0(struct impl, 1);
-
-  /* Set destroy callback for impl */
-  wp_module_set_destroy_callback (module, module_destroy, impl);
-
-  /* Set the core */
-  impl->wp_core = core;
-
-  /* Set the core remote */
-  impl->remote = wp_core_get_global(core, WP_GLOBAL_PW_REMOTE);
-
-  /* Add a state changed listener */
-  pw_remote_add_listener(impl->remote, &impl->remote_listener, &remote_events,
-      impl);
-
-  /* Register the endpoint added and removed callbacks */
-  g_signal_connect (core, "global-added::endpoint",
-    (GCallback) endpoint_added, impl);
-}
diff --git a/modules/module-simple-policy.c b/modules/module-simple-policy.c
new file mode 100644
index 0000000000000000000000000000000000000000..44032923f5cdfa92d5c695f0cac129b72ca94036
--- /dev/null
+++ b/modules/module-simple-policy.c
@@ -0,0 +1,136 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author George Kiagiadakis <george.kiagiadakis@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+
+struct _WpSimplePolicy
+{
+  WpPolicy parent;
+};
+
+G_DECLARE_FINAL_TYPE (WpSimplePolicy, simple_policy, WP, SIMPLE_POLICY, WpPolicy)
+G_DEFINE_TYPE (WpSimplePolicy, simple_policy, WP_TYPE_POLICY)
+
+static void
+simple_policy_init (WpSimplePolicy *self)
+{
+}
+
+static gboolean
+simple_policy_handle_endpoint (WpPolicy *policy, WpEndpoint *ep)
+{
+  const char *media_class = NULL;
+  GVariantDict d;
+  g_autoptr (GVariant) props = NULL;
+  g_autoptr (WpCore) core = NULL;
+  g_autoptr (WpEndpoint) target = NULL;
+  g_autoptr (GError) error = NULL;
+  guint32 stream_id;
+
+  /* TODO: For now we only accept audio output clients */
+  media_class = wp_endpoint_get_media_class(ep);
+  if (!g_str_equal (media_class, "Stream/Output/Audio"))
+    return FALSE;
+
+  /* Locate the target endpoint */
+  g_variant_dict_init (&d, NULL);
+  g_variant_dict_insert (&d, "action", "s", "link");
+  g_variant_dict_insert (&d, "media.class", "s", "Audio/Sink");
+  /* TODO: more properties are needed here */
+  props = g_variant_dict_end (&d);
+
+  core = wp_policy_get_core (policy);
+  target = wp_policy_find_endpoint (core, props, &stream_id);
+  if (!target) {
+    g_warning ("Could not find an Audio/Sink target endpoint\n");
+    /* TODO: we should kill the client, otherwise it's going to hang waiting */
+    return FALSE;
+  }
+
+  /* Link the client with the target */
+  if (!wp_endpoint_link_new (core, ep, 0, target, stream_id, &error)) {
+    g_warning ("Could not link endpoints: %s\n", error->message);
+  } else {
+    g_info ("Sucessfully linked '%s' to '%s'\n", wp_endpoint_get_name (ep),
+        wp_endpoint_get_name (target));
+  }
+
+  return TRUE;
+}
+
+static WpEndpoint *
+simple_policy_find_endpoint (WpPolicy *policy, GVariant *props,
+    guint32 *stream_id)
+{
+  g_autoptr (WpCore) core = NULL;
+  g_autoptr (GPtrArray) ptr_array = NULL;
+  const char *media_class = NULL;
+  WpEndpoint *ep;
+  int i;
+
+  core = wp_policy_get_core (policy);
+
+  /* Get all the endpoints with the specific media class*/
+  g_variant_lookup (props, "media.class", "&s", &media_class);
+  ptr_array = wp_endpoint_find (core, media_class);
+  if (!ptr_array)
+    return NULL;
+
+  /* TODO: for now we statically return the first stream
+   * we should be looking into the media.role eventually */
+  *stream_id = 0;
+
+  /* Find and return the "selected" endpoint */
+  /* FIXME: fix the endpoint API, this is terrible */
+  for (i = 0; i < ptr_array->len; i++) {
+    ep = g_ptr_array_index (ptr_array, i);
+    GVariantIter iter;
+    g_autoptr (GVariant) controls = NULL;
+    g_autoptr (GVariant) value = NULL;
+    const gchar *name;
+    guint id;
+
+    controls = wp_endpoint_list_controls (ep);
+    g_variant_iter_init (&iter, controls);
+    while ((value = g_variant_iter_next_value (&iter))) {
+      if (!g_variant_lookup (value, "name", "&s", &name)
+          || !g_str_equal (name, "selected")) {
+        g_variant_unref (value);
+        continue;
+      }
+      g_variant_lookup (value, "id", "u", &id);
+      g_variant_unref (value);
+    }
+
+    value = wp_endpoint_get_control_value (ep, id);
+    if (value && g_variant_get_boolean (value))
+      return g_object_ref (ep);
+  }
+
+  /* If not found, return the first endpoint */
+  ep = (ptr_array->len > 1) ? g_ptr_array_index (ptr_array, 0) : NULL;
+  return g_object_ref (ep);
+}
+
+static void
+simple_policy_class_init (WpSimplePolicyClass *klass)
+{
+  WpPolicyClass *policy_class = (WpPolicyClass *) klass;
+
+  policy_class->handle_endpoint = simple_policy_handle_endpoint;
+  policy_class->find_endpoint = simple_policy_find_endpoint;
+}
+
+void
+wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
+{
+  WpPolicy *p = g_object_new (simple_policy_get_type (),
+      "rank", WP_POLICY_RANK_UPSTREAM,
+      NULL);
+  wp_policy_register (p, core);
+}
diff --git a/src/wireplumber.conf b/src/wireplumber.conf
index 8a35ac0f16c07e40c463930fbf949890321e7b75..9748aa755f9c396643bb2831db3feb46458def47 100644
--- a/src/wireplumber.conf
+++ b/src/wireplumber.conf
@@ -1,4 +1,4 @@
 load-module C libwireplumber-module-pipewire
 load-module C libwireplumber-module-pw-audio-softdsp-endpoint
 load-module C libwireplumber-module-pw-alsa-udev
-load-module C libwireplumber-module-pw-simple-policy
+load-module C libwireplumber-module-simple-policy