LLAR Configuration

Runtime configuration is written in .llar files. These files define sources, schedules, processing hooks, highlights, and related behavior.

This page is generated from metadata attached to LLAR's config and source vars. The same content is rendered in the dashboard and exported for docs.llar.dev.

Three configuration sources

System config
Startup-only EDN for paths, databases, services, and host settings. LLAR reads the packaged resources/config.edn, then LLAR_CONFIG, then the JVM -Dconfig=... file; later files replace complete top-level values.
Runtime config
.llar files under :runtime-config-dir, watched and reloaded while LLAR runs. They define sources, schedules, processing, and runtime overrides.
Credentials
Secrets in the EDN file selected by :credentials-file. Runtime config and service settings refer to entries by keyword; PostgreSQL passwords are currently configured directly in the system-config pool maps.

Quick Start

A minimal config defines one or more sources and a schedule that updates them.

(fetch github-llar-releases (src/feed "https://github.com/irq0/llar/releases.atom") :tags #{:my-first-feed :github})
(fetch hn-frontpage (src/hn :front_page) :tags #{:my-first-feed :hackernews})
(sched-fetch my-first-feeds :now-and-hourly (some #{:my-first-feed} $TAGS))

Runtime Constructs

Top-level forms accepted in .llar files.

fetch

Define a source, its source tags, UI options, and optional item processing hooks.

(fetch src-key src & body)

Options and notes

  • :tags tags applied to the source. a set of keywords
  • :options reader behavior changes. a set of keywords. supported: :mark-read-on-view. :main-list-use-description remains accepted as a legacy no-op; Preview descriptions are automatic
  • :pre pre-processing function body
  • :pre-fns a list of functions to pre-process items
  • :rm a filter function body
  • :rm-fn a list of filter functions
  • :post a post-processing function body
  • :post-fns a list of post-processing functions

Example

(fetch github-llar-releases
  (src/feed "https://github.com/irq0/llar/releases.atom")
  :tags #{:github :release})

fetch-reddit

Define a Reddit source with a generated source key and the source tag :reddit.

(fetch-reddit src & body)

Options and notes

  • :min-score entries scoring below this are filtered out
  • :dynamic? also require the score to reach the top 5% of the fetched listing. combined with :min-score, whichever is higher wins
  • :tags see fetch
  • :options see fetch

Example

(fetch-reddit (src/reddit "clojure" :top :week)
  :min-score 50
  :tags #{:programming})

sched-fetch

Schedule updates for all fetchable sources matching a predicate.

(sched-fetch SCHED-NAME CHIME-TIMES PREDICATE)

Options and notes

  • PREDICATE can use the source predicate bindings
  • CHIME-TIMES can be a canned schedule keyword or a chime time sequence

Example

(sched-fetch my-feeds :now-and-hourly
  (some #{:my-feed-group} $TAGS))

autoread

Automatically remove :unread from old items fetched from matching sources.

(autoread SCHED-NAME PERIOD PREDICATE)

Options and notes

  • PERIOD is a java-time period such as (time/weeks 4)
  • PREDICATE can use $KEY, $SRC, and $TAGS

Example

(autoread reddit-ages-fast (time/weeks 4)
  (some #{:reddit} $TAGS))

highlight

Add :highlight to items matching configured words or authors.

(highlight words|authors VALUE...)

Options and notes

  • words are matched against extracted item terms
  • authors are matched case-insensitively

Example

(highlight words "llar" "rss")
(highlight authors "Douglas Engelbart")

sort-default

Set the default sort order for a Reader group item or the :all home view.

(sort-default TAG-KEY SORT-ORDER)

Options and notes

  • TAG-KEY is a source tag, item tag, type, or :all
  • SORT-ORDER is one of :newest, :ranked, or :oldest

Example

(sort-default :all :ranked)
(sort-default :blog :ranked)

rc

Read or set dynamic runtime behavior config. Reads resolve runtime overrides first, system config values that differ from shipped defaults second, and shipped defaults last.

(rc PATH VALUE)

Options and notes

  • PATH is a vector under a supported runtime config root
  • VALUE is validated with the path's runtime config spec
  • Supported roots include [:reader ...], [:podcast ...], [:digest], [:update], and [:throttle ...]

Example

(rc [:reader :ranking :highlight-boost-hours] 48)
(rc [:reader :default-list-view :storage] :headlines)

reader-favorites

Set the complete ordered reader favorite navigation list.

(reader-favorites FAVORITES)

Options and notes

  • FAVORITES is the complete ordered list of [KEY GROUP] pairs
  • KEY is a source tag, item tag, source key, or view key
  • GROUP is one of :default, :item-tags, :source-tag, or :type
  • Omit entries from FAVORITES to remove them from the reader navigation

Example

(reader-favorites
 [[:all :default]
  [:saved :item-tags]
  [:highlight :item-tags]
  [:bookmark :type]])

reader-default-list-view

Set the default reader list style for one group item.

(reader-default-list-view KEY STYLE)

Options and notes

  • KEY is a source tag, item tag, source key, or view key
  • STYLE is one of :headlines or :gallery

Example

(reader-default-list-view :storage :headlines)
(reader-default-list-view :tweet :gallery)

reader-ranking

Set reader ranking tuning values.

(reader-ranking KEY VALUE ...)

Options and notes

  • KEY is :highlight-boost-hours or :rarity-boost-cap-hours
  • VALUE is numeric
  • Multiple key/value pairs can be set in one form

Example

(reader-ranking :highlight-boost-hours 48
                :rarity-boost-cap-hours 168)

reader-url-handler

Configure reader annotation export through an external URL handler.

(reader-url-handler CONFIG)

Options and notes

  • CONFIG is nil, a map, or key/value pairs
  • Required key: :template
  • Optional keys: :name, :icon
  • The template supports {title}, {url}, {id}, {source}, and {body}

Example

(reader-url-handler :name "Org-roam"
                    :icon "fas fa-brain"
                    :template "org-protocol://roam-ref?ref={url}&title={title}&body={body}")

podcast-retention

Override podcast retention for a source.

(podcast-retention SOURCE-KEY LIMIT)

Options and notes

  • SOURCE-KEY is the configured source key
  • LIMIT is the episode count to retain

Example

(podcast-retention my-video-feed 10)

digest

Configure and enable digest delivery.

(digest KEY VALUE ...)

Options and notes

  • Required key: :to
  • Optional keys: :from, :limit, :inline-images?, :keep-unread-issues
  • Defining digest enables digest delivery.

Example

(digest :to "you_abc123@kindle.com"
        :from "llar@example.org"
        :limit 200
        :inline-images? true
        :keep-unread-issues 1)

podcast-download

Configure and enable podcast media downloads.

(podcast-download KEY VALUE ...)

Options and notes

  • Optional keys: :video-format, :extra-args, :max-attempts, :retry-cooldown-minutes
  • Defining podcast-download enables podcast media downloading and retention jobs.

Example

(podcast-download :video-format "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=1080]+bestaudio/best[height<=1080][vcodec!=none]/bestvideo+bestaudio/best[vcodec!=none]"
                  :extra-args ["--embed-metadata" "--embed-chapters"]
                  :max-attempts 3
                  :retry-cooldown-minutes 30)

Source Constructors

Source constructors live in the src namespace inside .llar files.

src/website

Generic website fetch.

(src/website url & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))
  • :irq0-src-args/user-agent (or :default keyword? :custom string?)

Example

(src/website "https://example.org/article.html")

src/custom

Custom function source returning LLAR item data.

(src/custom id fn)

Schemas

  • keyword? keyword?
  • fn? fn?

Example

(src/custom :my-source (fn [] []))

src/demo

Deterministic, network-free editorial content for demos and UI development.

(src/demo publication & {:as args})

Schemas

  • #{:after-hours :common-ground :field-notes :signal-wire} #{:after-hours :common-ground :field-notes :signal-wire}
  • :irq0-src-demo/count (and int? (<= 1 % 8))
  • :irq0-src-demo/seed int?

Defaults

{:count 8, :seed 4242}

Example

(src/demo :signal-wire :count 8 :seed 4242)

src/website+paywall

Website fetch using a cookie store function.

(src/website+paywall url cookie-getter & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))
  • fn? fn?
  • :irq0-src-args/user-agent (or :default keyword? :custom string?)

Example

(src/website+paywall "https://example.org" cookie-store)

src/feed

RSS, Atom, and similar feed formats.

(src/feed url & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))
  • :irq0-src-args/user-agent (or :default keyword? :custom string?)
  • :irq0-src-args/force-update? boolean?

Defaults

{:user-agent :default, :force-update? false}

Example

(src/feed "https://github.com/irq0/llar/releases.atom")

src/selector-feed

Build a feed from an HTML page using Hickory selectors.

(src/selector-feed url selectors extractors args)

Schemas

  • :irq0-src-selectors/selectors (keys :req-un [:irq0-src-selectors/urls] :opt-un [:irq0-src-selectors/ts :irq0-src-selectors/title :irq0-src-selectors/author :irq0-src-selectors/content :irq0-src-selectors/description])
  • :irq0-src-selectors/extractors (keys :opt-un [:irq0-src-selectors/urls :irq0-src-selectors/ts :irq0-src-selectors/title :irq0-src-extractors/author :irq0-src-selectors/content :irq0-src-selectors/description])

Example

(src/selector-feed "https://example.org" {:urls (S/tag :a)} {} {})

src/wp-json

WordPress REST API posts.

(src/wp-json url & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))
  • :irq0-src-args/user-agent (or :default keyword? :custom string?)
  • :irq0-src-args/force-update? boolean?

Defaults

{:user-agent :default, :force-update? false}

Example

(src/wp-json "https://example.org/wp-json/")

src/twitter-search

Twitter search source.

(src/twitter-search query oauth-creds)

Schemas

  • string? string?
  • :irq0-src-twitter/credentials (keys :req-un [:irq0-src-twitter/app-key :irq0-src-twitter/app-secret :irq0-src-twitter/user-token :irq0-src-twitter/user-token-secret])

Example

(src/twitter-search "clojure" ($credentials :twitter-api))

src/twitter-timeline

Twitter home timeline source.

(src/twitter-timeline oauth-creds)

Schemas

  • :irq0-src-twitter/credentials (keys :req-un [:irq0-src-twitter/app-key :irq0-src-twitter/app-secret :irq0-src-twitter/user-token :irq0-src-twitter/user-token-secret])

Example

(src/twitter-timeline ($credentials :twitter-api))

src/readability

Single web page processed through article extraction.

(src/readability url & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))
  • :irq0-src-args/user-agent (or :default keyword? :custom string?)

Example

(src/readability "https://example.org/article.html")

src/reddit

Reddit listing source.

(src/reddit subreddit listing)
(src/reddit subreddit listing timeframe)

Schemas

  • string? string?
  • #{:best :controversial :hot :new :rising :top} #{:best :controversial :hot :new :rising :top}
  • #{:all :day :hour :month :week :year} #{:all :day :hour :month :week :year}

Example

(src/reddit "clojure" :top :week)

src/imap

IMAP or IMAPS mailbox source using credentials from credentials.edn.

(src/imap url-str creds)

Schemas

  • string? string?
  • :irq0-src-mailbox/credentials (keys :req-un [:irq0-src-mailbox/username :irq0-src-mailbox/password])

Example

(src/imap "imaps://imap.example.org/INBOX" ($credentials :imap))

src/hn

Hacker News via Algolia.

(src/hn tag & {:as args})

Schemas

  • :irq0-hn/tag (and keyword? (#{:show_hn :comment :front_page :ask_hn :job} %))
  • :irq0-hn/args (keys :opt-un [:irq0-hn-filter/query :irq0-hn-filter/count :irq0-hn-filter/filters :irq0-hn-filter/count :irq0-hn-filter/min-score :irq0-hn-filter/min-comments :irq0-hn-filter/created-after])

Defaults

{:count 1000}

Example

(src/hn :front_page :query "clojure" :min-score 20)

src/streaming-channel

Streaming collection source such as supported video/audio channels or playlists.

(src/streaming-channel url & {:as args})

Schemas

  • :irq0/url-str (and string? (try (as-url %) (catch java.lang.Exception _ false)))

Defaults

{:max-results 30}

Example

(src/streaming-channel "https://www.youtube.com/@veritasium")

src/github-issues

Search GitHub issues/PRs. Query uses GitHub search syntax. Date tokens like {{last-week}} are expanded at fetch time.

(src/github-issues query & {:as args})

Schemas

  • :irq0-gh/query string?
  • :irq0-gh/args (keys :opt-un [:irq0-gh/per-page :irq0-gh/sort :irq0-gh/order])

Defaults

{:per-page 30, :order :desc}

Example

(src/github-issues "repo:ceph/ceph is:pr created:>{{last-week}}")

src/github-repos

Search GitHub repositories. Query uses GitHub search syntax. Date tokens like {{last-week}} are expanded at fetch time.

(src/github-repos query & {:as args})

Schemas

  • :irq0-gh/query string?
  • :irq0-gh/args (keys :opt-un [:irq0-gh/per-page :irq0-gh/sort :irq0-gh/order])

Defaults

{:per-page 30, :order :desc}

Example

(src/github-repos "language:clojure stars:>20" :sort :stars)

Canned Schedules

These keywords can be passed as the schedule argument to sched-fetch and internal LLAR schedulers.

:during-daytimeDaily at 10:00, 12:00, 13:00, 14:00, 16:00, and 18:00.
:sundaysWeekly on Sunday at 05:00.
:early-morningDaily at 07:00.
:now-and-early-morningOnce within the next 0-120 seconds, then daily at 07:00.
:noonDaily at 12:00.
:now-and-noonOnce within the next 0-120 seconds, then daily at 12:00.
:now-and-hourlyOnce within the next 0-120 seconds, then every hour.
:hourlyEvery hour.
:every-4-hoursEvery 4 hours at 00:00, 04:00, 08:00, 12:00, 16:00, and 20:00.
:now-and-every-4-hoursOnce within the next 0-120 seconds, then every 4 hours.
:now-and-every-15-minutesOnce within the next 0-120 seconds, then every 15 minutes.
:now-and-every-5-minutesOnce within the next 0-120 seconds, then every 5 minutes.
:now-and-every-minuteOnce within the next 0-120 seconds, then every minute.

Processing Hooks

:pre, :rm, and :post forms are evaluated with these bindings.

$itemThe full item.
$keySource key as keyword.
$titleItem title, or empty string.
$authorsItem authors, or empty string.
$tagsSource tags configured on the fetch definition.
$rawRaw fetched data when supported by the source.
$urlItem URL, or empty string.
$htmlHTML content, or empty string.
$textPlain text content, or empty string.
$scoreScore for sources such as Reddit or Hacker News, or -1.
$optionsSource options configured on the fetch definition.
$entryThe item entry map.

Source Predicate Bindings

sched-fetch and autoread predicates are evaluated with these bindings.

$KEYSource key as keyword.
$SRCSource constructor value.
$TAGSSource tags configured on the fetch definition.

Helper Bindings

$add-tagReturn a processor that adds an item tag.
$add-tag-filterReturn a processor that adds a tag when a predicate matches.
$category-rmBuild a filter that removes items by feed category.
$credentialsRead an entry from credentials.edn.
$ellipsifyTruncate text with an ellipsis.
$exchangeSwap two item paths.
$extractRun article extraction on HTML content.
$fetchRun LLAR HTTP fetch.
$hickory-sanitize-blobifySanitize and blobify Hickory content.
$hickory-to-htmlRender Hickory as HTML.
$html-to-hickoryParse HTML into Hickory.
$html2textConvert HTML to plain text.
$http-cookie-storeCreate a clj-http cookie store.
$http-getCall clj-http.client/get.
$http-postCall clj-http.client/post.
$make-item-hashCreate a stable LLAR item hash.
$parse-tsParse a timestamp into a zoned date time.
$parse-urlParse or absolutify URLs.
$uri-pathRead the path from a URI.

Available Namespaces

srcllar.src
stringclojure.string
logclojure.tools.logging
Shickory.select
timejava-time.api

Feature Examples

Feature-specific configuration examples that are not top-level .llar constructs.

zotero-export-links

credentials.edn

Options and notes

  • Enables the reader's Zotero annotation export action
  • The Zotero API key must allow item creation
  • The exported item is stored in a Zotero collection named llar

Example

{:zotero {:api-key "ZOTERO_API_KEY"
          :user-id "ZOTERO_USER_ID"}}

Examples

fetch

(fetch github-llar-releases
  (src/feed "https://github.com/irq0/llar/releases.atom")
  :tags #{:github :release})

fetch-reddit

(fetch-reddit (src/reddit "clojure" :top :week)
  :min-score 50
  :tags #{:programming})

sched-fetch

(sched-fetch my-feeds :now-and-hourly
  (some #{:my-feed-group} $TAGS))

autoread

(autoread reddit-ages-fast (time/weeks 4)
  (some #{:reddit} $TAGS))

highlight

(highlight words "llar" "rss")
(highlight authors "Douglas Engelbart")

sort-default

(sort-default :all :ranked)
(sort-default :blog :ranked)

rc

(rc [:reader :ranking :highlight-boost-hours] 48)
(rc [:reader :default-list-view :storage] :headlines)

reader-favorites

(reader-favorites
 [[:all :default]
  [:saved :item-tags]
  [:highlight :item-tags]
  [:bookmark :type]])

reader-default-list-view

(reader-default-list-view :storage :headlines)
(reader-default-list-view :tweet :gallery)

reader-ranking

(reader-ranking :highlight-boost-hours 48
                :rarity-boost-cap-hours 168)

reader-url-handler

(reader-url-handler :name "Org-roam"
                    :icon "fas fa-brain"
                    :template "org-protocol://roam-ref?ref={url}&title={title}&body={body}")

podcast-retention

(podcast-retention my-video-feed 10)

digest

(digest :to "you_abc123@kindle.com"
        :from "llar@example.org"
        :limit 200
        :inline-images? true
        :keep-unread-issues 1)

podcast-download

(podcast-download :video-format "bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/bestvideo[height<=1080]+bestaudio/best[height<=1080][vcodec!=none]/bestvideo+bestaudio/best[vcodec!=none]"
                  :extra-args ["--embed-metadata" "--embed-chapters"]
                  :max-attempts 3
                  :retry-cooldown-minutes 30)

src/website

(src/website "https://example.org/article.html")

src/custom

(src/custom :my-source (fn [] []))

src/demo

(src/demo :signal-wire :count 8 :seed 4242)

src/website+paywall

(src/website+paywall "https://example.org" cookie-store)

src/feed

(src/feed "https://github.com/irq0/llar/releases.atom")

src/selector-feed

(src/selector-feed "https://example.org" {:urls (S/tag :a)} {} {})

src/wp-json

(src/wp-json "https://example.org/wp-json/")

src/twitter-search

(src/twitter-search "clojure" ($credentials :twitter-api))

src/twitter-timeline

(src/twitter-timeline ($credentials :twitter-api))

src/readability

(src/readability "https://example.org/article.html")

src/reddit

(src/reddit "clojure" :top :week)

src/imap

(src/imap "imaps://imap.example.org/INBOX" ($credentials :imap))

src/hn

(src/hn :front_page :query "clojure" :min-score 20)

src/streaming-channel

(src/streaming-channel "https://www.youtube.com/@veritasium")

src/github-issues

(src/github-issues "repo:ceph/ceph is:pr created:>{{last-week}}")

src/github-repos

(src/github-repos "language:clojure stars:>20" :sort :stars)

zotero-export-links

{:zotero {:api-key "ZOTERO_API_KEY"
          :user-id "ZOTERO_USER_ID"}}

Runtime Config Settings

rc controls dynamic runtime behavior settings. It reads runtime overrides first, system config values that differ from shipped defaults second, and shipped defaults from resources/config.edn last.

PathDescriptionSpecSystem config pathExample
[:reader :favorites]Favorite reader navigation entries.:irq0-appconfig/favorites
(coll-of (tuple keyword? :irq0-appconfig/view-group))
[:ui :favorites](rc [:reader :favorites] VALUE)
[:reader :default-list-view]Default reader list style by group item.:irq0-appconfig/default-list-view
(map-of keyword? :irq0-appconfig/list-view)
[:ui :default-list-view](rc [:reader :default-list-view] VALUE)
[:reader :ranking]Ranking query tuning.:irq0-appconfig/ranking
(keys :opt-un [:irq0-appconfig/highlight-boost-hours :irq0-appconfig/rarity-boost-cap-hours])
[:ranking](rc [:reader :ranking] VALUE)
[:reader :vibe]Today’s Vibe source selection and Cobweb clustering tuning.:irq0-appconfig/vibe
(keys :req-un [:irq0-appconfig/source-tags :irq0-appconfig/hours :irq0-appconfig/limit :irq0-appconfig/acuity :irq0-appconfig/cutoff :irq0-appconfig/random-seed] :opt-un [:irq0-appconfig/max-feature-frequency-ratio :irq0-appconfig/min-match-score :irq0-appconfig/max-clusters :irq0-appconfig/max-single-source-clusters])
[:vibe](rc [:reader :vibe] VALUE)
[:reader :export :url-handler]External URL handler used for reader annotation export.:irq0-appconfig/url-handler
(nilable (keys :req-un [:irq0-appconfig/template] :opt-un [:irq0-appconfig/name :irq0-appconfig/icon]))
[:export :url-handler](rc [:reader :export :url-handler] VALUE)
[:podcast :retention]Podcast episode retention policy.:irq0-appconfig/podcast-retention
(keys :req-un [:irq0-appconfig/default-episode-limit] :opt-un [:irq0-appconfig/sources])
[:api :podcast :retention](rc [:podcast :retention] VALUE)
[:digest]Digest delivery and rendering policy.:irq0-appconfig/runtime-digest
(and (keys :opt-un [:irq0-appconfig/enabled? :irq0-appconfig/to :irq0-appconfig/from :irq0-appconfig/limit :irq0-appconfig/inline-images? :irq0-appconfig/keep-unread-issues]) (or (not (:enabled? %)) (string? (:to %))))
[:api :digest](rc [:digest] VALUE)
[:podcast :enabled?]Enable podcast media downloading and retention jobs.:irq0-appconfig/podcast-enabled
boolean?
(rc [:podcast :enabled?] VALUE)
[:podcast :download]Podcast media downloader policy.:irq0-appconfig/podcast-download
(keys :opt-un [:irq0-appconfig/video-format :irq0-appconfig/extra-args :irq0-appconfig/max-attempts :irq0-appconfig/retry-cooldown-minutes])
[:api :podcast](rc [:podcast :download] VALUE)
[:podcast :scan]Podcast scanner policy.:irq0-appconfig/podcast-scan
(keys :req-un [:irq0-appconfig/limit])
(rc [:podcast :scan] VALUE)
[:update]Source update retry policy.:irq0-appconfig/update
(keys :req-un [:irq0-appconfig/max-retry])
[:update-max-retry](rc [:update] VALUE)
[:throttle]Bounds on concurrent work: external commands, media downloads, streaming fetches, source updates and item post-processing.:irq0-appconfig/throttle
(keys :req-un [:irq0-appconfig/command-max-concurrent] :opt-un [:irq0-appconfig/av-downloader-max-concurrent :irq0-appconfig/streaming-max-concurrent :irq0-appconfig/source-update-max-concurrent :irq0-appconfig/item-postproc-max-concurrent])
[:throttle](rc [:throttle] VALUE)

System Config

System config is EDN, loaded at startup before runtime .llar files. It configures paths, commands, API ports, PostgreSQL pools, mail transport, credentials location, and other service-level settings.

Runtime behavior settings such as reader favorites, default list views, ranking tuning, and podcast retention are available through rc. Existing system config keys for those settings remain supported through their system config paths.

Use resources/config.edn as the complete default example and docker/docker-config.edn for Docker Compose deployments. Secrets belong in credentials.edn.

Overrides are shallow. Later config files replace complete top-level values. For example, an :api override must repeat every API entry that should remain enabled; it is not merged into the shipped :api map.

PostgreSQL Connection Pools

[:postgresql :frontend] and [:postgresql :backend] are independent HikariCP pool maps. LLAR passes their pool options to hikari-cp, whose configuration reference defines the accepted kebab-case pool keys and their defaults. LLAR adds the metrics tracker described below.

Both pools automatically publish HikariCP metrics to the dashboard's /metrics endpoint. The standard hikaricp_* series use the configured :pool-name as their pool label.

Connection fields such as :server-name, :database-name, :username, and :password select the PostgreSQL database. See the official pgJDBC data-source documentation for PostgreSQL connection properties. Start with the shipped Docker example and tune pool sizes only when deployment load requires it.

:postgresql
{:frontend {:adapter "postgresql"
            :server-name "db"
            :database-name "llar"
            :username "llar"
            :password "replace-me"
            :maximum-pool-size 5
            :pool-name "frontend"}
 :backend  {:adapter "postgresql"
            :server-name "db"
            :database-name "llar"
            :username "llar"
            :password "replace-me"
            :maximum-pool-size 10
            :pool-name "backend"}}
Database passwords are currently part of system config, unlike named application credentials. Restrict access to the system-config file and avoid committing deployment passwords.

Services and APIs

Service settings are part of the top-level :api map. The examples below are entries inside that map; preserve the other shipped entries when overriding it.

Dashboard

Config path: [:api :dashboard]

Administrative UI and Prometheus metrics endpoint. It starts when :port is present.

Example

:dashboard {:port 9999}

Reader

Config path: [:api :reader]

The browser-based reader. It starts when :port is present; :base-url is used when LLAR builds absolute reader links.

Example

:reader {:port 8023
         :base-url "https://reader.example.org"}

Bookmark capture

Config path: [:api :capture]

A reader-independent, durable save-for-later API for browser bookmarklets and iOS/macOS Shortcuts. It starts when :port is present.

  • Expose the service through HTTPS and have the reverse proxy preserve the Authorization header. The write endpoint is POST <base-url>/api/v1/captures with a JSON body containing url and optional title. The feedback assets and API use relative URLs, so :base-url may include a reverse-proxy path; keep its trailing slash in the bookmarklet. The service intentionally has no CORS support, cookies, or token-management UI.
  • Tokens. Generate a separate revocable token for each client with openssl rand -hex 32, store only the values in credentials.edn, restrict that file to the LLAR account (for example chmod 600 credentials.edn), and restart LLAR after changing them. Tokens must have at least 32 base64url-safe characters. A malformed or missing credential entry prevents the capture service from starting.
  • Firefox/Chrome bookmarklet. Create a bookmark whose URL is the following one-line script, replacing the public base URL and the token belonging to that browser:
    javascript:location.href='https://save.example.org/#REPLACE_WITH_TOKEN:'+location.href
    The current tab navigates to LLAR's feedback page. LLAR removes the secret fragment before making the authenticated request and reports success only after the database commit; use Back to return to the page. Keeping the token out of query parameters prevents it from entering proxy access logs.
  • Treat the bookmark itself as a credential: browser bookmark sync may copy its token to other signed-in devices. Use a token dedicated to that browser profile and revoke it from credentials.edn if the profile or synced account is compromised.
  • iOS/macOS Shortcut. Create a Shortcut that accepts URLs from the Share Sheet, uses Get Contents of URL on https://save.example.org/api/v1/captures with method POST, JSON body {"url": <Shortcut Input>}, and header Authorization: Bearer <iphone token>. Finish with Show Result using the response's message. Treat a non-2xx response as not saved; HTTP 409 means the existing failed capture needs Retry or Dismiss in the dashboard.
  • Queue operations. The dashboard's Bookmarks tab shows ready, processing, retry-wait, failed, and complete captures plus Retry/Dismiss recovery actions. The scheduler claims at most one leased capture per run. Set :schedule to a canned schedule keyword; it defaults to :now-and-every-minute. A duplicate pending capture returns already queued, a complete one returns already saved, and failed or dismissed captures are never implicitly recaptured.
  • Minimal alerts. Alert on a capture delayed more than 15 minutes with llar_bookmark_queue_items{state="ready"} > 0 and on() (time() - llar_bookmark_queue_oldest_ready_unixtime > 900), and on manual intervention with llar_bookmark_queue_items{state="failed"} > 0. The first means the capture latency objective is breached (stalled worker or sustained backlog); inspect the Bookmarks tab, the generic schedule metrics, and source/item-pool saturation. The second is resolved by Retry after correcting the cause, or Dismiss when the URL should be abandoned.

Example

:capture {:port 8026
          :base-url "https://save.example.org"
          :credentials :bookmark-capture
          :schedule :now-and-every-minute}

;; credentials.edn
{:bookmark-capture
 {:tokens {:iphone "<64 hex characters>"
           :macbook "<64 hex characters>"
           :firefox "<64 hex characters>"}}}

Podcast

Config path: [:api :podcast]

HTTP service for downloaded podcast and video media. It starts when :port is present.

Example

:podcast {:host "127.0.0.1"
          :port 8024
          :base-url "https://media.example.org"
          :retention {:default-episode-limit 25}}

Fever-compatible sync

Config path: [:api :fever]

Mobile sync endpoint for Fever clients such as Fiery Feeds and ReadKit. It starts when :port is present.

  • Only sources carrying :source-tag (default :mobile) are exposed.
  • Clients authenticate with the Fever MD5 API key derived from username and the dedicated password.
  • The working-set defaults are 30 initial days, 10 recent-read days, and 1048576 content bytes.
  • :base-url is important. Set it to the externally reachable URL of this Fever endpoint, including any reverse-proxy path. Privacy-rewritten images and media are stored in the local blobstore and served by this endpoint at <base-url>/blob/<hash>. Content is rewritten to those absolute URLs so mobile clients can load them; without a correct :base-url the relative /blob/ URLs break on the phone. The endpoint must be exposed over HTTPS.

Example

:fever {:port 8025
        :base-url "https://reader.example.org/api/fever"
        :username "llar"
        :credentials :mobile-sync
        :source-tag :mobile}

;; credentials.edn
{:mobile-sync {:password "replace-me"}}

Digest delivery

Config path: [:api :digest]

Scheduled EPUB delivery to an e-reader address. This is not an HTTP API and requires top-level :mail configuration.

Example

:digest {:to "you_abc123@kindle.com"
         :from "llar@example.org"
         :schedule :sundays
         :keep-unread-issues 1
         :inline-images? true}

System Configuration Reference

This reference is generated by walking :irq0-llar/appconfig. Required and optional status comes from the Clojure specs; displayed defaults come from packaged configuration and shared runtime defaults.

PathStatusSchemaDefault
[:blob-store-dir]required(and :irq0/path-exists-is-dir (.canWrite (file %)))"/var/lib/llar/blobs"
[:credentials-file]required(and :irq0/path (file %) (exists? (.toPath (file %))))"/var/lib/llar/credentials.edn"
[:runtime-config-dir]required(and :irq0/path-exists (.isDirectory (file %)))"/var/lib/llar/config"
[:commands]required(map-of keyword :irq0-appconfig/command){:pandoc "pandoc", :w3m "w3m", :lynx "lynx", :av-downloader "yt-dlp", :html2text "html2text"}
[:api]required(keys :opt-un [:irq0-appconfig/reader :irq0-appconfig/dashboard :irq0-appconfig/podcast :irq0-appconfig/fever :irq0-appconfig/capture :irq0-appconfig/digest]){:dashboard {:port 9999}, :reader {:port 8023, :base-url "http://localhost:8023"}, :podcast {:host "127.0.0.1", :port 8024, :base-url "http://localhost:8024", :video-format "bestvideo[height<=1080][ext=mp4][vcodec~='^(avc1|h264)']+bestaudio[ext=m4a][acodec~='^(mp4a|aac)']/best[height<=1080][ext=mp4][vcodec~='^(avc1|h264)'][acodec~='^(mp4a|aac)']/bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4][vcodec!=none]/bestvideo[height<=1080]+bestaudio/best[height<=1080][vcodec!=none]/bestvideo+bestaudio/best[vcodec!=none]", :av-downloader-extra-args ["--embed-metadata" "--embed-chapters" "--write-subs" "--write-auto-subs" "--sub-langs" "en,en-orig,en-en,-live_chat"], :retention {:default-episode-limit 25}}, :fever {:source-tag :mobile, :initial-days 30, :recent-read-days 10, :max-content-bytes 1048576}}
[:api :reader]optional(keys :req-un [:irq0-appconfig/port] :opt-un [:irq0-appconfig/base-url]){:port 8023, :base-url "http://localhost:8023"}
[:api :reader :port]requiredpos-int?8023
[:api :reader :base-url]optionalstring?"http://localhost:8023"
[:api :dashboard]optional(keys :req-un [:irq0-appconfig/port] :opt-un [:irq0-appconfig/config-lab]){:port 9999}
[:api :dashboard :port]requiredpos-int?9999
[:api :dashboard :config-lab]optional(keys :req-un [:irq0-appconfig/enabled?] :opt-un [:irq0-appconfig/credentials :irq0-appconfig/max-concurrent-runs :irq0-appconfig/run-timeout-ms :irq0-appconfig/session-ttl-minutes])
[:api :dashboard :config-lab :enabled?]requiredboolean?
[:api :dashboard :config-lab :credentials]optionalkeyword?
[:api :dashboard :config-lab :max-concurrent-runs]optionalpos-int?
[:api :dashboard :config-lab :run-timeout-ms]optionalpos-int?
[:api :dashboard :config-lab :session-ttl-minutes]optionalpos-int?
[:api :podcast]optional(keys :req-un [:irq0-appconfig/port] :opt-un [:irq0-appconfig/host :irq0-appconfig/base-url :irq0-appconfig/video-format :irq0-appconfig/av-downloader-extra-args :irq0-appconfig/retention]){:host "127.0.0.1", :port 8024, :base-url "http://localhost:8024", :video-format "bestvideo[height<=1080][ext=mp4][vcodec~='^(avc1|h264)']+bestaudio[ext=m4a][acodec~='^(mp4a|aac)']/best[height<=1080][ext=mp4][vcodec~='^(avc1|h264)'][acodec~='^(mp4a|aac)']/bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4][vcodec!=none]/bestvideo[height<=1080]+bestaudio/best[height<=1080][vcodec!=none]/bestvideo+bestaudio/best[vcodec!=none]", :av-downloader-extra-args ["--embed-metadata" "--embed-chapters" "--write-subs" "--write-auto-subs" "--sub-langs" "en,en-orig,en-en,-live_chat"], :retention {:default-episode-limit 25}}
[:api :podcast :port]requiredpos-int?8024
[:api :podcast :host]optionalstring?"127.0.0.1"
[:api :podcast :base-url]optionalstring?"http://localhost:8024"
[:api :podcast :video-format]optionalstring?"bestvideo[height<=1080][ext=mp4][vcodec~='^(avc1|h264)']+bestaudio[ext=m4a][acodec~='^(mp4a|aac)']/best[height<=1080][ext=mp4][vcodec~='^(avc1|h264)'][acodec~='^(mp4a|aac)']/bestvideo[height<=1080][ext=mp4]+bestaudio[ext=m4a]/best[height<=1080][ext=mp4][vcodec!=none]/bestvideo[height<=1080]+bestaudio/best[height<=1080][vcodec!=none]/bestvideo+bestaudio/best[vcodec!=none]"
[:api :podcast :av-downloader-extra-args]optional(coll-of string? :kind vector?)["--embed-metadata" "--embed-chapters" "--write-subs" "--write-auto-subs" "--sub-langs" "en,en-orig,en-en,-live_chat"]
[:api :podcast :retention]optional(keys :req-un [:irq0-appconfig/default-episode-limit] :opt-un [:irq0-appconfig/sources]){:default-episode-limit 25}
[:api :podcast :retention :default-episode-limit]requiredpos-int?25
[:api :podcast :retention :sources]optional(map-of keyword? pos-int?)
[:api :fever]optional(keys :req-un [:irq0-appconfig/port :irq0-appconfig/username :irq0-appconfig/credentials] :opt-un [:irq0-appconfig/source-tag :irq0-appconfig/base-url :irq0-appconfig/initial-days :irq0-appconfig/recent-read-days :irq0-appconfig/max-content-bytes]){:source-tag :mobile, :initial-days 30, :recent-read-days 10, :max-content-bytes 1048576}
[:api :fever :port]requiredpos-int?
[:api :fever :username]requiredstring?
[:api :fever :credentials]requiredkeyword?
[:api :fever :source-tag]optionalkeyword?:mobile
[:api :fever :base-url]optionalstring?
[:api :fever :initial-days]optionalpos-int?30
[:api :fever :recent-read-days]optionalpos-int?10
[:api :fever :max-content-bytes]optionalpos-int?1048576
[:api :capture]optional(keys :req-un [:irq0-appconfig/port :irq0-appconfig/base-url :irq0-appconfig/credentials] :opt-un [:irq0-appconfig/schedule])
[:api :capture :port]requiredpos-int?
[:api :capture :base-url]requiredstring?
[:api :capture :credentials]requiredkeyword?
[:api :capture :schedule]optionalkeyword?
[:api :digest]optional(keys :req-un [:irq0-appconfig/to] :opt-un [:irq0-appconfig/from :irq0-appconfig/schedule :irq0-appconfig/inline-images? :irq0-appconfig/keep-unread-issues])
[:api :digest :to]requiredstring?
[:api :digest :from]optionalstring?
[:api :digest :schedule]optionalkeyword?
[:api :digest :inline-images?]optionalboolean?
[:api :digest :keep-unread-issues]optionalnat-int?
[:ui]required(keys :req-un [:irq0-appconfig/default-list-view :irq0-appconfig/favorites]){:default-list-view {:bookmark :gallery, :tweet :gallery}, :favorites [[:all :default] [:saved :item-tags] [:highlight :item-tags] [:bookmark :type]]}
[:ui :default-list-view]required(map-of keyword? :irq0-appconfig/list-view){:bookmark :gallery, :tweet :gallery}
[:ui :favorites]required(coll-of (tuple keyword? :irq0-appconfig/view-group))[[:all :default] [:saved :item-tags] [:highlight :item-tags] [:bookmark :type]]
[:postgresql]required(keys :req-un [:irq0-appconfig/frontend :irq0-appconfig/backend])
[:postgresql :frontend]requiredvalidate-options
[:postgresql :backend]requiredvalidate-options
[:update-max-retry]optionalnat-int?5
[:throttle]optional(keys :req-un [:irq0-appconfig/command-max-concurrent] :opt-un [:irq0-appconfig/av-downloader-max-concurrent :irq0-appconfig/streaming-max-concurrent :irq0-appconfig/source-update-max-concurrent :irq0-appconfig/item-postproc-max-concurrent]){:command-max-concurrent 20, :av-downloader-max-concurrent 2, :streaming-max-concurrent 1, :source-update-max-concurrent 4, :item-postproc-max-concurrent :auto}
[:throttle :command-max-concurrent]requiredpos-int?20
[:throttle :av-downloader-max-concurrent]optionalpos-int?2
[:throttle :streaming-max-concurrent]optionalpos-int?1
[:throttle :source-update-max-concurrent]optionalpos-int?4
[:throttle :item-postproc-max-concurrent]optional(or :auto #{:auto} :fixed pos-int?):auto
[:timeouts]optional(map-of keyword pos-int?){:readability 60, :av-downloader 600, :av-downloader-transcode 1800, :html2text 30}
[:ranking]optional(keys :opt-un [:irq0-appconfig/highlight-boost-hours :irq0-appconfig/rarity-boost-cap-hours]){:highlight-boost-hours 48, :rarity-boost-cap-hours 168}
[:ranking :highlight-boost-hours]optionalnumber?48
[:ranking :rarity-boost-cap-hours]optionalnumber?168
[:vibe]optional(keys :req-un [:irq0-appconfig/source-tags :irq0-appconfig/hours :irq0-appconfig/limit :irq0-appconfig/acuity :irq0-appconfig/cutoff :irq0-appconfig/random-seed] :opt-un [:irq0-appconfig/max-feature-frequency-ratio :irq0-appconfig/min-match-score :irq0-appconfig/max-clusters :irq0-appconfig/max-single-source-clusters]){:source-tags #{:news}, :acuity 1.0, :min-match-score 0.15, :limit 350, :random-seed 1, :max-feature-frequency-ratio 0.2, :hours 24, :max-single-source-clusters 4, :max-clusters 12, :cutoff 0.002}
[:vibe :source-tags]required(coll-of keyword? :kind set?)#{:news}
[:vibe :hours]requiredpos-int?24
[:vibe :limit]requiredpos-int?350
[:vibe :acuity]requirednumber?1.0
[:vibe :cutoff]requirednumber?0.002
[:vibe :random-seed]requiredint?1
[:vibe :max-feature-frequency-ratio]optional(and number? (<= 0 % 1))0.2
[:vibe :min-match-score]optional(and number? (<= 0 % 1))0.15
[:vibe :max-clusters]optionalpos-int?12
[:vibe :max-single-source-clusters]optionalnat-int?4
[:export]optional(keys :opt-un [:irq0-appconfig/url-handler])
[:export :url-handler]optional(nilable (keys :req-un [:irq0-appconfig/template] :opt-un [:irq0-appconfig/name :irq0-appconfig/icon]))
[:http]optional(keys :opt-un [:irq0-appconfig/max-body-bytes :irq0-appconfig/max-blob-body-bytes :irq0-appconfig/connection-timeout-ms :irq0-appconfig/connection-request-timeout-ms :irq0-appconfig/socket-timeout-ms]){:connection-timeout-ms 10000, :connection-request-timeout-ms 10000, :socket-timeout-ms 120000, :max-body-bytes 15728640, :max-blob-body-bytes 52428800}
[:http :max-body-bytes]optionalpos-int?15728640
[:http :max-blob-body-bytes]optionalpos-int?52428800
[:http :connection-timeout-ms]optionalpos-int?10000
[:http :connection-request-timeout-ms]optionalpos-int?10000
[:http :socket-timeout-ms]optionalpos-int?120000
[:mail]optional(keys :opt-un [:irq0-appconfig/from :irq0-appconfig/host :irq0-appconfig/port :irq0-appconfig/credentials :irq0-appconfig/tls :irq0-appconfig/starttls])
[:mail :from]optionalstring?
[:mail :host]optionalstring?
[:mail :port]optionalpos-int?
[:mail :credentials]optionalkeyword?
[:mail :tls]optionalboolean?
[:mail :starttls]optionalboolean?