Building a Chat Server with Go: Make a Chat App

7 min read
Ayooluwa I.
Ayooluwa I.
Published April 20, 2020 Updated August 31, 2020

Are you thinking of building a chat application in Go? You’ve come to the right place! This post will walk you through everything you need to know to make a chat app with the Stream Chat API and will show a working example server that ties all the concepts discussed in this tutorial together.

The source code used for this tutorial can be found on GitHub if you'd like to take a peek at the finished product.

What Does a Chat Server Do?

For a chat server to be useful, it must be capable of performing at least the following tasks:

  • Receiving messages from client applications and distributing them to other clients
  • Broadcasting general notifications to all clients, such as those for when a user joins or leaves a channel
  • Raising events so that a client can make the appropriate response to an event
  • Managing the process of moderating users of the application

Stream Chat can do all these (and much more!), but we’ll only consider a subset of the features available in this article.

Signing Up for Stream

Before we continue, make sure to sign up for a free Stream account here and create a new application once you are signed in. You will find your application keys at the bottom of your Stream Dashboard:

A Stream key is the first step in setting up your chat server

Set these keys aside (and keep them safe!); we'll be using them later to authenticate our app.

Setting Up the SDK with Golang

Install the official Golang API client for Stream with the following command:

sh
1
$ go get github.com/GetStream/stream-chat-go/v2

You can import it into your project as follows:

package main

import (
   stream "github.com/GetStream/stream-chat-go/v2"
)

To instantiate the client object, you need to pass in the application key and secret that you retrieved from your dashboard. Ideally, you’d have them stored as environmental variables in a .env file at the root of your project, so that they are not publicly viewable:

PORT=4000
STREAM_API_KEY=<YOUR_STREAM_API_KEY>
STREAM_API_SECRET=<YOUR_STREAM_API_SECRET>

You can use godotenv to load the configuration variables from your .env file, and create a new Stream client with the code below:

package main

import (
    "log"
    "os"
    
    stream "github.com/GetStream/stream-chat-go/v2"
    "github.com/joho/godotenv"
)

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Error loading .env file")
    }

    APIKey := os.Getenv("STREAM_API_KEY")
    APISecret := os.Getenv("STREAM_API_SECRET")
  
    client, err := stream.NewClient(APIKey, []byte(APISecret))
    // handle error and do something with client 
}

Creating and Updating Users

To create or update users, Stream exposes the client.UpdateUser method. Here’s how you can use it to create a new user in your app:

client.UpdateUser(&stream.User{
    ID: "user_id",
})

By default, users are assigned the user role, but you can change it to any of the available channel roles including admin, channel_member, moderator, guest, or anonymous. You can also configure other properties such as name, image (avatar), and a map of user-related properties:

client.UpdateUser(&stream.User{
        ID:        "user_id",
        Name:      "User",
        Role:      "admin",
        Image:     "https://example.com/avatar.png",
        ExtraData: map[string]interface{}{"favourite_city": "Berlin"},
})

If you want to create or update multiple users at once, you can use the client.UpdateUsers method, which expects any number of Users. The updated info will be returned as a map of Users.

client.UpdateUsers(
    &stream.User{ID: "tomjagger", Role: "guest"},
    &stream.User{ID: "sarahyou", Role: "admin"},
)

Generating Authentication Tokens

Before a user is allowed to interact with your application, you need to generate a token that you’ll send back to the client-side to prove that the user was successfully authenticated.

To create a user token, you need to pass in the ID of the user, and the expiration time of the token:

client.CreateToken("danpetrescu", time.Now().Add(time.Minute*time.Duration(60)))

The token that is returned will be a byte slice, which you can convert to a string using string(token).

Querying Users

Stream allows you to fetch a user, or group of users, based on some query parameters. The example below shows how you can retrieve the details for three users in one API call:

client.QueryUsers(&stream.QueryOption{
    Filter: map[string]interface{}{
        "id": map\[string\][]string{
            "$in": {"thompson", "john", "trump"},
        },
    },
})

