Start a new project

View the code for this section.

As with any Biff project, we'll need to run the new project script. We'll add a tutorial argument so that your project will be based on the same version of Biff that was used for writing this tutorial. Important bits (mainly, stuff you should type) are highlighted in yellow:

$ clj -M -e '(load-string (slurp ""))' -M tutorial
Enter name for project directory: eelchat
Enter main namespace (e.g. com.example): com.eelchat

Your project is ready. Run the following commands to get started:

  cd eelchat
  git init
  git add .
  git commit -m "First commit"
  clj -M:dev dev

Run `clj -M:dev --help` for a list of available commands.

You'll need to install clj if you haven't already. After your project has been created, start up the app with clj -M:dev dev:

$ cd eelchat/

$ git init
Initialized empty Git repository in /home/jacob/dev/eelchat/.git/

$ git add .

$ git commit -m "First commit"
 [master (root-commit) 85f99db] First commit
 26 files changed, 1146 insertions(+)

$ clj -M:dev dev
New config generated and written to config.env.
[main] INFO hello.there - starting: com.biffweb$use_aero_config@1449ffba
[main] INFO hello.there - System started.
[main] INFO hello.there - Go to http://localhost:8080
[chime-1] INFO hello.there.worker - There are 0 users.
nREPL server started on port 7888 on host localhost - nrepl://localhost:7888

Once you see that "System started" message above, the application is running. Go to localhost:8080, then enter into the signin form. You'll see a link printed to the terminal:

[qtp1209549907-42] INFO com.biffweb.impl.middleware -  10ms 200 get  /
[qtp1209549907-41] INFO com.biffweb.impl.middleware -   1ms 200 get  /css/main.css?t=1707102698000
[qtp1209549907-44] INFO com.biffweb.impl.middleware -   0ms 200 get  /js/main.js?t=1707102552000
SUBJECT: Sign up for My Application

We received a request to sign up to My Application using this email address. Click this link to sign up:


This link will expire in one hour. If you did not request this link, you can ignore this email.

To send emails instead of printing them to the console, add your API keys for MailerSend and Recaptcha to config.edn.
[qtp1209549907-42] INFO com.biffweb.impl.middleware -  10ms 303 post /auth/send-link
[qtp1209549907-44] INFO com.biffweb.impl.middleware -   4ms 200 get  /link-sent?
[qtp1209549907-42] INFO com.biffweb.impl.middleware -   1ms 200 get  /css/main.css?t=1707102698000

Open that link, and you should be signed in!

A screenshot of the starter app after signing in

Delete some code

New projects come with a bunch of example code for you to inspect and tinker with. But since this is a tutorial, we'll delete most of it and start fresh. First, let's remove the com.eelchat.worker namespace. Remove it from com.eelchat first:

;; src/com/eelchat.clj
;; ...
             [com.eelchat.home :as home]
             [com.eelchat.middleware :as mid]
             [com.eelchat.ui :as ui]
-            [com.eelchat.worker :as worker]
             [com.eelchat.schema :as schema]
             [clojure.test :as test]
             [ :as log]
;; ...
    (biff/authentication-module {})
-   schema/module
-   worker/module])
+   schema/module])
 (def routes [["" {:middleware [mid/wrap-site-defaults]}
               (keep :routes modules)]

Then delete the src/com/eelchat/worker.clj file.

Next, we'll go to and delete, well, almost everything. This file is responsible for everything you see in the screenshot above. We'll replace it with a simple Nothing here yet message:

;; src/com/eelchat/app.clj
  (:require [com.biffweb :as biff :refer [q]]
            [com.eelchat.middleware :as mid]
            [com.eelchat.ui :as ui]
            [xtdb.api :as xt]))

(defn app [{:keys [session biff/db] :as ctx}]
  (let [{:user/keys [email]} (xt/entity db (:uid session))]
     [:div "Signed in as " email ". "
       {:action "/auth/signout"
        :class "inline"}
       [:button.text-blue-500.hover:text-blue-800 {:type "submit"}
        "Sign out"])
     [:div "Nothing here yet."])))

(def module
  {:routes ["/app" {:middleware [mid/wrap-signed-in]}
            ["" {:get app}]]})

Go back to localhost:8080, and it should look like this after you refresh the page:

A screenshot of the starter app after updating app.clj

Finally, let's update our schema (com.eelchat.schema). We'll remove the :msg document and the :user/foo and :user/bar attributes, all of which were used by the code in which we just deleted.

;; src/com/eelchat/schema.clj
;; ...
 (def schema
   {:user/id :uuid
    :user [:map {:closed true}
           [:xt/id                     :user/id]
           [:user/email                :string]
+          [:user/joined-at            inst?]]})
-          [:user/joined-at            inst?]
-          [:user/foo {:optional true} :string]
-          [:user/bar {:optional true} :string]]
-   :msg/id :uuid
-   :msg [:map {:closed true}
-         [:xt/id       :msg/id]
-         [:msg/user    :user/id]
-         [:msg/text    :string]
-         [:msg/sent-at inst?]]})

Removing the :msg schema will cause a test failure to show up in the console, so head over to com.eelchat-test and remove the two tests there:

;; test/com/eelchat_test.clj
;; ...
-(deftest send-message-test
-  (with-open [node (test-xtdb-node [])]
-    (let [message (mg/generate :string)
-          user    (mg/generate :user main/malli-opts)
-          ctx     (assoc (get-context node) :session {:uid (:xt/id user)})
-          _       (app/send-message ctx {:text (cheshire/generate-string {:text message})})
-          db      (xt/db node) ; get a fresh db value so it contains any transactions
-                               ; that send-message submitted.
-          doc     (biff/lookup db :msg/text message)]
-      (is (some? doc))
-      (is (= (:msg/user doc) (:xt/id user))))))
-(deftest chat-test
-  (let [n-messages (+ 3 (rand-int 10))
-        now        (java.util.Date.)
-        messages   (for [doc (mg/sample :msg (assoc main/malli-opts :size n-messages))]
-                     (assoc doc :msg/sent-at now))]
-    (with-open [node (test-xtdb-node messages)]
-      (let [response (app/chat {:biff/db (xt/db node)})
-            html     (rum/render-html response)]
-        (is (str/includes? html "Messages sent in the past 10 minutes:"))
-        (is (not (str/includes? html "No messages yet.")))
-        ;; If you add Jsoup to your dependencies, you can use DOM selectors instead of just regexes:
-        ;(is (= n-messages (count (.select (Jsoup/parse html) "#messages > *"))))
-        (is (= n-messages (count (re-seq #"init send newMessage to #message-header" html))))
-        (is (every? #(str/includes? html (:msg/text %)) messages))))))

If you played around with the starter app at all, you might already have some :msg documents or :user/foo/:user/bar attributes in your database. Let's clear out the database to ensure we don't have any non-schema-conforming documents. Hit Ctrl-C in your terminal to stop the app, then run rm -r storage/xtdb/. Afterward, start the app up again with clj -M:dev dev.

Have a question? Join the #biff channel on Clojurians Slack, or ask on GitHub.

Sign up for Biff: The Newsletter
Announcements, blog posts, et cetera et cetera.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.