diff --git a/cmd/jc/main.go b/cmd/jc/main.go
index bce4c488ba6f7867d2b390f34efc5c0f0a081a5a..08951fcad42b227e84915f917f4d5e939077e4e2 100644
--- a/cmd/jc/main.go
+++ b/cmd/jc/main.go
@@ -4,12 +4,11 @@ import (
 	"flag"
 	"io/ioutil"
 	"log"
-	"reflect"
-	"strings"
 
 	"gopkg.in/yaml.v2"
 
 	"git.iiens.net/morignot2011/jc"
+	"git.iiens.net/morignot2011/jc/dispatcher"
 	"git.iiens.net/morignot2011/jc/irc"
 	"git.iiens.net/morignot2011/jc/slack"
 )
@@ -28,7 +27,6 @@ type LinkDest struct {
 	filter    []string
 }
 
-var transports = make(map[string]jc.Transport)
 var links = make(Links)
 
 func main() {
@@ -46,6 +44,8 @@ func main() {
 		log.Fatal(err)
 	}
 
+	transports := make(map[string]jc.Transport)
+
 	for name, transport := range cfg.Transports {
 		type_, ok := transport["type"]
 		if !ok {
@@ -59,9 +59,9 @@ func main() {
 
 		switch type_.(string) {
 		case "irc":
-			t, err = irc.New(transport)
+			t, err = irc.New(name, transport)
 		case "slack":
-			t, err = slack.New(transport)
+			t, err = slack.New(name, transport)
 		}
 
 		if err != nil {
@@ -75,131 +75,10 @@ func main() {
 		}
 	}
 
-	for _, link := range cfg.Links {
-		filter, ok := link["filter"]
-		if !ok {
-			filter = []string{}
-		}
-
-		delete(link, "filter")
-
-		for a, b := range link {
-			cfgA := strings.Split(a, "@")
-			cfgB := strings.Split(b.(string), "@")
-
-			if len(cfgA) != 2 || len(cfgB) != 2 {
-				log.Fatalf("Invalid link '%s => %s', it must be at the format 'channelA@transportA => channelB@transportB'", a, b.(string))
-			}
-
-			if _, ok := transports[cfgA[1]]; !ok {
-				log.Fatalf("Unknown transport %s in link '%s => %s'", cfgA[1], a, b.(string))
-			}
-
-			if _, ok := transports[cfgB[1]]; !ok {
-				log.Fatalf("Unknown transport %s in link '%s => %s'", cfgB[1], a, b.(string))
-			}
-
-			var filters []string
-			for _, f := range filter.([]interface{}) {
-				filters = append(filters, f.(string))
-			}
-
-			if links[cfgA[1]] == nil {
-				links[cfgA[1]] = make(Link)
-			}
-			links[cfgA[1]][cfgA[0]] = LinkDest{
-				transport: cfgB[1],
-				channel:   cfgB[0],
-				filter:    filters,
-			}
-
-			if links[cfgB[1]] == nil {
-				links[cfgB[1]] = make(Link)
-			}
-			links[cfgB[1]][cfgB[0]] = LinkDest{
-				transport: cfgA[1],
-				channel:   cfgA[0],
-				filter:    filters,
-			}
-		}
-	}
+	dispatcher := dispatcher.NewDispatcher(cfg.Links, transports)
 
 	// remove sensitive data
 	cfg = Cfg{}
 
-	cases := make([]reflect.SelectCase, len(transports)*2)
-	i := 0
-	for _, t := range transports {
-		cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.GetEvents())}
-		i++
-		cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.GetEnd())}
-		i++
-	}
-
-	for {
-		idx, value, ok := reflect.Select(cases)
-
-		i := 0
-		var name string
-		for name, _ = range transports {
-			if i == idx || i+1 == idx {
-				break
-			}
-
-			i += 2
-		}
-
-		if !ok {
-			log.Fatalf("Transport %s close one of its channel!", name)
-		}
-
-		switch ev := value.Interface().(type) {
-		case *jc.JoinEvent:
-			dest, ok := links[name][ev.Channel]
-			if !ok {
-				continue
-			}
-
-			if isFiltered(dest.filter, ev.Nick) {
-				continue
-			}
-
-			t := transports[dest.transport]
-
-			go t.Join(&jc.JoinEvent{
-				Nick:    ev.Nick + "_jc",
-				Channel: dest.channel,
-			})
-		case *jc.MessageEvent:
-			dest, ok := links[name][ev.Channel]
-			if !ok {
-				continue
-			}
-
-			if isFiltered(dest.filter, ev.Nick) {
-				continue
-			}
-
-			t := transports[dest.transport]
-
-			go t.Message(&jc.MessageEvent{
-				Nick:    ev.Nick + "_jc",
-				Channel: dest.channel,
-				Text:    ev.Text,
-			})
-		case bool:
-			log.Fatalf("Disconnected from %s", name)
-			return
-		}
-	}
-}
-
-func isFiltered(filter []string, nick string) bool {
-	for _, v := range filter {
-		if v == nick {
-			return true
-		}
-	}
-
-	return false
+	dispatcher.Run()
 }
