diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index 32a3036b996420274c2acd5400c055a5032a6b12..83db67761d363a8f8951680667189b65d8a79c99 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -279,8 +279,8 @@ wp_endpoint_unregister (WpEndpoint * self)
     g_info ("WpEndpoint:%p unregistering '%s' (%s)", self, priv->name,
         priv->media_class);
 
-    priv->core = NULL;
     wp_core_remove_global (priv->core, WP_GLOBAL_ENDPOINT, self);
+    priv->core = NULL;
   }
 }
 
diff --git a/modules/meson.build b/modules/meson.build
index 9c0206f3598c9ec555f633397add25b1d181d2fb..606e510ef9c7c4f2b6659dc450c7e54bf48a46f5 100644
--- a/modules/meson.build
+++ b/modules/meson.build
@@ -39,3 +39,14 @@ shared_library(
   install_dir : wireplumber_module_dir,
   dependencies : [wp_dep, pipewire_dep],
 )
+
+shared_library(
+  'wireplumber-module-pw-simple-policy',
+  [
+    'module-pw-simple-policy.c'
+  ],
+  c_args : [common_c_args, '-DG_LOG_DOMAIN="m-pw-simple-policy"'],
+  install : true,
+  install_dir : wireplumber_module_dir,
+  dependencies : [wp_dep, pipewire_dep],
+)
diff --git a/modules/module-pipewire.c b/modules/module-pipewire.c
index fbcf04fcc552ae97d10006cde8fdb68735c8a2d6..b7a9ebd882c58f53cb433cfc27f8ca93e4910ffa 100644
--- a/modules/module-pipewire.c
+++ b/modules/module-pipewire.c
@@ -14,6 +14,7 @@
 
 #include <wp/wp.h>
 #include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
 
 #include "module-pipewire/loop-source.h"
 
