README.org

June 30, 2026 · View on GitHub

#+html:

  • Triangulum

A Clojure library that provides:

  • A web framework based around Jetty and Ring
  • Utility scripts for managing databases, email, logging, SSL certs, systemd services, and more
  • Utility namespaces with functions for common CLI and web server programming tasks

** Adding Triangulum to your Project

  1. Add the library's coordinates to the :deps map in your project's deps.edn file:

    #+begin_src clojure sig-gis/triangulum {:git/url "https://github.com/sig-gis/triangulum" :git/sha "REPLACE-WITH-CURRENT-SHA"} #+end_src

    If you want to pull Triangulum into a [[https://babashka.org][Babashka]] script, you can do so with babashka.deps as follows:

    #+begin_src clojure (require '[babashka.deps :refer [add-deps]])

    (add-deps '{:deps {sig-gis/triangulum {:git/url "https://github.com/sig-gis/triangulum" :git/sha "REPLACE-WITH-CURRENT-SHA"}}}) #+end_src

  2. Define one or more aliases for Triangulum's utility scripts in the :aliases map in your project's deps.edn file.

    NOTE: You can find examples of these aliases in the deps.example.edn file in the root directory of this repository.

  3. Configure the Triangulum features you want to use in a config.edn file in the root directory of your repository.

    Two syntaxes are provided for this file, one with nested keywords and one with namespaced keywords. You can see examples of the available features in config.nested-example.edn and config.namespaced-example.edn within this repository.

** Running Tests

To launch the test suite, run:

#+begin_src sh clojure -M:test #+end_src

** Web Framework *** triangulum.server

This namespace provides a batteries-included Ring Jetty web server, an nrepl server for connecting to the live web application, a timestamped file logging system, and worker functions for non-HTTP-related tasks.

  1. In config.edn:

    #+begin_src clojure :server {:http-port 8080 :https-port 8443 ; Only if you also include the keystore fields below :nrepl-bind 127.0.0.1 ; Must be paired with either :nrepl or :cider-repl below :nrepl-port 5555 ; Must be paired with either :nrepl or :cider-repl below :nrepl false ; For a vanilla nrepl :cider-nrepl true ; If your editor supports CIDER middleware :mode "dev" ; or prod :log-dir "logs" ; or "" for stdout :truncate-request true ; false by default :handler product-ns.routing/handler :workers {:scheduler {:start product-ns.jobs/start-scheduled-jobs! :stop product-ns.jobs/stop-scheduled-jobs!}} :response-type :json ; #{:json :edn :transit} :bad-tokens #{".php"} ; Reject URLs containing these strings :keystore-file "keystore.pkcs12" ; Contains your SSL certificate(s) :keystore-type "pkcs12" :keystore-password "foobar"} #+end_src

  2. At the command line:

    Start the server:

    #+begin_src sh clojure -M:server start #+end_src

    Stop the server (requires :nrepl or :cider-nrepl):

    #+begin_src sh clojure -M:server stop #+end_src

    Reload your namespaces into the running JVM (requires :nrepl or :cider-nrepl):

    #+begin_src sh clojure -M:server reload #+end_src

    NOTES:

    • To use :https-port, you must also specify :keystore-file, :keystore-type, and :keystore-password.
    • You may not use both :nrepl and :cider-nrepl at the same time.
    • The :start functions for all :workers will be run before the web server is started.
    • The :stop functions for all :workers will be run after the web server is stopped. They will receive the outputs of the :start functions.
    • Before starting the server, you must set :handler to a namespace-qualified symbol bound to a Ring handler function.
    • If you set :mode to "dev", then the wrap-reload middleware will be added to your handler function. This will automatically reload all of your namespaces into the running JVM on each HTTP request.
    • The Jetty Server response header is suppressed (:send-server-version? false) so the Jetty version is not disclosed.

*** triangulum.handler

The triangulum.handler namespace provides core request handling and middleware composition for Triangulum applications. It sets up a Ring handler stack that includes various middlewares, such as request/response logging, exception handling, and request parameter parsing. Optional middlewares like wrap-ssl-redirect and wrap-reload can be applied based on your config.edn settings.

triangulum.handler/authenticated-routing-handler is an optional generic Ring compliant routing handler that allows your project to provide custom authentication, redirection, and routing behavior based on your config.edn settings.

**** Usage

If you need to provide a symbol that is bound to a handler function to figwheel-main, you can use triangulum.handler/development-app to load in your project's handler function from config.edn with the standard Triangulum middlewares added.

If you choose to use triangulum.handler/authenticated-routing-handler:

  1. Set :handler to triangulum.handler/authenticated-routing-handler.

#+begin_src clojure ;; nested config {:server {:handler triangulum.handler/authenticated-routing-handler}}

;; namespaced config {:triangulum.server/handler triangulum.handler/authenticated-routing-handler} #+end_src

  1. triangulum.handler/authenticated-routing-handler is designed to optionally support being provided multiple route maps, so that you can compose app specific routes with generic routes provided by common libraries.

    To configure which route maps are used, provide :routing-tables with a vector of one or multiple route maps in your config.edn

#+begin_src clojure ;; nested config {:server {:routing-tables [common-libary-ns.routing/routes product-ns.routing/routes]}}

;; namespaced config {:triangulum.handler/routing-tables [common-libary-ns.routing/routes product-ns.routing/routes]} #+end_src

Note that authenticated-routing-handler will merge this vector of route maps into one; this enables you to place route maps that provide specific implementations of routes to the right of common libraries that provide generic implentations as a way of overriding defaults.

  1. triangulum.handler/authenticated-routing-handler will use any custom implementation of these handlers that you specify in your config.edn:
    • :not-found-handler (args: request), called when no corresponding route is found.
    • :route-authenticator (args: request, route :auth-type), determines if client is authenticated
    • :redirect-handler (args: request), called when client is not authenticated

#+begin_src clojure ;; nested config {:server {:not-found-handler product-ns.handlers/not-found-handler :redirect-handler product-ns.handlers/redirect-handler :route-authenticator product-ns.handlers/route-authenticator}}

;; namespaced config {:triangulum.handler/not-found-handler product-ns.handlers/not-found-handler :triangulum.handler/redirect-handler product-ns.handlers/redirect-handler :triangulum.handler/route-authenticator product-ns.handlers/route-authenticator} #+end_src

**** Functions *** triangulum.views

This namespace provides functions for rendering pages and handling resources in Triangulum. It defines functions for reading asset files, generating HTML, and handling various types of responses.

**** Prerequisites

Add "target" to your deps.edn :paths so that Triangulum can find the file target/public/cljs/app.js:

#+begin_src clojure :paths ["target"] ;; Include other necessary paths. #+end_src

**** Usage

  1. Require the namespace in your project.
  2. Use render-page to generate a handler that will return an HTML response with a standard template for React/Reagent web apps.

**** Example #+begin_src clojure (ns my-app.views (:require [triangulum.views :refer [render-page]]))

(def my-page-handler (render-page "/my-page")) #+end_src

**** Functions ***** render-page [uri]

Returns a function that takes a request and generates the HTML for the specified URI using the request's parameters and session data. The generated HTML includes the necessary head and body sections.

Example usage: (def my-page (render-page "/my-page"))

**** Caveat

In JavaScript projects, we assign the relative path (from the project root) to the main component JSX file to the :js-init key. This file should export a function called pageInit that expects two arguments: params and session. You only need to set this key for the development mode to work, which enables Vite hot reload. In production, we rely on a manifest file generated by the bundling process to find the entry point. However, we still need to define pageInit and export it in the main entry point file.

In ClojureScript projects, we need to assign the namespaced symbol of the init function to the :cljs-init key, which accepts params and session as arguments, for both production and development environments.

The session map will also contain the :client-keys that were added in Triangulum's config.edn.

#+begin_src clojure ;; nested config {:app {:client-keys {:token "client-token" }}}

;; namespaced config {:triangulum.views/client-keys {:token "client-token" }}} #+end_src

*** triangulum.git

You can provide :tags-url, which is a url to the git tags page of your repository. Triangulum will extract all tags beginning with "prod", sort them lexicographically, and return the last entry. If you use tags of the form "prod-YYYY.MM.DD-HASH", then this will return the one with the latest date.

This tag label will be passed to the browser code in the :session map under the :versionDeployed key.

** Utility Scripts *** triangulum.build-db **** Required Prerequisites

Add "src/sql" to your deps.edn paths:

#+begin_src clojure :paths ["src/sql"] #+end_src

To set up the folder and file structure for use with build-db, use the following directory structure:

#+begin_src sh src/ |clj/ | | | |cljs/ | | | |___sql/ |___create_db.sql |___changes/ |___default_data/ |___dev_data/ |___functions/ |___tables/ #+end_src

You may also run this command in your project root directory: mkdir -p src/sql/{changes,default_data,dev_data,functions,tables}

Postgresql needs to be installed on the machine that will be hosting this website. This installation task is system specific and is beyond the scope of this README, so please follow the instructions for your operating system and Postgresql version. However, please ensure that the database server's superuser account is named "postgres" and that you know its database connection password before proceeding.

Once the Postgresql database server is running on your machine, you should navigate to the top level directory (i.e., the directory containing this README) and add the following alias to your deps.edn file:

#+begin_src clojure {:aliases {:build-db {:main-opts ["-m" "triangulum.build-db"]}}} #+end_src

Then run the database build command as follows:

#+begin_src sh clojure -M:build-db build-all -d database [-u user] [-p admin password] #+end_src

This will call ./src/sql/create_db.sql, stored in the individual project repository. A variable database is set for the command line call to create_db.sql. This allows your project to generate the project database with a different name, depending on your deployment. To use this variable type :database in create_db.sql where needed. You can check out [[https://github.com/openforis/collect-earth-online/blob/main/src/sql/create_db.sql][Collect Earth Online]] to view an example.

A handy use of the build-db command is to backup and restore your database. Calling

#+begin_src sh clojure -M:build-db backup -f somefile.dump #+end_src

will create a .dump backup file using pg_dump.

To restore your database from a .dump file you will need a .dump file containg a copy of a database downloaded locally. Assuming you have a copy of a database, you can then run:

#+begin_src sh clojure -M:build-db restore -f somefile.dump #+end_src

This will copy the database from the .dump file into your local Postgres database of the same name as the one in the .dump file. Note that you will be prompted with a password after running this command. You should enter the Postgres master password that you first created when running Postgres after installing. Depending on the size of your .dump file, this command may take a couple of minutes. Note that if you are working on a development branch and your .dump file contains a copy of a production database you may also need to apply some of the SQL changes from the ./sql/changes directory. Assuming your database doesn't have any of the change files on development applied to it, you can apply all of them at once using the following command:

#+begin_src sh for filename in ./src/sql/changes/*.sql; do psql -U -f $filename; done #+end_src

triangulum.build-db can also be configured through config.edn. It uses the same configuration as [[#triangulumdatabase][triangulum.database]] (see above).

*** triangulum.config

To make organizing an application's configurations simpler, create a config.edn file in the project's root directory. The file is just a hashmap that is similar to:

#+begin_src clojure ;; config.edn {:database {:host "localhost" :port 5432 :dbname "dbname" :user "user" :password "super-secret-password"} :mail {:host "smtp.gmail.com" :user "test@example.com" :pass "3492734923742" :port 587} :server {:host "smtp.gmail.com" :user "" :pass "" :tls true :port 587 :base-url "https://my.domain/" :auto-validate? false} ...} #+end_src

You can find an up-to-date example in config.nested-example.edn file. It can be used as a configuration template for your project.

Add config.edn to your .gitignore file to keep sensitive information out of the git history.

To validate the config.edn file, run: #+begin_src sh clojure -M:config validate [-f FILE] #+end_src

To retrieve a configuration, use get-config. You can supply nested configuration keys as follows:

#+begin_src clojure (triangulum.config/get-config :database) ;; -> {:user "triangulum" :pass "..."} (triangulum.config/get-config :database :user) ;; -> "triangulum"

(triangulum.config/get-config :server) ;; -> {:http-port 8080 :mode "dev"} (triangulum.config/get-config :server :http-port) ;; -> 8080 #+end_src

See each section below for an example configuration if one is required for use.

*** triangulum.https **** Required Prerequisites

If you have not already created a SSL certificate, you must start a server without a https port specified. (e.g. clojure -M:run-server).

Add the following alias to your deps.edn file:

#+begin_src clojure {:aliases {:https {:main-opts ["-m" "triangulum.https"]}}} #+end_src

To automatically create an SSL certificate signed by [[https://letsencrypt.org][Let's Encrypt]], simply run the following command from your shell:

#+begin_src sh sudo clojure -M:https certbot-init -d mydomain.com [-p certbot-dir] [--cert-only] #+end_src

The certbot creation process will run automatically and silently.

Note: If your certbot installation stores its config files in a directory other than /etc/letsencrypt, you should specify it with the optional certbot-dir argument to certbot-init.

Certbot runs as a background task every 12 hours and will renew any certificate that is set to expire in 30 days or less. Each time the certificate is renewed, any script in /etc/letsencrypt/renewal-hooks/deploy will be run automatically to repackage the updated certificate into the correct format.

**** Default Renewal Hook

If certbot runs successfully and --cert-only is not specified, then a shell script [mydomain].sh will be created in the certbot deploy hooks folder. This script will run clojure -M:https package-cert. Scripts in this folder will run automatically when a new certificate is created.

While there should be no need to do so, if you ever want to perform this repackaging step manually, simply run this command from your shell:

#+begin_src sh sudo clojure -M:https package-cert -d mydomain.com [-p certbot-dir] #+end_src

**** Custom Renewal Hook

Create a shell script in /etc/letsencrypt/renewal-hooks/deploy and update permissions.

#+begin_src sh sudo nano /etc/letsencrypt/renewal-hooks/deploy/custom.sh sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/custom.sh #+end_src

*** triangulum.packaging

To build a library JAR from your repository, run:

#+begin_src sh clojure -X triangulum.packaging/build-jar :lib-name GROUPID/GROUP_ID/ARTIFACT_ID #+end_src

To build an application UberJAR from your repository, run:

#+begin_src sh clojure -X triangulum.packaging/build-uberjar :app-name ARTIFACTID:mainnsARTIFACT_ID :main-ns MAIN_NAMESPACE #+end_src

If your UberJAR relies on triangulum.build-db for managing your database, you will need to include "src/sql" in the :resource-dirs argument to build-uberjar.

#+begin_src sh clojure -X triangulum.packaging/build-uberjar :app-name ARTIFACTID:mainnsARTIFACT_ID :main-ns MAIN_NAMESPACE :resource-dirs ["src/sql"] # include other neccessary resource-dirs #+end_src

To deploy a library JAR to https://clojars.org, run:

#+begin_src sh env CLOJARS_USERNAME=YOURUSERNAMECLOJARSPASSWORD=YOUR_USERNAME CLOJARS_PASSWORD=YOUR_CLOJARS_TOKEN clojure -X triangulum.packaging/deploy-jar GROUPID/GROUP_ID/ARTIFACT_ID #+end_src

NOTE: As of 2020-06-27, Clojars will no longer accept your Clojars password when deploying. You will have to use a token instead. Please read more about this [[https://github.com/clojars/clojars-web/wiki/Deploy-Tokens][here]]

To clean up after yourself by deleting the build folder (target), run:

#+begin_src sh clojure -X triangulum.packaging/clean #+end_src

*** triangulum.systemd

To make sure your application starts up on system reboot, you can use Triangulum to create a systemd user .service file by adding the following to your :aliases section in the deps.edn file:

#+begin_src clojure {:aliases {:systemd {:main-opts ["-m" "triangulum.systemd"]}}} #+end_src

Modify your app code to call (triangulum.notify/ready!) after all of your application's services are started: #+begin_src clojure (ns .server (:require [triangulum.notify :as notify])) ...

(defn app-start [] (reset! db (jdbc/connect!)) (reset! queues (q/start!)) (reset! server (ring/start-server!) (when (notify/available?) (notify/ready!)))) #+end_src

And then run: #+begin_src sh clojure -M:systemd enable -r REPO[pREPO [-p HTTP_PORT] [-P HTTPSPORT][dHTTPS_PORT] [-d REPO_DIRECTORY] [-A $EXTRA_ALIASES] #+end_src

This will install a file named cljweb-.service into the ~~/.config/systemd/user/~ directory, reload the systemctl daemon, and enable your service. By default, the current directory will be used in the service as the working directory. To supply an alternative, you can use -d. This will look for a Clojure project in that directory.

The server will always be started using clojure -M:server start unless the --extra-aliases option is passed. In that case, it will run with clojure -M${EXTRA_ALIASES}:server start.

To enable your user services to start on system reboot, you will need to run: #+begin_src sh sudo loginctl enable-linger "$USER" #+end_src

Now your service will be enabled at startup. You can also start, stop, and restart your service with the following commands: #+begin_src sh clojure -M:systemd start -r clojure -M:systemd stop -r clojure -M:systemd restart -r #+end_src

** API See [[file:API.md][API.md]]

** Docs To generate docs, use: bb docs

** Vulerability checking To check for vulerabilities in this project, use clojure -M:clj-watson scan -p deps.edn -s

** License

Copyright © 2021-2025 Spatial Informatics Group, LLC.

Triangulum is distributed by Spatial Informatics Group, LLC. under the terms of the Eclipse Public License version 2.0 (EPLv2). See the LICENSE file in this directory for more information.