diff --git a/dispatcher.go b/dispatcher.go
new file mode 100644
index 0000000000000000000000000000000000000000..fbf977e1fd8eceb97688ec30a9770d3351cb6971
--- /dev/null
+++ b/dispatcher.go
@@ -0,0 +1,17 @@
+package jc
+
+type Dispatcher interface {
+	Run()
+}
+
+type Links []Link
+
+type Link struct {
+	endpoints []Endpoint
+	filters   []string
+}
+
+type Endpoint struct {
+	Channel   string
+	Transport string
+}
diff --git a/dispatcher/dispatcher.go b/dispatcher/dispatcher.go
new file mode 100644
index 0000000000000000000000000000000000000000..56ecdb646293b6de3bc5acb1c5693704da51e4fb
--- /dev/null
+++ b/dispatcher/dispatcher.go
@@ -0,0 +1,126 @@
+package dispatcher
+
+import (
+	"log"
+	"reflect"
+
+	"git.iiens.net/morignot2011/jc"
+)
+
+type Dispatcher struct {
+	links          []Link
+	transports     map[string]jc.Transport
+	transportNames []string
+}
+
+type Link struct {
+	endpoints []Endpoint
+	filters   []string
+}
+
+type Endpoint struct {
+	channel   string
+	transport string
+}
+
+func NewDispatcher(cfg []map[string]interface{}, transports map[string]jc.Transport) jc.Dispatcher {
+	d := &Dispatcher{
+		transports: transports,
+	}
+
+	for _, linkCfg := range cfg {
+		link := Link{}
+
+		filters, ok := linkCfg["filters"]
+		if ok {
+			for _, filter := range filters.([]interface{}) {
+				link.filters = append(link.filters, filter.(string))
+			}
+		}
+
+		delete(linkCfg, "filters")
+
+		for name, channels := range linkCfg {
+			for _, channel := range channels.([]interface{}) {
+				endpoint := Endpoint{
+					channel:   channel.(string),
+					transport: name,
+				}
+
+				link.endpoints = append(link.endpoints, endpoint)
+			}
+		}
+
+		d.links = append(d.links, link)
+	}
+
+	return d
+}
+
+func (d *Dispatcher) Run() {
+	var cases []reflect.SelectCase
+	var transportNames []string
+
+	for name, t := range d.transports {
+		cases = append(cases, reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(t.GetEvents())})
+		transportNames = append(transportNames, name)
+	}
+
+	for {
+		i, value, ok := reflect.Select(cases)
+		name := transportNames[i]
+
+		if !ok {
+			log.Fatalf("Transport %s closed its event channel!", name)
+		}
+
+		switch ev := value.Interface().(type) {
+		case *jc.JoinEvent:
+			d.join(name, ev)
+		case *jc.MessageEvent:
+			d.message(name, ev)
+		}
+	}
+}
+
+func (d *Dispatcher) findLink(transport string, channels ...string) []Link {
+	byChan := false
+	if len(channels) > 0 {
+		byChan = true
+	}
+
+	var links []Link
+
+	for _, link := range d.links {
+		match := false
+
+		for _, endpoint := range link.endpoints {
+			if endpoint.transport != transport {
+				continue
+			}
+
+			if byChan && endpoint.channel != channels[0] {
+				continue
+			}
+
+			match = true
+			break
+		}
+
+		if match {
+			links = append(links, link)
+		}
+	}
+
+	return links
+}
+
+func isFiltered(filters []string, nick string) bool {
+	for _, v := range filters {
+		if v == nick {
+			return true
+		}
+	}
+
+	return false
+}
diff --git a/dispatcher/events.go b/dispatcher/events.go
new file mode 100644
index 0000000000000000000000000000000000000000..7c2908bcd4a456a42b80aaae8bf5cc6dd5c77558
--- /dev/null
+++ b/dispatcher/events.go
@@ -0,0 +1,52 @@
+package dispatcher
+
+import (
+	"log"
+
+	"git.iiens.net/morignot2011/jc"
+)
+
+func (d *Dispatcher) join(transport string, ev *jc.JoinEvent) {
+	log.Printf("Receive JoinEvent from %s: %s on %s", transport, ev.Nick, ev.Channel)
+
+	links := d.findLink(transport, ev.Channel)
+	for _, link := range links {
+		if isFiltered(link.filters, ev.Nick) {
+			continue
+		}
+
+		for _, endpoint := range link.endpoints {
+			if endpoint.transport == transport && endpoint.channel == ev.Channel {
+				continue
+			}
+
+			d.transports[endpoint.transport].Join(&jc.JoinEvent{
+				Nick:    ev.Nick + "_jc",
+				Channel: endpoint.channel,
+			})
+		}
+	}
+}
+
+func (d *Dispatcher) message(transport string, ev *jc.MessageEvent) {
+	log.Printf("Receive MessageEvent from %s: %s on %s", transport, ev.Nick, ev.Channel)
+
+	links := d.findLink(transport, ev.Channel)
+	for _, link := range links {
+		if isFiltered(link.filters, ev.Nick) {
+			continue
+		}
+
+		for _, endpoint := range link.endpoints {
+			if endpoint.transport == transport && endpoint.channel == ev.Channel {
+				continue
+			}
+
+			d.transports[endpoint.transport].Message(&jc.MessageEvent{
+				Nick:    ev.Nick + "_jc",
+				Channel: endpoint.channel,
+				Text:    ev.Text,
+			})
+		}
+	}
+}
diff --git a/irc/commands.go b/irc/commands.go
index 2ec2001fb53b484dec7fd996242f74a3c5c7aff4..ab71f621f6922046dca36088e6250fcd5a9f75ff 100644
--- a/irc/commands.go
+++ b/irc/commands.go
@@ -1,22 +1,23 @@
 package irc
 
 import (
-	"log"
-
 	"git.iiens.net/morignot2011/jc"
 )
 
 func (t *Transport) Join(ev *jc.JoinEvent) {
+	t.Logger.Printf("Join: %s on %s", ev.Nick, ev.Channel)
 	if i := FindChannel(t.channels, ev.Channel); i == -1 {
 		// cannot join a channel not configured
+		t.Logger.Printf("Channel %s is not configured", ev.Channel)
 		return
 	}
 
 	client, ok := t.userClients[ev.Nick]
 	if !ok {
+		t.Logger.Printf("Client %s does not exist, creating it", ev.Nick)
 		ircCfg, err := t.newIrcConfig(ev.Nick, t.cfg)
 		if err != nil {
-			log.Print(err)
+			t.Logger.Print(err)
 			return
 		}
 
@@ -26,7 +27,7 @@ func (t *Transport) Join(ev *jc.JoinEvent) {
 		t.userChannels[ev.Nick] = []string{ev.Channel}
 
 		if err := client.Connect(); err != nil {
-			log.Print(err)
+			t.Logger.Print(err)
 			return
 		}
 	} else {
@@ -39,14 +40,17 @@ func (t *Transport) Join(ev *jc.JoinEvent) {
 }
 
 func (t *Transport) Message(ev *jc.MessageEvent) {
+	t.Logger.Printf("Message: %s on %s", ev.Nick, ev.Channel)
 	client, ok := t.userClients[ev.Nick]
 	if !ok {
 		// unknown client
+		t.Logger.Printf("Unknown client %s", ev.Nick)
 		return
 	}
 
 	if i := FindChannel(t.userChannels[ev.Nick], ev.Channel); i == -1 {
 		// this user is not on this channel
+		t.Logger.Printf("%s is not on %s", ev.Nick, ev.Channel)
 		return
 	}
 
diff --git a/irc/events.go b/irc/events.go
index 22e6ed34bed74bfc2c89e673d3fd4a6557803a64..b1f916b0575ba0da02d364c1cbd3936766b8d752 100644
--- a/irc/events.go
+++ b/irc/events.go
@@ -12,10 +12,11 @@ import (
 )
 
 func (t *Transport) connected(client *irc.Conn, line *irc.Line) {
+	t.Logger.Printf("%s is connected", client.Me().Nick)
 	if t.client != client {
 		// user's client
 		for _, channel := range t.userChannels[client.Me().Nick] {
-			log.Printf("join %s", channel)
+			log.Printf("Join %s", channel)
 			client.Join(channel)
 		}
 	} else {
diff --git a/irc/transport.go b/irc/transport.go
index 192fcd30a0df2d3fd5f2f2e244bad2ad95b7c0b9..e44a8344fbc3a50e64c428f8d44b0140e130e97f 100644
--- a/irc/transport.go
+++ b/irc/transport.go
@@ -4,6 +4,7 @@ import (
 	"crypto/tls"
 	"fmt"
 	"log"
+	"os"
 
 	irc "github.com/fluffle/goirc/client"
 
@@ -28,7 +29,7 @@ type Transport struct {
 	connectionError chan error
 }
 
-func New(cfg map[string]interface{}) (jc.Transport, error) {
+func New(name string, cfg map[string]interface{}) (jc.Transport, error) {
 	t := &Transport{
 		cfg:             cfg,
 		connectionError: make(chan error),
@@ -38,6 +39,8 @@ func New(cfg map[string]interface{}) (jc.Transport, error) {
 		BaseTransport: jc.BaseTransport{
 			Events: make(chan interface{}),
 			End:    make(chan bool),
+
+			Logger: log.New(os.Stdout, name+": ", log.LstdFlags),
 		},
 	}
 
diff --git a/slack/events.go b/slack/events.go
index 473295dbbf40cb5bd835b3f54a703cec45080f58..f71afef3465ef3394e481c8a40bb883d57de894c 100644
--- a/slack/events.go
+++ b/slack/events.go
@@ -9,6 +9,7 @@ import (
 )
 
 func (t *Transport) connected(ev *slack.ConnectedEvent) {
+	t.Logger.Printf("Connected event")
 	events := []*jc.JoinEvent{}
 
 	for name, id := range t.channelIDs {
@@ -18,7 +19,6 @@ func (t *Transport) connected(ev *slack.ConnectedEvent) {
 			return
 		}
 
-		fmt.Printf("%s: %d\n", name, len(channel.Members))
 		for _, id := range channel.Members {
 			user, err := t.api.GetUserInfo(id)
 			if err != nil {
@@ -47,6 +47,7 @@ func (t *Transport) invalidAuth(ev *slack.InvalidAuthEvent) {
 }
 
 func (t *Transport) message(ev *slack.MessageEvent) {
+	t.Logger.Printf("Message event")
 	channel, ok := t.channelNames[ev.Channel]
 	if !ok {
 		// probably we got invited on a channel after starting the bot
diff --git a/slack/transport.go b/slack/transport.go
index 59a04bf9f6a99ef85c00f61a5176af93ef018dcf..a36eefb67d1e8a903977aa1522719c3135ec53f0 100644
--- a/slack/transport.go
+++ b/slack/transport.go
@@ -1,6 +1,9 @@
 package slack
 
 import (
+	"log"
+	"os"
+
 	"github.com/nlopes/slack"
 
 	"git.iiens.net/morignot2011/jc"
@@ -17,7 +20,7 @@ type Transport struct {
 	userNames       map[string]string
 }
 
-func New(cfg map[string]interface{}) (jc.Transport, error) {
+func New(name string, cfg map[string]interface{}) (jc.Transport, error) {
 	token, ok := cfg["token"]
 	if !ok {
 		return nil, jc.ConfigError{"token"}
@@ -36,6 +39,8 @@ func New(cfg map[string]interface{}) (jc.Transport, error) {
 		BaseTransport: jc.BaseTransport{
 			Events: make(chan interface{}),
 			End:    make(chan bool),
+
+			Logger: log.New(os.Stdout, name+": ", log.LstdFlags),
 		},
 	}
 
diff --git a/transport.go b/transport.go
index 8190ad31a6884697105d1b37a019929379078b02..250b7bca9a7186bc94f053a3f86ae75fecac41a6 100644
--- a/transport.go
+++ b/transport.go
@@ -1,5 +1,9 @@
 package jc
 
+import (
+	"log"
+)
+
 type Transport interface {
 	Run() (err error)
 
@@ -17,6 +21,8 @@ type Transport interface {
 type BaseTransport struct {
 	Events chan interface{}
 	End    chan bool
+
+	Logger *log.Logger
 }
 
 func (t *BaseTransport) GetEvents() chan interface{} {