Index: /trunk/cmd/jounce/main.go
===================================================================
--- /trunk/cmd/jounce/main.go	(revision 76)
+++ /trunk/cmd/jounce/main.go	(revision 77)
@@ -34,4 +34,9 @@
 	}
 
+	db, err := jounce.OpenSQLDB(cfg.SQLDriver, cfg.SQLSource)
+	if err != nil {
+		log.Fatalf("failed to open database: %v", err)
+	}
+
 	var ln net.Listener
 	if cfg.TLS != nil {
@@ -54,18 +59,15 @@
 	}
 
-	srv := jounce.NewServer()
+	srv := jounce.NewServer(db)
 	// TODO: load from config/DB
 	srv.Hostname = cfg.Hostname
 	srv.Debug = debug
-	srv.Upstreams = []jounce.Upstream{{
-		Addr:     "chat.freenode.net:6697",
-		Nick:     "jounce",
-		Username: "jounce",
-		Realname: "jounce",
-		Channels: []string{"#jounce"},
-	}}
 
 	log.Printf("server listening on %q", cfg.Addr)
-	go srv.Run()
+	go func() {
+		if err := srv.Run(); err != nil {
+			log.Fatal(err)
+		}
+	}()
 	log.Fatal(srv.Serve(ln))
 }
Index: /trunk/config/config.go
===================================================================
--- /trunk/config/config.go	(revision 76)
+++ /trunk/config/config.go	(revision 77)
@@ -15,7 +15,9 @@
 
 type Server struct {
-	Addr     string
-	Hostname string
-	TLS      *TLS
+	Addr      string
+	Hostname  string
+	TLS       *TLS
+	SQLDriver string
+	SQLSource string
 }
 
@@ -26,6 +28,8 @@
 	}
 	return &Server{
-		Addr:     ":6667",
-		Hostname: hostname,
+		Addr:      ":6667",
+		Hostname:  hostname,
+		SQLDriver: "sqlite3",
+		SQLSource: "jounce.db",
 	}
 }
@@ -65,4 +69,8 @@
 			}
 			srv.TLS = tls
+		case "sql":
+			if err := d.parseParams(&srv.SQLDriver, &srv.SQLSource); err != nil {
+				return nil, err
+			}
 		default:
 			return nil, fmt.Errorf("unknown directive %q", d.Name)