@@ -50,6 +51,10 @@ registry_global (void * d, uint32_t id, uint32_t parent_id,
   g_autoptr (GVariant) endpoint_props = NULL;
   g_autoptr (WpCore) core = NULL;
   g_autoptr (WpEndpoint) endpoint = NULL;
+  struct spa_audio_info_raw format = { 0, };
+  struct spa_pod *param;
+  struct spa_pod_builder pod_builder = { 0, };
+  char buf[1024];
 
   /* listen for client "Stream" nodes and create endpoints for them */
   if (type == PW_TYPE_INTERFACE_Node &&
@@ -66,7 +71,26 @@ registry_global (void * d, uint32_t id, uint32_t parent_id,
     proxy = pw_registry_proxy_bind (data->registry_proxy,
         id, type, PW_VERSION_NODE, 0);
 
+    /* TODO: we need to get this from the EnumFormat event */
+    format.format = SPA_AUDIO_FORMAT_F32P;
+    format.flags = 1;
+    format.rate = 48000;
+    format.channels = 2;
+    format.position[0] = 0;
+    format.position[1] = 0;
+
+    /* Set the profile */
+    spa_pod_builder_init(&pod_builder, buf, sizeof(buf));
+    param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &format);
+    param = spa_pod_builder_add_object(&pod_builder,
+        SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
+        SPA_PARAM_PROFILE_direction,  SPA_POD_Id(PW_DIRECTION_OUTPUT),
+        SPA_PARAM_PROFILE_format,     SPA_POD_Pod(param));
+    pw_node_proxy_set_param((struct pw_node_proxy*)proxy,
+        SPA_PARAM_Profile, 0, param);
+
     g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
+    g_variant_builder_add (&b, "{sv}", "node-id", g_variant_new_uint32 (id));
     g_variant_builder_add (&b, "{sv}",
         "name", name ? g_variant_new_string (name) :
             g_variant_new_take_string (g_strdup_printf ("Stream %u", id)));
diff --git a/modules/module-pipewire/port.h b/modules/module-pipewire/port.h
new file mode 100644
index 0000000000000000000000000000000000000000..5fcf98bffabf2ba07c235f599223171ba8176ff7
--- /dev/null
+++ b/modules/module-pipewire/port.h
@@ -0,0 +1,30 @@
+/* WirePlumber
+ *
+ * Copyright © 2019 Collabora Ltd.
+ *    @author Julian Bouzas <julian.bouzas@collabora.com>
+ *
+ * SPDX-License-Identifier: MIT
+ */
+
+#include <wp/wp.h>
+#include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+
+struct _WpPort
+{
+  struct spa_list l;
+
+  /* Port proxy and listener */
+  struct pw_proxy *proxy;
+  struct spa_hook listener;
+
+  /* Port info */
+  uint32_t id;
+  uint32_t parent_id;
+  enum pw_direction direction;
+  uint32_t media_type;
+  uint32_t media_subtype;
+  struct pw_port_info *info;
+  struct spa_audio_info_raw format;
+};
+typedef struct _WpPort WpPort;
\ No newline at end of file
diff --git a/modules/module-pipewire/simple-endpoint-link.c b/modules/module-pipewire/simple-endpoint-link.c
index 512120d52b5ac334b48a70660cf1e5793107cf1c..0db10c3bc8893dce1d6a1687c9ebd067c46ec13b 100644
--- a/modules/module-pipewire/simple-endpoint-link.c
+++ b/modules/module-pipewire/simple-endpoint-link.c
@@ -26,6 +26,9 @@
 struct _WpPipewireSimpleEndpointLink
 {
   WpEndpointLink parent;
+
+  /* The core proxy */
+  struct pw_core_proxy *core_proxy;
 };
 
 G_DECLARE_FINAL_TYPE (WpPipewireSimpleEndpointLink,
@@ -40,10 +43,36 @@ simple_endpoint_link_init (WpPipewireSimpleEndpointLink * self)
 }
 
 static gboolean
-simple_endpoint_link_create (WpEndpointLink * self, GVariant * src_data,
+simple_endpoint_link_create (WpEndpointLink * epl, GVariant * src_data,
     GVariant * sink_data, GError ** error)
 {
-  /* TODO create pw_links based on the nodes & ports described in src/sink_data */
+  WpPipewireSimpleEndpointLink *self = WP_PIPEWIRE_SIMPLE_ENDPOINT_LINK(epl);
+  struct pw_properties *props;
+  guint32 output_node_id, input_node_id, output_port_id, input_port_id;
+
+  /* Get the node ids and port ids */
+  if (!g_variant_lookup (src_data, "node-id", "u", &output_node_id))
+      return FALSE;
+  if (!g_variant_lookup (src_data, "node-port-id", "u", &output_port_id))
+      return FALSE;
+  if (!g_variant_lookup (sink_data, "node-id", "u", &input_node_id))
+      return FALSE;
+  if (!g_variant_lookup (sink_data, "node-port-id", "u", &input_port_id))
+      return FALSE;
+
+  /* Create the properties */
+  props = pw_properties_new(NULL, NULL);
+  pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", output_node_id);
+  pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", output_port_id);
+  pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", input_node_id);
+  pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", input_port_id);
+
+  /* Create the link */
+  pw_core_proxy_create_object(self->core_proxy, "link-factory",
+      PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0);
+
+  /* Clean up */
+  pw_properties_free(props);
 
   return TRUE;
 }
@@ -67,7 +96,37 @@ gpointer
 simple_endpoint_link_factory (WpFactory * factory, GType type,
     GVariant * properties)
 {
+  WpCore *wp_core = NULL;
+  struct pw_remote *remote;
+
+  /* Make sure the type is an endpoint link */
   if (type != WP_TYPE_ENDPOINT_LINK)
     return NULL;
-  return g_object_new (simple_endpoint_link_get_type (), NULL);
+
+  /* Get the WirePlumber core */
+  wp_core = wp_factory_get_core(factory);
+  if (!wp_core) {
+    g_warning("failed to get wireplumbe core. Skipping...");
+    return NULL;
+  }
+
+  /* Get the remote */
+  remote = wp_core_get_global(wp_core, WP_GLOBAL_PW_REMOTE);
+  if (!remote) {
+    g_warning("failed to get core remote. Skipping...");
+    return NULL;
+  }
+
+  /* Create the endpoint link */
+  WpPipewireSimpleEndpointLink *epl = g_object_new (
+      simple_endpoint_link_get_type (), NULL);
+
+  /* Set the core proxy */
+  epl->core_proxy = pw_remote_get_core_proxy(remote);
+  if (!epl->core_proxy) {
+    g_warning("failed to get core proxy. Skipping...");
+    return NULL;
+  }
+
+  return epl;
 }
diff --git a/modules/module-pipewire/simple-endpoint.c b/modules/module-pipewire/simple-endpoint.c
index 7f94cd30fff53816862d858f846cf7d4233ab631..12d1e4e5b84d41ee92be9048cfd3eeaf0e9f7b79 100644
--- a/modules/module-pipewire/simple-endpoint.c
+++ b/modules/module-pipewire/simple-endpoint.c
@@ -18,12 +18,19 @@
 #include <spa/pod/parser.h>
 #include <spa/param/props.h>
 
+#include "port.h"
+
 struct _WpPipewireSimpleEndpoint
 {
   WpEndpoint parent;
+
+  /* Node */
   struct pw_node_proxy *node;
   struct spa_hook proxy_listener;
   struct spa_hook node_proxy_listener;
+  
+  /* Info */
+  struct pw_node_info *info;
 
   /* controls cache */
   gfloat volume;
@@ -45,19 +52,6 @@ G_DECLARE_FINAL_TYPE (WpPipewireSimpleEndpoint,
 
 G_DEFINE_TYPE (WpPipewireSimpleEndpoint, simple_endpoint, WP_TYPE_ENDPOINT)
 
-static void
-node_proxy_destroy (void *data)
-{
-  WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (data);
-  self->node = NULL;
-  wp_endpoint_unregister (WP_ENDPOINT (self));
-}
-
-static const struct pw_proxy_events node_proxy_events = {
-  PW_VERSION_PROXY_EVENTS,
-  .destroy = node_proxy_destroy,
-};
-
 static void
 node_proxy_param (void *object, int seq, uint32_t id,
     uint32_t index, uint32_t next, const struct spa_pod *param)
@@ -104,8 +98,15 @@ node_proxy_param (void *object, int seq, uint32_t id,
   }
 }
 
+static void node_proxy_info(void *object, const struct pw_node_info *info)
+{
+  WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (object);
+  self->info = pw_node_info_update(self->info, info);
+}
+
 static const struct pw_node_proxy_events node_node_proxy_events = {
   PW_VERSION_NODE_PROXY_EVENTS,
+  .info = node_proxy_info,
   .param = node_proxy_param,
 };
 
@@ -114,6 +115,21 @@ simple_endpoint_init (WpPipewireSimpleEndpoint * self)
 {
 }
 
+static void
+node_proxy_destroy (void *data)
+{
+  WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (data);
+  self->node = NULL;
+
+  wp_endpoint_unregister (WP_ENDPOINT (self));
+}
+
+static const struct pw_proxy_events node_proxy_events = {
+  PW_VERSION_PROXY_EVENTS,
+  .destroy = node_proxy_destroy,
+};
+
+
 static void
 simple_endpoint_constructed (GObject * object)
 {
@@ -124,6 +140,7 @@ simple_endpoint_constructed (GObject * object)
 
   pw_proxy_add_listener ((struct pw_proxy *) self->node, &self->proxy_listener,
       &node_proxy_events, self);
+
   pw_node_proxy_add_listener (self->node, &self->node_proxy_listener,
       &node_node_proxy_events, self);
   pw_node_proxy_subscribe_params (self->node, ids, n_ids);
@@ -202,11 +219,26 @@ simple_endpoint_get_property (GObject * object, guint property_id,
 }
 
 static gboolean
-simple_endpoint_prepare_link (WpEndpoint * self, guint32 stream_id,
+simple_endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id,
     WpEndpointLink * link, GVariant ** properties, GError ** error)
 {
-  /* TODO: verify that the remote end supports the same media type */
-  /* TODO: fill @properties with (node id, array(port ids)) */
+  WpPipewireSimpleEndpoint *self = WP_PIPEWIRE_SIMPLE_ENDPOINT (ep);
+  GVariantBuilder b;
+
+  /* TODO: Since the linking with a 1 port client works when passing -1 as
+   * a port parameter, there is no need to find the port and set it in the
+   * properties. However, we need to add logic here and select the correct
+   * port in case the client has more than 1 port */
+
+  /* Set the port format here */
+
+  /* Set the properties */
+  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&b, "{sv}", "node-id",
+      g_variant_new_uint32 (self->info->id));
+  g_variant_builder_add (&b, "{sv}", "node-port-id",
+      g_variant_new_uint32 (-1));
+  *properties = g_variant_builder_end (&b);
 
   return TRUE;
 }
@@ -299,7 +331,6 @@ gpointer
 simple_endpoint_factory (WpFactory * factory, GType type,
     GVariant * properties)
 {
-  WpPipewireSimpleEndpoint *ep;
   guint64 proxy;
   const gchar *name;
   const gchar *media_class;
@@ -316,11 +347,9 @@ simple_endpoint_factory (WpFactory * factory, GType type,
   if (!g_variant_lookup (properties, "node-proxy", "t", &proxy))
       return NULL;
 
-  ep = g_object_new (simple_endpoint_get_type (),
+  return g_object_new (simple_endpoint_get_type (),
       "name", name,
       "media-class", media_class,
       "node-proxy", (gpointer) proxy,
       NULL);
-
-  return ep;
 }
diff --git a/modules/module-pw-alsa-udev.c b/modules/module-pw-alsa-udev.c
index e04818b92c428aae31a1c8c072ac18c46fd32e64..8e35aa7592d28c8b24b9b9099b680b51c6a14ee7 100644
--- a/modules/module-pw-alsa-udev.c
+++ b/modules/module-pw-alsa-udev.c
@@ -13,26 +13,155 @@
 
 #include <wp/wp.h>
 #include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
+
+#include "module-pipewire/port.h"
+
+typedef void (*WpDoneCallback)(gpointer);
+
+struct done_data {
+  WpDoneCallback callback;
+  gpointer data;
+  GDestroyNotify data_destroy;
+};
 
 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;
+  GQueue *done_queue;
+
+  /* Registry */
   struct pw_registry_proxy *registry_proxy;
   struct spa_hook registry_listener;
+
+  /* Ports */
+  struct spa_list port_list;
 };
 
+struct endpoint_info {
+  struct impl *impl;
+  uint32_t id;
+  uint32_t parent_id;
+  gchar *name;
+  gchar *media_class;
+};
+
+static void endpoint_info_destroy(gpointer p) {
+  struct endpoint_info *ei = p;
+  if (ei->name) {
+    g_free (ei->name);
+    ei->name = NULL;
+  }
+  if (ei->media_class) {
+    g_free (ei->media_class);
+    ei->media_class = NULL;
+  }
+  g_slice_free (struct endpoint_info, p);
+}
+
+static void done_data_destroy(gpointer p) {
+  struct done_data *dd = p;
+  if (dd->data_destroy) {
+    dd->data_destroy(dd->data);
+    dd->data = NULL;
+  }
+  g_slice_free (struct done_data, dd);
+}
+
+static void sync_core_with_callabck(struct impl* impl,
+    WpDoneCallback callback, gpointer data, GDestroyNotify data_destroy) {
+  struct done_data *dd = g_new0(struct done_data, 1);
+  
+  /* Set the data */
+  dd->callback = callback;
+  dd->data = data;
+  dd->data_destroy = data_destroy;
+
+  /* Add the data to the queue */
+  g_queue_push_tail (impl->done_queue, dd);
+
+  /* Sync the core */
+  impl->core_seq = pw_core_proxy_sync(impl->core_proxy, 0, impl->core_seq);
+}
+
+static void create_endpoint(gpointer p) {
+  struct endpoint_info *ei = p;
+  struct spa_proxy *proxy = NULL;
+  GVariantBuilder b;
+  g_autoptr(GVariant) endpoint_props = NULL;
+  WpEndpoint *endpoint = NULL;
+
+  /* Make sure the endpoint info is valid */
+  if (!ei)
+    return;
+
+  /* Register the proxy */
+  proxy = pw_registry_proxy_bind (ei->impl->registry_proxy,
+      ei->id, PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
+
+  /* Build the GVariant properties for the endpoint */
+  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&b, "{sv}", "name",
+      g_variant_new_take_string (g_strdup_printf ("Endpoint %u: %s", ei->id,
+      ei->name)));
+  g_variant_builder_add (&b, "{sv}",
+      "media-class", g_variant_new_string (ei->media_class));
+  g_variant_builder_add (&b, "{sv}",
+      "node-proxy", g_variant_new_uint64 ((guint64) proxy));
+  g_variant_builder_add (&b, "{sv}",
+      "port-list", g_variant_new_uint64 ((guint64) &ei->impl->port_list));
+  endpoint_props = g_variant_builder_end (&b);
+
+  /* Create and register the endpoint */
+  endpoint = wp_factory_make (ei->impl->wp_core, "pw-audio-softdsp-endpoint",
+      WP_TYPE_ENDPOINT, endpoint_props);
+  wp_endpoint_register (endpoint, ei->impl->wp_core);
+}
+
+static void enum_format_and_create_endpoint(gpointer p) {
+  struct endpoint_info *ei = p, *ei_copy = NULL;
+  WpPort *port = NULL;
+
+  /* Make sure the endpoint info is valid */
+  if (!ei)
+    return;
+
+  /* Find the unique alsa port */
+  spa_list_for_each(port, &ei->impl->port_list, l) {
+    if (port->parent_id == ei->id)
+      break;
+  }
+  
+  /* Emit the port EnumFormat */
+  pw_port_proxy_enum_params((struct pw_port_proxy*)port->proxy, 0,
+          SPA_PARAM_EnumFormat, 0, -1, NULL);
+
+  /* Copy endpoint info */
+  ei_copy = g_new0(struct endpoint_info, 1);
+  ei_copy->impl = ei->impl;
+  ei_copy->id = ei->id;
+  ei_copy->name = g_strdup(ei->name);
+  ei_copy->media_class = g_strdup(ei->media_class);
+
+  /* Forward the endpoint creation until the port EnumFormat is emitted */
+  sync_core_with_callabck(ei->impl, create_endpoint, ei_copy,
+      endpoint_info_destroy);
+}
+
 static void
 handle_node(struct impl *impl, uint32_t id, uint32_t parent_id,
             const struct spa_dict *props)
 {
-  const gchar *media_class = NULL, *node_name = NULL;
-  struct spa_proxy *proxy = NULL;
-  GVariantBuilder b;
-  g_autoptr(GVariant) endpoint_props = NULL;
-  g_autoptr (WpEndpoint) endpoint = NULL;
+  struct endpoint_info *ei = NULL;
+  const gchar *media_class = NULL, *name = NULL;
 
   /* Make sure the node has properties */
   if (!props) {
@@ -40,33 +169,101 @@ handle_node(struct impl *impl, uint32_t id, uint32_t parent_id,
     return;
   }
 
-  /* Make sure the media class is audio */
-  /* FIXME: need to handle only alsa nodes */
+  /* Get the name and media_class */
+  name = spa_dict_lookup(props, "node.name");
   media_class = spa_dict_lookup(props, "media.class");
+
+  /* Make sure the media class is non-dsp audio */
   if (!g_str_has_prefix (media_class, "Audio/"))
     return;
+  if (g_str_has_prefix (media_class, "Audio/DSP"))
+    return;
+
+  /* Create the endpoint info */
+  ei = g_new0(struct endpoint_info, 1);
+  ei->impl = impl;
+  ei->id = id;
+  ei->name = g_strdup(name);
+  ei->media_class = g_strdup(media_class);
+
+  /* Delay the creation of the endpoint until all ports have been created */
+  sync_core_with_callabck(impl, enum_format_and_create_endpoint, ei,
+      endpoint_info_destroy);
+}
+
+static void port_event_info(void *data, const struct pw_port_info *info)
+{
+  WpPort *port = data;
+  port->info  = pw_port_info_update(port->info, info);
+}
+
+static void port_event_param(void *data, int seq, uint32_t id, uint32_t index,
+  uint32_t next, const struct spa_pod *param)
+{
+  WpPort *port = data;
+
+  /* Only handle EnumFormat */
+  if (id != SPA_PARAM_EnumFormat)
+    return;
+
+  /* Parse the format */
+  if (spa_format_parse(param, &port->media_type, &port->media_subtype) < 0)
+    return;
+
+  /* Only handle RAW audio types */
+  if (port->media_type != SPA_MEDIA_TYPE_audio ||
+      port->media_subtype != SPA_MEDIA_SUBTYPE_raw)
+    return;
+
+  /* Parse the raw audio format */
+  spa_pod_fixate((struct spa_pod*)param);
+  spa_format_audio_raw_parse(param, &port->format);
+}
+
+static const struct pw_port_proxy_events port_events = {
+  PW_VERSION_PORT_PROXY_EVENTS,
+  .info = port_event_info,
+  .param = port_event_param,
+};
+
+static void
+handle_port(struct impl *impl, uint32_t id, uint32_t parent_id,
+            const struct spa_dict *props)
+{
+  struct pw_proxy *proxy;
+  WpPort *port;
+  const char *direction_prop;
+  
+  /* Make sure the port has porperties */
+  if (!props)
+    return;
 
-  /* Get the device name */
-  node_name = spa_dict_lookup(props, "node.name");
+  /* Get the direction property */
+  direction_prop = spa_dict_lookup(props, "port.direction");
+  if (!direction_prop)
+    return;
 
   /* Get the proxy */
   proxy = pw_registry_proxy_bind (impl->registry_proxy, id,
-      PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, 0);
+      PW_TYPE_INTERFACE_Port, PW_VERSION_NODE, sizeof(WpPort));
+  if (!proxy)
+    return;
 
-  /* Build the GVariant properties for the endpoint */
-  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
-  g_variant_builder_add (&b, "{sv}", "name",
-      g_variant_new_take_string (g_strdup_printf ("Endpoint %u: %s", id, node_name)));
-  g_variant_builder_add (&b, "{sv}",
-      "media-class", g_variant_new_string (media_class));
-  g_variant_builder_add (&b, "{sv}",
-      "node-proxy", g_variant_new_uint64 ((guint64) proxy));
-  endpoint_props = g_variant_builder_end (&b);
+  /* Get the port */
+  port = pw_proxy_get_user_data(proxy);
 
-  /* Create the endpoint */
-  endpoint = wp_factory_make (impl->wp_core, "pw-audio-softdsp-endpoint",
-      WP_TYPE_ENDPOINT, endpoint_props);
-  wp_endpoint_register (endpoint, impl->wp_core);
+  /* Set the info */
+  port->id = id;
+  port->parent_id = parent_id;
+  port->direction =
+      !strcmp(direction_prop, "out") ? PW_DIRECTION_OUTPUT : PW_DIRECTION_INPUT;
+
+  /* Set the proxy and listener */
+  port->proxy = proxy;
+  pw_proxy_add_proxy_listener(proxy, &port->listener, &port_events, port);
+
+  /* Add the port to the list */
+  spa_list_append(&impl->port_list, &port->l);
 }
 
 static void
@@ -76,12 +273,15 @@ registry_global(void *data,uint32_t id, uint32_t parent_id,
 {
   struct impl *impl = data;
 
-  /* Only handle nodes */
   switch (type) {
   case PW_TYPE_INTERFACE_Node:
     handle_node(impl, id, parent_id, props);
     break;
 
+  case PW_TYPE_INTERFACE_Port:
+    handle_port(impl, id, parent_id, props);
+    break;
+
   default:
     break;
   }
@@ -92,16 +292,35 @@ static const struct pw_registry_proxy_events registry_events = {
   .global = registry_global,
 };
 
+static void core_done(void *d, uint32_t id, int seq)
+{
+  struct impl * impl = d;
+  struct done_data * dd = NULL;
+
+  /* Process all the done_data queue */
+  while ((dd = g_queue_pop_head(impl->done_queue))) {
+    if (dd->callback)
+      dd->callback(dd->data);
+    done_data_destroy(dd);
+  }
+}
+
+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;
-  struct pw_core_proxy *core_proxy;
 
   switch (state) {
   case PW_REMOTE_STATE_CONNECTED:
-    core_proxy = pw_remote_get_core_proxy (impl->remote);
-    impl->registry_proxy = pw_core_proxy_get_registry (core_proxy,
+    impl->core_proxy = pw_remote_get_core_proxy (impl->remote);
+    pw_core_proxy_add_listener(impl->core_proxy, &impl->core_listener,
+        &core_events, impl);
+    impl->registry_proxy = pw_core_proxy_get_registry (impl->core_proxy,
         PW_TYPE_INTERFACE_Registry, PW_VERSION_REGISTRY, 0);
     pw_registry_proxy_add_listener(impl->registry_proxy,
         &impl->registry_listener, &registry_events, impl);
@@ -121,6 +340,7 @@ static void
 module_destroy (gpointer data)
 {
   struct impl *impl = data;
+  g_queue_free_full(impl->done_queue, done_data_destroy);
   g_slice_free (struct impl, impl);
 }
 
@@ -136,10 +356,13 @@ wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
   struct impl *impl = g_new0(struct impl, 1);
   impl->wp_core = core;
   impl->remote = wp_core_get_global(core, WP_GLOBAL_PW_REMOTE);
+  impl->done_queue = g_queue_new();
+  spa_list_init(&impl->port_list);
 
   /* Set destroy callback for impl */
   wp_module_set_destroy_callback (module, module_destroy, impl);
 
   /* Add a state changed listener */
-  pw_remote_add_listener(impl->remote, &impl->remote_listener, &remote_events, impl);
+  pw_remote_add_listener(impl->remote, &impl->remote_listener, &remote_events,
+      impl);
 }
diff --git a/modules/module-pw-audio-softdsp-endpoint.c b/modules/module-pw-audio-softdsp-endpoint.c
index 052eff2fee350c8007c0d41658907a7fce2532b6..19d469b7d8a2f261bca086dfb7d1a4f21bbe15d6 100644
--- a/modules/module-pw-audio-softdsp-endpoint.c
+++ b/modules/module-pw-audio-softdsp-endpoint.c
@@ -15,10 +15,44 @@
 
 #include <wp/wp.h>
 #include <pipewire/pipewire.h>
+#include <spa/param/audio/format-utils.h>
 
-struct _WpPwAudioSoftdspEndpoint
-{
+#include "module-pipewire/port.h"
+
+#define MIN_QUANTUM_SIZE  64
+#define MAX_QUANTUM_SIZE  1024
+
+struct _WpPwAudioSoftdspEndpoint {
   WpEndpoint parent;
+  
+  /* The core proxy */
+  struct pw_core_proxy *core_proxy;
+
+  /* Node proxy and listener */
+  struct pw_proxy *node_proxy;
+  struct spa_hook listener;
+  struct spa_hook proxy_listener;
+
+  /* Node info */
+  struct pw_node_info *node_info;
+  uint32_t media_type;
+  uint32_t media_subtype;
+  struct spa_audio_info_raw format;
+  enum pw_direction direction;
+
+  /* DSP proxy and listener */
+  struct pw_proxy *dsp_proxy;
+  struct spa_hook dsp_listener;
+  
+  /* DSP info */
+  struct pw_node_info *dsp_info;
+
+  /* Link proxy and listener */
+  struct pw_proxy *link_proxy;
+
+  /* The all port list reference */
+  /* TODO: make it thread safe */
+  struct spa_list *port_list;
 };
 
 G_DECLARE_FINAL_TYPE (WpPwAudioSoftdspEndpoint, endpoint,
@@ -31,44 +65,325 @@ endpoint_init (WpPwAudioSoftdspEndpoint * self)
 {
 }
 
+static void
+endpoint_finalize (GObject * object)
+{
+  WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (object);
+
+  /* Remove and destroy the node_proxy */
+  if (self->node_proxy) {
+    spa_hook_remove (&self->listener);
+    pw_proxy_destroy ((struct pw_proxy *) self->node_proxy);
+  }
+
+  /* Remove and destroy the dsp_proxy */
+  if (self->dsp_proxy) {
+    spa_hook_remove (&self->dsp_listener);
+    pw_proxy_destroy ((struct pw_proxy *) self->dsp_proxy);
+  }
+
+  G_OBJECT_CLASS (endpoint_parent_class)->finalize (object);
+}
+
 static gboolean
-endpoint_prepare_link (WpEndpoint * self, guint32 stream_id,
+endpoint_prepare_link (WpEndpoint * ep, guint32 stream_id,
     WpEndpointLink * link, GVariant ** properties, GError ** error)
 {
+  WpPwAudioSoftdspEndpoint *self = WP_PW_AUDIO_SOFTDSP_ENDPOINT (ep);
+  WpPort *port = NULL, *node_port = NULL, *dsp_port = NULL;
+  GVariantBuilder b;
+
+  /* Find the node port */
+  spa_list_for_each(port, self->port_list, l) {
+    if (self->node_info->id == port->parent_id) {
+      node_port = port;
+      break;
+    }
+  }
+  if (!node_port)
+    return FALSE;
+  
+  /* Find the first dsp port with the same direction as the node port */
+  spa_list_for_each(port, self->port_list, l) {
+    if (self->dsp_info->id == port->parent_id
+        && port->direction == node_port->direction) {
+      dsp_port = port;
+      break;
+    }
+  }
+  if (!dsp_port)
+    return FALSE;
+
+  /* Set the properties */
+  g_variant_builder_init (&b, G_VARIANT_TYPE_VARDICT);
+  g_variant_builder_add (&b, "{sv}", "node-id",
+      g_variant_new_uint32 (self->dsp_info->id));
+  g_variant_builder_add (&b, "{sv}", "node-port-id",
+      g_variant_new_uint32 (dsp_port->id));
+  *properties = g_variant_builder_end (&b);
+
   return TRUE;
 }
 
 static void
 endpoint_class_init (WpPwAudioSoftdspEndpointClass * klass)
 {
+  GObjectClass *object_class = (GObjectClass *) klass;
   WpEndpointClass *endpoint_class = (WpEndpointClass *) klass;
 
+  object_class->finalize = endpoint_finalize;
+
   endpoint_class->prepare_link = endpoint_prepare_link;
 }
 
+static void on_dsp_running(WpPwAudioSoftdspEndpoint *self)
+{
+  struct pw_properties *props;
+
+  /* Return if the node has already been linked */
+  if (self->link_proxy)
+    return;
+
+  /* Create new properties */
+  props = pw_properties_new(NULL, NULL);
+
+  /* Set the new properties */
+  pw_properties_set(props, PW_LINK_PROP_PASSIVE, "true");
+  if (self->direction == PW_DIRECTION_OUTPUT) {
+    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", self->dsp_info->id);
+    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
+    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", self->node_info->id);
+    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
+  } else {
+    pw_properties_setf(props, PW_LINK_OUTPUT_NODE_ID, "%d", self->node_info->id);
+    pw_properties_setf(props, PW_LINK_OUTPUT_PORT_ID, "%d", -1);
+    pw_properties_setf(props, PW_LINK_INPUT_NODE_ID, "%d", self->dsp_info->id);
+    pw_properties_setf(props, PW_LINK_INPUT_PORT_ID, "%d", -1);
+  }
+
+  /* Create the link */
+  self->link_proxy = pw_core_proxy_create_object(self->core_proxy,
+      "link-factory", PW_TYPE_INTERFACE_Link, PW_VERSION_LINK, &props->dict, 0);
+
+  /* Clean up */
+  pw_properties_free(props);
+}
+
+static void dsp_node_event_info(void *data, const struct pw_node_info *info)
+{
+  WpPwAudioSoftdspEndpoint *self = data;
+
+  /* Set dsp info */
+  self->dsp_info = pw_node_info_update(self->dsp_info, info);
+  
+  /* Handle the different states */
+  switch (info->state) {
+  case PW_NODE_STATE_IDLE:
+          break;
+  case PW_NODE_STATE_RUNNING:
+          on_dsp_running(self);
+          break;
+  case PW_NODE_STATE_SUSPENDED:
+          break;
+  default:
+          break;
+  }
+}
+
+static const struct pw_node_proxy_events dsp_node_events = {
+  PW_VERSION_NODE_PROXY_EVENTS,
+  .info = dsp_node_event_info,
+};
+
+static void emit_audio_dsp_node(WpPwAudioSoftdspEndpoint *self)
+{
+  struct pw_properties *props;
+  const char *dsp_name = NULL;
+  uint8_t buf[1024];
+  struct spa_pod_builder pod_builder = { 0, };
+  struct spa_pod *param;
+  
+  /* Return if the node has been already emitted */
+  if (self->dsp_proxy)
+    return;
+  
+  /* Get the properties */
+  props = pw_properties_new_dict(self->node_info->props);
+  if (!props)
+    return;
+
+  /* Get the DSP name */
+  dsp_name = pw_properties_get(props, "device.nick");
+  if (!dsp_name)
+    dsp_name = self->node_info->name;
+
+  /* Set the properties */
+  pw_properties_set(props, "audio-dsp.name", dsp_name);
+  pw_properties_setf(props, "audio-dsp.direction", "%d", self->direction);
+  pw_properties_setf(props, "audio-dsp.maxbuffer", "%ld", MAX_QUANTUM_SIZE * sizeof(float));
+
+  /* Set the DSP proxy and listener */
+  self->dsp_proxy = pw_core_proxy_create_object(self->core_proxy, "audio-dsp",
+      PW_TYPE_INTERFACE_Node, PW_VERSION_NODE, &props->dict, 0);
+  pw_proxy_add_proxy_listener(self->dsp_proxy, &self->dsp_listener,
+      &dsp_node_events, self);
+
+  /* Set DSP proxy params */
+  spa_pod_builder_init(&pod_builder, buf, sizeof(buf));
+  param = spa_format_audio_raw_build(&pod_builder, SPA_PARAM_Format, &self->format);
+  param = spa_pod_builder_add_object(&pod_builder,
+      SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
+      SPA_PARAM_PROFILE_direction,  SPA_POD_Id(pw_direction_reverse(self->direction)),
+      SPA_PARAM_PROFILE_format,     SPA_POD_Pod(param));
+  pw_node_proxy_set_param((struct pw_node_proxy*)self->dsp_proxy,
+      SPA_PARAM_Profile, 0, param);
+  
+  /* Clean up */    
+  pw_properties_free(props);
+}
+
+static void node_event_info(void *data, const struct pw_node_info *info)
+{
+  WpPwAudioSoftdspEndpoint *self = data;
+  WpPort *port = NULL;
+
+  /* Set the node info */
+  self->node_info = pw_node_info_update(self->node_info, info);
+
+  /* Find the node port */
+  spa_list_for_each(port, self->port_list, l) {
+    if (port->parent_id == self->node_info->id)
+      break;
+  }
+
+  /* Set the format using the port format */
+  self->format.format = port->format.format;
+  self->format.flags = port->format.flags;
+  self->format.rate = port->format.rate;
+  self->format.channels = port->format.channels;
+  for (int i = 0; i < port->format.channels; ++i)
+    self->format.position[i] = port->format.position[i];
+
+  /* Emit the audio DSP node */
+  emit_audio_dsp_node(self);
+
+  /* TODO: Handle the different states */
+  switch (info->state) {
+  case PW_NODE_STATE_IDLE:
+    break;
+  case PW_NODE_STATE_RUNNING:
+    break;
+  case PW_NODE_STATE_SUSPENDED:
+    break;
+  default:
+    break;
+  }
+}
+
+static void node_proxy_destroy(void *data)
+{
+  WpPwAudioSoftdspEndpoint *self = data;
+
+  /* Clear node_info */
+  if (self->node_info)
+    pw_node_info_free(self->node_info);
+
+  /* Clear dsp_info */
+  if (self->dsp_info)
+    pw_node_info_free(self->dsp_info);
+
+  wp_endpoint_unregister (WP_ENDPOINT (self));
+}
+
+static const struct pw_proxy_events node_proxy_events = {
+  PW_VERSION_PROXY_EVENTS,
+  .destroy = node_proxy_destroy,
+};
+
+static const struct pw_node_proxy_events node_events = {
+  PW_VERSION_NODE_PROXY_EVENTS,
+  .info = node_event_info,
+};
+
 static gpointer
 endpoint_factory (WpFactory * factory, GType type, GVariant * properties)
 {
+  WpCore *wp_core = NULL;
+  struct pw_remote *remote;
   const gchar *name = NULL;
   const gchar *media_class = NULL;
+  guint64 proxy, port_list;
 
+  /* Make sure the type is not the base class type */
   if (type != WP_TYPE_ENDPOINT)
     return NULL;
 
+  /* Get the WirePlumber core */
+  wp_core = wp_factory_get_core(factory);
+  if (!wp_core) {
+    g_warning("failed to get wireplumbe core. Skipping...");
+    return NULL;
+  }
+
+  /* Get the remote */
+  remote = wp_core_get_global(wp_core, WP_GLOBAL_PW_REMOTE);
+  if (!remote) {
+    g_warning("failed to get core remote. Skipping...");
+    return NULL;
+  }
+
   /* Get the name and media-class */
   if (!g_variant_lookup (properties, "name", "&s", &name))
       return NULL;
   if (!g_variant_lookup (properties, "media-class", "&s", &media_class))
       return NULL;
+  if (!g_variant_lookup (properties, "node-proxy", "t", &proxy))
+      return NULL;
+  if (!g_variant_lookup (properties, "port-list", "t", &port_list))
+      return NULL;
 
-  return g_object_new (endpoint_get_type (),
+  /* Create the softdsp endpoint object */
+  WpPwAudioSoftdspEndpoint *ep = g_object_new (endpoint_get_type (),
       "name", name,
       "media-class", media_class,
       NULL);
+  if (!ep)
+    return NULL;
+
+  /* Set the direction */
+  if (g_str_has_suffix(media_class, "Source")) {
+    ep->direction = PW_DIRECTION_INPUT;
+  } else if (g_str_has_suffix(media_class, "Sink")) {
+    ep->direction = PW_DIRECTION_OUTPUT;
+  } else {
+    g_warning("failed to parse direction. Skipping...");
+    return NULL;
+  }
+
+  /* Set the port list reference */
+  ep->port_list = (gpointer) port_list;
+
+  /* Set the core proxy */
+  ep->core_proxy = pw_remote_get_core_proxy(remote);
+  if (!ep->core_proxy) {
+    g_warning("failed to get core proxy. Skipping...");
+    return NULL;
+  }
+
+  /* Set the node proxy and listener */
+  ep->node_proxy = (gpointer) proxy;
+  pw_proxy_add_listener (ep->node_proxy, &ep->listener, &node_proxy_events,
+      ep);
+  pw_proxy_add_proxy_listener(ep->node_proxy, &ep->proxy_listener,
+      &node_events, ep);
+
+  /* Return the object */
+  return ep;
 }
 
 void
 wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
 {
+  /* Register the softdsp endpoint */
   wp_factory_new (core, "pw-audio-softdsp-endpoint", endpoint_factory);
 }
diff --git a/modules/module-pw-simple-policy.c b/modules/module-pw-simple-policy.c
new file mode 100644
index 0000000000000000000000000000000000000000..1c9eecf731dc8904d85769d399c5f836b186d622
--- /dev/null
+++ b/modules/module-pw-simple-policy.c
@@ -0,0 +1,206 @@
+/* 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 void endpoint_first_foreach(WpEndpoint *ep, WpEndpoint **first)
+{
+  /* Just return if first is already set */
+  if (*first)
+    return;
+
+  /* Set first to the current endpoint */
+  *first = g_object_ref(ep);
+}
+
+static WpEndpoint *endpoint_get_first(WpCore *core,
+    const char *media_class)
+{
+  WpEndpoint *first = NULL;
+  GPtrArray *ptr_array = NULL;
+
+  /* Get all the endpoints with the specific media lcass*/
+  ptr_array = wp_endpoint_find (core, media_class);
+  if (!ptr_array)
+    return NULL;
+
+  /* Get the first endpoint of the list */
+  g_ptr_array_foreach(ptr_array, (GFunc)endpoint_first_foreach, &first);
+
+  /* Return the first endpoint */
+  return first;
+}
+
+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_first(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/src/wireplumber.conf b/src/wireplumber.conf
index f32fe61ad503761c18f4e7d2b73aa0278e4ec959..8a35ac0f16c07e40c463930fbf949890321e7b75 100644
--- a/src/wireplumber.conf
+++ b/src/wireplumber.conf
@@ -1,3 +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