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, thenLLAR_CONFIG, then the JVM-Dconfig=...file; later files replace complete top-level values. - Runtime config
.llarfiles 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/seedint?
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/querystring?: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/querystring?: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-daytime | Daily at 10:00, 12:00, 13:00, 14:00, 16:00, and 18:00. |
|---|---|
:sundays | Weekly on Sunday at 05:00. |
:early-morning | Daily at 07:00. |
:now-and-early-morning | Once within the next 0-120 seconds, then daily at 07:00. |
:noon | Daily at 12:00. |
:now-and-noon | Once within the next 0-120 seconds, then daily at 12:00. |
:now-and-hourly | Once within the next 0-120 seconds, then every hour. |
:hourly | Every hour. |
:every-4-hours | Every 4 hours at 00:00, 04:00, 08:00, 12:00, 16:00, and 20:00. |
:now-and-every-4-hours | Once within the next 0-120 seconds, then every 4 hours. |
:now-and-every-15-minutes | Once within the next 0-120 seconds, then every 15 minutes. |
:now-and-every-5-minutes | Once within the next 0-120 seconds, then every 5 minutes. |
:now-and-every-minute | Once within the next 0-120 seconds, then every minute. |
Processing Hooks
:pre, :rm, and :post forms are evaluated with these bindings.
$item | The full item. |
|---|---|
$key | Source key as keyword. |
$title | Item title, or empty string. |
$authors | Item authors, or empty string. |
$tags | Source tags configured on the fetch definition. |
$raw | Raw fetched data when supported by the source. |
$url | Item URL, or empty string. |
$html | HTML content, or empty string. |
$text | Plain text content, or empty string. |
$score | Score for sources such as Reddit or Hacker News, or -1. |
$options | Source options configured on the fetch definition. |
$entry | The item entry map. |
Source Predicate Bindings
sched-fetch and autoread predicates are evaluated with these bindings.
$KEY | Source key as keyword. |
|---|---|
$SRC | Source constructor value. |
$TAGS | Source tags configured on the fetch definition. |
Helper Bindings
$add-tag | Return a processor that adds an item tag. |
|---|---|
$add-tag-filter | Return a processor that adds a tag when a predicate matches. |
$category-rm | Build a filter that removes items by feed category. |
$credentials | Read an entry from credentials.edn. |
$ellipsify | Truncate text with an ellipsis. |
$exchange | Swap two item paths. |
$extract | Run article extraction on HTML content. |
$fetch | Run LLAR HTTP fetch. |
$hickory-sanitize-blobify | Sanitize and blobify Hickory content. |
$hickory-to-html | Render Hickory as HTML. |
$html-to-hickory | Parse HTML into Hickory. |
$html2text | Convert HTML to plain text. |
$http-cookie-store | Create a clj-http cookie store. |
$http-get | Call clj-http.client/get. |
$http-post | Call clj-http.client/post. |
$make-item-hash | Create a stable LLAR item hash. |
$parse-ts | Parse a timestamp into a zoned date time. |
$parse-url | Parse or absolutify URLs. |
$uri-path | Read the path from a URI. |
Available Namespaces
src | llar.src |
|---|---|
string | clojure.string |
log | clojure.tools.logging |
S | hickory.select |
time | java-time.api |
Feature Examples
Feature-specific configuration examples that are not top-level .llar constructs.
zotero-export-links
credentials.ednOptions 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.
| Path | Description | Spec | System config path | Example |
|---|---|---|---|---|
[: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-enabledboolean? | (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.
: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"}}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
Authorizationheader. The write endpoint isPOST <base-url>/api/v1/captureswith a JSON body containingurland optionaltitle. 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 incredentials.edn, restrict that file to the LLAR account (for examplechmod 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:
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.javascript:location.href='https://save.example.org/#REPLACE_WITH_TOKEN:'+location.href - 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.ednif 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/captureswith method POST, JSON body{"url": <Shortcut Input>}, and headerAuthorization: Bearer <iphone token>. Finish with Show Result using the response'smessage. 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
:scheduleto 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 withllar_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.
| Path | Status | Schema | Default |
|---|---|---|---|
[: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] | required | pos-int? | 8023 |
[:api :reader :base-url] | optional | string? | "http://localhost:8023" |
[:api :dashboard] | optional | (keys :req-un [:irq0-appconfig/port] :opt-un [:irq0-appconfig/config-lab]) | {:port 9999} |
[:api :dashboard :port] | required | pos-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?] | required | boolean? | |
[:api :dashboard :config-lab :credentials] | optional | keyword? | |
[:api :dashboard :config-lab :max-concurrent-runs] | optional | pos-int? | |
[:api :dashboard :config-lab :run-timeout-ms] | optional | pos-int? | |
[:api :dashboard :config-lab :session-ttl-minutes] | optional | pos-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] | required | pos-int? | 8024 |
[:api :podcast :host] | optional | string? | "127.0.0.1" |
[:api :podcast :base-url] | optional | string? | "http://localhost:8024" |
[:api :podcast :video-format] | optional | string? | "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] | required | pos-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] | required | pos-int? | |
[:api :fever :username] | required | string? | |
[:api :fever :credentials] | required | keyword? | |
[:api :fever :source-tag] | optional | keyword? | :mobile |
[:api :fever :base-url] | optional | string? | |
[:api :fever :initial-days] | optional | pos-int? | 30 |
[:api :fever :recent-read-days] | optional | pos-int? | 10 |
[:api :fever :max-content-bytes] | optional | pos-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] | required | pos-int? | |
[:api :capture :base-url] | required | string? | |
[:api :capture :credentials] | required | keyword? | |
[:api :capture :schedule] | optional | keyword? | |
[: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] | required | string? | |
[:api :digest :from] | optional | string? | |
[:api :digest :schedule] | optional | keyword? | |
[:api :digest :inline-images?] | optional | boolean? | |
[:api :digest :keep-unread-issues] | optional | nat-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] | required | validate-options | |
[:postgresql :backend] | required | validate-options | |
[:update-max-retry] | optional | nat-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] | required | pos-int? | 20 |
[:throttle :av-downloader-max-concurrent] | optional | pos-int? | 2 |
[:throttle :streaming-max-concurrent] | optional | pos-int? | 1 |
[:throttle :source-update-max-concurrent] | optional | pos-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] | optional | number? | 48 |
[:ranking :rarity-boost-cap-hours] | optional | number? | 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] | required | pos-int? | 24 |
[:vibe :limit] | required | pos-int? | 350 |
[:vibe :acuity] | required | number? | 1.0 |
[:vibe :cutoff] | required | number? | 0.002 |
[:vibe :random-seed] | required | int? | 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] | optional | pos-int? | 12 |
[:vibe :max-single-source-clusters] | optional | nat-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] | optional | pos-int? | 15728640 |
[:http :max-blob-body-bytes] | optional | pos-int? | 52428800 |
[:http :connection-timeout-ms] | optional | pos-int? | 10000 |
[:http :connection-request-timeout-ms] | optional | pos-int? | 10000 |
[:http :socket-timeout-ms] | optional | pos-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] | optional | string? | |
[:mail :host] | optional | string? | |
[:mail :port] | optional | pos-int? | |
[:mail :credentials] | optional | keyword? | |
[:mail :tls] | optional | boolean? | |
[:mail :starttls] | optional | boolean? |