Index: /trunk/db.go
===================================================================
--- /trunk/db.go	(revision 77)
+++ /trunk/db.go	(revision 77)
@@ -0,0 +1,134 @@
+package jounce
+
+import (
+	"database/sql"
+	"errors"
+	"sync"
+
+	_ "github.com/mattn/go-sqlite3"
+)
+
+var ErrNoSuchUser = errors.New("jounce: no such user")
+
+type User struct {
+	Username string
+	Password string
+}
+
+type Network struct {
+	ID       int64
+	Addr     string
+	Nick     string
+	Username string
+	Realname string
+}
+
+type Channel struct {
+	ID   int64
+	Name string
+}
+
+type DB struct {
+	lock sync.Mutex
+	db   *sql.DB
+}
+
+func OpenSQLDB(driver, source string) (*DB, error) {
+	db, err := sql.Open(driver, source)
+	if err != nil {
+		return nil, err
+	}
+	return &DB{db: db}, nil
+}
+
+func (db *DB) Close() error {
+	db.lock.Lock()
+	defer db.lock.Unlock()
+	return db.Close()
+}
+
+func (db *DB) ListUsers() ([]User, error) {
+	db.lock.Lock()
+	defer db.lock.Unlock()
+
+	rows, err := db.db.Query("SELECT username, password FROM User")
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var users []User
+	for rows.Next() {
+		var user User
+		var password *string
+		if err := rows.Scan(&user.Username, &password); err != nil {
+			return nil, err
+		}
+		if password != nil {
+			user.Password = *password
+		}
+		users = append(users, user)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	return users, nil
+}
+
+func (db *DB) ListNetworks(username string) ([]Network, error) {
+	db.lock.Lock()
+	defer db.lock.Unlock()
+
+	rows, err := db.db.Query("SELECT id, addr, nick, username, realname FROM Network WHERE user = ?", username)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var networks []Network
+	for rows.Next() {
+		var net Network
+		var username, realname *string
+		if err := rows.Scan(&net.ID, &net.Addr, &net.Nick, &username, &realname); err != nil {
+			return nil, err
+		}
+		if username != nil {
+			net.Username = *username
+		}
+		if realname != nil {
+			net.Realname = *realname
+		}
+		networks = append(networks, net)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	return networks, nil
+}
+
+func (db *DB) ListChannels(networkID int64) ([]Channel, error) {
+	db.lock.Lock()
+	defer db.lock.Unlock()
+
+	rows, err := db.db.Query("SELECT id, name FROM Channel WHERE network = ?", networkID)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var channels []Channel
+	for rows.Next() {
+		var ch Channel
+		if err := rows.Scan(&ch.ID, &ch.Name); err != nil {
+			return nil, err
+		}
+		channels = append(channels, ch)
+	}
+	if err := rows.Err(); err != nil {
+		return nil, err
+	}
+
+	return channels, nil
+}
Index: /trunk/downstream.go
===================================================================
--- /trunk/downstream.go	(revision 76)
+++ /trunk/downstream.go	(revision 77)
@@ -59,5 +59,5 @@
 	username   string
 	realname   string
-	upstream   *Upstream
+	network    *network // can be nil
 }
 
@@ -101,5 +101,5 @@
 func (dc *downstreamConn) forEachUpstream(f func(*upstreamConn)) {
 	dc.user.forEachUpstream(func(uc *upstreamConn) {
-		if dc.upstream != nil && uc.upstream != dc.upstream {
+		if dc.network != nil && uc.network != dc.network {
 			return
 		}
@@ -302,7 +302,7 @@
 func (dc *downstreamConn) register() error {
 	username := strings.TrimPrefix(dc.username, "~")
-	var upstreamName string
+	var networkName string
 	if i := strings.LastIndexAny(username, "/@"); i >= 0 {
-		upstreamName = username[i+1:]
+		networkName = username[i+1:]
 	}
 	if i := strings.IndexAny(username, "/@"); i >= 0 {
@@ -321,12 +321,12 @@
 	}
 
-	if upstreamName != "" {
-		dc.upstream = dc.user.getUpstream(upstreamName)
-		if dc.upstream == nil {
-			dc.logger.Printf("failed registration: unknown upstream %q", upstreamName)
+	if networkName != "" {
+		dc.network = dc.user.getNetwork(networkName)
+		if dc.network == nil {
+			dc.logger.Printf("failed registration: unknown network %q", networkName)
 			dc.SendMessage(&irc.Message{
 				Prefix:  dc.srv.prefix(),
 				Command: irc.ERR_PASSWDMISMATCH,
-				Params:  []string{"*", fmt.Sprintf("Unknown upstream server %q", upstreamName)},
+				Params:  []string{"*", fmt.Sprintf("Unknown network %q", networkName)},
 			})
 			return nil
Index: /trunk/go.mod
===================================================================
--- /trunk/go.mod	(revision 76)
+++ /trunk/go.mod	(revision 77)
@@ -3,3 +3,6 @@
 go 1.13
 
-require gopkg.in/irc.v3 v3.1.0
+require (
+	github.com/mattn/go-sqlite3 v2.0.3+incompatible
+	gopkg.in/irc.v3 v3.1.0
+)
Index: /trunk/go.sum
===================================================================
--- /trunk/go.sum	(revision 76)
+++ /trunk/go.sum	(revision 77)
@@ -1,2 +1,5 @@
+github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
+github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
 gopkg.in/irc.v3 v3.1.0 h1:AeDaEhQ/78gHfpbj/3mSi8FfiNIsFiVrWEgLzOwHWnU=
 gopkg.in/irc.v3 v3.1.0/go.mod h1:qE0DWv0j8Z8wCbFhA9783JBO0bufi3rttcV1Sjin8io=
Index: /trunk/schema.sql
===================================================================
--- /trunk/schema.sql	(revision 77)
+++ /trunk/schema.sql	(revision 77)
@@ -0,0 +1,21 @@
+CREATE TABLE User (
+	username VARCHAR(255) PRIMARY KEY,
+	password VARCHAR(255)
+);
+
+CREATE TABLE Network (
+	id INTEGER PRIMARY KEY,
+	user VARCHAR(255) NOT NULL,
+	addr VARCHAR(255) NOT NULL,
+	nick VARCHAR(255) NOT NULL,
+	username VARCHAR(255),
+	realname VARCHAR(255),
+	FOREIGN KEY(user) REFERENCES User(username)
+);
+
+CREATE TABLE Channel (
+	id INTEGER PRIMARY KEY,
+	network INTEGER NOT NULL,
+	name VARCHAR(255) NOT NULL,
+	FOREIGN KEY(network) REFERENCES Network(id)
+);
Index: /trunk/server.go
===================================================================
--- /trunk/server.go	(revision 76)
+++ /trunk/server.go	(revision 77)
@@ -48,17 +48,63 @@
 }
 
+type network struct {
+	Network
+	user *user
+	conn *upstreamConn
+}
+
+func newNetwork(user *user, record *Network) *network {
+	return &network{
+		Network: *record,
+		user:    user,
+	}
+}
+
+func (net *network) run() {
+	var lastTry time.Time
+	for {
+		if dur := time.Now().Sub(lastTry); dur < retryConnectMinDelay {
+			delay := retryConnectMinDelay - dur
+			net.user.srv.Logger.Printf("waiting %v before trying to reconnect to %q", delay.Truncate(time.Second), net.Addr)
+			time.Sleep(delay)
+		}
+		lastTry = time.Now()
+
+		uc, err := connectToUpstream(net)
+		if err != nil {
+			net.user.srv.Logger.Printf("failed to connect to upstream server %q: %v", net.Addr, err)
+			continue
+		}
+
+		uc.register()
+
+		net.user.lock.Lock()
+		net.conn = uc
+		net.user.lock.Unlock()
+
+		if err := uc.readMessages(); err != nil {
+			uc.logger.Printf("failed to handle messages: %v", err)
+		}
+		uc.Close()
+
+		net.user.lock.Lock()
+		net.conn = nil
+		net.user.lock.Unlock()
+	}
+}
+
 type user struct {
-	username string
+	User
 	srv      *Server
 
 	lock            sync.Mutex
-	upstreamConns   []*upstreamConn
+	networks        []*network
 	downstreamConns []*downstreamConn
 }
 
-func newUser(srv *Server, username string) *user {
+func newUser(srv *Server, record *User) *user {
 	return &user{
-		username: username,
-		srv:      srv,
+		User: *record,
+		srv:  srv,
 	}
 }
@@ -66,6 +112,7 @@
 func (u *user) forEachUpstream(f func(uc *upstreamConn)) {
 	u.lock.Lock()
-	for _, uc := range u.upstreamConns {
-		if !uc.registered || uc.closed {
+	for _, network := range u.networks {
+		uc := network.conn
+		if uc == nil || !uc.registered || uc.closed {
 			continue
 		}
@@ -83,8 +130,8 @@
 }
 
-func (u *user) getUpstream(name string) *Upstream {
-	for i, upstream := range u.srv.Upstreams {
-		if upstream.Addr == name {
-			return &u.srv.Upstreams[i]
+func (u *user) getNetwork(name string) *network {
+	for _, network := range u.networks {
+		if network.Addr == name {
+			return network
 		}
 	}
@@ -92,10 +139,19 @@
 }
 
-type Upstream struct {
-	Addr     string
-	Nick     string
-	Username string
-	Realname string
-	Channels []string
+func (u *user) run() {
+	networks, err := u.srv.db.ListNetworks(u.Username)
+	if err != nil {
+		u.srv.Logger.Printf("failed to list networks for user %q: %v", u.Username, err)
+		return
+	}
+
+	u.lock.Lock()
+	for _, record := range networks {
+		network := newNetwork(u, &record)
+		u.networks = append(u.networks, network)
+
+		go network.run()
+	}
+	u.lock.Unlock()
 }
 
@@ -105,5 +161,6 @@
 	RingCap   int
 	Debug     bool
-	Upstreams []Upstream // TODO: per-user
+
+	db *DB
 
 	lock            sync.Mutex
@@ -112,9 +169,10 @@
 }
 
-func NewServer() *Server {
+func NewServer(db *DB) *Server {
 	return &Server{
 		Logger:  log.New(log.Writer(), "", log.LstdFlags),
 		RingCap: 4096,
 		users:   make(map[string]*user),
+		db:      db,
 	}
 }
@@ -124,53 +182,21 @@
 }
 
-func (s *Server) runUpstream(u *user, upstream *Upstream) {
-	var lastTry time.Time
-	for {
-		if dur := time.Now().Sub(lastTry); dur < retryConnectMinDelay {
-			delay := retryConnectMinDelay - dur
-			s.Logger.Printf("waiting %v before trying to reconnect to %q", delay.Truncate(time.Second), upstream.Addr)
-			time.Sleep(delay)
-		}
-		lastTry = time.Now()
-
-		uc, err := connectToUpstream(u, upstream)
-		if err != nil {
-			s.Logger.Printf("failed to connect to upstream server %q: %v", upstream.Addr, err)
-			continue
-		}
-
-		uc.register()
-
-		u.lock.Lock()
-		u.upstreamConns = append(u.upstreamConns, uc)
-		u.lock.Unlock()
-
-		if err := uc.readMessages(); err != nil {
-			uc.logger.Printf("failed to handle messages: %v", err)
-		}
-		uc.Close()
-
-		u.lock.Lock()
-		for i := range u.upstreamConns {
-			if u.upstreamConns[i] == uc {
-				u.upstreamConns = append(u.upstreamConns[:i], u.upstreamConns[i+1:]...)
-				break
-			}
-		}
-		u.lock.Unlock()
-	}
-}
-
-func (s *Server) Run() {
-	// TODO: multi-user
-	u := newUser(s, "jounce")
+func (s *Server) Run() error {
+	users, err := s.db.ListUsers()
+	if err != nil {
+		return err
+	}
 
 	s.lock.Lock()
-	s.users[u.username] = u
+	for _, record := range users {
+		s.Logger.Printf("starting bouncer for user %q", record.Username)
+		u := newUser(s, &record)
+		s.users[u.Username] = u
+
+		go u.run()
+	}
 	s.lock.Unlock()
 
-	for i := range s.Upstreams {
-		go s.runUpstream(u, &s.Upstreams[i])
-	}
+	select {}
 }
 
Index: /trunk/upstream.go
===================================================================
--- /trunk/upstream.go	(revision 76)
+++ /trunk/upstream.go	(revision 77)
@@ -26,5 +26,5 @@
 
 type upstreamConn struct {
-	upstream *Upstream
+	network  *network
 	logger   Logger
 	net      net.Conn
@@ -42,4 +42,6 @@
 	registered bool
 	nick       string
+	username   string
+	realname   string
 	closed     bool
 	modes      modeSet
@@ -48,11 +50,16 @@
 }
 
-func connectToUpstream(u *user, upstream *Upstream) (*upstreamConn, error) {
-	logger := &prefixLogger{u.srv.Logger, fmt.Sprintf("upstream %q: ", upstream.Addr)}
-	logger.Printf("connecting to server")
-
-	netConn, err := tls.Dial("tcp", upstream.Addr, nil)
+func connectToUpstream(network *network) (*upstreamConn, error) {
+	logger := &prefixLogger{network.user.srv.Logger, fmt.Sprintf("upstream %q: ", network.Addr)}
+
+	addr := network.Addr
+	if !strings.ContainsRune(addr, ':') {
+		addr = addr + ":6697"
+	}
+
+	logger.Printf("connecting to TLS server at address %q", addr)
+	netConn, err := tls.Dial("tcp", addr, nil)
 	if err != nil {
-		return nil, fmt.Errorf("failed to dial %q: %v", upstream.Addr, err)
+		return nil, fmt.Errorf("failed to dial %q: %v", addr, err)
 	}
 
@@ -61,12 +68,12 @@
 	msgs := make(chan *irc.Message, 64)
 	uc := &upstreamConn{
-		upstream: upstream,
+		network: network,
 		logger:   logger,
 		net:      netConn,
 		irc:      irc.NewConn(netConn),
-		srv:      u.srv,
-		user:     u,
+		srv:      network.user.srv,
+		user:     network.user,
 		messages: msgs,
-		ring:     NewRing(u.srv.RingCap),
+		ring:     NewRing(network.user.srv.RingCap),
 		channels: make(map[string]*upstreamChannel),
 		history:  make(map[string]uint64),
@@ -103,5 +110,5 @@
 func (uc *upstreamConn) forEachDownstream(f func(*downstreamConn)) {
 	uc.user.forEachDownstream(func(dc *downstreamConn) {
-		if dc.upstream != nil && dc.upstream != uc.upstream {
+		if dc.network != nil && dc.network != uc.network {
 			return
 		}
@@ -164,8 +171,14 @@
 		uc.logger.Printf("connection registered")
 
-		for _, ch := range uc.upstream.Channels {
+		channels, err := uc.srv.db.ListChannels(uc.network.ID)
+		if err != nil {
+			uc.logger.Printf("failed to list channels from database: %v", err)
+			break
+		}
+
+		for _, ch := range channels {
 			uc.SendMessage(&irc.Message{
 				Command: "JOIN",
-				Params:  []string{ch},
+				Params:  []string{ch.Name},
 			})
 		}
@@ -372,5 +385,14 @@
 
 func (uc *upstreamConn) register() {
-	uc.nick = uc.upstream.Nick
+	uc.nick = uc.network.Nick
+	uc.username = uc.network.Username
+	if uc.username == "" {
+		uc.username = uc.nick
+	}
+	uc.realname = uc.network.Realname
+	if uc.realname == "" {
+		uc.realname = uc.nick
+	}
+
 	uc.SendMessage(&irc.Message{
 		Command: "NICK",
@@ -379,5 +401,5 @@
 	uc.SendMessage(&irc.Message{
 		Command: "USER",
-		Params:  []string{uc.upstream.Username, "0", "*", uc.upstream.Realname},
+		Params:  []string{uc.username, "0", "*", uc.realname},
 	})
 }