The QueryUsers method expects a QueryOption argument, in which you can specify filters, which will determine what set of users will be returned. Here’s another example showing how to retrieve banned users:

client.QueryUsers(&stream.QueryOption{
    Filter: map[string]interface{}{
        "banned": true,
    },
})

You can check out this link for other query filters available to you. The QueryOption argument also supports a Limit property, which you can use to limit the number of results, and an Offset property, which you can use for pagination.

Channels

Channels are an essential aspect of Stream’s Chat API. A "channel" is a single place for users to share messages, reactions, and files, and there are five types of channels available, by default, which are customized for different use cases.

The five default types of channels are:

  • Livestream: Chat for services, such as Twitch
  • Messaging: Configured for apps, such as WhatsApp or Facebook Messenger
  • Team: Good defaults for group messaging apps, such as Slack
  • Gaming: Configured for in-game chat
  • Commerce: Good defaults for building a live chat service, such as Intercom

Let’s assume you are building some sort of Slack clone. You might want to create a General channel where new users are added to by default. Here’s how you can do so with Stream:

client.CreateChannel("team", "general", "admin", map[string]interface{}{
    "name":  "General",
    "image": "http://example.com/general.png",
    "members": []string{"tom", "jerry"},
})

The first argument to CreateChannel is the channel type discussed above, followed by the ID of the channel, and the ID of the user creating this channel. The final argument is the channel data, which can contain anything you want, except for the following reserved fields: name, image, and members. One thing to note here is that the extra data for the channel must not exceed 5KB in size.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Next, let’s look at a few things you can do with a channel once it has been initialized.

Adding Members to a Channel

To add users as members of a channel, use the AddMembers method on the channel, as shown below:

channel.AddMembers([]string{"harrypotter"}, &stream.Message{
    User: &stream.User{
        ID: "harrypotter",
    },
    Text: "harrypotter Joined the General channel",
})

The Message object allows you to show a system message on the channel to indicate that something has changed. The above code will have the following effect in the application UI if you use Stream’s React components for your frontend.

A status message is displayed in the application UI

Remove Members from a Channel

Removing users from a channel is as easy calling the RemoveMembers method. It works the same way as AddMembers:

channel.RemoveMembers([]string{"harrypotter"}, &stream.Message{
    User: &stream.User{
        ID: "harrypotter",
    },
    Text: "harrypotter left the General channel",
})
A status message from the server is displayed in the application UI

Updating a Channel

You can change a channel’s properties by calling the Update method. You'll need to pass a map that contains the new channel information, as well as a message object that indicates what changed or nil if you don’t want to show a message.

Here’s how that works:

data := map[string]interface{}{
    "name": "Remote work",
}

channel.Update(data, &stream.Message{
    User: &stream.User{
        ID: "admin",
    },
    Text: "Admin changed the channel name to Remote work",
})

You can also change things like the channel image and roles for its members. If you mark a channel as frozen, new messages sent to the channel will be blocked.

Moderating a Channel

You can set users as moderators for a channel. This permits them to ban any user who does not adhere to the set standards for the channel or application. To give moderation capabilities to any user, use the AddModerators method:

channel.AddModerators("tomjagger")

If you want to produce a system message after making a user a moderator, use the AddModeratorsWithMessage method:

channel.AddModeratorsWithMessage([]string{"tomjagger"}, &stream.Message{
    User: &stream.User{
        ID: "tomjagger",
    },
    Text: "tomjagger was made a moderator",
})

A moderator can ban a user using the BanUser method:

channel.BanUser("paris", "helenoftroy", map[string]interface{}{
    "timeout": 3600,
    "reason":  "Offensive language is not allowed here",
})

The first argument is the userID to be banned, while the second is the userID of the admin or moderator who is effecting the ban. You can set the reason for the ban, and a duration for how long the ban should last in the final argument to BanUser. By default, a ban is indefinite.

Once a user is banned, they are no longer able to send messages to the channel

To restore channel access to a banned user, use the UnBanUser method:

channel.UnBanUser("paris", nil)

