ShipClojure: The Clojure Boilerplate to ship startups FAST - complete stack presentation

Table of Contents

  1. The complete stack
    1. Backend
      1. Underlying server - Jetty 12
      2. Routing provider - Reitit
      3. Database - Postgres
      4. Dependency & Lifecycle management - Integrant
      5. Environment & Secret management - aero
      6. Transactional emails - Resend
      7. Authentication - Ring cookie sessions
      8. Deployment - fly.io
    2. Frontend
      1. UI Rendering Framework - UIx
      2. State management - re-frame
      3. Styling - tailwind-css
      4. UI Components - daisyUI
      5. SEO & Blog Generator - borkdude's quickblog
      6. UI component documentation - Portfolio
      7. ClojureScript build tool
    3. Misc
      1. Transport
      2. Email components
      3. Dashboard & User Management
  2. Conclusion

ShipClojure is the Clojure boilerplate I've been working on for almost a year. It helps clojure developers ship SaaS products fast.

It allows you to start building your products core business logic from the first day without focusing on adjacent boilerplate code like authentication, database setup, deployment & UI base components.

The complete stack

In this post I will describe the entire list of tech choices I made for ShipClojure so you can get an overview of the project but also to show modern options for people trying to build a full SaaS product and don't know what to use.

Disclaimer: You may not agree with some of the choices I made or you may have different preferences. That is fine! You can change things if you need. I built the boilerplate to be modular to a certain extent - You're gonna have a hard time replacing tailwind ;)

Backend

1. Underlying server - Jetty 12

I chose Jetty 12 and specifically ring-jetty9-adapter (naming is confusing but it uses Jetty 12) because it uses the latest jetty version and it supports websocket connections from the outside as opposed to Jetty 9. I might have to fact check this but I couldn't do it with the default ring.adapter.jetty.

2. Routing provider - Reitit

Reitit has become the most popular router and based on the benchmarks, it's the fastest. I chose it because:

  • people are familiar with it
  • it is data oriented
  • it is the fastest
  • it supports frontend routing which means you can express your application routes in a .cljc file.

3. Database - Postgres

PostgreSQL is a battle-tested open source DB. I chose it because I used it a lot in the past, it never failed me and the tools to interact with it are robust:

  • DB interaction: next.jdbc
  • Query creation: honeysql - write clojure datastructures that transform to SQL

These two choices make it very easy to swap out postgres for any other SQL flavor database you want, which is awesome!

In full honesty, I am looking at bitemporal DBs because I see a raising interest in them. I am considering providing plugins or guides to change the database to datomic or xtdb.

This is not my main focus at the moment but it is in the backlog.

4. Dependency & Lifecycle management - Integrant

I love Integrant. It is seamless to describe your entire system and the dependencies between components. It provides a way to orchestrate system startup and shut down.

I designed ShipClojure to be a top down tree of dependencies. You create all the dependencies at the beginning (db connection, server instance etc.) and all of the functionality receives the specific component from the system it needs as a function call parameter.

