diff --git a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala
index 128dc097..05320b31 100644
--- a/modules/backend/src/main/scala/docspell/backend/JobFactory.scala
+++ b/modules/backend/src/main/scala/docspell/backend/JobFactory.scala
@@ -11,7 +11,6 @@ import cats.implicits._
 
 import docspell.backend.MailAddressCodec
 import docspell.common._
-import docspell.notification.api.ChannelOrRef._
 import docspell.notification.api.PeriodicQueryArgs
 import docspell.store.records.RJob
 
@@ -25,7 +24,7 @@ object JobFactory extends MailAddressCodec {
         PeriodicQueryArgs.taskName,
         submitter.collective,
         args,
-        s"Running periodic query, notify via ${args.channel.channelType}",
+        s"Running periodic query, notify via ${args.channels.map(_.channelType)}",
         now,
         submitter.user,
         Priority.Low,
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala
index efa54b71..1ed08014 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/ONotification.scala
@@ -39,7 +39,10 @@ trait ONotification[F[_]] {
       userId: Ident
   ): F[Vector[NotificationChannel]]
 
-  def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]]
+  def findNotificationChannel(
+      ref: ChannelRef,
+      account: AccountId
+  ): F[Vector[NotificationChannel]]
 
   def listChannels(account: AccountId): F[Vector[Channel]]
 
