In this post we will explore how to connect RethinkDBs changefeeds to Phoenix’s channels. We are going to build a chat app that stores messages in a RethinkDB database. When a user connects, we listen to a changefeed on the database. As soon as a new message row is inserted into the database, we push the JSON we receive from the changefeed directly to the user.

For this, we will use the very young exrethinkdb library. I’m assuming Elixir and Phoenix are already set up.

Then code of the finished example app can be found at https://github.com/manukall/phoenix_rethinkdb_chat.

First we create a new phoenix application:

mix phoenix.new phoenix_exrethinkdb_chat --no-ecto

Then we need to add exrethinkdb as a dependency. As it is not published on hex yet, we fetch it from Github. In phoenix_exrethinkdb_chat/mix.exs change the deps method to:

1 2 3 4 5 6 defp deps do [{ :phoenix , "~> 0.11" }, { :phoenix_live_reload , "~> 0.3" }, { :exrethinkdb , github : "hamiltop/exrethinkdb" , ref : "55fb5b5ed892f28b7ae8ee1b2f8e54fb651bd611" }, { :cowboy , "~> 1.0" }] end

Next we let mix install our new dependency. To do this run the following command (inside our new project’s directory):

mix deps.get

Because we do not want to reconnect to RethinkDB every time a new request happens, we will connect to it once when our application starts and keep a reference to the connection. We will use an Agent for this. Create the file lib/phoenix_exrethinkdb_chat/repo.ex with the following content:

1 2 3 4 5 6 7 8 9 10 11 12 13 defmodule PhoenixExrethinkdbChat.Repo do def start_link do conn = Exrethinkdb . local_connection Agent . start_link ( fn -> conn end , name : __MODULE__ ) { :ok , self } end def run ( query ) do Agent . get ( __MODULE__ , fn conn -> Exrethinkdb . run conn , query end ) end end

To actually connect to the database when our application starts, we need to add our repo to the supervisor tree. In lib/phoenix_exrethinkdb_chat.ex add the following to the children array:

worker(PhoenixExrethinkdbChat.Repo, []),

Next we are going to add our HTML. We just want the page to display a list of messages and one input each for the user’s name and the message. Replace web/templates/page/index.html.eex with the following code, which is pretty much stolen from chrismccord’s phoenix chat app:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <div id= "messages" class= "container" > </div> <div id= "footer" > <div class= "container" > <div class= "row" > <div class= "col-sm-2" > <div class= "input-group" > <span class= "input-group-addon" > @ </span> <input id= "username" type= "text" class= "form-control" placeholder= "username" > </div> <!-- /input-group --> </div> <!-- /.col-lg-6 --> <div class= "col-sm-10" > <input id= "message-input" class= "form-control" /> </div> <!-- /.col-lg-6 --> </div> <!-- /.row --> </div> </div>

We’re also going to shamelessly steal the javascript from the example chat app. Put the following into web/static/js/app.js :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import { Socket , LongPoller } from "phoenix" class App { static init (){ var socket = new Socket ( "ws://" + location . host + "/ws" ) socket . connect () var $status = $ ( "#status" ) var $messages = $ ( "#messages" ) var $input = $ ( "#message-input" ) var $username = $ ( "#username" ) socket . onClose ( e => console . log ( "CLOSE" , e )) socket . join ( "rooms:lobby" , {}) . receive ( "ignore" , () => console . log ( "auth error" ) ) . receive ( "ok" , chan => { chan . onError ( e => console . log ( "something went wrong" , e ) ) chan . onClose ( e => console . log ( "channel closed" , e ) ) $input . off ( "keypress" ). on ( "keypress" , e => { if ( e . keyCode == 13 ) { chan . push ( "new:msg" , { user : $username . val (), body : $input . val ()}) $input . val ( "" ) } }) chan . on ( "new:msg" , msg => { $messages . append ( this . messageTemplate ( msg )) scrollTo ( 0 , document . body . scrollHeight ) }) chan . on ( "user:entered" , msg => { var username = this . sanitize ( msg . user || "anonymous" ) $messages . append ( ` < br />< i > [ $ { username } entered ] < /i>`) }) }) . after ( 10000 , () => console . log ( "Connection interruption" ) ) } static sanitize ( html ){ return $ ( "<div/>" ). text ( html ). html () } static messageTemplate ( msg ){ let username = this . sanitize ( msg . user || "anonymous" ) let body = this . sanitize ( msg . body ) return ( ` < p >< a href = '#' > [ $ { username }] < /a> ${body}</p>`) } } $ ( () => App . init () ) export default App

This needs jQuery. Download this version directly from chrismccord’s repo to web/static/vendor/jquery.min.js .

You can start the server by running mix phoenix.server now and open http://localhost:4000. RethinkDB needs to be started for this. For toy projects like this, I prefer to start a local instance by simply running rethinkdb inside the projects folder.

If you check your browser console, you will see an error telling you that the app can’t connect to your server via websocket. Let’s fix that by adding a channel route.

Open web/router.ex and add the following:

1 2 3 socket "/ws" , PhoenixExrethinkdbChat do channel "rooms:*" , RoomChannel end

This tells the router to listen for websocket connections at /ws and use the PhoenixExrethinkdbChat.RoomChannel module for all requests to channels matching rooms:* .

Next we need to create this module. Create the file web/channels/room_channel.ex with the following content:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 defmodule PhoenixExrethinkdbChat.RoomChannel do use Phoenix.Channel require Logger alias Exrethinkdb.Query alias PhoenixExrethinkdbChat.Repo def join ( "rooms:lobby" , message , socket ) do send ( self , :after_join ) { :ok , socket } end def handle_info ( :after_join , socket ) do q = Query . table ( "messages" ) result = Repo . run ( q ) Enum . each ( result . data , fn message -> push socket , "new:msg" , message end ) changes = Query . changes ( q ) |> Repo . run Task . async fn -> Enum . each ( changes , fn change -> push socket , "new:msg" , change [ "new_val" ] end ) end { :noreply , socket } end def handle_in ( "new:msg" , msg , socket ) do q = Query . table ( "messages" ) |> Query . insert (%{ user : msg [ "user" ], body : msg [ "body" ]}) Repo . run ( q ) { :reply , { :ok , msg [ "body" ]}, assign ( socket , :user , msg [ "user" ])} end end

The join method get’s called when users open the website. It allows everyone to join the channel “rooms:lobby” and sends a after_join message. This is an Elixir/Erlang message (see http://elixir-lang.org/getting-started/processes.html#send-and-receive) - not a chat message. This message is handled at line 12. We first send all existing chat-messages back to the browser (line 15). Then we subscribe to the messages-table’s changefeed and send each new message as it comes (lines 17-23).

The last step before we can try our application is setting up the database. For now, it is easiest to just create the table manually. Run iex -S mix in the console to start IEx. In the IEx shell execute Exrethinkdb.Query.table_create("messages") |> PhoenixExrethinkdbChat.Repo.run .

This is all. Restart your server and reload the browser page. In IEx run the following and watch the new message appear in your browser.

Exrethinkdb.Query.table("messages") |> Exrethinkdb.Query.insert(%{user: "kai", body: "hi"}) |> PhoenixExrethinkdbChat.Repo.run

Changelog