Here's an example for a database query:

    (defn get-user-password
      "Get a user's password from the database.

      `db` - The database connection.
      `user-id` - The user's ID.

      Returns a map containing the user's password hash and updated_at:
      {`:hash` string
       `:updated_at` timestamp"
      [db user-id]
      (execute-one! db (-> (select :hash :updated_at)
                           (from :password)
       (where [:= :user_id user-id]))))

We pass the database connection (db) to the query function so this can be easily tested with a different system configuration specific for testing.

5. Environment & Secret management - aero

Aero is a library for intentful configuration with works really well with Integrant.

All of the secrets are in a file called .saas-secrets.edn (you can change the name), aero reads them into the system and then you can inject them into the integrant components so they access only relevant secrets at runtime:

{:saas/secrets #include ".saas-secrets.edn" ;; reading secrets

 :db.sql/connection #ref [:saas/secrets :db] ;; only accesses the db secrets
}

Here's a secret example

{:db {:dbtype "postgresql"
      :port 5432
      :host "localhost"
      :user "postgres"
      :password "secretpassword"
      :dbname "shipclojure"}}

And when we instantiate the DB through integrant, it looks like this:

(defmethod ig/init-key :db.sql/connection
  [_ secrets] ;; secrets read by aero
  (log/info "Configuring db")
  (-> (config->jdbc-url secrets)
      (datasource)
      (jdbc/with-options jdbc/snake-kebab-opts)))

You can configure this setup to use env variables for DB like this:

:saas/db {:host #or [#env DB_HOST #ref [:saas/secrets :db :host]]
          :dbtype #or [#ref [:saas/secrets :db :dbtype] "postgresql"]
          :port #or [#env DB_PORT #ref [:saas/secrets :db :port]]
          :user #or [#env DB_USER #ref [:saas/secrets :db :user]]
          :password #or [#env DB_PASSWORD #ref [:saas/secrets :db :password]]
          :dbname #or [#env DB_NAME #ref [:saas/secrets :db :dbname]]}

Shipclojure has support for different lifecycle dependencies based on the environment.

A good example is building clojurescript. In development mode, we spawn a shadow-cljs process to watch the clojurescript source. You don't need this process in production, so it doesn't start when the system starts.

I took the environment system from Kit, where you have different directories for each environment, each containing different versions of code.

6. Transactional emails - Resend

I chose Resend because it is a reliable service and it has a generous free offer of 3000 emails / month which should be enough to get you started. Another provider that you can look at if you send a massive number of emails is Amazon SES. This is the most cost effective provider at scale.

If you wonder why even go with a email provider at all and why not just use our own SMTP hosted service? Because deliverability will suffer if you don't use a service. You need a guarantee that the emails will arrive to the end user.

7. Authentication - Ring cookie sessions

Initially I implemented JWT Tokens with Refresh Token Rotation so the access tokens are only stored in memory. I hit a blocker with this strategy because most clojure libraries we use like ring-oauth2 rely on cookie sessions by default.

I'm not saying that this is a complete blocker, it just means going the JWT Token route requires more work, so I ended up going with http-only cookie sessions. This is a good thing too because clojurians seem to prefer cookie sessions.

To make this secure, I added CSRF & XSS protection so attackers cannot abuse the system.

Ship clojure also supports sign in with Google for passwordless sign-in.

8. Deployment - fly.io

I like Fly because it is easy to configure, without a bunch of magic underneath. You wrap your app inside a docker and just ship it. Fly comes with easy configurations for:

  • auto scaling machines
  • healthchecks
  • logs + sentry integration

To make the docker image, we bundle shipclojure in a jar which runs inside the docker. This handles the frontend & the backend.

This setup has a limitation that you don't have access to a production REPL which clojurians prefer. Biff already has a a guide on deploying clojure projects to a server and most of this guide applies to shipclojure.

Frontend

Let's discuss the stack used for ShipClojure's frontend. ShipClojure has a Single Page Application that is responsible for the dynamic part. It server renders static pages like the landing page, blog etc.

1. UI Rendering Framework - UIx

UIx is an idiomatic Clojure(script) interface to modern React. I explained here why I chose UIx over Reagent for ShipClojure. Here are the greatest features of UIx:

  • It's macro-based rendering, so no runtime performance cost, as opposed to Reagent
  • It builds on top of modern react so you get access to the entire ecosystem seamlessly
  • It has server-side rendering capabilities which I use for rendering performant, interactive landing pages inside ShipClojure
  • It has an in-built linter, so it is beginner friendly and fast to pick up.
  • It interops with Reagent and more importantly re-frame to build enjoyable state management, application flows that are easy to test.

2. State management - re-frame

re-frame is a framework for building scalable Single-Page applications. It's main focus is on high programmer productivity and seamless scaling.

I admit it wasn't my first choice because it is hard to pick up. It is a joy once you get it but it takes some time to reach this point. I tried to implement ShipClojure's own state management, and the more I worked on it, the more it felt like I was re-implementing re-frame so I accepted re-frame as the state management choice for the boilerplate.

State management is hard and this is why frameworks in javascript land move to server-based applications that don't keep frontend state. Because of this, I chose re-frame, so when it gets hard, you have a guiding hand, rich documentation and a rich community to help you in the journey.

3. Styling - tailwind-css

Tailwind is an utility-first CSS framework packed with classes like flex, pt-4 that you can compose directly in your markup.

I love tailwind because I can look at a component and understand fully what it does and it does away with the mental overhead of having to constantly name CSS classes. If you are not familiar with it, it is fast to pick up and you'll find a lot of documentation on it.

Why I chose tailwind:

  • Pure CSS so I can use all of the styling with server-side rendering
  • It is one of the most popular styling frameworks across languages, so you get infinite examples for pages
  • You reuse CSS classes so you serve less CSS over the wire

4. UI Components - daisyUI

Tailwind is great but it lacks a component system to move fast. DaisyUI is a comprehensive CSS component library built on top of tailwind.

Why I chose daisyUI:

  • Access to 35+ pre-made themes instantly
  • Pure CSS so we can reuse it for server-side rendering
  • It's just CSS, so you can change the behaviour easily
  • (Again) It's just CSS, so you don't have to do too much interop with javascript land
  • Integrates with the Tailwind JIT Compiler so you generate CSS only for the components you use.

ShipClojure takes all of the CSS components and comes with a in-house component library that is easy to use for clojure developers. All of the UI components are stateless, documented and full stack (you can use them for static pages too).

5. SEO & Blog Generator - borkdude's quickblog

Quickblog is a lightweight static blog engine that supports blogs written in markdown. It generates static blog pages for blogs, tags & authors.

For basic blogs it works well. It might require some changes for more advanced use-cases like:

  • popups for newsletters
  • analytics
  • next article recommandation

For now it suffices and it was easy to setup and style. I'm thinking to use react server-side rendering in order to create the templates that quickblog uses so I can add interactivity for the blog. This is not big in the list of priorities.

6. UI component documentation - Portfolio

I chose Portfolio because it is the Clojure version of storybook and it is easier to setup up for the average clojurian. Portfolio helps with documenting all of the UI components and all of thir possible states.

7. ClojureScript build tool

ShipClojure uses shadow-cljs as a build tool to enable users to tap into the NPM ecosystem. I reduced the bundle(s) shipped to the end user as much as possible using code splitting so we ship only the required code. shadow-cljs is also responsible to compile the code to hydrate server-side rendered static pages that have interactivity built-in.

Misc

1. Transport

ShipClojure uses the transit data format for over-the-wire communication between client and server to reduce payload size to the minimum required.

In practice, you, as a user, rarely need to interact with transport as it gets converted to and from edn from the backend to the frontend.

2. Email components

ShipClojure comes with most of the components from react-email ported to UIX to create beautiful transactional emails in the same way you would create UI pages. I wrote the email component library in cljc so you can use these components full stack: You can preview them on the UI but send the emails from the backend server.

3. Dashboard & User Management

ShipClojure gives a dashboard for purchase & user management through metabase as it is easy to setup, popular and best of all, created in clojure!

Conclusion

This post is a full presentation of all the tech choices for ShipClojure. I hope you learned something from reading it that you can apply to your own projects. If you have any questions about the tech choices or about the boilerplate, please DM me or write me an email and we will talk more

If you are interested in ShipClojure, visit shipclojure.com for more details.