@@ -65,7 +68,7 @@ trait ONotification[F[_]] {
 
   def sendSampleEvent(
       evt: EventType,
-      channel: Channel,
+      channel: Nel[ChannelRef],
       account: AccountId,
       baseUrl: Option[LenientUri]
   ): F[ONotification.SendTestResult]
@@ -89,7 +92,7 @@ object ONotification {
           .getOrElse(UpdateResult.notFound)
 
       def offerEvents(ev: Iterable[Event]): F[Unit] =
-        ev.toList.traverse(notMod.offer(_)).as(())
+        ev.toList.traverse(notMod.offer).as(())
 
       def sendMessage(
           logger: Logger[F],
@@ -109,33 +112,27 @@ object ONotification {
 
       def sendSampleEvent(
           evt: EventType,
-          channel: Channel,
+          channels: Nel[ChannelRef],
           account: AccountId,
           baseUrl: Option[LenientUri]
-      ): F[SendTestResult] = {
-        def doCreate(userId: Ident) =
-          (for {
-            ev <- sampleEvent(evt, account, baseUrl)
-            logbuf <- Logger.buffer()
-            ch <- mkNotificationChannel(channel, userId)
-            _ <- notMod.send(logbuf._2.andThen(log), ev, ch)
-            logs <- logbuf._1.get
-            res = SendTestResult(true, logs)
-          } yield res).attempt
-            .map {
-              case Right(res) => res
-              case Left(ex) =>
-                val ps = new StringWriter()
-                ex.printStackTrace(new PrintWriter(ps))
-                SendTestResult(false, Vector(s"${ex.getMessage}\n$ps"))
-            }
-
-        OptionT(store.transact(RUser.findIdByAccount(account)))
-          .semiflatMap(doCreate)
-          .getOrElse(
-            SendTestResult(false, Vector(s"No user found in db for: ${account.asString}"))
+      ): F[SendTestResult] =
+        (for {
+          ev <- sampleEvent(evt, account, baseUrl)
+          logbuf <- Logger.buffer()
+          ch <- channels.toList.toVector.flatTraverse(
+            findNotificationChannel(_, account)
           )
-      }
+          _ <- notMod.send(logbuf._2.andThen(log), ev, ch)
+          logs <- logbuf._1.get
+          res = SendTestResult(true, logs)
+        } yield res).attempt
+          .map {
+            case Right(res) => res
+            case Left(ex) =>
+              val ps = new StringWriter()
+              ex.printStackTrace(new PrintWriter(ps))
+              SendTestResult(false, Vector(s"${ex.getMessage}\n$ps"))
+          }
 
       def listChannels(account: AccountId): F[Vector[Channel]] =
         store
@@ -153,7 +150,7 @@ object ONotification {
         (for {
           newId <- OptionT.liftF(Ident.randomId[F])
           userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
-          r <- ChannelConv.makeRecord[F](store, log, Right(channel), newId, userId)
+          r <- ChannelConv.makeRecord[F](store, channel, newId, userId)
           _ <- OptionT.liftF(store.transact(RNotificationChannel.insert(r)))
           _ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
         } yield AddResult.Success)
@@ -162,7 +159,7 @@ object ONotification {
       def updateChannel(channel: Channel, account: AccountId): F[UpdateResult] =
         (for {
           userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
-          r <- ChannelConv.makeRecord[F](store, log, Right(channel), channel.id, userId)
+          r <- ChannelConv.makeRecord[F](store, channel, channel.id, userId)
           n <- OptionT.liftF(store.transact(RNotificationChannel.update(r)))
         } yield UpdateResult.fromUpdateRows(n)).getOrElse(UpdateResult.notFound)
 
@@ -179,16 +176,14 @@ object ONotification {
       def createHook(hook: Hook, account: AccountId): F[AddResult] =
         (for {
           _ <- OptionT.liftF(log.debug(s"Creating new notification hook: $hook"))
-          channelId <- OptionT.liftF(Ident.randomId[F])
           userId <- OptionT(store.transact(RUser.findIdByAccount(account)))
-          r <- ChannelConv.makeRecord[F](store, log, hook.channel, channelId, userId)
+          hr <- OptionT.liftF(Hook.makeRecord(userId, hook))
           _ <- OptionT.liftF(
-            if (channelId == r.id) store.transact(RNotificationChannel.insert(r))
-            else ().pure[F]
+            store.transact(
+              RNotificationHook.insert(hr) *> RNotificationHookChannel
+                .updateAll(hr.id, hook.channels.toList)
+            )
           )
-          _ <- OptionT.liftF(log.debug(s"Created channel $r for $account"))
-          hr <- OptionT.liftF(Hook.makeRecord(r, userId, hook))
-          _ <- OptionT.liftF(store.transact(RNotificationHook.insert(hr)))
           _ <- OptionT.liftF(
             store.transact(RNotificationHookEvent.insertAll(hr.id, hook.events))
           )
@@ -203,31 +198,25 @@ object ONotification {
               .getOrElse(UpdateResult.notFound)
           )
 
-        def withChannel(
-            r: RNotificationHook
-        )(f: RNotificationChannel => F[UpdateResult]): F[UpdateResult] =
-          ChannelConv
-            .makeRecord(store, log, hook.channel, r.channelId, r.uid)
-            .semiflatMap(f)
-            .getOrElse(UpdateResult.notFound)
-
         def doUpdate(r: RNotificationHook): F[UpdateResult] =
-          withChannel(r) { ch =>
-            UpdateResult.fromUpdate(store.transact(for {
-              nc <- RNotificationChannel.update(ch)
-              ne <- RNotificationHookEvent.updateAll(
-                r.id,
-                if (hook.allEvents) Nil else hook.events
+          UpdateResult.fromUpdate(store.transact(for {
+            ne <- RNotificationHookEvent.updateAll(
+              r.id,
+              if (hook.allEvents) Nil else hook.events
+            )
+            nc <- RNotificationHookChannel.updateAll(
+              r.id,
+              hook.channels.toList
+            )
+            nr <- RNotificationHook.update(
+              r.copy(
+                enabled = hook.enabled,
+                allEvents = hook.allEvents,
+                eventFilter = hook.eventFilter
               )
-              nr <- RNotificationHook.update(
-                r.copy(
-                  enabled = hook.enabled,
-                  allEvents = hook.allEvents,
-                  eventFilter = hook.eventFilter
-                )
-              )
-            } yield nc + ne + nr))
-          }
+            )
+
+          } yield nc + ne + nr))
 
         withHook(doUpdate)
       }
@@ -238,13 +227,17 @@ object ONotification {
       ): F[Vector[NotificationChannel]] =
         (for {
           rec <- ChannelConv
-            .makeRecord(store, log, Right(channel), channel.id, userId)
+            .makeRecord(store, channel, channel.id, userId)
           ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
         } yield ch).getOrElse(Vector.empty)
 
-      def findNotificationChannel(ref: ChannelRef): F[Vector[NotificationChannel]] =
+      def findNotificationChannel(
+          ref: ChannelRef,
+          accountId: AccountId
+      ): F[Vector[NotificationChannel]] =
         (for {
-          rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref)))
+          userId <- OptionT(store.transact(RUser.findIdByAccount(accountId)))
+          rec <- OptionT(store.transact(RNotificationChannel.getByRef(ref, userId)))
           ch <- OptionT.liftF(store.transact(QNotification.readChannel(rec)))
         } yield ch).getOrElse(Vector.empty)
     })
@@ -264,84 +257,30 @@ object ONotification {
           Channel.Gotify(r.id, gotify.name, gotify.url, gotify.appKey, gotify.priority),
         matrix =>
           Channel
-            .Matrix(r.id, matrix.name, matrix.homeServer, matrix.roomId, matrix.accessToken),
+            .Matrix(
+              r.id,
+              matrix.name,
+              matrix.homeServer,
+              matrix.roomId,
+              matrix.accessToken
+            ),
         http => Channel.Http(r.id, http.name, http.url)
       )
 
-    private[ops] def makeRecord[F[_]: Sync](
+    private[ops] def makeRecord[F[_]](
         store: Store[F],
-        logger: Logger[F],
-        channelIn: Either[ChannelRef, Channel],
+        channel: Channel,
         id: Ident,
         userId: Ident
     ): OptionT[F, RNotificationChannel] =
-      channelIn match {
-        case Left(ref) =>
-          OptionT.liftF(logger.debug(s"Loading channel for ref: ${ref}")) *>
-            OptionT(store.transact(RNotificationChannel.getByRef(ref)))
+      RNotificationChannel.fromChannel(channel, id, userId).mapK(store.transform)
 
-        case Right(channel) =>
-          for {
-            time <- OptionT.liftF(Timestamp.current[F])
-            r <-
-              channel match {
-                case Channel.Mail(_, name, conn, recipients) =>
-                  for {
-                    _ <- OptionT.liftF(
-                      logger.debug(
-                        s"Looking up user smtp for ${userId.id} and ${conn.id}"
-                      )
-                    )
-                    mailConn <- OptionT(
-                      store.transact(RUserEmail.getByUser(userId, conn))
-                    )
-                    rec = RNotificationChannelMail(
-                      id,
-                      userId,
-                      name,
-                      mailConn.id,
-                      recipients.toList,
-                      time
-                    ).vary
-                  } yield rec
-                case Channel.Gotify(_, name, url, appKey, prio) =>
-                  OptionT.pure[F](
-                    RNotificationChannelGotify(
-                      id,
-                      userId,
-                      name,
-                      url,
-                      appKey,
-                      prio,
-                      time
-                    ).vary
-                  )
-                case Channel.Matrix(_, name, homeServer, roomId, accessToken) =>
-                  OptionT.pure[F](
-                    RNotificationChannelMatrix(
-                      id,
-                      userId,
-                      name,
-                      homeServer,
-                      roomId,
-                      accessToken,
-                      "m.text",
-                      time
-                    ).vary
-                  )
-                case Channel.Http(_, name, url) =>
-                  OptionT.pure[F](
-                    RNotificationChannelHttp(id, userId, name, url, time).vary
-                  )
-              }
-          } yield r
-      }
   }
 
   final case class Hook(
       id: Ident,
       enabled: Boolean,
-      channel: Either[ChannelRef, Channel],
+      channels: List[ChannelRef],
       allEvents: Boolean,
       eventFilter: Option[JsonMiniQuery],
       events: List[EventType]
@@ -354,14 +293,12 @@ object ONotification {
         r: RNotificationHook,
         events: List[EventType]
     ): ConnectionIO[Hook] =
-      RNotificationChannel
-        .getByHook(r)
-        .map(_.head)
-        .map(ChannelConv.makeChannel)
-        .map(ch => Hook(r.id, r.enabled, Right(ch), r.allEvents, r.eventFilter, events))
+      RNotificationHookChannel
+        .allOfNel(r.id)
+        .flatMap(rhcs => RNotificationHookChannel.resolveRefs(rhcs))
+        .map(refs => Hook(r.id, r.enabled, refs, r.allEvents, r.eventFilter, events))
 
     private[ops] def makeRecord[F[_]: Sync](
-        ch: RNotificationChannel,
         userId: Ident,
         hook: Hook
     ): F[RNotificationHook] =
@@ -372,10 +309,6 @@ object ONotification {
           id,
           userId,
           hook.enabled,
-          ch.fold(_.id.some, _ => None, _ => None, _ => None),
-          ch.fold(_ => None, _.id.some, _ => None, _ => None),
-          ch.fold(_ => None, _ => None, _.id.some, _ => None),
-          ch.fold(_ => None, _ => None, _ => None, _.id.some),
           hook.allEvents,
           hook.eventFilter,
           time
diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
index dfc77491..9485e4e0 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OUserTask.scala
@@ -11,7 +11,6 @@ import cats.effect._
 import cats.implicits._
 import fs2.Stream
 
-import docspell.backend.MailAddressCodec._
 import docspell.common._
 import docspell.notification.api.PeriodicDueItemsArgs
 import docspell.notification.api.PeriodicQueryArgs
diff --git a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala
index ebaa60b5..8ef5bc33 100644
--- a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicDueItemsTask.scala
@@ -7,7 +7,6 @@
 package docspell.joex.notify
 
 import cats.data.NonEmptyList
-import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
 
@@ -24,7 +23,6 @@ import docspell.query.ItemQueryDsl._
 import docspell.store.qb.Batch
 import docspell.store.queries.ListItem
 import docspell.store.queries.{QItem, Query}
-import docspell.store.records.RUser
 
 object PeriodicDueItemsTask {
   val taskName = PeriodicDueItemsArgs.taskName
@@ -51,11 +49,7 @@ object PeriodicDueItemsTask {
   def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])(
       cont: Vector[NotificationChannel] => F[Unit]
   ): F[Unit] =
-    OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account)))
-      .semiflatMap(userId =>
-        TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont)
-      )
-      .getOrElse(())
+    TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont)
 
   def withItems[F[_]: Sync](ctx: Context[F, Args], limit: Int, now: Timestamp)(
       cont: Vector[ListItem] => F[Unit]
diff --git a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala
index ad16f2b9..51f06b20 100644
--- a/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala
+++ b/modules/joex/src/main/scala/docspell/joex/notify/PeriodicQueryTask.scala
@@ -26,7 +26,6 @@ import docspell.store.queries.ListItem
 import docspell.store.queries.{QItem, Query}
 import docspell.store.records.RQueryBookmark
 import docspell.store.records.RShare
-import docspell.store.records.RUser
 
 object PeriodicQueryTask {
   val taskName = PeriodicQueryArgs.taskName
@@ -53,11 +52,7 @@ object PeriodicQueryTask {
   def withChannel[F[_]: Sync](ctx: Context[F, Args], ops: ONotification[F])(
       cont: Vector[NotificationChannel] => F[Unit]
   ): F[Unit] =
-    OptionT(ctx.store.transact(RUser.findIdByAccount(ctx.args.account)))
-      .semiflatMap(userId =>
-        TaskOperations.withChannel(ctx.logger, ctx.args.channel, userId, ops)(cont)
-      )
-      .getOrElse(())
+    TaskOperations.withChannel(ctx.logger, ctx.args.channels, ctx.args.account, ops)(cont)
 
   private def queryString(q: ItemQuery.Expr) =
     ItemQueryParser.asString(q)
diff --git a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala
index a1152c13..d57ba05e 100644
--- a/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala
+++ b/modules/joex/src/main/scala/docspell/joex/notify/TaskOperations.scala
@@ -12,7 +12,7 @@ import cats.implicits._
 
 import docspell.backend.ops.ONotification
 import docspell.common._
-import docspell.notification.api.ChannelOrRef
+import docspell.notification.api.ChannelRef
 import docspell.notification.api.Event
 import docspell.notification.api.EventContext
 import docspell.notification.api.NotificationChannel
@@ -23,19 +23,18 @@ trait TaskOperations {
 
   def withChannel[F[_]: Sync](
       logger: Logger[F],
-      channel: ChannelOrRef,
-      userId: Ident,
+      channelsIn: NonEmptyList[ChannelRef],
+      accountId: AccountId,
       ops: ONotification[F]
   )(
       cont: Vector[NotificationChannel] => F[Unit]
   ): F[Unit] = {
-    val channels = channel match {
-      case Right(ch) => ops.mkNotificationChannel(ch, userId)
-      case Left(ref) => ops.findNotificationChannel(ref)
-    }
+    val channels =
+      channelsIn.toList.toVector.flatTraverse(ops.findNotificationChannel(_, accountId))
+
     channels.flatMap { ch =>
       if (ch.isEmpty)
-        logger.error(s"No channels found for the given data: ${channel}")
+        logger.error(s"No channels found for the given data: ${channelsIn}")
       else cont(ch)
     }
   }
diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala b/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala
index 1735b610..fa9f785f 100644
--- a/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala
+++ b/modules/notification/api/src/main/scala/docspell/notification/api/Channel.scala
@@ -19,13 +19,14 @@ import io.circe.{Decoder, Encoder}
 sealed trait Channel {
   def id: Ident
   def channelType: ChannelType
+  def name: Option[String]
   def fold[A](
       f1: Channel.Mail => A,
       f2: Channel.Gotify => A,
       f3: Channel.Matrix => A,
       f4: Channel.Http => A
   ): A
-  def asRef: ChannelRef = ChannelRef(id, channelType)
+  def asRef: ChannelRef = ChannelRef(id, channelType, name)
 }
 
 object Channel {
@@ -98,7 +99,8 @@ object Channel {
     implicit val jsonEncoder: Encoder[Matrix] = deriveConfiguredEncoder
   }
 
-  final case class Http(id: Ident, name: Option[String], url: LenientUri) extends Channel {
+  final case class Http(id: Ident, name: Option[String], url: LenientUri)
+      extends Channel {
     val channelType = ChannelType.Http
     def fold[A](
         f1: Mail => A,
diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala
index cfd21d48..1cc3d191 100644
--- a/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala
+++ b/modules/notification/api/src/main/scala/docspell/notification/api/ChannelRef.scala
@@ -12,7 +12,7 @@ import io.circe.Decoder
 import io.circe.Encoder
 import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
 
-final case class ChannelRef(id: Ident, channelType: ChannelType)
+final case class ChannelRef(id: Ident, channelType: ChannelType, name: Option[String])
 
 object ChannelRef {
 
diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala
index fc10767f..7de991cf 100644
--- a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala
+++ b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicDueItemsArgs.scala
@@ -6,9 +6,10 @@
 
 package docspell.notification.api
 
+import cats.data.NonEmptyList
+
 import docspell.common._
 
-import emil.MailAddress
 import io.circe.generic.semiauto
 import io.circe.{Decoder, Encoder}
 
@@ -21,7 +22,7 @@ import io.circe.{Decoder, Encoder}
   */
 final case class PeriodicDueItemsArgs(
     account: AccountId,
-    channel: ChannelOrRef,
+    channels: NonEmptyList[ChannelRef],
     remindDays: Int,
     daysBack: Option[Int],
     tagsInclude: List[Ident],
@@ -30,19 +31,11 @@ final case class PeriodicDueItemsArgs(
 )
 
 object PeriodicDueItemsArgs {
-  val taskName = Ident.unsafe("periodic-due-items-notify")
+  val taskName = Ident.unsafe("periodic-due-items-notify2")
 
-  implicit def jsonDecoder(implicit
-      mc: Decoder[MailAddress]
-  ): Decoder[PeriodicDueItemsArgs] = {
-    implicit val x = ChannelOrRef.jsonDecoder
+  implicit val jsonDecoder: Decoder[PeriodicDueItemsArgs] =
     semiauto.deriveDecoder
-  }
 
-  implicit def jsonEncoder(implicit
-      mc: Encoder[MailAddress]
-  ): Encoder[PeriodicDueItemsArgs] = {
-    implicit val x = ChannelOrRef.jsonEncoder
+  implicit val jsonEncoder: Encoder[PeriodicDueItemsArgs] =
     semiauto.deriveEncoder
-  }
 }
diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala
index cf1c16a1..8e0dd405 100644
--- a/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala
+++ b/modules/notification/api/src/main/scala/docspell/notification/api/PeriodicQueryArgs.scala
@@ -6,15 +6,16 @@
 
 package docspell.notification.api
 
+import cats.data.NonEmptyList
+
 import docspell.common._
 
-import emil.MailAddress
 import io.circe.generic.semiauto
 import io.circe.{Decoder, Encoder}
 
 final case class PeriodicQueryArgs(
     account: AccountId,
-    channel: ChannelOrRef,
+    channels: NonEmptyList[ChannelRef],
     query: Option[ItemQueryString],
     bookmark: Option[String],
     baseUrl: Option[LenientUri],
@@ -22,19 +23,11 @@ final case class PeriodicQueryArgs(
 )
 
 object PeriodicQueryArgs {
-  val taskName = Ident.unsafe("periodic-query-notify")
+  val taskName = Ident.unsafe("periodic-query-notify2")
 
-  implicit def jsonDecoder(implicit
-      mc: Decoder[MailAddress]
-  ): Decoder[PeriodicQueryArgs] = {
-    implicit val x = ChannelOrRef.jsonDecoder
+  implicit val jsonDecoder: Decoder[PeriodicQueryArgs] =
     semiauto.deriveDecoder
-  }
 
-  implicit def jsonEncoder(implicit
-      mc: Encoder[MailAddress]
-  ): Encoder[PeriodicQueryArgs] = {
-    implicit val x = ChannelOrRef.jsonEncoder
+  implicit def jsonEncoder: Encoder[PeriodicQueryArgs] =
     semiauto.deriveEncoder
-  }
 }
diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml
index 4e9a169e..52180b9f 100644
--- a/modules/restapi/src/main/resources/docspell-openapi.yml
+++ b/modules/restapi/src/main/resources/docspell-openapi.yml
@@ -1739,7 +1739,7 @@ paths:
               schema:
                 type: array
                 items:
-                  $ref: "#/extraSchemas/NotificationHook"
+                  $ref: "#/components/schemas/NotificationHook"
     post:
       operationId: "sec-notification-hook-post"
       tags: [ Notification ]
@@ -1753,7 +1753,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/NotificationHook"
+              $ref: "#/components/schemas/NotificationHook"
       responses:
         422:
           description: BadRequest
@@ -1775,7 +1775,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/NotificationHook"
+              $ref: "#/components/schemas/NotificationHook"
       responses:
         422:
           description: BadRequest
@@ -1821,7 +1821,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/NotificationHook"
+              $ref: "#/components/schemas/NotificationHook"
       responses:
         422:
           description: BadRequest
@@ -4917,7 +4917,7 @@ paths:
               schema:
                 type: array
                 items:
-                  $ref: "#/extraSchemas/PeriodicDueItemsSettings"
+                  $ref: "#/components/schemas/PeriodicDueItemsSettings"
     post:
       operationId: "sec-usertask-notify-new"
       tags: [ User Tasks ]
@@ -4931,7 +4931,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicDueItemsSettings"
+              $ref: "#/components/schemas/PeriodicDueItemsSettings"
       responses:
         422:
           description: BadRequest
@@ -4954,7 +4954,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicDueItemsSettings"
+              $ref: "#/components/schemas/PeriodicDueItemsSettings"
       responses:
         422:
           description: BadRequest
@@ -4984,7 +4984,7 @@ paths:
           content:
             application/json:
               schema:
-                $ref: "#/extraSchemas/PeriodicDueItemsSettings"
+                $ref: "#/components/schemas/PeriodicDueItemsSettings"
     delete:
       operationId: "sec-usertask-notify-delete"
       tags: [ User Tasks ]
@@ -5018,7 +5018,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicDueItemsSettings"
+              $ref: "#/components/schemas/PeriodicDueItemsSettings"
       responses:
         422:
           description: BadRequest
@@ -5048,7 +5048,7 @@ paths:
               schema:
                 type: array
                 items:
-                  $ref: "#/extraSchemas/PeriodicQuerySettings"
+                  $ref: "#/components/schemas/PeriodicQuerySettings"
     post:
       operationId: "sec-usertask-periodic-query-new"
       tags: [ User Tasks ]
@@ -5062,7 +5062,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicQuerySettings"
+              $ref: "#/components/schemas/PeriodicQuerySettings"
       responses:
         422:
           description: BadRequest
@@ -5085,7 +5085,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicQuerySettings"
+              $ref: "#/components/schemas/PeriodicQuerySettings"
       responses:
         422:
           description: BadRequest
@@ -5115,7 +5115,7 @@ paths:
           content:
             application/json:
               schema:
-                $ref: "#/extraSchemas/PeriodicQuerySettings"
+                $ref: "#/components/schemas/PeriodicQuerySettings"
     delete:
       operationId: "sec-usertask-periodic-query-delete"
       tags: [ User Tasks ]
@@ -5149,7 +5149,7 @@ paths:
         content:
           application/json:
             schema:
-              $ref: "#/extraSchemas/PeriodicQuerySettings"
+              $ref: "#/components/schemas/PeriodicQuerySettings"
       responses:
         422:
           description: BadRequest
@@ -5467,7 +5467,10 @@ components:
             type: string
     NotificationChannelRef:
       description: |
-        A reference to a channel.
+        A reference to a channel. The `id` and `channelType` are
+        required to identify a channel. The `name` attribute is as a
+        descriptive name and is returned by the server if it is
+        specified for the corresponding channel.
       required:
         - id
         - channelType
@@ -5478,6 +5481,8 @@ components:
         channelType:
           type: string
           format: channeltype
+        name:
+          type: string
     NotificationMatrix:
       description: |
         A notification channel for matrix.
@@ -5576,6 +5581,136 @@ components:
           items:
             type: string
 
+    NotificationHook:
+      description: |
+        Describes a notifcation hook. There must be at least one
+        channel specified. When creating hooks, the channels must
+        provide the `ìd` and the `channelType` while their `name`
+        attribute is optional.
+      required:
+        - id
+        - enabled
+        - channel
+        - events
+        - allEvents
+      properties:
+        id:
+          type: string
+          format: ident
+        enabled:
+          type: boolean
+        channels:
+          type: array
+          items:
+            $ref: "#/components/schemas/NotificationChannelRef"
+        allEvents:
+          type: boolean
+        eventFilter:
+          type: string
+          format: jsonminiq
+          description: |
+            A filter expression that is applied to the event to be able
+            to ignore a subset of them. See its
+            [documentation](https://docspell.org/docs/jsonminiquery/).
+        events:
+          type: array
+          items:
+            type: string
+            format: eventtype
+            enum:
+              - tagsAdded
+              - tagsSet
+  
+    PeriodicQuerySettings:
+      description: |
+        Settings for the periodc-query task. At least one of `query`
+        and `bookmark` is required! There must be at least one channel
+        specified when creating settings. A channel must provide its
+        `id` and `channelType`, while its `name` is optional.
+      required:
+        - id
+        - enabled
+        - channel
+        - schedule
+      properties:
+        id:
+          type: string
+          format: ident
+        enabled:
+          type: boolean
+        summary:
+          type: string
+        channels:
+          type: array
+          items:
+            $ref: "#/components/schemas/NotificationChannelRef"
+        schedule:
+          type: string
+          format: calevent
+        query:
+          type: string
+          format: itemquery
+        bookmark:
+          type: string
+          description: |
+            Name or ID of bookmark to use.
+        contentStart:
+          type: string
+  
+    PeriodicDueItemsSettings:
+      description: |
+        Settings for notifying about due items. At least one of
+        `query` and `bookmark` is required! There must be at least one
+        channel specified when creating settings. A channel must
+        provide its `id` and `channelType`, while its `name` is
+        optional.
+      required:
+        - id
+        - enabled
+        - channel
+        - schedule
+        - remindDays
+        - capOverdue
+        - tagsInclude
+        - tagsExclude
+      properties:
+        id:
+          type: string
+          format: ident
+        enabled:
+          type: boolean
+        summary:
+          type: string
+        channels:
+          type: array
+          items:
+            $ref: "#/components/schemas/NotificationChannelRef"
+        schedule:
+          type: string
+          format: calevent
+        remindDays:
+          type: integer
+          format: int32
+          description: |
+            Used to restrict items by their due dates. All items with
+            a due date lower than (now + remindDays) are searched.
+        capOverdue:
+          type: boolean
+          description: |
+            If this is true, the search is also restricted to due
+            dates greater than `now - remindDays'. Otherwise, due date
+            are not restricted in that direction (only lower than `now
+            + remindDays' applies) and it is expected to restrict it
+            more using custom tags.
+        tagsInclude:
+          type: array
+          items:
+            $ref: "#/components/schemas/Tag"
+        tagsExclude:
+          type: array
+          items:
+            $ref: "#/components/schemas/Tag"
+  
     ShareSecret:
       description: |
         The secret (the share id + optional password) to access a
@@ -8009,137 +8144,3 @@ components:
       schema:
         type: string
         format: ident
-
-# sadly no generator support for these.
-# Changes here requires corresponding changes in:
-# - NotificationHook.elm
-# - routes.model.*
-extraSchemas:
-  NotificationHook:
-    description: |
-      Describes a notifcation hook. There must be exactly one channel
-      specified, so either use a `channelRef` or one `channel`.
-    required:
-      - id
-      - enabled
-      - channel
-      - events
-      - allEvents
-    properties:
-      id:
-        type: string
-        format: ident
-      enabled:
-        type: boolean
-      channel:
-        oneOf:
-          - $ref: "#/components/schemas/NotificationMail"
-          - $ref: "#/components/schemas/NotificationGotify"
-          - $ref: "#/components/schemas/NotificationMatrix"
-          - $ref: "#/components/schemas/NotificationHttp"
-          - $ref: "#/components/schemas/NotificationChannelRef"
-      allEvents:
-        type: boolean
-      eventFilter:
-        type: string
-        format: jsonminiq
-        description: |
-          A filter expression that is applied to the event to be able
-          to ignore a subset of them. See its
-          [documentation](https://docspell.org/docs/jsonminiquery/).
-      events:
-        type: array
-        items:
-          type: string
-          format: eventtype
-          enum:
-            - tagsAdded
-            - tagsSet
-
-  PeriodicQuerySettings:
-    description: |
-      Settings for the periodc-query task. At least one of `query` and
-      `bookmark` is required!
-    required:
-      - id
-      - enabled
-      - channel
-      - schedule
-    properties:
-      id:
-        type: string
-        format: ident
-      enabled:
-        type: boolean
-      summary:
-        type: string
-      channel:
-        oneOf:
-          - $ref: "#/components/schemas/NotificationMail"
-          - $ref: "#/components/schemas/NotificationGotify"
-          - $ref: "#/components/schemas/NotificationMatrix"
-          - $ref: "#/components/schemas/NotificationHttp"
-          - $ref: "#/components/schemas/NotificationChannelRef"
-      schedule:
-        type: string
-        format: calevent
-      query:
-        type: string
-        format: itemquery
-      bookmark:
-        type: string
-        description: |
-          Name or ID of bookmark to use. 
-
-  PeriodicDueItemsSettings:
-    description: |
-      Settings for notifying about due items.
-    required:
-      - id
-      - enabled
-      - channel
-      - schedule
-      - remindDays
-      - capOverdue
-      - tagsInclude
-      - tagsExclude
-    properties:
-      id:
-        type: string
-        format: ident
-      enabled:
-        type: boolean
-      summary:
-        type: string
-      channel:
-        oneOf:
-          - $ref: "#/components/schemas/NotificationMail"
-          - $ref: "#/components/schemas/NotificationGotify"
-          - $ref: "#/components/schemas/NotificationMatrix"
-          - $ref: "#/components/schemas/NotificationHttp"
-          - $ref: "#/components/schemas/NotificationChannelRef"
-      schedule:
-        type: string
-        format: calevent
-      remindDays:
-        type: integer
-        format: int32
-        description: |
-          Used to restrict items by their due dates. All items with
-          a due date lower than (now + remindDays) are searched.
-      capOverdue:
-        type: boolean
-        description: |
-          If this is true, the search is also restricted to due
-          dates greater than `now - remindDays'. Otherwise, due date
-          are not restricted in that direction (only lower than `now
-          + remindDays' applies) and it is expected to restrict it
-          more using custom tags.
-      tagsInclude:
-        type: array
-        items:
-          $ref: "#/components/schemas/Tag"
-      tagsExclude:
-        type: array
-        items:
-          $ref: "#/components/schemas/Tag"
diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala b/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala
deleted file mode 100644
index 172a2c5f..00000000
--- a/modules/restapi/src/main/scala/docspell/restapi/model/NotificationHook.scala
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright 2020 Eike K. & Contributors
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package docspell.restapi.model
-
-import docspell.common._
-import docspell.jsonminiq.JsonMiniQuery
-import docspell.notification.api.{ChannelRef, EventType}
-import docspell.restapi.codec.ChannelEitherCodec
-
-import io.circe.{Decoder, Encoder}
-
-// this must comply to the definition in openapi.yml in `extraSchemas`
-final case class NotificationHook(
-    id: Ident,
-    enabled: Boolean,
-    channel: Either[ChannelRef, NotificationChannel],
-    allEvents: Boolean,
-    eventFilter: Option[JsonMiniQuery],
-    events: List[EventType]
-)
-
-object NotificationHook {
-  import ChannelEitherCodec._
-
-  implicit val jsonDecoder: Decoder[NotificationHook] =
-    io.circe.generic.semiauto.deriveDecoder
-  implicit val jsonEncoder: Encoder[NotificationHook] =
-    io.circe.generic.semiauto.deriveEncoder
-}
diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala
deleted file mode 100644
index 5f4b7fac..00000000
--- a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicDueItemsSettings.scala
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright 2020 Eike K. & Contributors
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package docspell.restapi.model
-
-import docspell.common._
-import docspell.restapi.model._
-
-import com.github.eikek.calev.CalEvent
-import com.github.eikek.calev.circe.CalevCirceCodec._
-import io.circe.generic.semiauto
-import io.circe.{Decoder, Encoder}
-
-// this must comply to the definition in openapi.yml in `extraSchemas`
-final case class PeriodicDueItemsSettings(
-    id: Ident,
-    enabled: Boolean,
-    summary: Option[String],
-    channel: NotificationChannel,
-    schedule: CalEvent,
-    remindDays: Int,
-    capOverdue: Boolean,
-    tagsInclude: List[Tag],
-    tagsExclude: List[Tag]
-)
-object PeriodicDueItemsSettings {
-
-  implicit val jsonDecoder: Decoder[PeriodicDueItemsSettings] =
-    semiauto.deriveDecoder[PeriodicDueItemsSettings]
-  implicit val jsonEncoder: Encoder[PeriodicDueItemsSettings] =
-    semiauto.deriveEncoder[PeriodicDueItemsSettings]
-}
diff --git a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala b/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala
deleted file mode 100644
index a3cc89ab..00000000
--- a/modules/restapi/src/main/scala/docspell/restapi/model/PeriodicQuerySettings.scala
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2020 Eike K. & Contributors
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package docspell.restapi.model
-
-import docspell.common._
-import docspell.query.ItemQuery
-import docspell.restapi.codec.ItemQueryJson._
-
-import com.github.eikek.calev.CalEvent
-import com.github.eikek.calev.circe.CalevCirceCodec._
-import io.circe.generic.semiauto
-import io.circe.{Decoder, Encoder}
-
-// this must comply to the definition in openapi.yml in `extraSchemas`
-final case class PeriodicQuerySettings(
-    id: Ident,
-    summary: Option[String],
-    enabled: Boolean,
-    channel: NotificationChannel,
-    query: Option[ItemQuery],
-    bookmark: Option[String],
-    contentStart: Option[String],
-    schedule: CalEvent
-) {}
-
-object PeriodicQuerySettings {
-
-  implicit val jsonDecoder: Decoder[PeriodicQuerySettings] =
-    semiauto.deriveDecoder
-
-  implicit val jsonEncoder: Encoder[PeriodicQuerySettings] =
-    semiauto.deriveEncoder
-}
diff --git a/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala b/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala
deleted file mode 100644
index 32052466..00000000
--- a/modules/restapi/src/test/scala/docspell/restapi/model/NotificationCodecTest.scala
+++ /dev/null
@@ -1,89 +0,0 @@
-/*
- * Copyright 2020 Eike K. & Contributors
- *
- * SPDX-License-Identifier: AGPL-3.0-or-later
- */
-
-package docspell.restapi.model
-
-import docspell.common._
-import docspell.notification.api.ChannelRef
-import docspell.notification.api.ChannelType
-
-import io.circe.Decoder
-import io.circe.parser
-import munit._
-
-class NotificationCodecTest extends FunSuite {
-
-  def parse[A: Decoder](str: String): A =
-    parser.parse(str).fold(throw _, identity).as[A].fold(throw _, identity)
-
-  def id(str: String): Ident =
-    Ident.unsafe(str)
-
-  test("decode with channelref") {
-    val json = """{"id":"",
- "enabled": true,
- "channel": {"id":"abcde", "channelType":"matrix"},
- "allEvents": false,
- "events": ["TagsChanged", "SetFieldValue"]
-}"""
-
-    val hook = parse[NotificationHook](json)
-    assertEquals(hook.enabled, true)
-    assertEquals(hook.channel, Left(ChannelRef(id("abcde"), ChannelType.Matrix)))
-  }
-
-  test("decode with gotify data") {
-    val json = """{"id":"",
- "enabled": true,
- "channel": {"id":"", "channelType":"gotify", "url":"http://test.gotify.com", "appKey": "abcde"},
- "allEvents": false,
- "eventFilter": null,
- "events": ["TagsChanged", "SetFieldValue"]
-}"""
-    val hook = parse[NotificationHook](json)
-    assertEquals(hook.enabled, true)
-    assertEquals(
-      hook.channel,
-      Right(
-        NotificationChannel.Gotify(
-          NotificationGotify(
-            id(""),
-            ChannelType.Gotify,
-            LenientUri.unsafe("http://test.gotify.com"),
-            Password("abcde"),
-            None
-          )
-        )
-      )
-    )
-  }
-
-  test("decode with gotify data with prio") {
-    val json = """{"id":"",
- "enabled": true,
- "channel": {"id":"", "channelType":"gotify", "url":"http://test.gotify.com", "appKey": "abcde", "priority":9},
- "allEvents": false,
- "eventFilter": null,
- "events": ["TagsChanged", "SetFieldValue"]
-}"""
-    val hook = parse[NotificationHook](json)
-    assertEquals(hook.enabled, true)
-    assertEquals(
-      hook.channel,
-      Right(
-        NotificationChannel.Gotify(
-          NotificationGotify(
-            id(""),
-            ChannelType.Gotify,
-            LenientUri.unsafe("http://test.gotify.com"),
-            Password("abcde"),
-            Some(9)
-          )
-        )
-      )
-    )
-  }
-}
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala
index 22d6ec87..cdd4370c 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotificationRoutes.scala
@@ -6,6 +6,7 @@
 
 package docspell.restserver.routes
 
+import cats.data.NonEmptyList
 import cats.effect._
 import cats.implicits._
 
@@ -14,10 +15,10 @@ import docspell.backend.auth.AuthToken
 import docspell.common._
 import docspell.joexapi.model.BasicResult
 import docspell.jsonminiq.JsonMiniQuery
-import docspell.notification.api.EventType
+import docspell.notification.api.{ChannelRef, EventType}
 import docspell.restapi.model._
 import docspell.restserver.Config
-import docspell.restserver.conv.Conversions
+import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
 import docspell.restserver.http4s.ClientRequestInfo
 
 import org.http4s._
@@ -26,7 +27,7 @@ import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 import org.http4s.server.Router
 
-object NotificationRoutes {
+object NotificationRoutes extends NonEmptyListSupport {
 
   def apply[F[_]: Async](
       cfg: Config,
@@ -126,17 +127,11 @@ object NotificationRoutes {
       case req @ POST -> Root / "sendTestEvent" =>
         for {
           input <- req.as[NotificationHook]
-          ch <- Sync[F]
-            .pure(
-              input.channel.left
-                .map(_ => new Exception(s"ChannelRefs not allowed for testing"))
-                .flatMap(NotificationChannel.convert)
-            )
-            .rethrow
+          ch <- requireNonEmpty(input.channels)
           baseUrl = ClientRequestInfo.getBaseUrl(cfg, req)
           res <- backend.notification.sendSampleEvent(
             input.events.headOption.getOrElse(EventType.all.head),
-            ch,
+            ch.map(r => ChannelRef(r.id, r.channelType, r.name)),
             user.account,
             baseUrl.some
           )
@@ -179,33 +174,26 @@ object NotificationRoutes {
       NotificationHook(
         h.id,
         h.enabled,
-        h.channel.map(NotificationChannel.convert),
+        h.channels.map(c => NotificationChannelRef(c.id, c.channelType, c.name)).toList,
         h.allEvents,
         h.eventFilter,
         h.events
       )
 
     def convertHook(h: NotificationHook): Either[Throwable, ONotification.Hook] =
-      h.channel match {
-        case Left(cref) =>
-          Right(
-            ONotification.Hook(
-              h.id,
-              h.enabled,
-              Left(cref),
-              h.allEvents,
-              h.eventFilter,
-              h.events
-            )
+      NonEmptyList
+        .fromList(h.channels)
+        .toRight(new IllegalArgumentException(s"Empty channels not allowed!"))
+        .map(_ =>
+          ONotification.Hook(
+            h.id,
+            h.enabled,
+            h.channels.map(c => ChannelRef(c.id, c.channelType, c.name)),
+            h.allEvents,
+            h.eventFilter,
+            h.events
           )
-        case Right(channel) =>
-          NotificationChannel
-            .convert(channel)
-            .map(ch =>
-              ONotification
-                .Hook(h.id, h.enabled, Right(ch), h.allEvents, h.eventFilter, h.events)
-            )
-      }
+        )
 
   }
 }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
index 7ae72706..e53bef82 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/NotifyDueItemsRoutes.scala
@@ -14,10 +14,10 @@ import docspell.backend.BackendApp
 import docspell.backend.MailAddressCodec
 import docspell.backend.auth.AuthToken
 import docspell.common._
-import docspell.notification.api.PeriodicDueItemsArgs
+import docspell.notification.api.{ChannelRef, PeriodicDueItemsArgs}
 import docspell.restapi.model._
 import docspell.restserver.Config
-import docspell.restserver.conv.Conversions
+import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
 import docspell.restserver.http4s.ClientRequestInfo
 import docspell.store.usertask._
 
@@ -26,7 +26,7 @@ import org.http4s.circe.CirceEntityDecoder._
 import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 
-object NotifyDueItemsRoutes extends MailAddressCodec {
+object NotifyDueItemsRoutes extends MailAddressCodec with NonEmptyListSupport {
 
   def apply[F[_]: Async](
       cfg: Config,
@@ -113,7 +113,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
       user: AccountId,
       settings: PeriodicDueItemsSettings
   ): F[UserTask[PeriodicDueItemsArgs]] =
-    Sync[F].pure(NotificationChannel.convert(settings.channel)).rethrow.map { channel =>
+    requireNonEmpty(settings.channels).map { ch =>
       UserTask(
         id,
         PeriodicDueItemsArgs.taskName,
@@ -122,7 +122,7 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
         settings.summary,
         PeriodicDueItemsArgs(
           user,
-          Right(channel),
+          ch.map(c => ChannelRef(c.id, c.channelType, c.name)),
           settings.remindDays,
           if (settings.capOverdue) Some(settings.remindDays)
           else None,
@@ -140,20 +140,13 @@ object NotifyDueItemsRoutes extends MailAddressCodec {
     for {
       tinc <- backend.tag.loadAll(task.args.tagsInclude)
       texc <- backend.tag.loadAll(task.args.tagsExclude)
-
-      ch <- task.args.channel match {
-        case Right(c) => NotificationChannel.convert(c).pure[F]
-        case Left(ref) =>
-          Sync[F].raiseError(
-            new IllegalStateException(s"ChannelRefs are not supported: $ref")
-          )
-      }
-
     } yield PeriodicDueItemsSettings(
       task.id,
       task.enabled,
       task.summary,
-      ch,
+      task.args.channels
+        .map(c => NotificationChannelRef(c.id, c.channelType, c.name))
+        .toList,
       task.timer,
       task.args.remindDays,
       task.args.daysBack.isDefined,
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala
index 32e767b8..0efd2661 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/PeriodicQueryRoutes.scala
@@ -6,7 +6,7 @@
 
 package docspell.restserver.routes
 
-import cats.data.OptionT
+import cats.data.{NonEmptyList, OptionT}
 import cats.effect._
 import cats.implicits._
 
@@ -14,11 +14,11 @@ import docspell.backend.BackendApp
 import docspell.backend.MailAddressCodec
 import docspell.backend.auth.AuthToken
 import docspell.common._
-import docspell.notification.api.PeriodicQueryArgs
+import docspell.notification.api.{ChannelRef, PeriodicQueryArgs}
 import docspell.query.ItemQueryParser
 import docspell.restapi.model._
 import docspell.restserver.Config
-import docspell.restserver.conv.Conversions
+import docspell.restserver.conv.{Conversions, NonEmptyListSupport}
 import docspell.restserver.http4s.ClientRequestInfo
 import docspell.store.usertask._
 
@@ -27,7 +27,7 @@ import org.http4s.circe.CirceEntityDecoder._
 import org.http4s.circe.CirceEntityEncoder._
 import org.http4s.dsl.Http4sDsl
 
-object PeriodicQueryRoutes extends MailAddressCodec {
+object PeriodicQueryRoutes extends MailAddressCodec with NonEmptyListSupport {
 
   def apply[F[_]: Async](
       cfg: Config,
@@ -116,7 +116,9 @@ object PeriodicQueryRoutes extends MailAddressCodec {
   ): F[UserTask[PeriodicQueryArgs]] =
     Sync[F]
       .pure(for {
-        ch <- NotificationChannel.convert(settings.channel)
+        ch <- NonEmptyList
+          .fromList(settings.channels)
+          .toRight(new Exception(s"No channels found for: ${settings.channels}"))
         qstr <- settings.query match {
           case Some(q) =>
             ItemQueryParser
@@ -132,7 +134,7 @@ object PeriodicQueryRoutes extends MailAddressCodec {
           else Left(new IllegalArgumentException("No query or bookmark provided"))
       } yield (ch, qstr.map(ItemQueryString.apply)))
       .rethrow
-      .map { case (channel, qstr) =>
+      .map { case (channels, qstr) =>
         UserTask(
           id,
           PeriodicQueryArgs.taskName,
@@ -141,7 +143,7 @@ object PeriodicQueryRoutes extends MailAddressCodec {
           settings.summary,
           PeriodicQueryArgs(
             user,
-            Right(channel),
+            channels.map(r => ChannelRef(r.id, r.channelType, r.name)),
             qstr,
             settings.bookmark,
             Some(baseUrl / "app" / "item"),
@@ -153,22 +155,18 @@ object PeriodicQueryRoutes extends MailAddressCodec {
   def taskToSettings[F[_]: Sync](
       task: UserTask[PeriodicQueryArgs]
   ): F[PeriodicQuerySettings] =
-    for {
-      ch <- task.args.channel match {
-        case Right(c) => NotificationChannel.convert(c).pure[F]
-        case Left(ref) =>
-          Sync[F].raiseError(
-            new IllegalStateException(s"ChannelRefs are not supported: $ref")
-          )
-      }
-    } yield PeriodicQuerySettings(
-      task.id,
-      task.summary,
-      task.enabled,
-      ch,
-      task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe),
-      task.args.bookmark,
-      task.args.contentStart,
-      task.timer
+    Sync[F].pure(
+      PeriodicQuerySettings(
+        task.id,
+        task.enabled,
+        task.summary,
+        task.args.channels
+          .map(c => NotificationChannelRef(c.id, c.channelType, c.name))
+          .toList,
+        task.timer,
+        task.args.query.map(_.query).map(ItemQueryParser.parseUnsafe),
+        task.args.bookmark,
+        task.args.contentStart
+      )
     )
 }
diff --git a/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql
new file mode 100644
index 00000000..9ba8788e
--- /dev/null
+++ b/modules/store/src/main/resources/db/migration/h2/V1.32.1__notification_hook_multi_channel.sql
@@ -0,0 +1,33 @@
+CREATE TABLE "notification_hook_channel" (
+  "id" varchar(254) not null primary key,
+  "hook_id" varchar(254) not null,
+  "channel_mail" varchar(254),
+  "channel_gotify" varchar(254),
+  "channel_matrix" varchar(254),
+  "channel_http" varchar(254),
+  foreign key ("hook_id") references "notification_hook"("id") on delete cascade,
+  foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade,
+  foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade,
+  foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade,
+  foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade,
+  unique("hook_id", "channel_mail"),
+  unique("hook_id", "channel_gotify"),
+  unique("hook_id", "channel_matrix"),
+  unique("hook_id", "channel_http")
+);
+
+insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http")
+select random_uuid(), id, channel_mail, channel_gotify, channel_matrix, channel_http
+from "notification_hook";
+
+alter table "notification_hook"
+drop column "channel_mail";
+
+alter table "notification_hook"
+drop column "channel_gotify";
+
+alter table "notification_hook"
+drop column "channel_matrix";
+
+alter table "notification_hook"
+drop column "channel_http";
diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql
new file mode 100644
index 00000000..be6198f8
--- /dev/null
+++ b/modules/store/src/main/resources/db/migration/mariadb/V1.32.1__notification_hook_multi_channel.sql
@@ -0,0 +1,42 @@
+CREATE TABLE `notification_hook_channel` (
+  `id` varchar(254) not null primary key,
+  `hook_id` varchar(254) not null,
+  `channel_mail` varchar(254),
+  `channel_gotify` varchar(254),
+  `channel_matrix` varchar(254),
+  `channel_http` varchar(254),
+  foreign key (`hook_id`) references `notification_hook`(`id`) on delete cascade,
+  foreign key (`channel_mail`) references `notification_channel_mail`(`id`) on delete cascade,
+  foreign key (`channel_gotify`) references `notification_channel_gotify`(`id`) on delete cascade,
+  foreign key (`channel_matrix`) references `notification_channel_matrix`(`id`) on delete cascade,
+  foreign key (`channel_http`) references `notification_channel_http`(`id`) on delete cascade,
+  unique(`hook_id`, `channel_mail`),
+  unique(`hook_id`, `channel_gotify`),
+  unique(`hook_id`, `channel_matrix`),
+  unique(`hook_id`, `channel_http`)
+);
+
+insert into `notification_hook_channel`
+select md5(rand()), id, channel_mail, channel_gotify, channel_matrix, channel_http
+from `notification_hook`;
+
+alter table `notification_hook`
+drop constraint `notification_hook_ibfk_2`;
+alter table `notification_hook`
+drop constraint `notification_hook_ibfk_3`;
+alter table `notification_hook`
+drop constraint `notification_hook_ibfk_4`;
+alter table `notification_hook`
+drop constraint `notification_hook_ibfk_5`;
+
+alter table `notification_hook`
+drop column `channel_mail`;
+
+alter table `notification_hook`
+drop column `channel_gotify`;
+
+alter table `notification_hook`
+drop column `channel_matrix`;
+
+alter table `notification_hook`
+drop column `channel_http`;
diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql
new file mode 100644
index 00000000..2f116f99
--- /dev/null
+++ b/modules/store/src/main/resources/db/migration/postgresql/V1.32.1__notification_hook_multi_channel.sql
@@ -0,0 +1,33 @@
+CREATE TABLE "notification_hook_channel" (
+  "id" varchar(254) not null primary key,
+  "hook_id" varchar(254) not null,
+  "channel_mail" varchar(254),
+  "channel_gotify" varchar(254),
+  "channel_matrix" varchar(254),
+  "channel_http" varchar(254),
+  foreign key ("hook_id") references "notification_hook"("id") on delete cascade,
+  foreign key ("channel_mail") references "notification_channel_mail"("id") on delete cascade,
+  foreign key ("channel_gotify") references "notification_channel_gotify"("id") on delete cascade,
+  foreign key ("channel_matrix") references "notification_channel_matrix"("id") on delete cascade,
+  foreign key ("channel_http") references "notification_channel_http"("id") on delete cascade,
+  unique("hook_id", "channel_mail"),
+  unique("hook_id", "channel_gotify"),
+  unique("hook_id", "channel_matrix"),
+  unique("hook_id", "channel_http")
+);
+
+insert into "notification_hook_channel" ("id", "hook_id", "channel_mail", "channel_gotify", "channel_matrix", "channel_http")
+select md5(random()::text), id, channel_mail, channel_gotify, channel_matrix, channel_http
+from "notification_hook";
+
+alter table "notification_hook"
+drop column "channel_mail";
+
+alter table "notification_hook"
+drop column "channel_gotify";
+
+alter table "notification_hook"
+drop column "channel_matrix";
+
+alter table "notification_hook"
+drop column "channel_http";
diff --git a/modules/store/src/main/scala/db/migration/MigrationTasks.scala b/modules/store/src/main/scala/db/migration/MigrationTasks.scala
index 47c814cd..f8e2bf55 100644
--- a/modules/store/src/main/scala/db/migration/MigrationTasks.scala
+++ b/modules/store/src/main/scala/db/migration/MigrationTasks.scala
@@ -6,23 +6,23 @@
 
 package db.migration
 
-import cats.data.NonEmptyList
+import cats.data.{NonEmptyList, OptionT}
 import cats.effect.{IO, Sync}
 import cats.implicits._
 
 import docspell.common._
 import docspell.common.syntax.StringSyntax._
-import docspell.notification.api.Channel
-import docspell.notification.api.PeriodicDueItemsArgs
-import docspell.store.records.RPeriodicTask
+import docspell.notification.api._
+import docspell.store.records._
 
+import db.migration.data.{PeriodicDueItemsArgsOld, PeriodicQueryArgsOld}
 import doobie._
 import doobie.implicits._
 import doobie.util.transactor.Strategy
 import emil.MailAddress
 import emil.javamail.syntax._
-import io.circe.Encoder
 import io.circe.syntax._
+import io.circe.{Decoder, Encoder}
 import org.flywaydb.core.api.migration.Context
 
 trait MigrationTasks {
@@ -31,6 +31,8 @@ trait MigrationTasks {
 
   implicit val jsonEncoder: Encoder[MailAddress] =
     Encoder.encodeString.contramap(_.asUnicodeString)
+  implicit val jsonDecoder: Decoder[MailAddress] =
+    Decoder.decodeString.emap(MailAddress.parse)
 
   def migrateDueItemTasks: ConnectionIO[Unit] =
     for {
@@ -42,20 +44,114 @@ trait MigrationTasks {
       _ <- RPeriodicTask.setEnabledByTask(NotifyDueItemsArgs.taskName, false)
     } yield ()
 
-  def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = {
-    val converted = old.args
-      .parseJsonAs[NotifyDueItemsArgs]
-      .leftMap(_.getMessage())
-      .flatMap(convertArgs)
+  def migratePeriodicItemTasks: ConnectionIO[Unit] =
+    for {
+      tasks2 <- RPeriodicTask.findByTask(PeriodicDueItemsArgsOld.taskName)
+      tasks3 <- RPeriodicTask.findByTask(PeriodicQueryArgsOld.taskName)
+      size = tasks2.size + tasks3.size
+      _ <- Sync[ConnectionIO].delay(
+        logger.info(s"Starting to migrate $size user tasks")
+      )
+      _ <- tasks2.traverse(migratePeriodicDueItemsTask)
+      _ <- tasks3.traverse(migratePeriodicQueryTask)
+      _ <- RPeriodicTask.setEnabledByTask(PeriodicQueryArgsOld.taskName, false)
+      _ <- RPeriodicTask.setEnabledByTask(PeriodicDueItemsArgsOld.taskName, false)
+    } yield ()
 
-    converted match {
-      case Right(args) =>
-        Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *>
+  private def migratePeriodicQueryTask(old: RPeriodicTask): ConnectionIO[Int] =
+    old.args
+      .parseJsonAs[PeriodicQueryArgsOld]
+      .leftMap { ex =>
+        logger.error(ex)(s"Error migrating tasks")
+        0.pure[ConnectionIO]
+      }
+      .map { oldArgs =>
+        val ref = oldArgs.channel match {
+          case Right(c)  => saveChannel(c, oldArgs.account)
+          case Left(ref) => ref.pure[ConnectionIO]
+        }
+
+        ref.flatMap(channelRef =>
+          RPeriodicTask.updateTask(
+            old.id,
+            PeriodicQueryArgs.taskName,
+            PeriodicQueryArgs(
+              oldArgs.account,
+              NonEmptyList.of(channelRef),
+              oldArgs.query,
+              oldArgs.bookmark,
+              oldArgs.baseUrl,
+              oldArgs.contentStart
+            ).asJson.noSpaces
+          )
+        )
+      }
+      .fold(identity, identity)
+
+  private def migratePeriodicDueItemsTask(old: RPeriodicTask): ConnectionIO[Int] =
+    old.args
+      .parseJsonAs[PeriodicDueItemsArgsOld]
+      .leftMap { ex =>
+        logger.error(ex)(s"Error migrating tasks")
+        0.pure[ConnectionIO]
+      }
+      .map { oldArgs =>
+        val ref = oldArgs.channel match {
+          case Right(c)  => saveChannel(c, oldArgs.account)
+          case Left(ref) => ref.pure[ConnectionIO]
+        }
+
+        ref.flatMap(channelRef =>
           RPeriodicTask.updateTask(
             old.id,
             PeriodicDueItemsArgs.taskName,
-            args.asJson.noSpaces
+            PeriodicDueItemsArgs(
+              oldArgs.account,
+              NonEmptyList.of(channelRef),
+              oldArgs.remindDays,
+              oldArgs.daysBack,
+              oldArgs.tagsInclude,
+              oldArgs.tagsExclude,
+              oldArgs.baseUrl
+            ).asJson.noSpaces
           )
+        )
+      }
+      .fold(identity, identity)
+
+  private def saveChannel(ch: Channel, account: AccountId): ConnectionIO[ChannelRef] =
+    (for {
+      newId <- OptionT.liftF(Ident.randomId[ConnectionIO])
+      userId <- OptionT(RUser.findIdByAccount(account))
+      r <- RNotificationChannel.fromChannel(ch, newId, userId)
+      _ <- OptionT.liftF(RNotificationChannel.insert(r))
+      _ <- OptionT.liftF(
+        Sync[ConnectionIO].delay(logger.debug(s"Created channel $r for $account"))
+      )
+      ref = r.asRef
+    } yield ref)
+      .getOrElseF(Sync[ConnectionIO].raiseError(new Exception("User not found!")))
+
+  private def migrateDueItemTask1(old: RPeriodicTask): ConnectionIO[Int] = {
+    val converted = old.args
+      .parseJsonAs[NotifyDueItemsArgs]
+      .leftMap(_.getMessage())
+      .map(convertArgs)
+
+    converted match {
+      case Right(args) =>
+        val task = args
+          .semiflatMap(a =>
+            RPeriodicTask
+              .updateTask(
+                old.id,
+                PeriodicDueItemsArgs.taskName,
+                a.asJson.noSpaces
+              )
+          )
+          .getOrElse(0)
+
+        Sync[ConnectionIO].delay(logger.info(s"Converting user task: $old")) *> task
 
       case Left(err) =>
         logger.error(s"Error converting user task: $old. $err")
@@ -63,22 +159,44 @@ trait MigrationTasks {
     }
   }
 
-  def convertArgs(old: NotifyDueItemsArgs): Either[String, PeriodicDueItemsArgs] =
-    old.recipients
-      .traverse(MailAddress.parse)
-      .flatMap(l => NonEmptyList.fromList(l).toRight("No recipients provided"))
-      .map { rec =>
-        PeriodicDueItemsArgs(
-          old.account,
-          Right(Channel.Mail(Ident.unsafe(""), None, old.smtpConnection, rec)),
-          old.remindDays,
-          old.daysBack,
-          old.tagsInclude,
-          old.tagsExclude,
-          old.itemDetailUrl
-        )
+  private def convertArgs(
+      old: NotifyDueItemsArgs
+  ): OptionT[ConnectionIO, PeriodicDueItemsArgs] = {
+    val recs = old.recipients
+      .map(MailAddress.parse)
+      .flatMap {
+        case Right(m) => Some(m)
+        case Left(err) =>
+          logger.warn(s"Cannot read mail address: $err. Skip this while migrating.")
+          None
       }
 
+    for {
+      userId <- OptionT(RUser.findIdByAccount(old.account))
+      id <- OptionT.liftF(Ident.randomId[ConnectionIO])
+      now <- OptionT.liftF(Timestamp.current[ConnectionIO])
+      chName = Some("migrate notify items")
+      ch = RNotificationChannelMail(
+        id,
+        userId,
+        chName,
+        old.smtpConnection,
+        recs,
+        now
+      )
+      _ <- OptionT.liftF(RNotificationChannelMail.insert(ch))
+      args = PeriodicDueItemsArgs(
+        old.account,
+        NonEmptyList.of(ChannelRef(ch.id, ChannelType.Mail, chName)),
+        old.remindDays,
+        old.daysBack,
+        old.tagsInclude,
+        old.tagsExclude,
+        old.itemDetailUrl
+      )
+    } yield args
+  }
+
   def mkTransactor(ctx: Context): Transactor[IO] = {
     val xa = Transactor.fromConnection[IO](ctx.getConnection())
     Transactor.strategy.set(xa, Strategy.void) // transactions are handled by flyway
diff --git a/modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala b/modules/store/src/main/scala/db/migration/data/NotifyDueItemsArgs.scala
similarity index 100%
rename from modules/store/src/main/scala/db/migration/NotifyDueItemsArgs.scala
rename to modules/store/src/main/scala/db/migration/data/NotifyDueItemsArgs.scala
diff --git a/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala b/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala
new file mode 100644
index 00000000..c10630d4
--- /dev/null
+++ b/modules/store/src/main/scala/db/migration/data/PeriodicDueItemsArgsOld.scala
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package db.migration.data
+
+import docspell.common._
+
+import emil.MailAddress
+import io.circe.generic.semiauto
+import io.circe.{Decoder, Encoder}
+
+/** Arguments to the notification task.
+  *
+  * This tasks queries items with a due date and informs the user via mail.
+  *
+  * If the structure changes, there must be some database migration to update or remove
+  * the json data of the corresponding task.
+  */
+final case class PeriodicDueItemsArgsOld(
+    account: AccountId,
+    channel: ChannelOrRef,
+    remindDays: Int,
+    daysBack: Option[Int],
+    tagsInclude: List[Ident],
+    tagsExclude: List[Ident],
+    baseUrl: Option[LenientUri]
+)
+
+object PeriodicDueItemsArgsOld {
+  val taskName = Ident.unsafe("periodic-due-items-notify")
+
+  implicit def jsonDecoder(implicit
+      mc: Decoder[MailAddress]
+  ): Decoder[PeriodicDueItemsArgsOld] = {
+    implicit val x = ChannelOrRef.jsonDecoder
+    semiauto.deriveDecoder
+  }
+
+  implicit def jsonEncoder(implicit
+      mc: Encoder[MailAddress]
+  ): Encoder[PeriodicDueItemsArgsOld] = {
+    implicit val x = ChannelOrRef.jsonEncoder
+    semiauto.deriveEncoder
+  }
+}
diff --git a/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala b/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala
new file mode 100644
index 00000000..403d4567
--- /dev/null
+++ b/modules/store/src/main/scala/db/migration/data/PeriodicQueryArgsOld.scala
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package db.migration.data
+
+import docspell.common._
+
+import emil.MailAddress
+import io.circe.generic.semiauto
+import io.circe.{Decoder, Encoder}
+
+final case class PeriodicQueryArgsOld(
+    account: AccountId,
+    channel: ChannelOrRef,
+    query: Option[ItemQueryString],
+    bookmark: Option[String],
+    baseUrl: Option[LenientUri],
+    contentStart: Option[String]
+)
+
+object PeriodicQueryArgsOld {
+  val taskName = Ident.unsafe("periodic-query-notify")
+
+  implicit def jsonDecoder(implicit
+      mc: Decoder[MailAddress]
+  ): Decoder[PeriodicQueryArgsOld] = {
+    implicit val x = ChannelOrRef.jsonDecoder
+    semiauto.deriveDecoder
+  }
+
+  implicit def jsonEncoder(implicit
+      mc: Encoder[MailAddress]
+  ): Encoder[PeriodicQueryArgsOld] = {
+    implicit val x = ChannelOrRef.jsonEncoder
+    semiauto.deriveEncoder
+  }
+}
diff --git a/modules/notification/api/src/main/scala/docspell/notification/api/package.scala b/modules/store/src/main/scala/db/migration/data/package.scala
similarity index 90%
rename from modules/notification/api/src/main/scala/docspell/notification/api/package.scala
rename to modules/store/src/main/scala/db/migration/data/package.scala
index 74f2e41e..246b460d 100644
--- a/modules/notification/api/src/main/scala/docspell/notification/api/package.scala
+++ b/modules/store/src/main/scala/db/migration/data/package.scala
@@ -4,13 +4,14 @@
  * SPDX-License-Identifier: AGPL-3.0-or-later
  */
 
-package docspell.notification
+package db.migration
+
+import docspell.notification.api._
 
 import emil.MailAddress
 import io.circe.{Decoder, Encoder}
 
-package object api {
-
+package object data {
   type ChannelOrRef = Either[ChannelRef, Channel]
 
   object ChannelOrRef {
@@ -25,5 +26,4 @@ package object api {
         cr.fold(_.channelType, _.channelType)
     }
   }
-
 }
diff --git a/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala
new file mode 100644
index 00000000..83983d4a
--- /dev/null
+++ b/modules/store/src/main/scala/db/migration/h2/V1_32_2__MigrateChannels.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package db.migration.h2
+
+import cats.effect.unsafe.implicits._
+
+import db.migration.MigrationTasks
+import doobie.implicits._
+import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
+
+class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
+  val logger = org.log4s.getLogger
+
+  override def migrate(ctx: Context): Unit = {
+    val xa = mkTransactor(ctx)
+    migratePeriodicItemTasks.transact(xa).unsafeRunSync()
+  }
+}
diff --git a/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala
new file mode 100644
index 00000000..ee524572
--- /dev/null
+++ b/modules/store/src/main/scala/db/migration/mariadb/V1_32_2__MigrateChannels.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package db.migration.mariadb
+
+import cats.effect.unsafe.implicits._
+
+import db.migration.MigrationTasks
+import doobie.implicits._
+import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
+
+class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
+  val logger = org.log4s.getLogger
+
+  override def migrate(ctx: Context): Unit = {
+    val xa = mkTransactor(ctx)
+    migratePeriodicItemTasks.transact(xa).unsafeRunSync()
+  }
+}
diff --git a/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala b/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala
new file mode 100644
index 00000000..e887f510
--- /dev/null
+++ b/modules/store/src/main/scala/db/migration/postgresql/V1_32_2__MigrateChannels.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package db.migration.postgresql
+
+import cats.effect.unsafe.implicits._
+
+import db.migration.MigrationTasks
+import doobie.implicits._
+import org.flywaydb.core.api.migration.{BaseJavaMigration, Context}
+
+class V1_32_2__MigrateChannels extends BaseJavaMigration with MigrationTasks {
+  val logger = org.log4s.getLogger
+
+  override def migrate(ctx: Context): Unit = {
+    val xa = mkTransactor(ctx)
+    migratePeriodicItemTasks.transact(xa).unsafeRunSync()
+  }
+}
diff --git a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
index 2b76e98a..dee20f76 100644
--- a/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
+++ b/modules/store/src/main/scala/docspell/store/impl/DoobieMeta.scala
@@ -12,7 +12,7 @@ import java.time.{Instant, LocalDate}
 import docspell.common._
 import docspell.common.syntax.all._
 import docspell.jsonminiq.JsonMiniQuery
-import docspell.notification.api.EventType
+import docspell.notification.api.{ChannelType, EventType}
 import docspell.query.{ItemQuery, ItemQueryParser}
 import docspell.totp.Key
 
@@ -156,6 +156,9 @@ trait DoobieMeta extends EmilDoobieMeta {
 
   implicit val metaJsonMiniQuery: Meta[JsonMiniQuery] =
     Meta[String].timap(JsonMiniQuery.unsafeParse)(_.unsafeAsString)
+
+  implicit val channelTypeRead: Read[ChannelType] =
+    Read[String].map(ChannelType.unsafeFromString)
 }
 
 object DoobieMeta extends DoobieMeta {
diff --git a/modules/store/src/main/scala/docspell/store/queries/QNotification.scala b/modules/store/src/main/scala/docspell/store/queries/QNotification.scala
index b76dbde7..e7f0175b 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QNotification.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QNotification.scala
@@ -27,7 +27,11 @@ object QNotification {
   def findChannelsForEvent(event: Event): ConnectionIO[Vector[HookChannel]] =
     for {
       hooks <- listHooks(event.account.collective, event.eventType)
-      chs <- hooks.traverse(readHookChannel)
+      chs <- hooks.traverse(h =>
+        listChannels(h.id)
+          .flatMap(_.flatTraverse(hc => readHookChannel(h.uid, hc)))
+          .map(c => HookChannel(h, c))
+      )
     } yield chs
 
   // --
@@ -50,21 +54,27 @@ object QNotification {
         )
     ).query[RNotificationHook].to[Vector]
 
+  def listChannels(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] =
+    RNotificationHookChannel.allOf(hookId)
+
   def readHookChannel(
-      hook: RNotificationHook
-  ): ConnectionIO[HookChannel] =
+      userId: Ident,
+      hook: RNotificationHookChannel
+  ): ConnectionIO[Vector[NotificationChannel]] =
     for {
-      c1 <- read(hook.channelMail)(RNotificationChannelMail.getById)(
+      c1 <- read(hook.channelMail)(RNotificationChannelMail.getById(userId))(
         ChannelMap.readMail
       )
-      c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById)(
+      c2 <- read(hook.channelGotify)(RNotificationChannelGotify.getById(userId))(
         ChannelMap.readGotify
       )
-      c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById)(
+      c3 <- read(hook.channelMatrix)(RNotificationChannelMatrix.getById(userId))(
         ChannelMap.readMatrix
       )
-      c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById)(ChannelMap.readHttp)
-    } yield HookChannel(hook, c1 ++ c2 ++ c3 ++ c4)
+      c4 <- read(hook.channelHttp)(RNotificationChannelHttp.getById(userId))(
+        ChannelMap.readHttp
+      )
+    } yield c1 ++ c2 ++ c3 ++ c4
 
   def readChannel(ch: RNotificationChannel): ConnectionIO[Vector[NotificationChannel]] =
     ch.fold(
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala
index 4de2675e..b51e366c 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannel.scala
@@ -7,10 +7,10 @@
 package docspell.store.records
 
 import cats.data.OptionT
+import cats.implicits._
 
 import docspell.common._
-import docspell.notification.api.ChannelRef
-import docspell.notification.api.ChannelType
+import docspell.notification.api.{Channel, ChannelRef, ChannelType}
 
 import doobie._
 
@@ -20,6 +20,17 @@ sealed trait RNotificationChannel {
 
   def name: Option[String] = fold(_.name, _.name, _.name, _.name)
 
+  def channelType: ChannelType =
+    fold(
+      _ => ChannelType.Mail,
+      _ => ChannelType.Gotify,
+      _ => ChannelType.Matrix,
+      _ => ChannelType.Http
+    )
+
+  def asRef: ChannelRef =
+    ChannelRef(id, channelType, name)
+
   def fold[A](
       f1: RNotificationChannelMail => A,
       f2: RNotificationChannelGotify => A,
@@ -93,42 +104,60 @@ object RNotificationChannel {
       Matrix.apply
     ) ++ http.map(Http.apply)
 
-  def getById(id: Ident): ConnectionIO[Vector[RNotificationChannel]] =
+  def getById(id: Ident, userId: Ident): ConnectionIO[Vector[RNotificationChannel]] =
     for {
-      mail <- RNotificationChannelMail.getById(id)
-      gotify <- RNotificationChannelGotify.getById(id)
-      matrix <- RNotificationChannelMatrix.getById(id)
-      http <- RNotificationChannelHttp.getById(id)
+      mail <- RNotificationChannelMail.getById(userId)(id)
+      gotify <- RNotificationChannelGotify.getById(userId)(id)
+      matrix <- RNotificationChannelMatrix.getById(userId)(id)
+      http <- RNotificationChannelHttp.getById(userId)(id)
     } yield mail.map(Email.apply).toVector ++
       gotify.map(Gotify.apply).toVector ++
       matrix.map(Matrix.apply).toVector ++
       http.map(Http.apply).toVector
 
-  def getByRef(ref: ChannelRef): ConnectionIO[Option[RNotificationChannel]] =
+  def getByRef(
+      ref: ChannelRef,
+      userId: Ident
+  ): ConnectionIO[Option[RNotificationChannel]] =
     ref.channelType match {
       case ChannelType.Mail =>
-        RNotificationChannelMail.getById(ref.id).map(_.map(Email.apply))
+        RNotificationChannelMail.getById(userId)(ref.id).map(_.map(Email.apply))
       case ChannelType.Matrix =>
-        RNotificationChannelMatrix.getById(ref.id).map(_.map(Matrix.apply))
+        RNotificationChannelMatrix.getById(userId)(ref.id).map(_.map(Matrix.apply))
       case ChannelType.Gotify =>
-        RNotificationChannelGotify.getById(ref.id).map(_.map(Gotify.apply))
+        RNotificationChannelGotify.getById(userId)(ref.id).map(_.map(Gotify.apply))
       case ChannelType.Http =>
-        RNotificationChannelHttp.getById(ref.id).map(_.map(Http.apply))
+        RNotificationChannelHttp.getById(userId)(ref.id).map(_.map(Http.apply))
     }
 
-  def getByHook(r: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = {
+  def getByHook(hook: RNotificationHook): ConnectionIO[Vector[RNotificationChannel]] = {
     def opt(id: Option[Ident]): OptionT[ConnectionIO, Ident] =
       OptionT.fromOption(id)
 
-    for {
-      mail <- opt(r.channelMail).flatMapF(RNotificationChannelMail.getById).value
-      gotify <- opt(r.channelGotify).flatMapF(RNotificationChannelGotify.getById).value
-      matrix <- opt(r.channelMatrix).flatMapF(RNotificationChannelMatrix.getById).value
-      http <- opt(r.channelHttp).flatMapF(RNotificationChannelHttp.getById).value
-    } yield mail.map(Email.apply).toVector ++
-      gotify.map(Gotify.apply).toVector ++
-      matrix.map(Matrix.apply).toVector ++
-      http.map(Http.apply).toVector
+    def find(
+        r: RNotificationHookChannel
+    ): ConnectionIO[Vector[RNotificationChannel]] =
+      for {
+        mail <- opt(r.channelMail)
+          .flatMapF(RNotificationChannelMail.getById(hook.uid))
+          .value
+        gotify <- opt(r.channelGotify)
+          .flatMapF(RNotificationChannelGotify.getById(hook.uid))
+          .value
+        matrix <- opt(r.channelMatrix)
+          .flatMapF(RNotificationChannelMatrix.getById(hook.uid))
+          .value
+        http <- opt(r.channelHttp)
+          .flatMapF(RNotificationChannelHttp.getById(hook.uid))
+          .value
+      } yield mail.map(Email.apply).toVector ++
+        gotify.map(Gotify.apply).toVector ++
+        matrix.map(Matrix.apply).toVector ++
+        http.map(Http.apply).toVector
+
+    RNotificationHookChannel
+      .allOf(hook.id)
+      .flatMap(_.flatTraverse(find))
   }
 
   def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] =
@@ -138,4 +167,63 @@ object RNotificationChannel {
       n3 <- RNotificationChannelMatrix.deleteByAccount(id, account)
       n4 <- RNotificationChannelHttp.deleteByAccount(id, account)
     } yield n1 + n2 + n3 + n4
+
+  def fromChannel(
+      channel: Channel,
+      id: Ident,
+      userId: Ident
+  ): OptionT[ConnectionIO, RNotificationChannel] =
+    for {
+      time <- OptionT.liftF(Timestamp.current[ConnectionIO])
+      logger = Logger.log4s[ConnectionIO](org.log4s.getLogger)
+      r <-
+        channel match {
+          case Channel.Mail(_, name, conn, recipients) =>
+            for {
+              _ <- OptionT.liftF(
+                logger.debug(
+                  s"Looking up user smtp for ${userId.id} and ${conn.id}"
+                )
+              )
+              mailConn <- OptionT(RUserEmail.getByUser(userId, conn))
+              rec = RNotificationChannelMail(
+                id,
+                userId,
+                name,
+                mailConn.id,
+                recipients.toList,
+                time
+              ).vary
+            } yield rec
+          case Channel.Gotify(_, name, url, appKey, prio) =>
+            OptionT.pure[ConnectionIO](
+              RNotificationChannelGotify(
+                id,
+                userId,
+                name,
+                url,
+                appKey,
+                prio,
+                time
+              ).vary
+            )
+          case Channel.Matrix(_, name, homeServer, roomId, accessToken) =>
+            OptionT.pure[ConnectionIO](
+              RNotificationChannelMatrix(
+                id,
+                userId,
+                name,
+                homeServer,
+                roomId,
+                accessToken,
+                "m.text",
+                time
+              ).vary
+            )
+          case Channel.Http(_, name, url) =>
+            OptionT.pure[ConnectionIO](
+              RNotificationChannelHttp(id, userId, name, url, time).vary
+            )
+        }
+    } yield r
 }
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala
index a5c93a61..b0da9e79 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelGotify.scala
@@ -49,8 +49,12 @@ object RNotificationChannelGotify {
   def as(alias: String): Table =
     Table(Some(alias))
 
-  def getById(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] =
-    run(select(T.all), from(T), T.id === id).query[RNotificationChannelGotify].option
+  def getById(
+      userId: Ident
+  )(id: Ident): ConnectionIO[Option[RNotificationChannelGotify]] =
+    run(select(T.all), from(T), T.id === id && T.uid === userId)
+      .query[RNotificationChannelGotify]
+      .option
 
   def insert(r: RNotificationChannelGotify): ConnectionIO[Int] =
     DML.insert(
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala
index 9c3d6ad0..a70eb8d8 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelHttp.scala
@@ -45,8 +45,10 @@ object RNotificationChannelHttp {
   def as(alias: String): Table =
     Table(Some(alias))
 
-  def getById(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] =
-    run(select(T.all), from(T), T.id === id).query[RNotificationChannelHttp].option
+  def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelHttp]] =
+    run(select(T.all), from(T), T.id === id && T.uid === userId)
+      .query[RNotificationChannelHttp]
+      .option
 
   def insert(r: RNotificationChannelHttp): ConnectionIO[Int] =
     DML.insert(T, T.all, sql"${r.id},${r.uid},${r.name},${r.url},${r.created}")
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala
index 6c11eb0c..afa2d34c 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMail.scala
@@ -65,8 +65,10 @@ object RNotificationChannelMail {
       )
     )
 
-  def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
-    run(select(T.all), from(T), T.id === id).query[RNotificationChannelMail].option
+  def getById(userId: Ident)(id: Ident): ConnectionIO[Option[RNotificationChannelMail]] =
+    run(select(T.all), from(T), T.id === id && T.uid === userId)
+      .query[RNotificationChannelMail]
+      .option
 
   def getByAccount(account: AccountId): ConnectionIO[Vector[RNotificationChannelMail]] = {
     val user = RUser.as("u")
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala
index 386f9910..d6981906 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationChannelMatrix.scala
@@ -77,8 +77,12 @@ object RNotificationChannelMatrix {
       )
     )
 
-  def getById(id: Ident): ConnectionIO[Option[RNotificationChannelMatrix]] =
-    run(select(T.all), from(T), T.id === id).query[RNotificationChannelMatrix].option
+  def getById(userId: Ident)(
+      id: Ident
+  ): ConnectionIO[Option[RNotificationChannelMatrix]] =
+    run(select(T.all), from(T), T.id === id && T.uid === userId)
+      .query[RNotificationChannelMatrix]
+      .option
 
   def getByAccount(
       account: AccountId
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala
index 50b21f1e..75c22d85 100644
--- a/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationHook.scala
@@ -7,7 +7,6 @@
 package docspell.store.records
 
 import cats.data.NonEmptyList
-import cats.implicits._
 
 import docspell.common._
 import docspell.jsonminiq.JsonMiniQuery
@@ -22,115 +21,18 @@ final case class RNotificationHook(
     id: Ident,
     uid: Ident,
     enabled: Boolean,
-    channelMail: Option[Ident],
-    channelGotify: Option[Ident],
-    channelMatrix: Option[Ident],
-    channelHttp: Option[Ident],
     allEvents: Boolean,
     eventFilter: Option[JsonMiniQuery],
     created: Timestamp
-) {
-  def channelId: Ident =
-    channelMail
-      .orElse(channelGotify)
-      .orElse(channelMatrix)
-      .orElse(channelHttp)
-      .getOrElse(
-        sys.error(s"Illegal internal state: notification hook has no channel: ${id.id}")
-      )
-}
+) {}
 
 object RNotificationHook {
-  def mail(
-      id: Ident,
-      uid: Ident,
-      enabled: Boolean,
-      channelMail: Ident,
-      created: Timestamp
-  ): RNotificationHook =
-    RNotificationHook(
-      id,
-      uid,
-      enabled,
-      channelMail.some,
-      None,
-      None,
-      None,
-      false,
-      None,
-      created
-    )
-
-  def gotify(
-      id: Ident,
-      uid: Ident,
-      enabled: Boolean,
-      channelGotify: Ident,
-      created: Timestamp
-  ): RNotificationHook =
-    RNotificationHook(
-      id,
-      uid,
-      enabled,
-      None,
-      channelGotify.some,
-      None,
-      None,
-      false,
-      None,
-      created
-    )
-
-  def matrix(
-      id: Ident,
-      uid: Ident,
-      enabled: Boolean,
-      channelMatrix: Ident,
-      created: Timestamp
-  ): RNotificationHook =
-    RNotificationHook(
-      id,
-      uid,
-      enabled,
-      None,
-      None,
-      channelMatrix.some,
-      None,
-      false,
-      None,
-      created
-    )
-
-  def http(
-      id: Ident,
-      uid: Ident,
-      enabled: Boolean,
-      channelHttp: Ident,
-      created: Timestamp
-  ): RNotificationHook =
-    RNotificationHook(
-      id,
-      uid,
-      enabled,
-      None,
-      None,
-      None,
-      channelHttp.some,
-      false,
-      None,
-      created
-    )
-
   final case class Table(alias: Option[String]) extends TableDef {
     val tableName = "notification_hook"
 
     val id = Column[Ident]("id", this)
     val uid = Column[Ident]("uid", this)
     val enabled = Column[Boolean]("enabled", this)
-    val channelMail = Column[Ident]("channel_mail", this)
-    val channelGotify = Column[Ident]("channel_gotify", this)
-    val channelMatrix = Column[Ident]("channel_matrix", this)
-    val channelHttp = Column[Ident]("channel_http", this)
     val allEvents = Column[Boolean]("all_events", this)
     val eventFilter = Column[JsonMiniQuery]("event_filter", this)
     val created = Column[Timestamp]("created", this)
@@ -140,10 +42,6 @@ object RNotificationHook {
         id,
         uid,
         enabled,
-        channelMail,
-        channelGotify,
-        channelMatrix,
-        channelHttp,
         allEvents,
         eventFilter,
         created
@@ -157,7 +55,7 @@ object RNotificationHook {
     DML.insert(
       T,
       T.all,
-      sql"${r.id},${r.uid},${r.enabled},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp},${r.allEvents},${r.eventFilter},${r.created}"
+      sql"${r.id},${r.uid},${r.enabled},${r.allEvents},${r.eventFilter},${r.created}"
     )
 
   def deleteByAccount(id: Ident, account: AccountId): ConnectionIO[Int] = {
@@ -174,10 +72,6 @@ object RNotificationHook {
       T.id === r.id && T.uid === r.uid,
       DML.set(
         T.enabled.setTo(r.enabled),
-        T.channelMail.setTo(r.channelMail),
-        T.channelGotify.setTo(r.channelGotify),
-        T.channelMatrix.setTo(r.channelMatrix),
-        T.channelHttp.setTo(r.channelHttp),
         T.allEvents.setTo(r.allEvents),
         T.eventFilter.setTo(r.eventFilter)
       )
diff --git a/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala b/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala
new file mode 100644
index 00000000..d499c593
--- /dev/null
+++ b/modules/store/src/main/scala/docspell/store/records/RNotificationHookChannel.scala
@@ -0,0 +1,236 @@
+/*
+ * Copyright 2020 Eike K. & Contributors
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+package docspell.store.records
+
+import cats.data.{NonEmptyList => Nel}
+import cats.effect.Sync
+import cats.implicits._
+
+import docspell.common._
+import docspell.notification.api.{ChannelRef, ChannelType}
+import docspell.store.qb.DSL._
+import docspell.store.qb._
+
+import doobie._
+import doobie.implicits._
+
+final case class RNotificationHookChannel(
+    id: Ident,
+    hookId: Ident,
+    channelMail: Option[Ident],
+    channelGotify: Option[Ident],
+    channelMatrix: Option[Ident],
+    channelHttp: Option[Ident]
+) {
+
+  def channelId: Ident =
+    channelMail
+      .orElse(channelGotify)
+      .orElse(channelMatrix)
+      .orElse(channelHttp)
+      .getOrElse(
+        sys.error(s"Illegal internal state: notification hook has no channel: $this")
+      )
+
+  def channelType: ChannelType =
+    channelMail
+      .map(_ => ChannelType.Mail)
+      .orElse(channelGotify.map(_ => ChannelType.Gotify))
+      .orElse(channelMatrix.map(_ => ChannelType.Matrix))
+      .orElse(channelHttp.map(_ => ChannelType.Http))
+      .getOrElse(
+        sys.error(s"Illegal internal state: notification hook has no channel: $this")
+      )
+}
+object RNotificationHookChannel {
+  def fromRef(id: Ident, hookId: Ident, ref: ChannelRef): RNotificationHookChannel =
+    ref.channelType match {
+      case ChannelType.Mail   => mail(id, hookId, ref.id)
+      case ChannelType.Gotify => gotify(id, hookId, ref.id)
+      case ChannelType.Matrix => matrix(id, hookId, ref.id)
+      case ChannelType.Http   => http(id, hookId, ref.id)
+    }
+
+  def mail(
+      id: Ident,
+      hookId: Ident,
+      channelMail: Ident
+  ): RNotificationHookChannel =
+    RNotificationHookChannel(
+      id,
+      hookId,
+      channelMail.some,
+      None,
+      None,
+      None
+    )
+
+  def gotify(
+      id: Ident,
+      hookId: Ident,
+      channelGotify: Ident
+  ): RNotificationHookChannel =
+    RNotificationHookChannel(
+      id,
+      hookId,
+      None,
+      channelGotify.some,
+      None,
+      None
+    )
+
+  def matrix(
+      id: Ident,
+      hookId: Ident,
+      channelMatrix: Ident
+  ): RNotificationHookChannel =
+    RNotificationHookChannel(
+      id,
+      hookId,
+      None,
+      None,
+      channelMatrix.some,
+      None
+    )
+
+  def http(
+      id: Ident,
+      hookId: Ident,
+      channelHttp: Ident
+  ): RNotificationHookChannel =
+    RNotificationHookChannel(
+      id,
+      hookId,
+      None,
+      None,
+      None,
+      channelHttp.some
+    )
+
+  final case class Table(alias: Option[String]) extends TableDef {
+    val tableName = "notification_hook_channel"
+
+    val id = Column[Ident]("id", this)
+    val hookId = Column[Ident]("hook_id", this)
+    val channelMail = Column[Ident]("channel_mail", this)
+    val channelGotify = Column[Ident]("channel_gotify", this)
+    val channelMatrix = Column[Ident]("channel_matrix", this)
+    val channelHttp = Column[Ident]("channel_http", this)
+
+    val all: Nel[Column[_]] =
+      Nel.of(id, hookId, channelMail, channelGotify, channelMatrix, channelHttp)
+  }
+
+  def as(alias: String): Table =
+    Table(Some(alias))
+
+  val T: Table = Table(None)
+
+  def insert(r: RNotificationHookChannel): ConnectionIO[Int] =
+    DML.insert(
+      T,
+      T.all,
+      sql"${r.id},${r.hookId},${r.channelMail},${r.channelGotify},${r.channelMatrix},${r.channelHttp}"
+    )
+
+  def update(r: RNotificationHookChannel): ConnectionIO[Int] =
+    DML.update(
+      T,
+      T.id === r.id && T.hookId === r.hookId,
+      DML.set(
+        T.channelMail.setTo(r.channelMail),
+        T.channelGotify.setTo(r.channelGotify),
+        T.channelMatrix.setTo(r.channelMatrix),
+        T.channelHttp.setTo(r.channelHttp)
+      )
+    )
+
+  def deleteByHook(hookId: Ident): ConnectionIO[Int] =
+    DML.delete(T, T.hookId === hookId)
+
+  def insertAll(rs: List[RNotificationHookChannel]): ConnectionIO[Int] =
+    rs.traverse(insert).map(_.sum)
+
+  def updateAll(hookId: Ident, channels: List[ChannelRef]): ConnectionIO[Int] =
+    channels
+      .traverse(ref => Ident.randomId[ConnectionIO].map(id => fromRef(id, hookId, ref)))
+      .flatMap(all => deleteByHook(hookId) *> insertAll(all))
+
+  def allOf(hookId: Ident): ConnectionIO[Vector[RNotificationHookChannel]] =
+    Select(select(T.all), from(T), T.hookId === hookId).build
+      .query[RNotificationHookChannel]
+      .to[Vector]
+
+  def allOfNel(hookId: Ident): ConnectionIO[Nel[RNotificationHookChannel]] =
+    allOf(hookId)
+      .map(Nel.fromFoldable[Vector, RNotificationHookChannel])
+      .flatMap(
+        _.map(_.pure[ConnectionIO]).getOrElse(
+          Sync[ConnectionIO]
+            .raiseError(new Exception(s"Hook '${hookId.id}' has no associated channels!"))
+        )
+      )
+
+  def resolveRefs(rs: Nel[RNotificationHookChannel]): ConnectionIO[List[ChannelRef]] = {
+    val cmail = RNotificationChannelMail.as("cmail")
+    val cgotify = RNotificationChannelGotify.as("cgotify")
+    val cmatrix = RNotificationChannelMatrix.as("cmatrix")
+    val chttp = RNotificationChannelHttp.as("chttp")
+
+    def selectRef(
+        idList: List[Ident],
+        idCol: Column[Ident],
+        nameCol: Column[String],
+        ctype: ChannelType,
+        table: TableDef
+    ) =
+      Nel
+        .fromList(idList)
+        .map(ids =>
+          Select(
+            select(idCol.s, const(ctype.name), nameCol.s),
+            from(table),
+            idCol.in(ids)
+          )
+        )
+
+    val mailRefs = selectRef(
+      rs.toList.flatMap(_.channelMail),
+      cmail.id,
+      cmail.name,
+      ChannelType.Mail,
+      cmail
+    )
+    val gotifyRefs = selectRef(
+      rs.toList.flatMap(_.channelGotify),
+      cgotify.id,
+      cgotify.name,
+      ChannelType.Gotify,
+      cgotify
+    )
+    val matrixRefs = selectRef(
+      rs.toList.flatMap(_.channelMatrix),
+      cmatrix.id,
+      cmatrix.name,
+      ChannelType.Matrix,
+      cmatrix
+    )
+    val httpRefs = selectRef(
+      rs.toList.flatMap(_.channelHttp),
+      chttp.id,
+      chttp.name,
+      ChannelType.Http,
+      chttp
+    )
+
+    val queries = List(mailRefs, gotifyRefs, matrixRefs, httpRefs).flatten
+    Nel.fromList(queries) match {
+      case Some(nel) => union(nel.head, nel.tail: _*).build.query[ChannelRef].to[List]
+      case None      => List.empty[ChannelRef].pure[ConnectionIO]
+    }
+  }
+}
diff --git a/modules/webapp/src/main/elm/Api.elm b/modules/webapp/src/main/elm/Api.elm
index 5e7f3c2c..3b9f63d4 100644
--- a/modules/webapp/src/main/elm/Api.elm
+++ b/modules/webapp/src/main/elm/Api.elm
@@ -23,6 +23,7 @@ module Api exposing
     , checkCalEvent
     , confirmMultiple
     , confirmOtp
+    , createChannel
     , createHook
     , createImapSettings
     , createMailSettings
@@ -34,6 +35,7 @@ module Api exposing
     , deleteAttachment
     , deleteAttachments
     , deleteBookmark
+    , deleteChannel
     , deleteCustomField
     , deleteCustomValue
     , deleteCustomValueMultiple
@@ -56,6 +58,8 @@ module Api exposing
     , fileURL
     , getAttachmentMeta
     , getBookmarks
+    , getChannels
+    , getChannelsIgnoreError
     , getClientSettings
     , getCollective
     , getCollectiveSettings
@@ -172,6 +176,7 @@ module Api exposing
     , twoFactor
     , unconfirmMultiple
     , updateBookmark
+    , updateChannel
     , updateHook
     , updateNotifyDueItems
     , updatePeriodicQuery
@@ -229,6 +234,7 @@ import Api.Model.MoveAttachment exposing (MoveAttachment)
 import Api.Model.NewCustomField exposing (NewCustomField)
 import Api.Model.NewFolder exposing (NewFolder)
 import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult)
+import Api.Model.NotificationHook exposing (NotificationHook)
 import Api.Model.NotificationSampleEventReq exposing (NotificationSampleEventReq)
 import Api.Model.OptionalDate exposing (OptionalDate)
 import Api.Model.OptionalId exposing (OptionalId)
@@ -239,6 +245,8 @@ import Api.Model.OtpConfirm exposing (OtpConfirm)
 import Api.Model.OtpResult exposing (OtpResult)
 import Api.Model.OtpState exposing (OtpState)
 import Api.Model.PasswordChange exposing (PasswordChange)
+import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
+import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Api.Model.Person exposing (Person)
 import Api.Model.PersonList exposing (PersonList)
 import Api.Model.ReferenceList exposing (ReferenceList)
@@ -274,10 +282,8 @@ import Data.EquipmentOrder exposing (EquipmentOrder)
 import Data.EventType exposing (EventType)
 import Data.Flags exposing (Flags)
 import Data.FolderOrder exposing (FolderOrder)
-import Data.NotificationHook exposing (NotificationHook)
+import Data.NotificationChannel exposing (NotificationChannel)
 import Data.OrganizationOrder exposing (OrganizationOrder)
-import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
-import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Data.PersonOrder exposing (PersonOrder)
 import Data.Priority exposing (Priority)
 import Data.TagOrder exposing (TagOrder)
@@ -604,7 +610,7 @@ startOnceNotifyDueItems flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems/startonce"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -618,7 +624,7 @@ updateNotifyDueItems flags settings receive =
     Http2.authPut
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -632,7 +638,7 @@ createNotifyDueItems flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -645,7 +651,7 @@ getNotifyDueItems flags receive =
     Http2.authGet
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
         , account = getAccount flags
-        , expect = Http.expectJson receive (JsonDecode.list Data.PeriodicDueItemsSettings.decoder)
+        , expect = Http.expectJson receive (JsonDecode.list Api.Model.PeriodicDueItemsSettings.decoder)
         }
 
 
@@ -658,7 +664,7 @@ submitNotifyDueItems flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/notifydueitems"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicDueItemsSettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicDueItemsSettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -689,7 +695,7 @@ startOncePeriodicQuery flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery/startonce"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -703,7 +709,7 @@ updatePeriodicQuery flags settings receive =
     Http2.authPut
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -717,7 +723,7 @@ createPeriodicQuery flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -730,7 +736,7 @@ getPeriodicQuery flags receive =
     Http2.authGet
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
         , account = getAccount flags
-        , expect = Http.expectJson receive (JsonDecode.list Data.PeriodicQuerySettings.decoder)
+        , expect = Http.expectJson receive (JsonDecode.list Api.Model.PeriodicQuerySettings.decoder)
         }
 
 
@@ -743,7 +749,7 @@ submitPeriodicQuery flags settings receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/usertask/periodicquery"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.PeriodicQuerySettings.encode settings)
+        , body = Http.jsonBody (Api.Model.PeriodicQuerySettings.encode settings)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -2576,6 +2582,63 @@ shareFileURL attachId =
 
 
 
+--- NotificationChannel
+
+
+getChannelsTask : Flags -> Task.Task Http.Error (List NotificationChannel)
+getChannelsTask flags =
+    Http2.authTask
+        { method = "GET"
+        , url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel"
+        , account = getAccount flags
+        , body = Http.emptyBody
+        , resolver = Http2.jsonResolver (JsonDecode.list Data.NotificationChannel.decoder)
+        , headers = []
+        , timeout = Nothing
+        }
+
+
+getChannelsIgnoreError : Flags -> (List NotificationChannel -> msg) -> Cmd msg
+getChannelsIgnoreError flags tagger =
+    getChannelsTask flags
+        |> Task.attempt (Result.map tagger >> Result.withDefault (tagger []))
+
+
+getChannels : Flags -> (Result Http.Error (List NotificationChannel) -> msg) -> Cmd msg
+getChannels flags receive =
+    getChannelsTask flags |> Task.attempt receive
+
+
+deleteChannel : Flags -> String -> (Result Http.Error BasicResult -> msg) -> Cmd msg
+deleteChannel flags id receive =
+    Http2.authDelete
+        { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel/" ++ id
+        , account = getAccount flags
+        , expect = Http.expectJson receive Api.Model.BasicResult.decoder
+        }
+
+
+createChannel : Flags -> NotificationChannel -> (Result Http.Error BasicResult -> msg) -> Cmd msg
+createChannel flags hook receive =
+    Http2.authPost
+        { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel"
+        , account = getAccount flags
+        , body = Http.jsonBody (Data.NotificationChannel.encode hook)
+        , expect = Http.expectJson receive Api.Model.BasicResult.decoder
+        }
+
+
+updateChannel : Flags -> NotificationChannel -> (Result Http.Error BasicResult -> msg) -> Cmd msg
+updateChannel flags hook receive =
+    Http2.authPut
+        { url = flags.config.baseUrl ++ "/api/v1/sec/notification/channel"
+        , account = getAccount flags
+        , body = Http.jsonBody (Data.NotificationChannel.encode hook)
+        , expect = Http.expectJson receive Api.Model.BasicResult.decoder
+        }
+
+
+
 --- NotificationHook
 
 
@@ -2584,7 +2647,7 @@ getHooks flags receive =
     Http2.authGet
         { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
         , account = getAccount flags
-        , expect = Http.expectJson receive (JsonDecode.list Data.NotificationHook.decoder)
+        , expect = Http.expectJson receive (JsonDecode.list Api.Model.NotificationHook.decoder)
         }
 
 
@@ -2602,7 +2665,7 @@ createHook flags hook receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.NotificationHook.encode hook)
+        , body = Http.jsonBody (Api.Model.NotificationHook.encode hook)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -2612,7 +2675,7 @@ updateHook flags hook receive =
     Http2.authPut
         { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.NotificationHook.encode hook)
+        , body = Http.jsonBody (Api.Model.NotificationHook.encode hook)
         , expect = Http.expectJson receive Api.Model.BasicResult.decoder
         }
 
@@ -2642,7 +2705,7 @@ testHook flags hook receive =
     Http2.authPost
         { url = flags.config.baseUrl ++ "/api/v1/sec/notification/hook/sendTestEvent"
         , account = getAccount flags
-        , body = Http.jsonBody (Data.NotificationHook.encode hook)
+        , body = Http.jsonBody (Api.Model.NotificationHook.encode hook)
         , expect = Http.expectJson receive Api.Model.NotificationChannelTestResult.decoder
         }
 
diff --git a/modules/webapp/src/main/elm/Comp/ChannelForm.elm b/modules/webapp/src/main/elm/Comp/ChannelForm.elm
index 203fca16..83bcfee7 100644
--- a/modules/webapp/src/main/elm/Comp/ChannelForm.elm
+++ b/modules/webapp/src/main/elm/Comp/ChannelForm.elm
@@ -48,17 +48,11 @@ type alias HttpModel =
     }
 
 
-type alias RefModel =
-    { channelType : ChannelType
-    }
-
-
 type Model
     = Matrix MatrixModel
     | Gotify GotifyModel
     | Mail MailModel
     | Http HttpModel
-    | Ref RefModel
 
 
 type Msg
@@ -147,11 +141,6 @@ initWith flags channel =
             , Cmd.none
             )
 
-        Data.NotificationChannel.Ref m ->
-            ( Ref { channelType = m.channelType }
-            , Cmd.none
-            )
-
 
 channelType : Model -> ChannelType
 channelType model =
@@ -168,9 +157,6 @@ channelType model =
         Http _ ->
             Data.ChannelType.Http
 
-        Ref ref ->
-            ref.channelType
-
 
 getChannel : Model -> Maybe NotificationChannel
 getChannel model =
@@ -187,9 +173,6 @@ getChannel model =
         Http mm ->
             Maybe.map Data.NotificationChannel.Http mm.value
 
-        Ref _ ->
-            Nothing
-
 
 
 --- Update
@@ -269,12 +252,3 @@ view texts settings model =
         Http m ->
             Html.map HttpMsg
                 (Comp.NotificationHttpForm.view texts.httpForm m.form)
-
-        -- Note: currently when retrieving hooks, this is not
-        -- send from the server. The server always sends
-        -- concrete channel details. However, it is possible
-        -- to create hooks with a reference to an existing
-        -- channel, but this is not supported in this client.
-        -- So this channel is ignored here.
-        Ref _ ->
-            span [ class "hidden" ] []
diff --git a/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm b/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm
new file mode 100644
index 00000000..e7d61ab9
--- /dev/null
+++ b/modules/webapp/src/main/elm/Comp/ChannelRefInput.elm
@@ -0,0 +1,154 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Comp.ChannelRefInput exposing (Model, Msg, getSelected, init, initSelected, initWith, setOptions, setSelected, update, view)
+
+import Api
+import Api.Model.NotificationChannelRef exposing (NotificationChannelRef)
+import Comp.Dropdown exposing (Option)
+import Data.ChannelType
+import Data.DropdownStyle
+import Data.Flags exposing (Flags)
+import Data.NotificationChannel
+import Data.UiSettings exposing (UiSettings)
+import Html exposing (Html)
+import Messages.Comp.ChannelRefInput exposing (Texts)
+import Util.String
+
+
+type alias Model =
+    { ddm : Comp.Dropdown.Model NotificationChannelRef
+    , all : List NotificationChannelRef
+    }
+
+
+type Msg
+    = DropdownMsg (Comp.Dropdown.Msg NotificationChannelRef)
+    | LoadChannelsResp (List NotificationChannelRef)
+
+
+emptyModel : Model
+emptyModel =
+    { ddm = makeDropdownModel
+    , all = []
+    }
+
+
+init : Flags -> ( Model, Cmd Msg )
+init flags =
+    ( emptyModel, getOptions flags )
+
+
+getOptions : Flags -> Cmd Msg
+getOptions flags =
+    Api.getChannelsIgnoreError flags (List.map Data.NotificationChannel.getRef >> LoadChannelsResp)
+
+
+setOptions : List NotificationChannelRef -> Msg
+setOptions refs =
+    LoadChannelsResp refs
+
+
+initSelected : Flags -> List NotificationChannelRef -> ( Model, Cmd Msg )
+initSelected flags selected =
+    ( update (setSelected selected) emptyModel
+        |> Tuple.first
+    , getOptions flags
+    )
+
+
+initWith : List NotificationChannelRef -> List NotificationChannelRef -> Model
+initWith options selected =
+    update (setSelected selected) emptyModel
+        |> Tuple.first
+        |> update (setOptions options)
+        |> Tuple.first
+
+
+getSelected : Model -> List NotificationChannelRef
+getSelected model =
+    Comp.Dropdown.getSelected model.ddm
+
+
+setSelected : List NotificationChannelRef -> Msg
+setSelected refs =
+    DropdownMsg (Comp.Dropdown.SetSelection refs)
+
+
+
+--- Update
+
+
+update : Msg -> Model -> ( Model, Cmd Msg )
+update msg model =
+    case msg of
+        DropdownMsg lm ->
+            let
+                ( dm, dc ) =
+                    Comp.Dropdown.update lm model.ddm
+            in
+            ( { model | ddm = dm }
+            , Cmd.map DropdownMsg dc
+            )
+
+        LoadChannelsResp refs ->
+            let
+                ( dm, dc ) =
+                    Comp.Dropdown.update (Comp.Dropdown.SetOptions refs) model.ddm
+            in
+            ( { model
+                | all = refs
+                , ddm = dm
+              }
+            , Cmd.map DropdownMsg dc
+            )
+
+
+
+--- View
+
+
+view : Texts -> UiSettings -> Model -> Html Msg
+view texts settings model =
+    let
+        idShort id =
+            String.slice 0 6 id
+
+        joinName name ct =
+            Option (ct ++ " (" ++ name ++ ")") ""
+
+        mkName ref =
+            Data.ChannelType.fromString ref.channelType
+                |> Maybe.map texts.channelType
+                |> Maybe.withDefault ref.channelType
+                |> joinName (Maybe.withDefault (idShort ref.id) ref.name)
+
+        viewCfg =
+            { makeOption = mkName
+            , placeholder = texts.placeholder
+            , labelColor = \_ -> \_ -> ""
+            , style = Data.DropdownStyle.mainStyle
+            }
+    in
+    Html.map DropdownMsg
+        (Comp.Dropdown.view2 viewCfg settings model.ddm)
+
+
+
+--- Helpers
+
+
+makeDropdownModel : Comp.Dropdown.Model NotificationChannelRef
+makeDropdownModel =
+    let
+        m =
+            Comp.Dropdown.makeModel
+                { multiple = True
+                , searchable = \n -> n > 0
+                }
+    in
+    { m | searchWithAdditional = True }
diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm
index aea8a484..da412623 100644
--- a/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm
+++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskForm.elm
@@ -16,22 +16,18 @@ module Comp.DueItemsTaskForm exposing
     )
 
 import Api
-import Api.Model.EmailSettingsList exposing (EmailSettingsList)
-import Api.Model.Tag exposing (Tag)
+import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Api.Model.TagList exposing (TagList)
 import Comp.Basic as B
 import Comp.CalEventInput
-import Comp.ChannelForm
+import Comp.ChannelRefInput
 import Comp.IntField
 import Comp.MenuBar as MB
 import Comp.TagDropdown
 import Comp.YesNoDimmer
 import Data.CalEvent exposing (CalEvent)
-import Data.ChannelType exposing (ChannelType)
 import Data.DropdownStyle as DS
 import Data.Flags exposing (Flags)
-import Data.NotificationChannel
-import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Data.TagOrder
 import Data.UiSettings exposing (UiSettings)
 import Data.Validated exposing (Validated(..))
@@ -43,13 +39,11 @@ import Markdown
 import Messages.Comp.DueItemsTaskForm exposing (Texts)
 import Styles as S
 import Util.Maybe
-import Util.Tag
-import Util.Update
 
 
 type alias Model =
     { settings : PeriodicDueItemsSettings
-    , channelModel : Comp.ChannelForm.Model
+    , channelModel : Comp.ChannelRefInput.Model
     , tagInclModel : Comp.TagDropdown.Model
     , tagExclModel : Comp.TagDropdown.Model
     , remindDays : Maybe Int
@@ -99,18 +93,14 @@ type Msg
     | RequestDelete
     | YesNoDeleteMsg Comp.YesNoDimmer.Msg
     | SetSummary String
-    | ChannelMsg Comp.ChannelForm.Msg
+    | ChannelMsg Comp.ChannelRefInput.Msg
 
 
 initWith : Flags -> PeriodicDueItemsSettings -> ( Model, Cmd Msg )
 initWith flags s =
     let
-        ct =
-            Data.NotificationChannel.channelType s.channel
-                |> Maybe.withDefault Data.ChannelType.Matrix
-
         ( im, ic ) =
-            init flags ct
+            init flags
 
         newSchedule =
             Data.CalEvent.fromEvent s.schedule
@@ -120,7 +110,7 @@ initWith flags s =
             Comp.CalEventInput.init flags newSchedule
 
         ( cfm, cfc ) =
-            Comp.ChannelForm.initWith flags s.channel
+            Comp.ChannelRefInput.initSelected flags s.channels
     in
     ( { im
         | settings = s
@@ -145,8 +135,8 @@ initWith flags s =
     )
 
 
-init : Flags -> ChannelType -> ( Model, Cmd Msg )
-init flags ct =
+init : Flags -> ( Model, Cmd Msg )
+init flags =
     let
         initialSchedule =
             Data.CalEvent.everyMonth
@@ -155,9 +145,9 @@ init flags ct =
             Comp.CalEventInput.init flags initialSchedule
 
         ( cfm, cfc ) =
-            Comp.ChannelForm.init flags ct
+            Comp.ChannelRefInput.init flags
     in
-    ( { settings = Data.PeriodicDueItemsSettings.empty ct
+    ( { settings = Api.Model.PeriodicDueItemsSettings.empty
       , channelModel = cfm
       , tagInclModel = Comp.TagDropdown.initWith [] []
       , tagExclModel = Comp.TagDropdown.initWith [] []
@@ -203,11 +193,17 @@ makeSettings model =
                     Err ValidateCalEventInvalid
 
         channelM =
-            Result.fromMaybe
-                ValidateChannelRequired
-                (Comp.ChannelForm.getChannel model.channelModel)
+            let
+                list =
+                    Comp.ChannelRefInput.getSelected model.channelModel
+            in
+            if list == [] then
+                Err ValidateChannelRequired
 
-        make days timer channel =
+            else
+                Ok list
+
+        make days timer channels =
             { prev
                 | tagsInclude = Comp.TagDropdown.getSelected model.tagInclModel
                 , tagsExclude = Comp.TagDropdown.getSelected model.tagExclModel
@@ -216,7 +212,7 @@ makeSettings model =
                 , enabled = model.enabled
                 , schedule = Data.CalEvent.makeEvent timer
                 , summary = model.summary
-                , channel = channel
+                , channels = channels
             }
     in
     Result.map3 make
@@ -247,7 +243,7 @@ update flags msg model =
         ChannelMsg lm ->
             let
                 ( cfm, cfc ) =
-                    Comp.ChannelForm.update flags lm model.channelModel
+                    Comp.ChannelRefInput.update lm model.channelModel
             in
             ( { model | channelModel = cfm }
             , NoAction
@@ -538,9 +534,9 @@ view2 texts extraClasses settings model =
                 ]
             ]
         , div [ class "mb-4" ]
-            [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel))
+            [ formHeader texts.channelHeader
             , Html.map ChannelMsg
-                (Comp.ChannelForm.view texts.channelForm settings model.channelModel)
+                (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
             ]
         , formHeader texts.queryLabel
         , div [ class "mb-4" ]
diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm
index 565d00ba..95f17cdb 100644
--- a/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm
+++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskList.elm
@@ -14,15 +14,16 @@ module Comp.DueItemsTaskList exposing
     , view2
     )
 
+import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Comp.Basic as B
+import Data.ChannelRef
 import Data.ChannelType
-import Data.NotificationChannel
-import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Messages.Comp.DueItemsTaskList exposing (Texts)
 import Styles as S
 import Util.Html
+import Util.List
 
 
 type alias Model =
@@ -94,9 +95,7 @@ viewItem2 texts item =
                 ]
             ]
         , td [ class "text-left mr-2" ]
-            [ Data.NotificationChannel.channelType item.channel
-                |> Maybe.map Data.ChannelType.asString
-                |> Maybe.withDefault "-"
-                |> text
+            [ div [ class " space-x-1" ]
+                (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels)
             ]
         ]
diff --git a/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm b/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm
index 6baa28d5..3846b9b4 100644
--- a/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm
+++ b/modules/webapp/src/main/elm/Comp/DueItemsTaskManage.elm
@@ -15,13 +15,11 @@ module Comp.DueItemsTaskManage exposing
 
 import Api
 import Api.Model.BasicResult exposing (BasicResult)
-import Comp.ChannelMenu
+import Api.Model.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Comp.DueItemsTaskForm
 import Comp.DueItemsTaskList
 import Comp.MenuBar as MB
-import Data.ChannelType exposing (ChannelType)
 import Data.Flags exposing (Flags)
-import Data.PeriodicDueItemsSettings exposing (PeriodicDueItemsSettings)
 import Data.UiSettings exposing (UiSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
@@ -35,7 +33,6 @@ type alias Model =
     , detailModel : Maybe Comp.DueItemsTaskForm.Model
     , items : List PeriodicDueItemsSettings
     , formState : FormState
-    , channelMenuOpen : Bool
     }
 
 
@@ -57,9 +54,8 @@ type Msg
     = ListMsg Comp.DueItemsTaskList.Msg
     | DetailMsg Comp.DueItemsTaskForm.Msg
     | GetDataResp (Result Http.Error (List PeriodicDueItemsSettings))
-    | NewTaskInit ChannelType
+    | NewTaskInit
     | SubmitResp SubmitType (Result Http.Error BasicResult)
-    | ToggleChannelMenu
 
 
 initModel : Model
@@ -68,7 +64,6 @@ initModel =
     , detailModel = Nothing
     , items = []
     , formState = FormStateInitial
-    , channelMenuOpen = False
     }
 
 
@@ -89,11 +84,6 @@ init flags =
 update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
 update flags msg model =
     case msg of
-        ToggleChannelMenu ->
-            ( { model | channelMenuOpen = not model.channelMenuOpen }
-            , Cmd.none
-            )
-
         GetDataResp (Ok items) ->
             ( { model
                 | items = items
@@ -194,12 +184,12 @@ update flags msg model =
                 Nothing ->
                     ( model, Cmd.none )
 
-        NewTaskInit ct ->
+        NewTaskInit ->
             let
                 ( mm, mc ) =
-                    Comp.DueItemsTaskForm.init flags ct
+                    Comp.DueItemsTaskForm.init flags
             in
-            ( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc )
+            ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc )
 
         SubmitResp submitType (Ok res) ->
             ( { model
@@ -295,18 +285,15 @@ viewForm2 texts settings model =
 
 viewList2 : Texts -> Model -> List (Html Msg)
 viewList2 texts model =
-    let
-        menuModel =
-            { menuOpen = model.channelMenuOpen
-            , toggleMenu = ToggleChannelMenu
-            , menuLabel = texts.newTask
-            , onItem = NewTaskInit
-            }
-    in
     [ MB.view
         { start = []
         , end =
-            [ Comp.ChannelMenu.channelMenu texts.channelType menuModel
+            [ MB.PrimaryButton
+                { tagger = NewTaskInit
+                , title = texts.newTask
+                , icon = Just "fa fa-plus"
+                , label = texts.newTask
+                }
             ]
         , rootClasses = "mb-4"
         }
diff --git a/modules/webapp/src/main/elm/Comp/EventSample.elm b/modules/webapp/src/main/elm/Comp/EventSample.elm
index 0516e1b0..556e847b 100644
--- a/modules/webapp/src/main/elm/Comp/EventSample.elm
+++ b/modules/webapp/src/main/elm/Comp/EventSample.elm
@@ -115,8 +115,8 @@ dropdownCfg texts =
     }
 
 
-viewJson : Texts -> Model -> Html Msg
-viewJson texts model =
+viewJson : Texts -> Bool -> Model -> Html Msg
+viewJson texts enableEventChooser model =
     let
         json =
             Result.withDefault ""
@@ -125,7 +125,10 @@ viewJson texts model =
     div
         [ class "flex flex-col w-full relative"
         ]
-        [ div [ class "flex inline-flex items-center absolute top-2 right-4" ]
+        [ div
+            [ class "flex inline-flex items-center absolute top-2 right-4"
+            , classList [ ( "hidden", not enableEventChooser ) ]
+            ]
             [ Html.map EventTypeMsg
                 (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
                     False
@@ -144,8 +147,8 @@ viewJson texts model =
         ]
 
 
-viewMessage : Texts -> Model -> Html Msg
-viewMessage texts model =
+viewMessage : Texts -> Bool -> Model -> Html Msg
+viewMessage texts enableEventChooser model =
     let
         titleDecoder =
             D.at [ "message", "title" ] D.string
@@ -162,7 +165,10 @@ viewMessage texts model =
     div
         [ class "flex flex-col w-full relative"
         ]
-        [ div [ class "flex inline-flex items-center absolute top-2 right-4" ]
+        [ div
+            [ class "flex inline-flex items-center absolute top-2 right-4"
+            , classList [ ( "hidden", not enableEventChooser ) ]
+            ]
             [ Html.map EventTypeMsg
                 (Comp.FixedDropdown.viewStyled2 (dropdownCfg texts)
                     False
diff --git a/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm b/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm
new file mode 100644
index 00000000..27af5a51
--- /dev/null
+++ b/modules/webapp/src/main/elm/Comp/NotificationChannelManage.elm
@@ -0,0 +1,454 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Comp.NotificationChannelManage exposing
+    ( Model
+    , Msg
+    , init
+    , update
+    , view
+    )
+
+import Api
+import Api.Model.BasicResult exposing (BasicResult)
+import Comp.Basic as B
+import Comp.ChannelForm
+import Comp.ChannelMenu
+import Comp.MenuBar as MB
+import Comp.NotificationChannelTable
+import Data.ChannelType exposing (ChannelType)
+import Data.Flags exposing (Flags)
+import Data.NotificationChannel exposing (NotificationChannel)
+import Data.UiSettings exposing (UiSettings)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Html.Events exposing (onClick)
+import Http
+import Messages.Comp.NotificationChannelManage exposing (Texts)
+import Styles as S
+
+
+type alias Model =
+    { listModel : Comp.NotificationChannelTable.Model
+    , detailModel : Maybe Comp.ChannelForm.Model
+    , items : List NotificationChannel
+    , deleteConfirm : DeleteConfirm
+    , loading : Bool
+    , formState : FormState
+    , newChannelMenuOpen : Bool
+    , jsonFilterError : Maybe String
+    }
+
+
+type DeleteConfirm
+    = DeleteConfirmOff
+    | DeleteConfirmOn
+
+
+type SubmitType
+    = SubmitDelete
+    | SubmitUpdate
+    | SubmitCreate
+
+
+type FormState
+    = FormStateInitial
+    | FormErrorHttp Http.Error
+    | FormSubmitSuccessful SubmitType
+    | FormErrorSubmit String
+    | FormErrorInvalid
+
+
+type Msg
+    = TableMsg Comp.NotificationChannelTable.Msg
+    | DetailMsg Comp.ChannelForm.Msg
+    | GetDataResp (Result Http.Error (List NotificationChannel))
+    | ToggleNewChannelMenu
+    | SubmitResp SubmitType (Result Http.Error BasicResult)
+    | NewChannelInit ChannelType
+    | BackToTable
+    | Submit
+    | RequestDelete
+    | CancelDelete
+    | DeleteChannelNow String
+
+
+initModel : Model
+initModel =
+    { listModel = Comp.NotificationChannelTable.init
+    , detailModel = Nothing
+    , items = []
+    , loading = False
+    , formState = FormStateInitial
+    , newChannelMenuOpen = False
+    , deleteConfirm = DeleteConfirmOff
+    , jsonFilterError = Nothing
+    }
+
+
+initCmd : Flags -> Cmd Msg
+initCmd flags =
+    Api.getChannels flags GetDataResp
+
+
+init : Flags -> ( Model, Cmd Msg )
+init flags =
+    ( initModel, initCmd flags )
+
+
+
+--- Update
+
+
+update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
+update flags msg model =
+    case msg of
+        GetDataResp (Ok res) ->
+            ( { model
+                | items = res
+                , formState = FormStateInitial
+              }
+            , Cmd.none
+            )
+
+        GetDataResp (Err err) ->
+            ( { model | formState = FormErrorHttp err }
+            , Cmd.none
+            )
+
+        TableMsg lm ->
+            let
+                ( mm, action ) =
+                    Comp.NotificationChannelTable.update flags lm model.listModel
+
+                ( detail, cmd ) =
+                    case action of
+                        Comp.NotificationChannelTable.NoAction ->
+                            ( Nothing, Cmd.none )
+
+                        Comp.NotificationChannelTable.EditAction channel ->
+                            let
+                                ( dm, dc ) =
+                                    Comp.ChannelForm.initWith flags channel
+                            in
+                            ( Just dm, Cmd.map DetailMsg dc )
+            in
+            ( { model
+                | listModel = mm
+                , detailModel = detail
+              }
+            , cmd
+            )
+
+        DetailMsg lm ->
+            case model.detailModel of
+                Just dm ->
+                    let
+                        ( mm, mc ) =
+                            Comp.ChannelForm.update flags lm dm
+                    in
+                    ( { model | detailModel = Just mm }
+                    , Cmd.map DetailMsg mc
+                    )
+
+                Nothing ->
+                    ( model, Cmd.none )
+
+        ToggleNewChannelMenu ->
+            ( { model | newChannelMenuOpen = not model.newChannelMenuOpen }, Cmd.none )
+
+        SubmitResp submitType (Ok res) ->
+            ( { model
+                | formState =
+                    if res.success then
+                        FormSubmitSuccessful submitType
+
+                    else
+                        FormErrorSubmit res.message
+                , detailModel =
+                    if submitType == SubmitDelete then
+                        Nothing
+
+                    else
+                        model.detailModel
+                , loading = False
+              }
+            , if submitType == SubmitDelete then
+                initCmd flags
+
+              else
+                Cmd.none
+            )
+
+        SubmitResp _ (Err err) ->
+            ( { model | formState = FormErrorHttp err, loading = False }
+            , Cmd.none
+            )
+
+        NewChannelInit ct ->
+            let
+                ( mm, mc ) =
+                    Comp.ChannelForm.init flags ct
+            in
+            ( { model | detailModel = Just mm, newChannelMenuOpen = False }, Cmd.map DetailMsg mc )
+
+        BackToTable ->
+            ( { model | detailModel = Nothing }, initCmd flags )
+
+        Submit ->
+            case model.detailModel of
+                Just dm ->
+                    case Comp.ChannelForm.getChannel dm of
+                        Just data ->
+                            postChannel flags data model
+
+                        Nothing ->
+                            ( { model | formState = FormErrorInvalid }, Cmd.none )
+
+                Nothing ->
+                    ( model, Cmd.none )
+
+        RequestDelete ->
+            ( { model | deleteConfirm = DeleteConfirmOn }, Cmd.none )
+
+        CancelDelete ->
+            ( { model | deleteConfirm = DeleteConfirmOff }, Cmd.none )
+
+        DeleteChannelNow id ->
+            ( { model | deleteConfirm = DeleteConfirmOff, loading = True }
+            , Api.deleteChannel flags id (SubmitResp SubmitDelete)
+            )
+
+
+postChannel : Flags -> NotificationChannel -> Model -> ( Model, Cmd Msg )
+postChannel flags channel model =
+    if (Data.NotificationChannel.getRef channel |> .id) == "" then
+        ( { model | loading = True }, Api.createChannel flags channel (SubmitResp SubmitCreate) )
+
+    else
+        ( { model | loading = True }, Api.updateChannel flags channel (SubmitResp SubmitUpdate) )
+
+
+
+--- View2
+
+
+view : Texts -> UiSettings -> Model -> Html Msg
+view texts settings model =
+    div [ class "flex flex-col" ]
+        (case model.detailModel of
+            Just msett ->
+                viewForm texts settings model msett
+
+            Nothing ->
+                viewList texts model
+        )
+
+
+viewState : Texts -> Model -> Html Msg
+viewState texts model =
+    div
+        [ classList
+            [ ( S.errorMessage, not (isSuccess model.formState) )
+            , ( S.successMessage, isSuccess model.formState )
+            , ( "hidden", model.formState == FormStateInitial )
+            ]
+        , class "mb-2"
+        ]
+        [ case model.formState of
+            FormStateInitial ->
+                text ""
+
+            FormSubmitSuccessful SubmitCreate ->
+                text texts.channelCreated
+
+            FormSubmitSuccessful SubmitUpdate ->
+                text texts.channelUpdated
+
+            FormSubmitSuccessful SubmitDelete ->
+                text texts.channelDeleted
+
+            FormErrorSubmit m ->
+                text m
+
+            FormErrorHttp err ->
+                text (texts.httpError err)
+
+            FormErrorInvalid ->
+                text texts.formInvalid
+        ]
+
+
+isSuccess : FormState -> Bool
+isSuccess state =
+    case state of
+        FormSubmitSuccessful _ ->
+            True
+
+        _ ->
+            False
+
+
+viewForm : Texts -> UiSettings -> Model -> Comp.ChannelForm.Model -> List (Html Msg)
+viewForm texts settings outerModel model =
+    let
+        channelId =
+            Comp.ChannelForm.getChannel model
+                |> Maybe.map Data.NotificationChannel.getRef
+                |> Maybe.map .id
+
+        newChannel =
+            channelId |> (==) (Just "")
+
+        headline =
+            case Comp.ChannelForm.channelType model of
+                Data.ChannelType.Matrix ->
+                    span []
+                        [ text texts.integrate
+                        , a
+                            [ href "https://matrix.org"
+                            , target "_blank"
+                            , class S.link
+                            , class "mx-3"
+                            ]
+                            [ i [ class "fa fa-external-link-alt mr-1" ] []
+                            , text "Matrix"
+                            ]
+                        , text texts.intoDocspell
+                        ]
+
+                Data.ChannelType.Mail ->
+                    span []
+                        [ text texts.notifyEmailInfo
+                        ]
+
+                Data.ChannelType.Gotify ->
+                    span []
+                        [ text texts.integrate
+                        , a
+                            [ href "https://gotify.net"
+                            , target "_blank"
+                            , class S.link
+                            , class "mx-3"
+                            ]
+                            [ i [ class "fa fa-external-link-alt mr-1" ] []
+                            , text "Gotify"
+                            ]
+                        , text texts.intoDocspell
+                        ]
+
+                Data.ChannelType.Http ->
+                    span []
+                        [ text texts.postRequestInfo
+                        ]
+    in
+    [ h1 [ class S.header2 ]
+        [ Data.ChannelType.icon (Comp.ChannelForm.channelType model) "w-8 h-8 inline-block mr-2"
+        , if newChannel then
+            text texts.addChannel
+
+          else
+            text texts.updateChannel
+        , div [ class "text-xs opacity-50 font-mono" ]
+            [ Maybe.withDefault "" channelId |> text
+            ]
+        ]
+    , div [ class "pt-2 pb-4 font-medium" ]
+        [ headline
+        ]
+    , MB.view
+        { start =
+            [ MB.CustomElement <|
+                B.primaryButton
+                    { handler = onClick Submit
+                    , title = texts.basics.submitThisForm
+                    , icon = "fa fa-save"
+                    , label = texts.basics.submit
+                    , disabled = False
+                    , attrs = [ href "#" ]
+                    }
+            , MB.SecondaryButton
+                { tagger = BackToTable
+                , title = texts.basics.backToList
+                , icon = Just "fa fa-arrow-left"
+                , label = texts.basics.backToList
+                }
+            ]
+        , end =
+            if not newChannel then
+                [ MB.DeleteButton
+                    { tagger = RequestDelete
+                    , title = texts.deleteThisChannel
+                    , icon = Just "fa fa-trash"
+                    , label = texts.basics.delete
+                    }
+                ]
+
+            else
+                []
+        , rootClasses = "mb-4"
+        }
+    , div [ class "mt-2" ]
+        [ viewState texts outerModel
+        ]
+    , Html.map DetailMsg
+        (Comp.ChannelForm.view texts.notificationForm settings model)
+    , B.loadingDimmer
+        { active = outerModel.loading
+        , label = texts.basics.loading
+        }
+    , B.contentDimmer
+        (outerModel.deleteConfirm == DeleteConfirmOn)
+        (div [ class "flex flex-col" ]
+            [ div [ class "text-lg" ]
+                [ i [ class "fa fa-info-circle mr-2" ] []
+                , text texts.reallyDeleteChannel
+                ]
+            , div [ class "mt-4 flex flex-row items-center" ]
+                [ B.deleteButton
+                    { label = texts.basics.yes
+                    , icon = "fa fa-check"
+                    , disabled = False
+                    , handler = onClick (DeleteChannelNow (Maybe.withDefault "" channelId))
+                    , attrs = [ href "#" ]
+                    }
+                , B.secondaryButton
+                    { label = texts.basics.no
+                    , icon = "fa fa-times"
+                    , disabled = False
+                    , handler = onClick CancelDelete
+                    , attrs = [ href "#", class "ml-2" ]
+                    }
+                ]
+            ]
+        )
+    ]
+
+
+viewList : Texts -> Model -> List (Html Msg)
+viewList texts model =
+    let
+        menuModel =
+            { menuOpen = model.newChannelMenuOpen
+            , toggleMenu = ToggleNewChannelMenu
+            , menuLabel = texts.newChannel
+            , onItem = NewChannelInit
+            }
+    in
+    [ MB.view
+        { start = []
+        , end =
+            [ Comp.ChannelMenu.channelMenu texts.channelType menuModel
+            ]
+        , rootClasses = "mb-4"
+        }
+    , Html.map TableMsg
+        (Comp.NotificationChannelTable.view texts.notificationTable
+            model.listModel
+            model.items
+        )
+    ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm b/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm
new file mode 100644
index 00000000..31c9e061
--- /dev/null
+++ b/modules/webapp/src/main/elm/Comp/NotificationChannelTable.elm
@@ -0,0 +1,87 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Comp.NotificationChannelTable exposing (..)
+
+import Comp.Basic as B
+import Data.ChannelType
+import Data.Flags exposing (Flags)
+import Data.NotificationChannel exposing (NotificationChannel)
+import Html exposing (..)
+import Html.Attributes exposing (..)
+import Messages.Comp.NotificationChannelTable exposing (Texts)
+import Styles as S
+
+
+type alias Model =
+    {}
+
+
+type Action
+    = NoAction
+    | EditAction NotificationChannel
+
+
+init : Model
+init =
+    {}
+
+
+type Msg
+    = Select NotificationChannel
+
+
+update : Flags -> Msg -> Model -> ( Model, Action )
+update _ msg model =
+    case msg of
+        Select channel ->
+            ( model, EditAction channel )
+
+
+
+--- View
+
+
+view : Texts -> Model -> List NotificationChannel -> Html Msg
+view texts model channels =
+    table [ class S.tableMain ]
+        [ thead []
+            [ tr []
+                [ th [ class "" ] []
+                , th [ class "text-left" ]
+                    [ text texts.basics.name
+                    ]
+                , th [ class "text-left" ]
+                    [ text texts.channelType
+                    ]
+                ]
+            ]
+        , tbody []
+            (List.map (renderNotificationChannelLine texts model) channels)
+        ]
+
+
+renderNotificationChannelLine : Texts -> Model -> NotificationChannel -> Html Msg
+renderNotificationChannelLine texts _ channel =
+    let
+        ref =
+            Data.NotificationChannel.getRef channel
+    in
+    tr
+        [ class S.tableRow
+        ]
+        [ B.editLinkTableCell texts.basics.edit (Select channel)
+        , td
+            [ class "text-left "
+            , classList [ ( "font-mono", ref.name == Nothing ) ]
+            ]
+            [ Maybe.withDefault (String.slice 0 10 ref.id) ref.name |> text
+            ]
+        , td [ class "text-left py-4 md:py-2" ]
+            [ text ref.channelType
+            ]
+        ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm b/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm
index 83454297..a94eda54 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationGotifyForm.elm
@@ -17,24 +17,25 @@ import Html.Attributes exposing (..)
 import Html.Events exposing (onInput)
 import Messages.Comp.NotificationGotifyForm exposing (Texts)
 import Styles as S
+import Util.Maybe
 
 
 type alias Model =
-    { hook : NotificationGotify
+    { channel : NotificationGotify
     , prioModel : Comp.FixedDropdown.Model Int
     }
 
 
 init : Model
 init =
-    { hook = Data.NotificationChannel.setTypeGotify Api.Model.NotificationGotify.empty
+    { channel = Data.NotificationChannel.setTypeGotify Api.Model.NotificationGotify.empty
     , prioModel = Comp.FixedDropdown.init (List.range 0 10)
     }
 
 
 initWith : NotificationGotify -> Model
-initWith hook =
-    { hook = Data.NotificationChannel.setTypeGotify hook
+initWith channel =
+    { channel = Data.NotificationChannel.setTypeGotify channel
     , prioModel = Comp.FixedDropdown.init (List.range 0 10)
     }
 
@@ -42,6 +43,7 @@ initWith hook =
 type Msg
     = SetUrl String
     | SetAppKey String
+    | SetName String
     | PrioMsg (Comp.FixedDropdown.Msg Int)
 
 
@@ -52,30 +54,33 @@ type Msg
 update : Msg -> Model -> ( Model, Maybe NotificationGotify )
 update msg model =
     let
-        hook =
-            model.hook
+        channel =
+            model.channel
 
         newModel =
             case msg of
                 SetUrl s ->
-                    { model | hook = { hook | url = s } }
+                    { model | channel = { channel | url = s } }
 
                 SetAppKey s ->
-                    { model | hook = { hook | appKey = s } }
+                    { model | channel = { channel | appKey = s } }
+
+                SetName s ->
+                    { model | channel = { channel | name = Util.Maybe.fromString s } }
 
                 PrioMsg lm ->
                     let
                         ( m, sel ) =
                             Comp.FixedDropdown.update lm model.prioModel
                     in
-                    { model | hook = { hook | priority = sel }, prioModel = m }
+                    { model | channel = { channel | priority = sel }, prioModel = m }
     in
-    ( newModel, check newModel.hook )
+    ( newModel, check newModel.channel )
 
 
 check : NotificationGotify -> Maybe NotificationGotify
-check hook =
-    Just hook
+check channel =
+    Just channel
 
 
 
@@ -94,6 +99,25 @@ view texts model =
     in
     div []
         [ div
+            [ class "mb-2"
+            ]
+            [ label
+                [ for "name"
+                , class S.inputLabel
+                ]
+                [ text texts.basics.name
+                ]
+            , input
+                [ type_ "text"
+                , onInput SetName
+                , placeholder texts.basics.name
+                , value (Maybe.withDefault "" model.channel.name)
+                , name "name"
+                , class S.textInput
+                ]
+                []
+            ]
+        , div
             [ class "mb-2"
             ]
             [ label
@@ -107,7 +131,7 @@ view texts model =
                 [ type_ "text"
                 , onInput SetUrl
                 , placeholder texts.gotifyUrl
-                , value model.hook.url
+                , value model.channel.url
                 , name "gotifyurl"
                 , class S.textInput
                 ]
@@ -127,7 +151,7 @@ view texts model =
                 [ type_ "text"
                 , onInput SetAppKey
                 , placeholder texts.appKey
-                , value model.hook.appKey
+                , value model.channel.appKey
                 , name "appkey"
                 , class S.textInput
                 ]
@@ -142,7 +166,7 @@ view texts model =
                 ]
                 [ text texts.priority
                 ]
-            , Html.map PrioMsg (Comp.FixedDropdown.viewStyled2 cfg False model.hook.priority model.prioModel)
+            , Html.map PrioMsg (Comp.FixedDropdown.viewStyled2 cfg False model.channel.priority model.prioModel)
             , span [ class "text-sm opacity-75" ]
                 [ text texts.priorityInfo
                 ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm b/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm
index f1067a25..bfaa19ae 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationHookForm.elm
@@ -8,7 +8,6 @@
 module Comp.NotificationHookForm exposing
     ( Model
     , Msg(..)
-    , channelType
     , getHook
     , init
     , initWith
@@ -16,17 +15,16 @@ module Comp.NotificationHookForm exposing
     , view
     )
 
+import Api.Model.NotificationHook exposing (NotificationHook)
 import Comp.Basic as B
-import Comp.ChannelForm
+import Comp.ChannelRefInput
 import Comp.Dropdown
 import Comp.EventSample
 import Comp.MenuBar as MB
 import Comp.NotificationTest
-import Data.ChannelType exposing (ChannelType)
 import Data.DropdownStyle as DS
 import Data.EventType exposing (EventType)
 import Data.Flags exposing (Flags)
-import Data.NotificationHook exposing (NotificationHook)
 import Data.UiSettings exposing (UiSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
@@ -39,7 +37,7 @@ import Util.Maybe
 type alias Model =
     { hook : NotificationHook
     , enabled : Bool
-    , channelModel : Comp.ChannelForm.Model
+    , channelModel : Comp.ChannelRefInput.Model
     , eventsDropdown : Comp.Dropdown.Model EventType
     , eventSampleModel : Comp.EventSample.Model
     , testDeliveryModel : Comp.NotificationTest.Model
@@ -48,16 +46,16 @@ type alias Model =
     }
 
 
-init : Flags -> ChannelType -> ( Model, Cmd Msg )
-init flags ct =
+init : Flags -> ( Model, Cmd Msg )
+init flags =
     let
         ( cm, cc ) =
-            Comp.ChannelForm.init flags ct
+            Comp.ChannelRefInput.init flags
 
         ( esm, esc ) =
             Comp.EventSample.initWith flags Data.EventType.TagsChanged
     in
-    ( { hook = Data.NotificationHook.empty ct
+    ( { hook = Api.Model.NotificationHook.empty
       , enabled = True
       , channelModel = cm
       , eventsDropdown =
@@ -81,7 +79,7 @@ initWith : Flags -> NotificationHook -> ( Model, Cmd Msg )
 initWith flags h =
     let
         ( cm, cc ) =
-            Comp.ChannelForm.initWith flags h.channel
+            Comp.ChannelRefInput.initSelected flags h.channels
 
         ( esm, esc ) =
             Comp.EventSample.initWith flags Data.EventType.TagsChanged
@@ -92,7 +90,7 @@ initWith flags h =
       , eventsDropdown =
             Comp.Dropdown.makeMultipleList
                 { options = Data.EventType.all
-                , selected = h.events
+                , selected = List.filterMap Data.EventType.fromString h.events
                 }
       , eventSampleModel = esm
       , testDeliveryModel = Comp.NotificationTest.init
@@ -106,11 +104,6 @@ initWith flags h =
     )
 
 
-channelType : Model -> ChannelType
-channelType model =
-    Comp.ChannelForm.channelType model.channelModel
-
-
 getHook : Model -> Maybe NotificationHook
 getHook model =
     let
@@ -123,20 +116,28 @@ getHook model =
                 Nothing
 
             else
-                Just ev
+                Just (List.map Data.EventType.asString ev)
 
-        channel =
-            Comp.ChannelForm.getChannel model.channelModel
+        channels =
+            let
+                list =
+                    Comp.ChannelRefInput.getSelected model.channelModel
+            in
+            if list == [] then
+                Nothing
+
+            else
+                Just list
 
         mkHook ev ch =
             NotificationHook model.hook.id model.enabled ch model.allEvents model.eventFilter ev
     in
-    Maybe.map2 mkHook events channel
+    Maybe.map2 mkHook events channels
 
 
 type Msg
     = ToggleEnabled
-    | ChannelFormMsg Comp.ChannelForm.Msg
+    | ChannelFormMsg Comp.ChannelRefInput.Msg
     | EventMsg (Comp.Dropdown.Msg EventType)
     | EventSampleMsg Comp.EventSample.Msg
     | DeliveryTestMsg Comp.NotificationTest.Msg
@@ -163,7 +164,7 @@ update flags msg model =
         ChannelFormMsg lm ->
             let
                 ( cm, cc ) =
-                    Comp.ChannelForm.update flags lm model.channelModel
+                    Comp.ChannelRefInput.update lm model.channelModel
             in
             ( { model | channelModel = cm }, Cmd.map ChannelFormMsg cc )
 
@@ -229,9 +230,9 @@ view texts settings model =
                     }
             ]
         , div [ class "mb-4" ]
-            [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel))
+            [ formHeader texts.channelHeader
             , Html.map ChannelFormMsg
-                (Comp.ChannelForm.view texts.channelForm settings model.channelModel)
+                (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
             ]
         , div [ class "mb-4" ]
             [ formHeader texts.events
@@ -290,21 +291,21 @@ view texts settings model =
             ]
         , div
             [ class "mt-4"
-            , classList [ ( "hidden", channelType model /= Data.ChannelType.Http ) ]
-            ]
-            [ h3 [ class S.header3 ]
-                [ text texts.samplePayload
-                ]
-            , Html.map EventSampleMsg
-                (Comp.EventSample.viewJson texts.eventSample model.eventSampleModel)
-            ]
-        , div
-            [ class "mt-4"
-            , classList [ ( "hidden", channelType model == Data.ChannelType.Http ) ]
             ]
             [ formHeader texts.samplePayload
+            , div [ class "opacity-80 mb-1" ]
+                [ text texts.payloadInfo
+                ]
             , Html.map EventSampleMsg
-                (Comp.EventSample.viewMessage texts.eventSample model.eventSampleModel)
+                (Comp.EventSample.viewMessage texts.eventSample True model.eventSampleModel)
+            , div [ class "py-2 text-center text-sm" ]
+                [ text texts.jsonPayload
+                , i [ class "fa fa-arrow-down ml-1 mr-3" ] []
+                , i [ class "fa fa-arrow-up mr-1" ] []
+                , text texts.messagePayload
+                ]
+            , Html.map EventSampleMsg
+                (Comp.EventSample.viewJson texts.eventSample False model.eventSampleModel)
             ]
         , div [ class "mt-4" ]
             [ formHeader "Test Delviery"
diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm b/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm
index d15d4e7f..921d5f2f 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationHookManage.elm
@@ -15,14 +15,12 @@ module Comp.NotificationHookManage exposing
 
 import Api
 import Api.Model.BasicResult exposing (BasicResult)
+import Api.Model.NotificationHook exposing (NotificationHook)
 import Comp.Basic as B
-import Comp.ChannelMenu
 import Comp.MenuBar as MB
 import Comp.NotificationHookForm
 import Comp.NotificationHookTable
-import Data.ChannelType exposing (ChannelType)
 import Data.Flags exposing (Flags)
-import Data.NotificationHook exposing (NotificationHook)
 import Data.UiSettings exposing (UiSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
@@ -39,7 +37,6 @@ type alias Model =
     , deleteConfirm : DeleteConfirm
     , loading : Bool
     , formState : FormState
-    , newHookMenuOpen : Bool
     , jsonFilterError : Maybe String
     }
 
@@ -67,9 +64,8 @@ type Msg
     = TableMsg Comp.NotificationHookTable.Msg
     | DetailMsg Comp.NotificationHookForm.Msg
     | GetDataResp (Result Http.Error (List NotificationHook))
-    | ToggleNewHookMenu
     | SubmitResp SubmitType (Result Http.Error BasicResult)
-    | NewHookInit ChannelType
+    | NewHookInit
     | BackToTable
     | Submit
     | RequestDelete
@@ -85,7 +81,6 @@ initModel =
     , items = []
     , loading = False
     , formState = FormStateInitial
-    , newHookMenuOpen = False
     , deleteConfirm = DeleteConfirmOff
     , jsonFilterError = Nothing
     }
@@ -177,9 +172,6 @@ update flags msg model =
                 Nothing ->
                     ( model, Cmd.none )
 
-        ToggleNewHookMenu ->
-            ( { model | newHookMenuOpen = not model.newHookMenuOpen }, Cmd.none )
-
         SubmitResp submitType (Ok res) ->
             ( { model
                 | formState =
@@ -208,12 +200,12 @@ update flags msg model =
             , Cmd.none
             )
 
-        NewHookInit ct ->
+        NewHookInit ->
             let
                 ( mm, mc ) =
-                    Comp.NotificationHookForm.init flags ct
+                    Comp.NotificationHookForm.init flags
             in
-            ( { model | detailModel = Just mm, newHookMenuOpen = False }, Cmd.map DetailMsg mc )
+            ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc )
 
         BackToTable ->
             ( { model | detailModel = Nothing }, initCmd flags )
@@ -327,60 +319,14 @@ viewForm texts settings outerModel model =
     let
         newHook =
             model.hook.id == ""
-
-        headline =
-            case Comp.NotificationHookForm.channelType model of
-                Data.ChannelType.Matrix ->
-                    span []
-                        [ text texts.integrate
-                        , a
-                            [ href "https://matrix.org"
-                            , target "_blank"
-                            , class S.link
-                            , class "mx-3"
-                            ]
-                            [ i [ class "fa fa-external-link-alt mr-1" ] []
-                            , text "Matrix"
-                            ]
-                        , text texts.intoDocspell
-                        ]
-
-                Data.ChannelType.Mail ->
-                    span []
-                        [ text texts.notifyEmailInfo
-                        ]
-
-                Data.ChannelType.Gotify ->
-                    span []
-                        [ text texts.integrate
-                        , a
-                            [ href "https://gotify.net"
-                            , target "_blank"
-                            , class S.link
-                            , class "mx-3"
-                            ]
-                            [ i [ class "fa fa-external-link-alt mr-1" ] []
-                            , text "Gotify"
-                            ]
-                        , text texts.intoDocspell
-                        ]
-
-                Data.ChannelType.Http ->
-                    span []
-                        [ text texts.postRequestInfo
-                        ]
     in
     [ h1 [ class S.header2 ]
-        [ Data.ChannelType.icon (Comp.NotificationHookForm.channelType model) "w-8 h-8 inline-block mr-4"
-        , if newHook then
+        [ if newHook then
             text texts.addWebhook
 
           else
             text texts.updateWebhook
         ]
-    , div [ class "pt-2 pb-4 font-medium" ]
-        [ headline
-        ]
     , MB.view
         { start =
             [ MB.CustomElement <|
@@ -452,18 +398,15 @@ viewForm texts settings outerModel model =
 
 viewList : Texts -> Model -> List (Html Msg)
 viewList texts model =
-    let
-        menuModel =
-            { menuOpen = model.newHookMenuOpen
-            , toggleMenu = ToggleNewHookMenu
-            , menuLabel = texts.newHook
-            , onItem = NewHookInit
-            }
-    in
     [ MB.view
         { start = []
         , end =
-            [ Comp.ChannelMenu.channelMenu texts.channelType menuModel
+            [ MB.PrimaryButton
+                { tagger = NewHookInit
+                , title = texts.newHook
+                , icon = Just "fa fa-plus"
+                , label = texts.newHook
+                }
             ]
         , rootClasses = "mb-4"
         }
diff --git a/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm b/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm
index 8188a1df..8091e7f3 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationHookTable.elm
@@ -14,15 +14,13 @@ module Comp.NotificationHookTable exposing
     , view
     )
 
+import Api.Model.NotificationHook exposing (NotificationHook)
 import Comp.Basic as B
-import Data.ChannelType
+import Data.ChannelRef
 import Data.EventType
 import Data.Flags exposing (Flags)
-import Data.NotificationChannel
-import Data.NotificationHook exposing (NotificationHook)
 import Html exposing (..)
 import Html.Attributes exposing (..)
-import Html.Events exposing (onClick)
 import Messages.Comp.NotificationHookTable exposing (Texts)
 import Styles as S
 import Util.Html
@@ -80,7 +78,7 @@ view texts model hooks =
 
 
 renderNotificationHookLine : Texts -> Model -> NotificationHook -> Html Msg
-renderNotificationHookLine texts model hook =
+renderNotificationHookLine texts _ hook =
     let
         eventName =
             texts.eventType >> .name
@@ -93,14 +91,17 @@ renderNotificationHookLine texts model hook =
             [ Util.Html.checkbox2 hook.enabled
             ]
         , td [ class "text-left py-4 md:py-2" ]
-            [ Data.NotificationChannel.channelType hook.channel
-                |> Maybe.map Data.ChannelType.asString
-                |> Maybe.withDefault "-"
-                |> text
+            [ div [ class "space-x-1" ]
+                (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] hook.channels)
             ]
         , td [ class "text-left hidden sm:table-cell" ]
-            [ List.map eventName hook.events
-                |> String.join ", "
-                |> text
+            [ if hook.allEvents then
+                text texts.allEvents
+
+              else
+                List.filterMap Data.EventType.fromString hook.events
+                    |> List.map eventName
+                    |> String.join ", "
+                    |> text
             ]
         ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm b/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm
index 1e637796..4f898ad9 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationHttpForm.elm
@@ -15,29 +15,31 @@ import Html.Attributes exposing (..)
 import Html.Events exposing (onInput)
 import Messages.Comp.NotificationHttpForm exposing (Texts)
 import Styles as S
+import Util.Maybe
 
 
 type alias Model =
-    { hook : NotificationHttp
+    { channel : NotificationHttp
     }
 
 
 init : Model
 init =
-    { hook =
+    { channel =
         Data.NotificationChannel.setTypeHttp
             Api.Model.NotificationHttp.empty
     }
 
 
 initWith : NotificationHttp -> Model
-initWith hook =
-    { hook = Data.NotificationChannel.setTypeHttp hook
+initWith channel =
+    { channel = Data.NotificationChannel.setTypeHttp channel
     }
 
 
 type Msg
     = SetUrl String
+    | SetName String
 
 
 
@@ -47,26 +49,29 @@ type Msg
 update : Msg -> Model -> ( Model, Maybe NotificationHttp )
 update msg model =
     let
-        newHook =
-            updateHook msg model.hook
+        newChannel =
+            updateChannel msg model.channel
     in
-    ( { model | hook = newHook }, check newHook )
+    ( { model | channel = newChannel }, check newChannel )
 
 
 check : NotificationHttp -> Maybe NotificationHttp
-check hook =
-    if hook.url == "" then
+check channel =
+    if channel.url == "" then
         Nothing
 
     else
-        Just hook
+        Just channel
 
 
-updateHook : Msg -> NotificationHttp -> NotificationHttp
-updateHook msg hook =
+updateChannel : Msg -> NotificationHttp -> NotificationHttp
+updateChannel msg channel =
     case msg of
         SetUrl s ->
-            { hook | url = s }
+            { channel | url = s }
+
+        SetName s ->
+            { channel | name = Util.Maybe.fromString s }
 
 
 
@@ -77,6 +82,25 @@ view : Texts -> Model -> Html Msg
 view texts model =
     div []
         [ div
+            [ class "mb-2"
+            ]
+            [ label
+                [ for "name"
+                , class S.inputLabel
+                ]
+                [ text texts.basics.name
+                ]
+            , input
+                [ type_ "text"
+                , onInput SetName
+                , placeholder texts.basics.name
+                , value (Maybe.withDefault "" model.channel.name)
+                , name "name"
+                , class S.textInput
+                ]
+                []
+            ]
+        , div
             [ class "mb-2"
             ]
             [ label
@@ -90,7 +114,7 @@ view texts model =
                 [ type_ "text"
                 , onInput SetUrl
                 , placeholder texts.httpUrl
-                , value model.hook.url
+                , value model.channel.url
                 , name "httpurl"
                 , class S.textInput
                 ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm b/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm
index ce756826..0faa0563 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationMailForm.elm
@@ -23,13 +23,15 @@ import Html.Events exposing (onInput)
 import Http
 import Messages.Comp.NotificationMailForm exposing (Texts)
 import Styles as S
+import Util.Maybe
 
 
 type alias Model =
-    { hook : NotificationMail
+    { channel : NotificationMail
     , connectionModel : Comp.Dropdown.Model String
     , recipients : List String
     , recipientsModel : Comp.EmailInput.Model
+    , name : Maybe String
     , formState : FormState
     }
 
@@ -46,10 +48,11 @@ type ValidateError
 
 init : Flags -> ( Model, Cmd Msg )
 init flags =
-    ( { hook = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty
+    ( { channel = Data.NotificationChannel.setTypeMail Api.Model.NotificationMail.empty
       , connectionModel = Comp.Dropdown.makeSingle
       , recipients = []
       , recipientsModel = Comp.EmailInput.init
+      , name = Nothing
       , formState = FormStateInitial
       }
     , Cmd.batch
@@ -59,17 +62,17 @@ init flags =
 
 
 initWith : Flags -> NotificationMail -> ( Model, Cmd Msg )
-initWith flags hook =
+initWith flags channel =
     let
         ( mm, mc ) =
             init flags
 
         ( cm, _ ) =
-            Comp.Dropdown.update (Comp.Dropdown.SetSelection [ hook.connection ]) mm.connectionModel
+            Comp.Dropdown.update (Comp.Dropdown.SetSelection [ channel.connection ]) mm.connectionModel
     in
     ( { mm
-        | hook = Data.NotificationChannel.setTypeMail hook
-        , recipients = hook.recipients
+        | channel = Data.NotificationChannel.setTypeMail channel
+        , recipients = channel.recipients
         , connectionModel = cm
       }
     , mc
@@ -80,6 +83,7 @@ type Msg
     = ConnResp (Result Http.Error EmailSettingsList)
     | ConnMsg (Comp.Dropdown.Msg String)
     | RecipientMsg Comp.EmailInput.Msg
+    | SetName String
 
 
 
@@ -108,12 +112,12 @@ check model =
                 |> List.head
 
         h =
-            model.hook
+            model.channel
 
-        makeHook _ rec conn =
-            { h | connection = conn, recipients = rec }
+        makeChannel _ rec conn =
+            { h | connection = conn, recipients = rec, name = model.name }
     in
-    Maybe.map3 makeHook formState recipients connection
+    Maybe.map3 makeChannel formState recipients connection
 
 
 update : Flags -> Msg -> Model -> ( Model, Cmd Msg, Maybe NotificationMail )
@@ -152,6 +156,16 @@ update flags msg model =
             , Nothing
             )
 
+        SetName s ->
+            let
+                model_ =
+                    { model | name = Util.Maybe.fromString s }
+            in
+            ( model_
+            , Cmd.none
+            , check model_
+            )
+
         ConnMsg lm ->
             let
                 ( cm, cc ) =
@@ -201,7 +215,26 @@ view texts settings model =
             }
     in
     div []
-        [ div [ class "mb-4" ]
+        [ div
+            [ class "mb-2"
+            ]
+            [ label
+                [ for "name"
+                , class S.inputLabel
+                ]
+                [ text texts.basics.name
+                ]
+            , input
+                [ type_ "text"
+                , onInput SetName
+                , placeholder texts.basics.name
+                , value (Maybe.withDefault "" model.name)
+                , name "name"
+                , class S.textInput
+                ]
+                []
+            ]
+        , div [ class "mb-4" ]
             [ label [ class S.inputLabel ]
                 [ text texts.sendVia
                 , B.inputRequired
diff --git a/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm b/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm
index 8f4104b0..76969267 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationMatrixForm.elm
@@ -15,22 +15,23 @@ import Html.Attributes exposing (..)
 import Html.Events exposing (onInput)
 import Messages.Comp.NotificationMatrixForm exposing (Texts)
 import Styles as S
+import Util.Maybe
 
 
 type alias Model =
-    { hook : NotificationMatrix
+    { channel : NotificationMatrix
     }
 
 
 init : Model
 init =
-    { hook = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty
+    { channel = Data.NotificationChannel.setTypeMatrix Api.Model.NotificationMatrix.empty
     }
 
 
 initWith : NotificationMatrix -> Model
-initWith hook =
-    { hook = Data.NotificationChannel.setTypeMatrix hook
+initWith channel =
+    { channel = Data.NotificationChannel.setTypeMatrix channel
     }
 
 
@@ -38,6 +39,7 @@ type Msg
     = SetHomeServer String
     | SetRoomId String
     | SetAccessKey String
+    | SetName String
 
 
 
@@ -47,28 +49,31 @@ type Msg
 update : Msg -> Model -> ( Model, Maybe NotificationMatrix )
 update msg model =
     let
-        newHook =
-            updateHook msg model.hook
+        newChannel =
+            updateChannel msg model.channel
     in
-    ( { model | hook = newHook }, check newHook )
+    ( { model | channel = newChannel }, check newChannel )
 
 
 check : NotificationMatrix -> Maybe NotificationMatrix
-check hook =
-    Just hook
+check channel =
+    Just channel
 
 
-updateHook : Msg -> NotificationMatrix -> NotificationMatrix
-updateHook msg hook =
+updateChannel : Msg -> NotificationMatrix -> NotificationMatrix
+updateChannel msg channel =
     case msg of
         SetHomeServer s ->
-            { hook | homeServer = s }
+            { channel | homeServer = s }
 
         SetRoomId s ->
-            { hook | roomId = s }
+            { channel | roomId = s }
 
         SetAccessKey s ->
-            { hook | accessToken = s }
+            { channel | accessToken = s }
+
+        SetName s ->
+            { channel | name = Util.Maybe.fromString s }
 
 
 
@@ -79,6 +84,25 @@ view : Texts -> Model -> Html Msg
 view texts model =
     div []
         [ div
+            [ class "mb-2"
+            ]
+            [ label
+                [ for "name"
+                , class S.inputLabel
+                ]
+                [ text texts.basics.name
+                ]
+            , input
+                [ type_ "text"
+                , onInput SetName
+                , placeholder texts.basics.name
+                , value (Maybe.withDefault "" model.channel.name)
+                , name "name"
+                , class S.textInput
+                ]
+                []
+            ]
+        , div
             [ class "mb-2"
             ]
             [ label
@@ -92,7 +116,7 @@ view texts model =
                 [ type_ "text"
                 , onInput SetHomeServer
                 , placeholder texts.homeServer
-                , value model.hook.homeServer
+                , value model.channel.homeServer
                 , name "homeserver"
                 , class S.textInput
                 ]
@@ -112,7 +136,7 @@ view texts model =
                 [ type_ "text"
                 , onInput SetRoomId
                 , placeholder texts.roomId
-                , value model.hook.roomId
+                , value model.channel.roomId
                 , name "roomid"
                 , class S.textInput
                 ]
@@ -131,7 +155,7 @@ view texts model =
             , textarea
                 [ onInput SetAccessKey
                 , placeholder texts.accessKey
-                , value model.hook.accessToken
+                , value model.channel.accessToken
                 , name "accesskey"
                 , class S.textAreaInput
                 ]
diff --git a/modules/webapp/src/main/elm/Comp/NotificationTest.elm b/modules/webapp/src/main/elm/Comp/NotificationTest.elm
index 81003721..5af2c2a9 100644
--- a/modules/webapp/src/main/elm/Comp/NotificationTest.elm
+++ b/modules/webapp/src/main/elm/Comp/NotificationTest.elm
@@ -9,10 +9,10 @@ module Comp.NotificationTest exposing (Model, Msg, ViewConfig, init, update, vie
 
 import Api
 import Api.Model.NotificationChannelTestResult exposing (NotificationChannelTestResult)
+import Api.Model.NotificationHook exposing (NotificationHook)
 import Comp.Basic as B
 import Comp.MenuBar as MB
 import Data.Flags exposing (Flags)
-import Data.NotificationHook exposing (NotificationHook)
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Html.Events exposing (onClick)
diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm
index 154c6ab5..8560b331 100644
--- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm
+++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskForm.elm
@@ -16,16 +16,15 @@ module Comp.PeriodicQueryTaskForm exposing
     , view
     )
 
+import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Comp.Basic as B
 import Comp.BookmarkDropdown
 import Comp.CalEventInput
-import Comp.ChannelForm
+import Comp.ChannelRefInput
 import Comp.MenuBar as MB
 import Comp.PowerSearchInput
 import Data.CalEvent exposing (CalEvent)
-import Data.ChannelType exposing (ChannelType)
 import Data.Flags exposing (Flags)
-import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Data.UiSettings exposing (UiSettings)
 import Data.Validated exposing (Validated(..))
 import Html exposing (..)
@@ -44,7 +43,7 @@ type alias Model =
     , schedule : Maybe CalEvent
     , scheduleModel : Comp.CalEventInput.Model
     , queryModel : Comp.PowerSearchInput.Model
-    , channelModel : Comp.ChannelForm.Model
+    , channelModel : Comp.ChannelRefInput.Model
     , bookmarkDropdown : Comp.BookmarkDropdown.Model
     , contentStart : Maybe String
     , formState : FormState
@@ -78,7 +77,7 @@ type Msg
     | ToggleEnabled
     | CalEventMsg Comp.CalEventInput.Msg
     | QueryMsg Comp.PowerSearchInput.Msg
-    | ChannelMsg Comp.ChannelForm.Msg
+    | ChannelMsg Comp.ChannelRefInput.Msg
     | BookmarkMsg Comp.BookmarkDropdown.Msg
     | SetContentStart String
     | StartOnce
@@ -105,7 +104,7 @@ initWith flags s =
                 Comp.PowerSearchInput.init
 
         ( cfm, cfc ) =
-            Comp.ChannelForm.initWith flags s.channel
+            Comp.ChannelRefInput.initSelected flags s.channels
 
         ( bm, bc ) =
             Comp.BookmarkDropdown.init flags s.bookmark
@@ -132,8 +131,8 @@ initWith flags s =
     )
 
 
-init : Flags -> ChannelType -> ( Model, Cmd Msg )
-init flags ct =
+init : Flags -> ( Model, Cmd Msg )
+init flags =
     let
         initialSchedule =
             Data.CalEvent.everyMonth
@@ -142,12 +141,12 @@ init flags ct =
             Comp.CalEventInput.init flags initialSchedule
 
         ( cfm, cfc ) =
-            Comp.ChannelForm.init flags ct
+            Comp.ChannelRefInput.init flags
 
         ( bm, bc ) =
             Comp.BookmarkDropdown.init flags Nothing
     in
-    ( { settings = Data.PeriodicQuerySettings.empty ct
+    ( { settings = Api.Model.PeriodicQuerySettings.empty
       , enabled = False
       , schedule = Just initialSchedule
       , scheduleModel = sm
@@ -210,16 +209,22 @@ makeSettings model =
                     Result.Ok ( qstr, bm )
 
         channelM =
-            Result.fromMaybe
-                ValidateChannelRequired
-                (Comp.ChannelForm.getChannel model.channelModel)
+            let
+                list =
+                    Comp.ChannelRefInput.getSelected model.channelModel
+            in
+            if list == [] then
+                Err ValidateChannelRequired
 
-        make timer channel q =
+            else
+                Ok list
+
+        make timer channels q =
             { prev
                 | enabled = model.enabled
                 , schedule = Data.CalEvent.makeEvent timer
                 , summary = model.summary
-                , channel = channel
+                , channels = channels
                 , query = Tuple.first q
                 , bookmark = Tuple.second q
                 , contentStart = model.contentStart
@@ -285,7 +290,7 @@ update flags msg model =
         ChannelMsg lm ->
             let
                 ( cfm, cfc ) =
-                    Comp.ChannelForm.update flags lm model.channelModel
+                    Comp.ChannelRefInput.update lm model.channelModel
             in
             { model = { model | channelModel = cfm }
             , action = NoAction
@@ -536,9 +541,9 @@ view texts extraClasses settings model =
                 ]
             ]
         , div [ class "mb-4" ]
-            [ formHeader (texts.channelHeader (Comp.ChannelForm.channelType model.channelModel)) False
+            [ formHeader texts.channelHeader True
             , Html.map ChannelMsg
-                (Comp.ChannelForm.view texts.channelForm settings model.channelModel)
+                (Comp.ChannelRefInput.view texts.channelRef settings model.channelModel)
             ]
         , div [ class "mb-4" ]
             [ formHeader texts.queryLabel True
diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm
index f654d11e..c1a83a07 100644
--- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm
+++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskList.elm
@@ -14,15 +14,17 @@ module Comp.PeriodicQueryTaskList exposing
     , view2
     )
 
+import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Comp.Basic as B
+import Data.ChannelRef
 import Data.ChannelType
 import Data.NotificationChannel
-import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
 import Messages.Comp.PeriodicQueryTaskList exposing (Texts)
 import Styles as S
 import Util.Html
+import Util.List
 
 
 type alias Model =
@@ -94,9 +96,7 @@ viewItem2 texts item =
                 ]
             ]
         , td [ class "text-left py-4 md:py-2" ]
-            [ Data.NotificationChannel.channelType item.channel
-                |> Maybe.map Data.ChannelType.asString
-                |> Maybe.withDefault "-"
-                |> text
+            [ div [ class " space-x-1" ]
+                (Data.ChannelRef.asDivs texts.channelType [ class "inline" ] item.channels)
             ]
         ]
diff --git a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm
index 000bc7db..03647749 100644
--- a/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm
+++ b/modules/webapp/src/main/elm/Comp/PeriodicQueryTaskManage.elm
@@ -15,17 +15,16 @@ module Comp.PeriodicQueryTaskManage exposing
 
 import Api
 import Api.Model.BasicResult exposing (BasicResult)
+import Api.Model.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Comp.ChannelMenu
 import Comp.MenuBar as MB
 import Comp.PeriodicQueryTaskForm
 import Comp.PeriodicQueryTaskList
 import Data.ChannelType exposing (ChannelType)
 import Data.Flags exposing (Flags)
-import Data.PeriodicQuerySettings exposing (PeriodicQuerySettings)
 import Data.UiSettings exposing (UiSettings)
 import Html exposing (..)
 import Html.Attributes exposing (..)
-import Html.Events exposing (onClick)
 import Http
 import Messages.Comp.PeriodicQueryTaskManage exposing (Texts)
 import Styles as S
@@ -36,7 +35,6 @@ type alias Model =
     , detailModel : Maybe Comp.PeriodicQueryTaskForm.Model
     , items : List PeriodicQuerySettings
     , formState : FormState
-    , channelMenuOpen : Bool
     }
 
 
@@ -58,9 +56,8 @@ type Msg
     = ListMsg Comp.PeriodicQueryTaskList.Msg
     | DetailMsg Comp.PeriodicQueryTaskForm.Msg
     | GetDataResp (Result Http.Error (List PeriodicQuerySettings))
-    | NewTaskInit ChannelType
+    | NewTaskInit
     | SubmitResp SubmitType (Result Http.Error BasicResult)
-    | ToggleChannelMenu
 
 
 initModel : Model
@@ -69,7 +66,6 @@ initModel =
     , detailModel = Nothing
     , items = []
     , formState = FormStateInitial
-    , channelMenuOpen = False
     }
 
 
@@ -195,12 +191,12 @@ update flags msg model =
                 Nothing ->
                     ( model, Cmd.none, Sub.none )
 
-        NewTaskInit ct ->
+        NewTaskInit ->
             let
                 ( mm, mc ) =
-                    Comp.PeriodicQueryTaskForm.init flags ct
+                    Comp.PeriodicQueryTaskForm.init flags
             in
-            ( { model | detailModel = Just mm, channelMenuOpen = False }, Cmd.map DetailMsg mc, Sub.none )
+            ( { model | detailModel = Just mm }, Cmd.map DetailMsg mc, Sub.none )
 
         SubmitResp submitType (Ok res) ->
             ( { model
@@ -231,9 +227,6 @@ update flags msg model =
             , Sub.none
             )
 
-        ToggleChannelMenu ->
-            ( { model | channelMenuOpen = not model.channelMenuOpen }, Cmd.none, Sub.none )
-
 
 
 --- View2
@@ -301,18 +294,15 @@ viewForm2 texts settings model =
 
 viewList2 : Texts -> Model -> List (Html Msg)
 viewList2 texts model =
-    let
-        menuModel =
-            { menuOpen = model.channelMenuOpen
-            , toggleMenu = ToggleChannelMenu
-            , menuLabel = texts.newTask
-            , onItem = NewTaskInit
-            }
-    in
     [ MB.view
         { start = []
         , end =
-            [ Comp.ChannelMenu.channelMenu texts.channelType menuModel
+            [ MB.PrimaryButton
+                { tagger = NewTaskInit
+                , title = texts.newTask
+                , icon = Just "fa fa-plus"
+                , label = texts.newTask
+                }
             ]
         , rootClasses = "mb-4"
         }
diff --git a/modules/webapp/src/main/elm/Data/ChannelRef.elm b/modules/webapp/src/main/elm/Data/ChannelRef.elm
index 4c69cb74..440e66ff 100644
--- a/modules/webapp/src/main/elm/Data/ChannelRef.elm
+++ b/modules/webapp/src/main/elm/Data/ChannelRef.elm
@@ -7,27 +7,63 @@
 
 module Data.ChannelRef exposing (..)
 
+import Api.Model.NotificationChannelRef exposing (NotificationChannelRef)
 import Data.ChannelType exposing (ChannelType)
-import Json.Decode as D
-import Json.Encode as E
+import Html exposing (Attribute, Html, div, span, text)
+import Html.Attributes exposing (class)
+import Messages.Data.ChannelType as M
+import Util.List
 
 
-type alias ChannelRef =
-    { id : String
-    , channelType : ChannelType
-    }
+channelType : NotificationChannelRef -> Maybe ChannelType
+channelType ref =
+    Data.ChannelType.fromString ref.channelType
 
 
-decoder : D.Decoder ChannelRef
-decoder =
-    D.map2 ChannelRef
-        (D.field "id" D.string)
-        (D.field "channelType" Data.ChannelType.decoder)
+split : M.Texts -> NotificationChannelRef -> ( String, String )
+split texts ref =
+    let
+        chStr =
+            channelType ref
+                |> Maybe.map texts
+                |> Maybe.withDefault ref.channelType
+
+        name =
+            Maybe.withDefault (String.slice 0 6 ref.id) ref.name
+    in
+    ( chStr, name )
 
 
-encode : ChannelRef -> E.Value
-encode cref =
-    E.object
-        [ ( "id", E.string cref.id )
-        , ( "channelType", Data.ChannelType.encode cref.channelType )
+asString : M.Texts -> NotificationChannelRef -> String
+asString texts ref =
+    let
+        ( chStr, name ) =
+            split texts ref
+    in
+    chStr ++ " (" ++ name ++ ")"
+
+
+asDiv : List (Attribute msg) -> M.Texts -> NotificationChannelRef -> Html msg
+asDiv attrs texts ref =
+    let
+        ( chStr, name ) =
+            split texts ref
+    in
+    div attrs
+        [ text chStr
+        , span [ class "ml-1 text-xs opacity-75" ]
+            [ text ("(" ++ name ++ ")")
+            ]
         ]
+
+
+asStringJoined : M.Texts -> List NotificationChannelRef -> String
+asStringJoined texts refs =
+    List.map (asString texts) refs
+        |> Util.List.distinct
+        |> String.join ", "
+
+
+asDivs : M.Texts -> List (Attribute msg) -> List NotificationChannelRef -> List (Html msg)
+asDivs texts inner refs =
+    List.map (asDiv inner texts) refs
diff --git a/modules/webapp/src/main/elm/Data/NotificationChannel.elm b/modules/webapp/src/main/elm/Data/NotificationChannel.elm
index 643cac06..63fe34f5 100644
--- a/modules/webapp/src/main/elm/Data/NotificationChannel.elm
+++ b/modules/webapp/src/main/elm/Data/NotificationChannel.elm
@@ -12,17 +12,18 @@ module Data.NotificationChannel exposing
     , decoder
     , empty
     , encode
+    , getRef
     , setTypeGotify
     , setTypeHttp
     , setTypeMail
     , setTypeMatrix
     )
 
+import Api.Model.NotificationChannelRef exposing (NotificationChannelRef)
 import Api.Model.NotificationGotify exposing (NotificationGotify)
 import Api.Model.NotificationHttp exposing (NotificationHttp)
 import Api.Model.NotificationMail exposing (NotificationMail)
 import Api.Model.NotificationMatrix exposing (NotificationMatrix)
-import Data.ChannelRef exposing (ChannelRef)
 import Data.ChannelType exposing (ChannelType)
 import Json.Decode as D
 import Json.Encode as E
@@ -33,7 +34,6 @@ type NotificationChannel
     | Mail NotificationMail
     | Gotify NotificationGotify
     | Http NotificationHttp
-    | Ref ChannelRef
 
 
 empty : ChannelType -> NotificationChannel
@@ -87,46 +87,49 @@ decoder =
         , D.map Mail Api.Model.NotificationMail.decoder
         , D.map Matrix Api.Model.NotificationMatrix.decoder
         , D.map Http Api.Model.NotificationHttp.decoder
-        , D.map Ref Data.ChannelRef.decoder
         ]
 
 
+fold :
+    (NotificationMail -> a)
+    -> (NotificationGotify -> a)
+    -> (NotificationMatrix -> a)
+    -> (NotificationHttp -> a)
+    -> NotificationChannel
+    -> a
+fold fa fb fc fd channel =
+    case channel of
+        Mail ch ->
+            fa ch
+
+        Gotify ch ->
+            fb ch
+
+        Matrix ch ->
+            fc ch
+
+        Http ch ->
+            fd ch
+
+
 encode : NotificationChannel -> E.Value
 encode channel =
-    case channel of
-        Matrix ch ->
-            Api.Model.NotificationMatrix.encode ch
-
-        Mail ch ->
-            Api.Model.NotificationMail.encode ch
-
-        Gotify ch ->
-            Api.Model.NotificationGotify.encode ch
-
-        Http ch ->
-            Api.Model.NotificationHttp.encode ch
-
-        Ref ch ->
-            Data.ChannelRef.encode ch
+    fold
+        Api.Model.NotificationMail.encode
+        Api.Model.NotificationGotify.encode
+        Api.Model.NotificationMatrix.encode
+        Api.Model.NotificationHttp.encode
+        channel
 
 
 channelType : NotificationChannel -> Maybe ChannelType
 channelType ch =
-    case ch of
-        Matrix m ->
-            Data.ChannelType.fromString m.channelType
-
-        Mail m ->
-            Data.ChannelType.fromString m.channelType
-
-        Gotify m ->
-            Data.ChannelType.fromString m.channelType
-
-        Http m ->
-            Data.ChannelType.fromString m.channelType
-
-        Ref m ->
-            Just m.channelType
+    fold
+        (.channelType >> Data.ChannelType.fromString)
+        (.channelType >> Data.ChannelType.fromString)
+        (.channelType >> Data.ChannelType.fromString)
+        (.channelType >> Data.ChannelType.fromString)
+        ch
 
 
 asString : NotificationChannel -> String
@@ -144,5 +147,12 @@ asString channel =
         Http ch ->
             "Http @ " ++ ch.url
 
-        Ref ch ->
-            "Ref(" ++ Data.ChannelType.asString ch.channelType ++ "/" ++ ch.id ++ ")"
+
+getRef : NotificationChannel -> NotificationChannelRef
+getRef channel =
+    fold
+        (\c -> NotificationChannelRef c.id c.channelType c.name)
+        (\c -> NotificationChannelRef c.id c.channelType c.name)
+        (\c -> NotificationChannelRef c.id c.channelType c.name)
+        (\c -> NotificationChannelRef c.id c.channelType c.name)
+        channel
diff --git a/modules/webapp/src/main/elm/Data/NotificationHook.elm b/modules/webapp/src/main/elm/Data/NotificationHook.elm
deleted file mode 100644
index 91c53b60..00000000
--- a/modules/webapp/src/main/elm/Data/NotificationHook.elm
+++ /dev/null
@@ -1,62 +0,0 @@
-{-
-   Copyright 2020 Eike K. & Contributors
-
-   SPDX-License-Identifier: AGPL-3.0-or-later
--}
-
-
-module Data.NotificationHook exposing (NotificationHook, decoder, empty, encode)
-
-import Data.ChannelType exposing (ChannelType)
-import Data.EventType exposing (EventType)
-import Data.NotificationChannel exposing (NotificationChannel)
-import Json.Decode as D
-import Json.Encode as E
-
-
-type alias NotificationHook =
-    { id : String
-    , enabled : Bool
-    , channel : NotificationChannel
-    , allEvents : Bool
-    , eventFilter : Maybe String
-    , events : List EventType
-    }
-
-
-empty : ChannelType -> NotificationHook
-empty ct =
-    { id = ""
-    , enabled = True
-    , channel = Data.NotificationChannel.empty ct
-    , allEvents = False
-    , eventFilter = Nothing
-    , events = []
-    }
-
-
-decoder : D.Decoder NotificationHook
-decoder =
-    D.map6 NotificationHook
-        (D.field "id" D.string)
-        (D.field "enabled" D.bool)
-        (D.field "channel" Data.NotificationChannel.decoder)
-        (D.field "allEvents" D.bool)
-        (D.field "eventFilter" (D.maybe D.string))
-        (D.field "events" (D.list Data.EventType.decoder))
-
-
-encode : NotificationHook -> E.Value
-encode hook =
-    E.object
-        [ ( "id", E.string hook.id )
-        , ( "enabled", E.bool hook.enabled )
-        , ( "channel", Data.NotificationChannel.encode hook.channel )
-        , ( "allEvents", E.bool hook.allEvents )
-        , ( "eventFilter", Maybe.map E.string hook.eventFilter |> Maybe.withDefault E.null )
-        , ( "events", E.list Data.EventType.encode hook.events )
-        ]
-
-
-
---- private
diff --git a/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm b/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm
deleted file mode 100644
index d1058d18..00000000
--- a/modules/webapp/src/main/elm/Data/PeriodicDueItemsSettings.elm
+++ /dev/null
@@ -1,77 +0,0 @@
-{-
-   Copyright 2020 Eike K. & Contributors
-
-   SPDX-License-Identifier: AGPL-3.0-or-later
--}
-
-
-module Data.PeriodicDueItemsSettings exposing (..)
-
-import Api.Model.Tag exposing (Tag)
-import Data.ChannelType exposing (ChannelType)
-import Data.NotificationChannel exposing (NotificationChannel)
-import Json.Decode as Decode
-import Json.Decode.Pipeline as P
-import Json.Encode as Encode
-
-
-
-{--
-  - Settings for notifying about due items.
- --}
-
-
-type alias PeriodicDueItemsSettings =
-    { id : String
-    , enabled : Bool
-    , summary : Maybe String
-    , channel : NotificationChannel
-    , schedule : String
-    , remindDays : Int
-    , capOverdue : Bool
-    , tagsInclude : List Tag
-    , tagsExclude : List Tag
-    }
-
-
-empty : ChannelType -> PeriodicDueItemsSettings
-empty ct =
-    { id = ""
-    , enabled = False
-    , summary = Nothing
-    , channel = Data.NotificationChannel.empty ct
-    , schedule = ""
-    , remindDays = 0
-    , capOverdue = False
-    , tagsInclude = []
-    , tagsExclude = []
-    }
-
-
-decoder : Decode.Decoder PeriodicDueItemsSettings
-decoder =
-    Decode.succeed PeriodicDueItemsSettings
-        |> P.required "id" Decode.string
-        |> P.required "enabled" Decode.bool
-        |> P.optional "summary" (Decode.maybe Decode.string) Nothing
-        |> P.required "channel" Data.NotificationChannel.decoder
-        |> P.required "schedule" Decode.string
-        |> P.required "remindDays" Decode.int
-        |> P.required "capOverdue" Decode.bool
-        |> P.required "tagsInclude" (Decode.list Api.Model.Tag.decoder)
-        |> P.required "tagsExclude" (Decode.list Api.Model.Tag.decoder)
-
-
-encode : PeriodicDueItemsSettings -> Encode.Value
-encode value =
-    Encode.object
-        [ ( "id", Encode.string value.id )
-        , ( "enabled", Encode.bool value.enabled )
-        , ( "summary", (Maybe.map Encode.string >> Maybe.withDefault Encode.null) value.summary )
-        , ( "channel", Data.NotificationChannel.encode value.channel )
-        , ( "schedule", Encode.string value.schedule )
-        , ( "remindDays", Encode.int value.remindDays )
-        , ( "capOverdue", Encode.bool value.capOverdue )
-        , ( "tagsInclude", Encode.list Api.Model.Tag.encode value.tagsInclude )
-        , ( "tagsExclude", Encode.list Api.Model.Tag.encode value.tagsExclude )
-        ]
diff --git a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm b/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm
deleted file mode 100644
index 73603fc1..00000000
--- a/modules/webapp/src/main/elm/Data/PeriodicQuerySettings.elm
+++ /dev/null
@@ -1,65 +0,0 @@
-{-
-   Copyright 2020 Eike K. & Contributors
-
-   SPDX-License-Identifier: AGPL-3.0-or-later
--}
-
-
-module Data.PeriodicQuerySettings exposing (PeriodicQuerySettings, decoder, empty, encode)
-
-import Data.ChannelType exposing (ChannelType)
-import Data.NotificationChannel exposing (NotificationChannel)
-import Json.Decode as D
-import Json.Encode as E
-
-
-type alias PeriodicQuerySettings =
-    { id : String
-    , enabled : Bool
-    , summary : Maybe String
-    , channel : NotificationChannel
-    , query : Maybe String
-    , bookmark : Maybe String
-    , contentStart : Maybe String
-    , schedule : String
-    }
-
-
-empty : ChannelType -> PeriodicQuerySettings
-empty ct =
-    { id = ""
-    , enabled = False
-    , summary = Nothing
-    , channel = Data.NotificationChannel.empty ct
-    , query = Nothing
-    , bookmark = Nothing
-    , contentStart = Nothing
-    , schedule = ""
-    }
-
-
-decoder : D.Decoder PeriodicQuerySettings
-decoder =
-    D.map8 PeriodicQuerySettings
-        (D.field "id" D.string)
-        (D.field "enabled" D.bool)
-        (D.maybe (D.field "summary" D.string))
-        (D.field "channel" Data.NotificationChannel.decoder)
-        (D.maybe (D.field "query" D.string))
-        (D.maybe (D.field "bookmark" D.string))
-        (D.maybe (D.field "contentStart" D.string))
-        (D.field "schedule" D.string)
-
-
-encode : PeriodicQuerySettings -> E.Value
-encode s =
-    E.object
-        [ ( "id", E.string s.id )
-        , ( "enabled", E.bool s.enabled )
-        , ( "summary", Maybe.map E.string s.summary |> Maybe.withDefault E.null )
-        , ( "channel", Data.NotificationChannel.encode s.channel )
-        , ( "query", Maybe.map E.string s.query |> Maybe.withDefault E.null )
-        , ( "bookmark", Maybe.map E.string s.bookmark |> Maybe.withDefault E.null )
-        , ( "contentStart", Maybe.map E.string s.contentStart |> Maybe.withDefault E.null )
-        , ( "schedule", E.string s.schedule )
-        ]
diff --git a/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm b/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm
new file mode 100644
index 00000000..e5730b19
--- /dev/null
+++ b/modules/webapp/src/main/elm/Messages/Comp/ChannelRefInput.elm
@@ -0,0 +1,41 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Messages.Comp.ChannelRefInput exposing
+    ( Texts
+    , de
+    , gb
+    )
+
+import Messages.Basics
+import Messages.Data.ChannelType
+
+
+type alias Texts =
+    { basics : Messages.Basics.Texts
+    , channelType : Messages.Data.ChannelType.Texts
+    , placeholder : String
+    , noCategory : String
+    }
+
+
+gb : Texts
+gb =
+    { basics = Messages.Basics.gb
+    , channelType = Messages.Data.ChannelType.gb
+    , placeholder = "Choose…"
+    , noCategory = "No channel"
+    }
+
+
+de : Texts
+de =
+    { basics = Messages.Basics.de
+    , channelType = Messages.Data.ChannelType.de
+    , placeholder = "Wähle…"
+    , noCategory = "Kein Kanal"
+    }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm
index c19c0152..33bd1533 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskForm.elm
@@ -15,6 +15,7 @@ import Http
 import Messages.Basics
 import Messages.Comp.CalEventInput
 import Messages.Comp.ChannelForm
+import Messages.Comp.ChannelRefInput
 import Messages.Comp.HttpError
 import Messages.Comp.TagDropdown
 import Messages.Data.ChannelType
@@ -26,6 +27,8 @@ type alias Texts =
     , httpError : Http.Error -> String
     , channelForm : Messages.Comp.ChannelForm.Texts
     , tagDropdown : Messages.Comp.TagDropdown.Texts
+    , channelType : Messages.Data.ChannelType.Texts
+    , channelRef : Messages.Comp.ChannelRefInput.Texts
     , reallyDeleteTask : String
     , startOnce : String
     , startTaskNow : String
@@ -50,7 +53,7 @@ type alias Texts =
     , recipientsRequired : String
     , queryLabel : String
     , channelRequired : String
-    , channelHeader : Messages.Data.ChannelType.Texts
+    , channelHeader : String
     }
 
 
@@ -61,6 +64,8 @@ gb =
     , httpError = Messages.Comp.HttpError.gb
     , channelForm = Messages.Comp.ChannelForm.gb
     , tagDropdown = Messages.Comp.TagDropdown.gb
+    , channelType = Messages.Data.ChannelType.gb
+    , channelRef = Messages.Comp.ChannelRefInput.gb
     , reallyDeleteTask = "Really delete this notification task?"
     , startOnce = "Start Once"
     , startTaskNow = "Start this task now"
@@ -89,7 +94,7 @@ gb =
     , recipientsRequired = "At least one recipient is required."
     , queryLabel = "Query"
     , channelRequired = "A valid channel must be given."
-    , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct
+    , channelHeader = "Channels"
     }
 
 
@@ -98,8 +103,10 @@ de =
     { basics = Messages.Basics.de
     , calEventInput = Messages.Comp.CalEventInput.de
     , httpError = Messages.Comp.HttpError.de
-    , channelForm = Messages.Comp.ChannelForm.gb
-    , tagDropdown = Messages.Comp.TagDropdown.gb
+    , channelForm = Messages.Comp.ChannelForm.de
+    , tagDropdown = Messages.Comp.TagDropdown.de
+    , channelType = Messages.Data.ChannelType.de
+    , channelRef = Messages.Comp.ChannelRefInput.de
     , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?"
     , startOnce = "Jetzt starten"
     , startTaskNow = "Starte den Auftrag sofort"
@@ -128,5 +135,5 @@ de =
     , recipientsRequired = "Mindestens ein Empfänger muss angegeben werden."
     , queryLabel = "Abfrage"
     , channelRequired = "Ein Versandkanal muss angegeben werden."
-    , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct
+    , channelHeader = "Kanäle"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm
index 3efa4228..0da1b2d9 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/DueItemsTaskList.elm
@@ -12,32 +12,33 @@ module Messages.Comp.DueItemsTaskList exposing
     )
 
 import Messages.Basics
+import Messages.Data.ChannelType
 
 
 type alias Texts =
     { basics : Messages.Basics.Texts
+    , channelType : Messages.Data.ChannelType.Texts
     , summary : String
     , schedule : String
     , connection : String
-    , recipients : String
     }
 
 
 gb : Texts
 gb =
     { basics = Messages.Basics.gb
+    , channelType = Messages.Data.ChannelType.gb
     , summary = "Summary"
     , schedule = "Schedule"
-    , connection = "Connection"
-    , recipients = "Recipients"
+    , connection = "Channel"
     }
 
 
 de : Texts
 de =
     { basics = Messages.Basics.de
+    , channelType = Messages.Data.ChannelType.de
     , summary = "Kurzbeschreibung"
     , schedule = "Zeitplan"
-    , connection = "Verbindung"
-    , recipients = "Empfänger"
+    , connection = "Kanal"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm
new file mode 100644
index 00000000..fec7ee3a
--- /dev/null
+++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelManage.elm
@@ -0,0 +1,83 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Messages.Comp.NotificationChannelManage exposing (Texts, de, gb)
+
+import Http
+import Messages.Basics
+import Messages.Comp.ChannelForm
+import Messages.Comp.HttpError
+import Messages.Comp.NotificationChannelTable
+import Messages.Data.ChannelType
+
+
+type alias Texts =
+    { basics : Messages.Basics.Texts
+    , notificationForm : Messages.Comp.ChannelForm.Texts
+    , notificationTable : Messages.Comp.NotificationChannelTable.Texts
+    , httpError : Http.Error -> String
+    , channelType : Messages.Data.ChannelType.Texts
+    , newChannel : String
+    , channelCreated : String
+    , channelUpdated : String
+    , channelDeleted : String
+    , formInvalid : String
+    , integrate : String
+    , intoDocspell : String
+    , postRequestInfo : String
+    , notifyEmailInfo : String
+    , addChannel : String
+    , updateChannel : String
+    , deleteThisChannel : String
+    , reallyDeleteChannel : String
+    }
+
+
+gb : Texts
+gb =
+    { basics = Messages.Basics.gb
+    , notificationForm = Messages.Comp.ChannelForm.gb
+    , notificationTable = Messages.Comp.NotificationChannelTable.gb
+    , httpError = Messages.Comp.HttpError.gb
+    , channelType = Messages.Data.ChannelType.gb
+    , newChannel = "New Channel"
+    , channelCreated = "Channel created"
+    , channelUpdated = "Channel updated"
+    , channelDeleted = "Channel deleted"
+    , formInvalid = "Please fill in all required fields"
+    , integrate = "Integrate"
+    , intoDocspell = "into Docspell"
+    , postRequestInfo = "Docspell will send POST requests with JSON payload."
+    , notifyEmailInfo = "Get notified via e-mail."
+    , addChannel = "Add new channel"
+    , updateChannel = "Update channel"
+    , deleteThisChannel = "Kanal löschen"
+    , reallyDeleteChannel = "Really delete this channel?"
+    }
+
+
+de : Texts
+de =
+    { basics = Messages.Basics.de
+    , notificationForm = Messages.Comp.ChannelForm.de
+    , notificationTable = Messages.Comp.NotificationChannelTable.de
+    , httpError = Messages.Comp.HttpError.de
+    , channelType = Messages.Data.ChannelType.de
+    , newChannel = "Neuer Kanal"
+    , channelCreated = "Kanal wurde angelegt."
+    , channelUpdated = "Kanal wurde aktualisiert."
+    , channelDeleted = "Kanal wurde entfernt."
+    , formInvalid = "Bitte alle erforderlichen Felder ausfüllen"
+    , integrate = "Integriere"
+    , intoDocspell = "in Docspell"
+    , postRequestInfo = "Docspell wird JSON POST requests senden."
+    , notifyEmailInfo = "Werde per E-Mail benachrichtigt."
+    , addChannel = "Neuen Kanal hinzufügen"
+    , updateChannel = "Kanal aktualisieren"
+    , deleteThisChannel = "Kanal löschen"
+    , reallyDeleteChannel = "Den Kanal wirklich löschen?"
+    }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm
new file mode 100644
index 00000000..b511163a
--- /dev/null
+++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationChannelTable.elm
@@ -0,0 +1,35 @@
+{-
+   Copyright 2020 Eike K. & Contributors
+
+   SPDX-License-Identifier: AGPL-3.0-or-later
+-}
+
+
+module Messages.Comp.NotificationChannelTable exposing (..)
+
+import Data.EventType exposing (EventType)
+import Messages.Basics
+import Messages.Data.EventType
+
+
+type alias Texts =
+    { basics : Messages.Basics.Texts
+    , eventType : EventType -> Messages.Data.EventType.Texts
+    , channelType : String
+    }
+
+
+gb : Texts
+gb =
+    { basics = Messages.Basics.gb
+    , eventType = Messages.Data.EventType.gb
+    , channelType = "Channel type"
+    }
+
+
+de : Texts
+de =
+    { basics = Messages.Basics.de
+    , eventType = Messages.Data.EventType.de
+    , channelType = "Kanaltyp"
+    }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm
index e2b30382..21189adc 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookForm.elm
@@ -13,18 +13,17 @@ module Messages.Comp.NotificationHookForm exposing
 
 import Data.EventType exposing (EventType)
 import Messages.Basics
-import Messages.Comp.ChannelForm
+import Messages.Comp.ChannelRefInput
 import Messages.Comp.EventSample
-import Messages.Data.ChannelType
 import Messages.Data.EventType
 
 
 type alias Texts =
     { basics : Messages.Basics.Texts
-    , channelForm : Messages.Comp.ChannelForm.Texts
+    , channelRef : Messages.Comp.ChannelRefInput.Texts
     , eventType : EventType -> Messages.Data.EventType.Texts
     , eventSample : Messages.Comp.EventSample.Texts
-    , channelHeader : Messages.Data.ChannelType.Texts
+    , channelHeader : String
     , enableDisable : String
     , eventsInfo : String
     , selectEvents : String
@@ -34,16 +33,19 @@ type alias Texts =
     , eventFilter : String
     , eventFilterInfo : String
     , eventFilterClickForHelp : String
+    , jsonPayload : String
+    , messagePayload : String
+    , payloadInfo : String
     }
 
 
 gb : Texts
 gb =
     { basics = Messages.Basics.gb
-    , channelForm = Messages.Comp.ChannelForm.gb
+    , channelRef = Messages.Comp.ChannelRefInput.gb
     , eventType = Messages.Data.EventType.gb
     , eventSample = Messages.Comp.EventSample.gb
-    , channelHeader = Messages.Data.ChannelType.gb
+    , channelHeader = "Select channels"
     , enableDisable = "Enabled / Disabled"
     , eventsInfo = "Select events that trigger this webhook"
     , selectEvents = "Select…"
@@ -53,16 +55,19 @@ gb =
     , eventFilter = "Event Filter Expression"
     , eventFilterInfo = "Optional specify an expression to filter events based on their JSON structure."
     , eventFilterClickForHelp = "Click here for help"
+    , jsonPayload = "JSON"
+    , messagePayload = "Message"
+    , payloadInfo = "Message payloads are sent to gotify, email and matrix. The JSON is sent to http channel."
     }
 
 
 de : Texts
 de =
     { basics = Messages.Basics.de
-    , channelForm = Messages.Comp.ChannelForm.de
+    , channelRef = Messages.Comp.ChannelRefInput.de
     , eventType = Messages.Data.EventType.de
     , eventSample = Messages.Comp.EventSample.de
-    , channelHeader = Messages.Data.ChannelType.de
+    , channelHeader = "Kanäle"
     , enableDisable = "Aktiviert / Deaktivert"
     , eventsInfo = "Wähle die Ereignisse, die diesen webhook auslösen"
     , selectEvents = "Wähle…"
@@ -72,4 +77,7 @@ de =
     , eventFilter = "Ereignisfilter"
     , eventFilterInfo = "Optionaler Ausdruck zum filtern von Ereignissen auf Basis ihrer JSON Struktur."
     , eventFilterClickForHelp = "Klicke für Hilfe"
+    , jsonPayload = "JSON"
+    , messagePayload = "Nachricht"
+    , payloadInfo = "Es werden abhängig vom Kanal JSON oder Nachricht-Formate versendet. Der HTTP Kanal empfängt nur JSON, an die anderen wird das Nachrichtformat gesendet."
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm
index d215ff32..6b4c7f2f 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookManage.elm
@@ -11,7 +11,6 @@ module Messages.Comp.NotificationHookManage exposing
     , gb
     )
 
-import Html exposing (Html, text)
 import Http
 import Messages.Basics
 import Messages.Comp.HttpError
@@ -27,9 +26,6 @@ type alias Texts =
     , httpError : Http.Error -> String
     , channelType : Messages.Data.ChannelType.Texts
     , newHook : String
-    , matrix : String
-    , gotify : String
-    , email : String
     , httpRequest : String
     , hookCreated : String
     , hookUpdated : String
@@ -39,12 +35,8 @@ type alias Texts =
     , reallyDeleteHook : String
     , formInvalid : String
     , invalidJsonFilter : String -> String
-    , integrate : String
-    , intoDocspell : String
-    , postRequestInfo : String
     , updateWebhook : String
     , addWebhook : String
-    , notifyEmailInfo : String
     }
 
 
@@ -56,9 +48,6 @@ gb =
     , httpError = Messages.Comp.HttpError.gb
     , channelType = Messages.Data.ChannelType.gb
     , newHook = "New Webhook"
-    , matrix = "Matrix"
-    , gotify = "Gotify"
-    , email = "E-Mail"
     , httpRequest = "HTTP Request"
     , hookCreated = "Webhook created"
     , hookUpdated = "Webhook updated"
@@ -68,12 +57,8 @@ gb =
     , reallyDeleteHook = "Really delete this webhook?"
     , formInvalid = "Please fill in all required fields"
     , invalidJsonFilter = \m -> "Event filter invalid: " ++ m
-    , integrate = "Integrate"
-    , intoDocspell = "into Docspell"
-    , postRequestInfo = "Docspell will send POST requests with JSON payload."
     , updateWebhook = "Update webhook"
     , addWebhook = "Add new webhook"
-    , notifyEmailInfo = "Get notified via e-mail."
     }
 
 
@@ -85,9 +70,6 @@ de =
     , httpError = Messages.Comp.HttpError.de
     , channelType = Messages.Data.ChannelType.de
     , newHook = "Neuer Webhook"
-    , matrix = "Matrix"
-    , gotify = "Gotify"
-    , email = "E-Mail"
     , httpRequest = "HTTP Request"
     , hookCreated = "Webhook erstellt"
     , hookUpdated = "Webhook aktualisiert"
@@ -97,10 +79,6 @@ de =
     , reallyDeleteHook = "Den webhook wirklich löschen?"
     , formInvalid = "Bitte alle erforderlichen Felder ausfüllen"
     , invalidJsonFilter = \m -> "Ereignisfilter ist falsch: " ++ m
-    , integrate = "Integriere"
-    , intoDocspell = "in Docspell"
-    , postRequestInfo = "Docspell wird JSON POST requests senden."
     , updateWebhook = "Webhook aktualisieren"
     , addWebhook = "Neuen Webhook hinzufügen"
-    , notifyEmailInfo = "Werde per E-Mail benachrichtigt."
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm
index ddf8c8ff..411a646b 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/NotificationHookTable.elm
@@ -13,15 +13,18 @@ module Messages.Comp.NotificationHookTable exposing
 
 import Data.EventType exposing (EventType)
 import Messages.Basics
+import Messages.Data.ChannelType
 import Messages.Data.EventType
 
 
 type alias Texts =
     { basics : Messages.Basics.Texts
     , eventType : EventType -> Messages.Data.EventType.Texts
+    , channelType : Messages.Data.ChannelType.Texts
     , enabled : String
     , channel : String
     , events : String
+    , allEvents : String
     }
 
 
@@ -29,9 +32,11 @@ gb : Texts
 gb =
     { basics = Messages.Basics.gb
     , eventType = Messages.Data.EventType.gb
+    , channelType = Messages.Data.ChannelType.gb
     , enabled = "Enabled"
     , channel = "Channel"
     , events = "Events"
+    , allEvents = "All"
     }
 
 
@@ -39,7 +44,9 @@ de : Texts
 de =
     { basics = Messages.Basics.de
     , eventType = Messages.Data.EventType.de
+    , channelType = Messages.Data.ChannelType.de
     , enabled = "Aktiv"
     , channel = "Kanal"
     , events = "Ereignisse"
+    , allEvents = "Alle"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm
index 49e717a7..7f9afbf1 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskForm.elm
@@ -11,14 +11,13 @@ module Messages.Comp.PeriodicQueryTaskForm exposing
     , gb
     )
 
-import Data.ChannelType exposing (ChannelType)
 import Http
 import Messages.Basics
 import Messages.Comp.BookmarkDropdown
 import Messages.Comp.CalEventInput
 import Messages.Comp.ChannelForm
+import Messages.Comp.ChannelRefInput
 import Messages.Comp.HttpError
-import Messages.Data.ChannelType
 
 
 type alias Texts =
@@ -26,6 +25,7 @@ type alias Texts =
     , calEventInput : Messages.Comp.CalEventInput.Texts
     , channelForm : Messages.Comp.ChannelForm.Texts
     , bookmarkDropdown : Messages.Comp.BookmarkDropdown.Texts
+    , channelRef : Messages.Comp.ChannelRefInput.Texts
     , httpError : Http.Error -> String
     , reallyDeleteTask : String
     , startOnce : String
@@ -41,7 +41,7 @@ type alias Texts =
     , invalidCalEvent : String
     , channelRequired : String
     , queryStringRequired : String
-    , channelHeader : ChannelType -> String
+    , channelHeader : String
     , messageContentTitle : String
     , messageContentLabel : String
     , messageContentInfo : String
@@ -56,6 +56,7 @@ gb =
     , channelForm = Messages.Comp.ChannelForm.gb
     , httpError = Messages.Comp.HttpError.gb
     , bookmarkDropdown = Messages.Comp.BookmarkDropdown.gb
+    , channelRef = Messages.Comp.ChannelRefInput.gb
     , reallyDeleteTask = "Really delete this notification task?"
     , startOnce = "Start Once"
     , startTaskNow = "Start this task now"
@@ -74,7 +75,7 @@ gb =
     , queryLabel = "Query"
     , channelRequired = "A valid channel must be given."
     , queryStringRequired = "A query string and/or bookmark must be supplied"
-    , channelHeader = \ct -> "Connection details for " ++ Messages.Data.ChannelType.gb ct
+    , channelHeader = "Channels"
     , messageContentTitle = "Customize message"
     , messageContentLabel = "Beginning of message"
     , messageContentInfo = "Insert text that is prependend to the generated message."
@@ -89,6 +90,7 @@ de =
     , channelForm = Messages.Comp.ChannelForm.de
     , httpError = Messages.Comp.HttpError.de
     , bookmarkDropdown = Messages.Comp.BookmarkDropdown.de
+    , channelRef = Messages.Comp.ChannelRefInput.de
     , reallyDeleteTask = "Diesen Benachrichtigungsauftrag wirklich löschen?"
     , startOnce = "Jetzt starten"
     , startTaskNow = "Starte den Auftrag sofort"
@@ -107,7 +109,7 @@ de =
     , queryLabel = "Abfrage"
     , channelRequired = "Ein Versandkanal muss angegeben werden."
     , queryStringRequired = "Eine Suchabfrage und/oder ein Bookmark muss angegeben werden."
-    , channelHeader = \ct -> "Details für " ++ Messages.Data.ChannelType.de ct
+    , channelHeader = "Kanäle"
     , messageContentTitle = "Nachricht anpassen"
     , messageContentLabel = "Anfang der Nachricht"
     , messageContentInfo = "Dieser Text wird an den Anfang der generierten Nachricht angefügt."
diff --git a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm
index 0920b6f5..57f8c287 100644
--- a/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm
+++ b/modules/webapp/src/main/elm/Messages/Comp/PeriodicQueryTaskList.elm
@@ -12,32 +12,33 @@ module Messages.Comp.PeriodicQueryTaskList exposing
     )
 
 import Messages.Basics
+import Messages.Data.ChannelType
 
 
 type alias Texts =
     { basics : Messages.Basics.Texts
+    , channelType : Messages.Data.ChannelType.Texts
     , summary : String
     , schedule : String
     , connection : String
-    , recipients : String
     }
 
 
 gb : Texts
 gb =
     { basics = Messages.Basics.gb
+    , channelType = Messages.Data.ChannelType.gb
     , summary = "Summary"
     , schedule = "Schedule"
-    , connection = "Connection"
-    , recipients = "Recipients"
+    , connection = "Channel"
     }
 
 
 de : Texts
 de =
     { basics = Messages.Basics.de
+    , channelType = Messages.Data.ChannelType.de
     , summary = "Kurzbeschreibung"
     , schedule = "Zeitplan"
-    , connection = "Verbindung"
-    , recipients = "Empfänger"
+    , connection = "Kanal"
     }
diff --git a/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm b/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm
index 4522fa27..2003e6e1 100644
--- a/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm
+++ b/modules/webapp/src/main/elm/Messages/Data/ChannelType.elm
@@ -27,7 +27,7 @@ gb ct =
             "E-Mail"
 
         Data.ChannelType.Http ->
-            "HTTP request"
+            "JSON"
 
 
 de : Texts
@@ -43,4 +43,4 @@ de ct =
             "E-Mail"
 
         Data.ChannelType.Http ->
-            "HTTP Request"
+            "JSON"
diff --git a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm
index a1bb4e39..dcef8b6c 100644
--- a/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm
+++ b/modules/webapp/src/main/elm/Messages/Page/UserSettings.elm
@@ -15,6 +15,7 @@ import Messages.Comp.ChangePasswordForm
 import Messages.Comp.DueItemsTaskManage
 import Messages.Comp.EmailSettingsManage
 import Messages.Comp.ImapSettingsManage
+import Messages.Comp.NotificationChannelManage
 import Messages.Comp.NotificationHookManage
 import Messages.Comp.OtpSetup
 import Messages.Comp.PeriodicQueryTaskManage
@@ -31,6 +32,7 @@ type alias Texts =
     , scanMailboxManage : Messages.Comp.ScanMailboxManage.Texts
     , notificationHookManage : Messages.Comp.NotificationHookManage.Texts
     , periodicQueryTask : Messages.Comp.PeriodicQueryTaskManage.Texts
+    , channelManage : Messages.Comp.NotificationChannelManage.Texts
     , otpSetup : Messages.Comp.OtpSetup.Texts
     , userSettings : String
     , uiSettings : String
@@ -38,6 +40,7 @@ type alias Texts =
     , scanMailbox : String
     , emailSettingSmtp : String
     , emailSettingImap : String
+    , channelSettings : String
     , changePassword : String
     , uiSettingsInfo : String
     , scanMailboxInfo1 : String
@@ -50,6 +53,8 @@ type alias Texts =
     , webhookInfoText : String
     , dueItemsInfoText : String
     , periodicQueryInfoText : String
+    , channels : String
+    , channelInfoText : String
     }
 
 
@@ -63,6 +68,7 @@ gb =
     , scanMailboxManage = Messages.Comp.ScanMailboxManage.gb
     , notificationHookManage = Messages.Comp.NotificationHookManage.gb
     , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.gb
+    , channelManage = Messages.Comp.NotificationChannelManage.gb
     , otpSetup = Messages.Comp.OtpSetup.gb
     , userSettings = "User Settings"
     , uiSettings = "UI Settings"
@@ -71,6 +77,7 @@ gb =
     , emailSettingSmtp = "E-Mail Settings (SMTP)"
     , emailSettingImap = "E-Mail Settings (IMAP)"
     , changePassword = "Change Password"
+    , channelSettings = "Notification Channels"
     , uiSettingsInfo =
         "These settings only affect the web ui. They are stored in the browser, "
             ++ "so they are separated between browsers and devices."
@@ -103,14 +110,16 @@ its payload.
 Additionally, you can setup queries that are executed periodically.
 The results are send as a notification message.
 
-When creating a new notification task, choose first the communication
-channel.
+A notification setting needs at least one communication channel, which
+must be created before.
 
 """
     , webhookInfoText = """Webhooks execute http request upon certain events in docspell.
 """
     , dueItemsInfoText = """Docspell can notify you once the due dates of your items come closer.  """
     , periodicQueryInfoText = "You can define a custom query that gets executed periodically."
+    , channels = "Notification Channels"
+    , channelInfoText = "Channels are used to send notification messages."
     }
 
 
@@ -124,6 +133,7 @@ de =
     , scanMailboxManage = Messages.Comp.ScanMailboxManage.de
     , notificationHookManage = Messages.Comp.NotificationHookManage.de
     , periodicQueryTask = Messages.Comp.PeriodicQueryTaskManage.de
+    , channelManage = Messages.Comp.NotificationChannelManage.de
     , otpSetup = Messages.Comp.OtpSetup.de
     , userSettings = "Benutzereinstellung"
     , uiSettings = "Oberfläche"
@@ -131,6 +141,7 @@ de =
     , scanMailbox = "E-Mail-Import"
     , emailSettingSmtp = "E-Mail-Einstellungen (SMTP)"
     , emailSettingImap = "E-Mail-Einstellungen (IMAP)"
+    , channelSettings = "Benachrichtigungskanäle"
     , changePassword = "Passwort ändern"
     , uiSettingsInfo =
         "Diese Einstellungen sind für die Web-Oberfläche."
@@ -161,15 +172,16 @@ Es kann aus diesen Versandkanälen gewählt werden:
 E-Mail. Zusätzlich kann das HTTP request direkt empfangen werden, was
 alle Details zu einem Ereignis enthält.
 
-
 Ausserdem können periodische Suchabfragen erstellt werden, dessen
 Ergebnis dann als Benachrichtigung versendet wird.
 
-Beim Erstellen eines neuen Auftrags muss zunächst der gewünschte
-Versandkanal gewählt werden.
+Für eine Notifikation ist ein Kommunikationskanal notwendig, der zuvor
+erstellt werden muss.
 
 """
     , webhookInfoText = """Webhooks versenden HTTP Requests wenn bestimmte Ereignisse in Docspell auftreten."""
     , dueItemsInfoText = """Docspell kann dich benachrichtigen, sobald das Fälligkeitsdatum von Dokumenten näher kommt. """
     , periodicQueryInfoText = "Hier können beliebige Abfragen definiert werden, welche regelmäßig ausgeführt werden."
+    , channels = "Benachrichtigungskanäle"
+    , channelInfoText = "Über Kanäle werden Notifizierungen versendet."
     }
diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm
index 58b761e0..2c71dda6 100644
--- a/modules/webapp/src/main/elm/Page/UserSettings/Data.elm
+++ b/modules/webapp/src/main/elm/Page/UserSettings/Data.elm
@@ -16,6 +16,7 @@ import Comp.ChangePasswordForm
 import Comp.DueItemsTaskManage
 import Comp.EmailSettingsManage
 import Comp.ImapSettingsManage
+import Comp.NotificationChannelManage
 import Comp.NotificationHookManage
 import Comp.OtpSetup
 import Comp.PeriodicQueryTaskManage
@@ -35,6 +36,7 @@ type alias Model =
     , uiSettingsModel : Comp.UiSettingsManage.Model
     , otpSetupModel : Comp.OtpSetup.Model
     , notificationHookModel : Comp.NotificationHookManage.Model
+    , channelModel : Comp.NotificationChannelManage.Model
     , periodicQueryModel : Comp.PeriodicQueryTaskManage.Model
     }
 
@@ -53,6 +55,9 @@ init flags settings =
 
         ( pqm, pqc ) =
             Comp.PeriodicQueryTaskManage.init flags
+
+        ( ncm, ncc ) =
+            Comp.NotificationChannelManage.init flags
     in
     ( { currentTab = Just UiSettingsTab
       , changePassModel = Comp.ChangePasswordForm.emptyModel
@@ -64,12 +69,14 @@ init flags settings =
       , otpSetupModel = otpm
       , notificationHookModel = nhm
       , periodicQueryModel = pqm
+      , channelModel = ncm
       }
     , Cmd.batch
         [ Cmd.map UiSettingsMsg uc
         , Cmd.map OtpSetupMsg otpc
         , Cmd.map NotificationHookMsg nhc
         , Cmd.map PeriodicQueryMsg pqc
+        , Cmd.map ChannelMsg ncc
         ]
     )
 
@@ -85,6 +92,7 @@ type Tab
     | ScanMailboxTab
     | UiSettingsTab
     | OtpTab
+    | ChannelTab
 
 
 type Msg
@@ -98,5 +106,6 @@ type Msg
     | OtpSetupMsg Comp.OtpSetup.Msg
     | NotificationHookMsg Comp.NotificationHookManage.Msg
     | PeriodicQueryMsg Comp.PeriodicQueryTaskManage.Msg
+    | ChannelMsg Comp.NotificationChannelManage.Msg
     | UpdateSettings
     | ReceiveBrowserSettings StoredUiSettings
diff --git a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm
index 263288a2..656674ad 100644
--- a/modules/webapp/src/main/elm/Page/UserSettings/Update.elm
+++ b/modules/webapp/src/main/elm/Page/UserSettings/Update.elm
@@ -11,6 +11,7 @@ import Comp.ChangePasswordForm
 import Comp.DueItemsTaskManage
 import Comp.EmailSettingsManage
 import Comp.ImapSettingsManage
+import Comp.NotificationChannelManage
 import Comp.NotificationHookManage
 import Comp.OtpSetup
 import Comp.PeriodicQueryTaskManage
@@ -71,8 +72,12 @@ update flags settings msg model =
                     }
 
                 NotificationWebhookTab ->
+                    let
+                        ( _, nc ) =
+                            Comp.NotificationHookManage.init flags
+                    in
                     { model = m
-                    , cmd = Cmd.none
+                    , cmd = Cmd.map NotificationHookMsg nc
                     , sub = Sub.none
                     , newSettings = Nothing
                     }
@@ -107,6 +112,9 @@ update flags settings msg model =
                 OtpTab ->
                     UpdateResult m Cmd.none Sub.none Nothing
 
+                ChannelTab ->
+                    UpdateResult m Cmd.none Sub.none Nothing
+
         ChangePassMsg m ->
             let
                 ( m2, c2 ) =
@@ -195,6 +203,17 @@ update flags settings msg model =
             , newSettings = Nothing
             }
 
+        ChannelMsg lm ->
+            let
+                ( cm, cc ) =
+                    Comp.NotificationChannelManage.update flags lm model.channelModel
+            in
+            { model = { model | channelModel = cm }
+            , cmd = Cmd.map ChannelMsg cc
+            , sub = Sub.none
+            , newSettings = Nothing
+            }
+
         UpdateSettings ->
             update flags
                 settings
diff --git a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm
index b466f038..50193b85 100644
--- a/modules/webapp/src/main/elm/Page/UserSettings/View2.elm
+++ b/modules/webapp/src/main/elm/Page/UserSettings/View2.elm
@@ -11,6 +11,7 @@ import Comp.ChangePasswordForm
 import Comp.DueItemsTaskManage
 import Comp.EmailSettingsManage
 import Comp.ImapSettingsManage
+import Comp.NotificationChannelManage
 import Comp.NotificationHookManage
 import Comp.OtpSetup
 import Comp.PeriodicQueryTaskManage
@@ -77,7 +78,7 @@ viewSidebar texts visible _ _ model =
                     , menuEntryActive model NotificationTab
                     , class S.sidebarLink
                     ]
-                    [ i [ class "fa fa-bullhorn" ] []
+                    [ i [ class "fa fa-comment font-thin" ] []
                     , span
                         [ class "ml-3" ]
                         [ text texts.notifications ]
@@ -121,6 +122,17 @@ viewSidebar texts visible _ _ model =
                         ]
                     ]
                 ]
+            , a
+                [ href "#"
+                , onClick (SetTab ChannelTab)
+                , menuEntryActive model ChannelTab
+                , class S.sidebarLink
+                ]
+                [ i [ class "fa fa-bullhorn" ] []
+                , span
+                    [ class "ml-3" ]
+                    [ text texts.channelSettings ]
+                ]
             , a
                 [ href "#"
                 , onClick (SetTab ScanMailboxTab)
@@ -217,6 +229,9 @@ viewContent texts flags settings model =
             Just OtpTab ->
                 viewOtpSetup texts settings model
 
+            Just ChannelTab ->
+                viewChannels texts settings model
+
             Nothing ->
                 []
         )
@@ -235,6 +250,26 @@ menuEntryActive model tab =
         class ""
 
 
+viewChannels : Texts -> UiSettings -> Model -> List (Html Msg)
+viewChannels texts settings model =
+    [ h2
+        [ class S.header1
+        , class "inline-flex items-center"
+        ]
+        [ i [ class "fa fa-bell" ] []
+        , div [ class "ml-3" ]
+            [ text texts.channels
+            ]
+        ]
+    , Markdown.toHtml [ class "opacity-80  text-lg mb-3 markdown-preview" ] texts.channelInfoText
+    , Html.map ChannelMsg
+        (Comp.NotificationChannelManage.view texts.channelManage
+            settings
+            model.channelModel
+        )
+    ]
+
+
 viewOtpSetup : Texts -> UiSettings -> Model -> List (Html Msg)
 viewOtpSetup texts _ model =
     [ h2