Clack comes with separate middleware modules for session management and for storing session information in a database. I discovered the storage middleware shortly after implementing my own version.

This post describes what I learnt about these two options. Though the code to implement this feature is minimal, it took me much too long to figure out the details necessary to do so.

Writing your own storage layer has the following advantages:

  • You can use storage not supported by cl-dbi, i.e. MySQL, PostgreSQL and SQLite.
  • You determine the table layout.

My suggestion is to use the included version. It will save you time. I only created my own version because I did not know Clack already included the necessary module.

Using Clack’s storage middleware

This information is correct at least up to commit c4f3e09 (2015-10-27)

The class name is <clack-session-store-dbi> located at v1-compat/src/contrib/session/store/.

The code below creates a Clack application which handles the storage of session information completely automatically.

(clack:clackup
 (builder
  (<clack-middleware-session>
   :store (make-instance 'clack.session.store.dbi:<clack-session-store-dbi>
                         :connect-args '(:postgresql
                                         :database-name "DBNAME"
                                         :username "ROLE"
                                         :password "PASSWORD")))
  #'app))

A hash table containing the session information is available inside request handlers. It is obtained with (getf env :clack.session).

The hash table content is serialized before being stored to disk so it can be used to store any arbitrary data.

Session tracking is done using cookies which contain only a session ID. All other session information are kept server side.

Writing your own persistence middleware

The code below implements middleware for a storage layer. The actual database access is done by the model package.

(in-package :cl-user)
(defpackage sltv.session.store
  (:use :cl
        :clack.session.store))
(in-package :sltv.session.store)

(cl-syntax:use-syntax :annot)

@export
(defclass <sltv-session-store> (<clack-session-store>)
  ())

@export
(defmethod fetch ((this <sltv-session-store>) sid)
  (let* ((session (model:fetch-session-as-hashtable sid)))
    (setf (gethash :id session) sid)
    session))

@export
(defmethod store-session ((this <sltv-session-store>) sid session)
  (model:store-session sid session))

@export
(defmethod remove-session ((this <sltv-session-store>) sid)
  (model:remove-session sid))

Notes:

  • sid is the session ID generated by Clack.
  • The session hash table must contain a key ID which is set to the sid not the database record’s ID field.
  • The session must be defined with (make-hash-table :test 'equal).
  • fetch obtains an existing session from the database or creates a new entry if it doesn’t exist.
  • store-session stores the session hash table and sid in the database.
  • remove-session deletes the database record.

When an HTTP request is received the session middleware fetches the session for the received sid. If the request did not include an sid (i.e. no cookie) or it included an expired sid, Clack generates a new one and fetches (which should create an new record) the session.

The request handler has three option settings with which it can control some of the session middleware’s actions.

:no-store
Do not store the session object. It is still created in the database and available in the request handler but the database entry is deleted before the response is returned.
:expire
Expire the session so that a new one is generated next time the user visits.
:change-id
Create a new session and populate it with the current data.

An option can be changed with the form

(setf (gethash :change-id (getf env :clack.session.options)) t)