If you want to ban or unban a user from the chat app entirely, and not just a specific channel, use the BanUser and UnBanUser methods on the client instead.

client.BanUser("paris", "helenoftroy", map[string]interface{}{
    "timeout": 3600,
    "reason":  "Offensive language is not allowed here",
})

client.UnBanUser("paris", nil)

You can read more about the moderation tools available to you here.

Sending Messages

To send messages to a channel, you can use the SendMessage method. It accepts a Message object and the userID of the sender. Here’s the simplest example of how to send a text message with Stream chat:

channel.SendMessage(&stream.Message{
    Text: "Hello",
}, "helenoftroy")

And here’s a more complicated example showing how you can attach an image, and mention a user in a message:

message := &stream.Message{
    Text: "@paris Everything was your fault",
    Attachments: []*stream.Attachment{
        &stream.Attachment{
            Type:     "image",
            ThumbURL: "https://i.imgur.com/1UtEbH7.jpg",
            AssetURL: "https://i.imgur.com/1UtEbH7.jpg",
        },
    },

    MentionedUsers: []*stream.User{
        &stream.User{ID: "paris"},
    },
}

channel.SendMessage(message, "helenoftroy")
Example message would be displayed from the server

Working Example

Here’s a working example that creates a new user, generates an authentication token for the user, and automatically adds them to a default "General" channel:

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	stream "github.com/GetStream/stream-chat-go/v2"
	"github.com/joho/godotenv"
)

var APIKey string
var APISecret string
var ServerSideClient stream.Client

func init() {
	err := godotenv.Load()
	if err != nil {
		log.Fatal("Error loading .env file")
	}

	APIKey = os.Getenv("STREAM_API_KEY")
	APISecret = os.Getenv("STREAM_API_SECRET")
}

func join(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case "POST":
		newUser := &stream.User{}

		err := json.NewDecoder(r.Body).Decode(newUser)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Create the user or update if user already exists
		user, err := ServerSideClient.UpdateUser(newUser)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		token, err := ServerSideClient.CreateToken(user.ID, time.Now().Add(time.Minute*time.Duration(60)))
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Return the already created General channel
		channel, err := ServerSideClient.CreateChannel("team", "general", "admin", nil)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// Add the user to the General channel
		err = channel.AddMembers([]string{user.ID}, &stream.Message{
			User: &stream.User{
				ID: user.ID,
			},
			Text: user.ID + " Joined the General channel",
		})
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(struct {
			User   stream.User `json:"user"`
			Token  string      `json:"token"`
			APIKey string      `json:"api_key"`
		}{
			User:   *user,
			Token:  string(token),
			APIKey: APIKey,
		})

	default:
		fmt.Fprintf(w, "Wrong Method.")
	}
}

func main() {
	port := os.Getenv("PORT")

	client, err := stream.NewClient(APIKey, []byte(APISecret))
	if err != nil {
		log.Fatal(err)
	}

	ServerSideClient = *client

	// Create admin user or update if admin already exists
	_, err = client.UpdateUser(&stream.User{
		ID:   "admin",
		Role: "admin",
	})
	if err != nil {
		log.Fatal(err)
	}

	// Create the General channel
	_, err = client.CreateChannel("team", "general", "admin", map[string]interface{}{
		"name": "General",
	})
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/join", join)

	if err := http.ListenAndServe(":"+port, mux); err != nil {
		log.Fatal(err)
	}
}

To test it out, paste this code into your main.go file, and start the server using go run main.go.

Note: Make sure that your .env file is configured correctly before starting the server.

Send a POST request to the /join endpoint using Postman, as shown below. You will receive a response with the user details, authentication token for the user, and API key if all goes well:

Screenshot of server response in Postman

Wrapping Up

Building a chat server is a complex undertaking, but with Stream’s Chat service and Go, it’s much easier to get one up and running quickly. We’ve only scratched the surface of all the features available to you in this post, so be sure to check the chat docs, as well as the Go SDK docs if you want to furnish your server with additional functionality.

Thanks for reading and happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->