espial: Espial is an open-source, web-based bookmarking server.

[ agpl, library, web ] [ Propose Tags ] [ Report a vulnerability ]

Espial is an open-source, web-based bookmarking server. - Yesod + TypeScript + sqlite3 - multi-user (w/ privacy scopes) - multi-language (i18n) - tags, stars, editing, deleting


[Skip to Readme]

Flags

Manual Flags

NameDescriptionDefault
usebuildinfo

read buildinfo from generated file buildinfo/BuildInfo.hs

Disabled
werror

Treat warnings as errors in executables. Off by default so the package still builds against future GHC versions that add new warnings; enabled for development via stack.yaml.

Disabled
Automatic Flags
NameDescriptionDefault
dev

Turn on development settings, like auto-reload templates.

Disabled
library-only

Build for use with "yesod devel"

Disabled

Use -f <flag> to enable a flag, or -f -<flag> to disable that flag. More info

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

  • No Candidates
Versions [RSS] 0.0.1, 0.0.2, 0.0.3, 0.0.4, 0.0.5, 0.0.5.1, 0.0.7, 0.0.9, 0.0.10, 0.0.11, 0.0.16, 0.0.19, 0.0.20, 0.0.26, 0.0.34, 0.0.35, 0.0.36, 0.0.37, 0.0.38, 0.0.39, 0.0.40, 0.0.41 (info)
Change log changelog.md
Dependencies aeson (>=2.0), attoparsec, base (>=4.18.1.0 && <5), base64 (>=1 && <2), bcrypt (>=0.0.8), blaze-html (>=0.9 && <1), blaze-markup, bytestring (>=0.11.5 && <0.13), case-insensitive, classy-prelude (>=1.4 && <1.6), classy-prelude-conduit (>=1.4 && <1.6), classy-prelude-yesod (>=1.4 && <1.6), conduit (>=1.0 && <2), containers, cryptohash-sha256, crypton (>=0.30), crypton-connection, data-default, directory (>=1.1 && <1.4), entropy, espial, esqueleto (>=3.5.11.1 && <3.7), fast-logger (>=2.2 && <4), fgl, file-embed, filepath, foreign-store, gitrev, hjsmin (>=0.1 && <0.3), hscolour, html-entities, http-api-data (>=0.3.4), http-client, http-client-tls (>=0.3 && <0.5), http-conduit (>=2.3 && <2.4), http-types, iproute, microlens, monad-logger (>=0.3 && <0.4), mtl, network, optparse-applicative, optparse-generic (>=1.2.3), parser-combinators, persistent (>=2.14 && <2.19), persistent-sqlite (==2.13.3.0), persistent-template (>=2.12 && <2.13), pretty-show, safe, shakespeare (>=2.0 && <2.3), stm, template-haskell, text (>=2 && <3), text-short, time, tls, transformers (>=0.2.2), unix, unordered-containers, vector, wai, wai-extra (>=3.0 && <3.2), wai-logger, warp (>=3.0 && <3.5), warp-tls (>=3.0 && <3.5), yaml (>=0.8 && <0.12), yesod (>=1.6 && <1.8), yesod-auth (>=1.6 && <1.8), yesod-core (>=1.6 && <1.8), yesod-form (>=1.6 && <1.8), yesod-newsfeed (>=1.6 && <1.8), yesod-static (>=1.6 && <1.8) [details]
Tested with ghc ==9.10.3
License AGPL-3.0-or-later
Copyright Copyright (c) 2018 Jon Schoning
Author Jon Schoning
Maintainer jonschoning@gmail.com
Uploaded by jonschoning at 2026-07-18T19:27:54Z
Category Web
Home page https://github.com/jonschoning/espial
Bug tracker https://github.com/jonschoning/espial/issues
Source repo head: git clone https://github.com/jonschoning/espial.git
Distributions
Executables migration, espial
Downloads 4498 total (79 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for espial-0.0.41

[back to package description]

Espial

Espial is an open-source, web-based bookmarking server.

It supports multiple user accounts and is primarily intended for self-hosted deployments.

Bookmarks are stored in a SQLite database to keep setup and maintenance straightforward.

Espial also includes internationalization support.

Adding Bookmarks

The easist way for logged-in users to add bookmarks, is with the "bookmarklet", found on the Settings page.

Espial also supports file import options.

Demo Server

Log in with:

  • username: demo
  • password: demo

https://espdemo.ae8.org/u:demo

jpg

Also, see the android app for adding bookmarks via an Android Share intent:

https://github.com/jonschoning/espial-share-android

Installation

Docker installation is the recommended approach for most deployments.

See:

https://github.com/jonschoning/espial-docker

Setup From Source

  1. Install Haskell tooling (choose one):
  2. Build executables:
stack build
  1. Create the database:
stack exec migration -- createdb
  1. Create a user:
stack exec migration -- createuser --userName myusername --userPassword myuserpassword
  1. Import a pinboard bookmark file for a user (optional):
stack exec migration -- importbookmarks --userName myusername --bookmarkFile sample-bookmarks.json
  1. Import a firefox bookmark file for a user (optional):
stack exec migration -- importfirefoxbookmarks --userName myusername --bookmarkFile firefox-bookmarks.json
  1. Start a production server:
stack exec espial

API

Adding a Bookmark via curl

Bookmarks can be added programmatically by POSTing JSON to /api/add, authenticated with an API key.

  1. Generate an API key for a user, either:

    • From the Account Settings (settings) page in the web UI, under API Key — click Create API Key (or Reset API Key to replace an existing one). The key is shown only once, so copy it immediately.

    • Or via the CLI:

      stack exec migration -- createapikey --userName myusername
      
  2. Call the endpoint, passing the key in the Authorization: ApiKey <key> header:

Example Request:

curl -X POST https://your-espial-host/api/add \
  -H "Authorization: ApiKey <key>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
        "url": "https://example.com",
        "title": "Example Site",
        "description": "",
        "tags": "example some-tag",
        "private": false,
        "toread": false
      }'

Only url is required; all other fields are optional. On success the response is 201 Created with the new bookmark id, or 204 No Content if an existing bookmark was updated instead — matched by bid if given, otherwise by the (userid, url) pair.

/api/add Fields

Field Type Description
url string The bookmark's URL. Required.
title string The bookmark's title/description line. Defaults to empty.
description string Extended/longer notes for the bookmark. Defaults to empty.
tags string Space-separated list of tags, e.g. "example some-tag".
private boolean true keeps the bookmark private to the owner; false (or omitted) makes it shared/public.
toread boolean Marks the bookmark on the "to read" list. Defaults to false.
bid number Existing bookmark id to update. Omit to create a new bookmark (or update by matching url — see above).
slug string URL-friendly identifier used in bookmark links (u:<user>/<slug>). Auto-generated when omitted.
selected boolean Whether the bookmark is "starred". Defaults to false.
time string Creation timestamp, ISO 8601 (e.g. "2018-02-26T22:57:20Z"). Defaults to the current time; useful when importing bookmarks with a historical date.
archiveUrl string Link to an already-archived copy of the page. Normally set by the archiver, not supplied by clients.
archiveRequested boolean Whether to kick off archiving of url after saving. Only takes effect when an archive backend is configured (see Archive Backends). Not stored on the bookmark itself.

Adding Multiple Bookmarks via curl

Multiple bookmarks can be added in a single request by POSTing a JSON array to /api/addBulk, using the same fields and auth as /api/add.

Example Request:

curl -X POST https://your-espial-host/api/addBulk \
  -H "Authorization: ApiKey <key>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '
  [
    {
      "url": "https://example.com/11111111",
      "title": "Example Site 1",
      "description": "Random test bookmark 11111111",
      "tags": "example test run1",
      "private": false,
      "toread": false
    },
    {
      "url": "https://example.com/22222222",
      "title": "Example Site 2",
      "description": "Random test bookmark 22222222",
      "tags": "example test run2",
      "private": false,
      "toread": false
    },
    {
      "url": "33333333",
      "title": "Example Site 3",
      "description": "Random test bookmark 33333333",
      "tags": "example test run3",
      "private": false,
      "toread": false
    }
  ]'

The response is 200 OK with a JSON array of per-bookmark results, in the same order as the request:

[
  { "status": "created", "id": 105831 },
  { "status": "updated", "id": 105832 },
  { "status": "failed", "error": "Invalid URL: InvalidUrlException \"33333333\" \"Invalid URL\"" }
]

If the request array has more items than the add-bulk-max-items setting allows (default 200), the response is 413 Payload Too Large and no bookmarks are saved.

Configuration

See config/settings.yml for changing default run-time parameters & environment variables.

  • config/settings.yml is embedded into the app executable when compiled and also read once when the app starts. Current settings in config/settings.yml will override the embedded compile-time settings.
  • config/settings.yml values formatted like _env:ENV_VAR_NAME:default_value can be overridden by the specified environment variable.
  • Example:
    • _env:PORT:3000
    • environment variable PORT
    • default app http port: 3000

Internationalization

Espial's frontend supports a selectable UI language per account, on the Account Settings (settings) page.

The Server Language default is controlled by language-default in config/settings.yml, which can also be set with environment variable LANGUAGE_DEFAULT to the language code; the default value is en.

Supported Languages:

Code English name Native name
en English English
de German Deutsch
es Spanish Español
fr French Français
it Italian Italiano
ja Japanese 日本語
ko Korean 한국어
pl Polish Polski
pt-BR Portuguese (Brazil) Português (Brasil)
ru Russian Русский
tr Turkish Türkçe
uk Ukrainian Українська
zh-Hans Chinese (Simplified) 简体中文
zh-Hant Chinese (Traditional) 繁體中文

Request IP Logging

Espial supports the IP_FROM_HEADER environment variable for request logging.

  • IP_FROM_HEADER=true: log the client IP from the X-Real-IP or X-Forwarded-For header when present, and fall back to the peer address if neither header is available.
  • IP_FROM_HEADER=false: log the peer address from the HTTP connection.

Only set IP_FROM_HEADER=true if your application is safely positioned behind a trusted reverse proxy.

TLS / Reverse Proxy

A reverse proxy is the recommended approach for production and most self-hosted deployments. For simple local or LAN setups where that is impractical, Espial can also terminate TLS directly — see Optional: In-Process TLS below.

Set SSL_ONLY=true whenever Espial is served over HTTPS (via reverse proxy or in-process TLS) to enable the Secure cookie flag and HTTP→HTTPS redirects.

Running Espial behind a reverse proxy is the recommended approach for production and most self-hosted deployments. The proxy terminates TLS and forwards plain HTTP to Espial. This gives you automatic certificate management, HTTP/2 and HTTP/3 at the edge, and cleaner separation of concerns.

For container-based deployment examples, including production-oriented layouts, see the espial-docker repository:

Minimal Caddy example:

Localhost without a real domain:

https://localhost:3050 {
    reverse_proxy localhost:3000
}

or with a domain:

espial.example.com {
  reverse_proxy 127.0.0.1:3000
}

With the domain setup:

  • Caddy terminates TLS for espial.example.com.
  • Espial continues listening on HTTP, locally on 127.0.0.1:3000
    • If using Docker Compose, it would look like espial:3000
  • Set IP_FROM_HEADER=true only when Espial is reachable solely through that trusted proxy.

If you are using Cloudflare:

  • Prefer Cloudflare SSL mode Full (strict).
  • use header_up X-Forwarded-For {http.request.header.CF-Connecting-IP}
  • If traffic can reach Espial directly without passing through your trusted proxy, do not enable IP_FROM_HEADER=true, because client IP headers can be spoofed.

Running on a Subpath

Espial can also be served under a path prefix on a shared domain, e.g. https://www.domain.com/espial alongside other apps on the same host. This needs two pieces working together:

  1. APPROOT — tell Espial the full external URL (including the subpath) it's being served at, so generated links, redirects, and static asset URLs come out correct:

    approot: "_env:APPROOT:https://www.domain.com/espial"
    

    or via environment variable:

    APPROOT=https://www.domain.com/espial stack exec espial
    
  2. Reverse proxy — strip the /espial prefix before forwarding to Espial, since Espial itself always routes as if mounted at /. Also redirect the bare /espial (no trailing slash) to /espial/ so relative URLs in the page resolve against the right base.

    Minimal Caddy example (see caddy/Caddyfile-Standalone-Subpath, which runs Caddy in Docker alongside an Espial instance on the host):

    :80 {
        redir /espial /espial/
    
        handle_path /espial/* {
            # Caddy pools and reuses idle connections to the backend
            # by default, but Espial's Warp server closes idle connections after 30s
            # set keepalive to off or 15s
            reverse_proxy host.docker.internal:3000 {
                transport http {
                    keepalive off
                }
            }
        }
    }
    

    handle_path strips the /espial prefix before proxying, matching what APPROOT told Espial to expect. If Caddy and Espial run on the same host (not in Docker), replace host.docker.internal:3000 with localhost:3000.

Optional: In-Process TLS

For simple local or LAN deployments where adding a reverse proxy is impractical, Espial can terminate TLS directly using your own certificate and key files (PEM format, unencrypted key).

Set the TLS_CERT_FILE and TLS_KEY_FILE environment variables (or the corresponding tls-cert-file / tls-key-file keys in config/settings.yml) to the paths of your certificate and private key:

TLS_CERT_FILE=/path/to/cert.pem TLS_KEY_FILE=/path/to/key.pem stack exec espial

Or in config/settings.yml:

tls-cert-file: "/path/to/cert.pem"
tls-key-file: "/path/to/key.pem"

When both values are set Espial listens on HTTPS with HTTP/2 enabled. When either is absent Espial falls back to plain HTTP.

Certificate rotation — Espial reloads the certificate from disk automatically every 12 hours without restarting or dropping existing connections. To trigger an immediate reload send SIGHUP to the process:

kill -HUP <espial-pid>

Notes:

  • Let's Encrypt certificates (fullchain.pem + privkey.pem) work directly.
  • A self-signed certificate for local testing can be generated with:
    openssl req -x509 -newkey rsa:2048 -nodes \
      -keyout key.pem -out cert.pem -days 3650 \
      -subj "/CN=localhost" \
      -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
    
  • A reverse proxy is still preferred for production: it provides HTTP/3, edge caching, and hides the Espial process from the public internet.

Archive Backends

Espial supports configurable archive backends for saving bookmark snapshots.

Set the backend with archive-backend in config/settings.yml:

  • disabled: archiving is turned off (default).

  • wayback-machine: enables submission to the Internet Archive Wayback Machine.

    Wayback Machine support requires the following settings:

    • wayback-machine-access-key

    • wayback-machine-secret-key

      Create these by signing in to your Internet Archive account and generating S3-style API credentials at https://archive.org/account/s3.php.
      If wayback-machine is selected but the access key or secret key is missing, archiving is disabled at runtime.

  • archivebox07: queues the URL in a local ArchiveBox 0.7 instance and stores an ArchiveBox link on the bookmark.

    IMPORTANT - ArchiveBox stores all archive data in a single global index space, so this arcive-backend is best suited to single-user Espial instances.

    Recommended setup is to use Docker Compose to run the ArchiveBox instance

    ArchiveBox support requires the following settings:

    • archivebox-url

      archivebox-url is the URL espial uses to sign in to ArchiveBox and submit URLs through the web UI. In Docker Compose this is typically http://archivebox:8000.

    • archivebox-public-url (optional)

      Public ArchiveBox URL stored on bookmarks.

    • archivebox-username plus archivebox-password

      Espial signs in to the ArchiveBox web UI with these credentials before submitting URLs.

    • archivebox-tag (optional)

      A tag Espial adds to submissions (example: espial).

    • archivebox-plugins (optional)

      Comma-separated list of ArchiveBox methods (plugins) to request when submitting URLs, e.g. title,favicon,singlefile,screenshot.

    Set the ArchiveBox admin credentials in the override path by supplying:

    • ARCHIVEBOX_USERNAME=...
    • ARCHIVEBOX_PASSWORD=...

    The Makefile includes the following helpers:

    • docker-compose-up-archivebox07
    • docker-compose-up-d-archivebox07
    • docker-compose-exec-archivebox07

    Or start the instance manually via docker compose, example:

    docker compose -f docker-compose.archivebox07.yml up
    

    Configure the enrivonment variable ARCHIVE_METHODS to control which archive methods ArchiveBox uses:

    environment:
      - ARCHIVE_METHODS=title,favicon,singlefile,screenshot
    

    Available ARCHIVE_METHODS plugins:

    • archive_org, dom, favicon, git, headers, htmltotext, media, mercury, pdf, readability, screenshot, singlefile, title, wget

    If ARCHIVE_METHODS is unset/not-present, ArchiveBox will uses all plugins.

    For additional information and configuration, refer to the ArchiveBox repository

Optional proxy settings for archive requests:

  • archive-socks-proxy-host
  • archive-socks-proxy-port

CLI

Migration commands are run via:

stack exec migration -- <command> [options]

All commands take an optional --conn parameter for the database location; if omitted, the database location is loaded from config/settings.yml or environment variable SQLITE_DATABASE

Commands

Command Example
createdb stack exec migration -- createdb
createuser stack exec migration -- createuser --userName myusername --userPassword myuserpassword
createuser (password file) stack exec migration -- createuser --userName myusername --userPasswordFile mypassword.txt
deleteuser stack exec migration -- deleteuser --userName myusername
createapikey stack exec migration -- createapikey --userName myusername
deleteapikey stack exec migration -- deleteapikey --userName myusername
importbookmarks stack exec migration -- importbookmarks --userName myusername --bookmarkFile sample-bookmarks.json
importfirefoxbookmarks stack exec migration -- importfirefoxbookmarks --userName myusername --bookmarkFile firefox-bookmarks.json
importnetscapebookmarks stack exec migration -- importnetscapebookmarks --userName myusername --bookmarkFile bookmarks.html
importnotes stack exec migration -- importnotes --userName myusername --noteDirectory ./notes
importnotesjson stack exec migration -- importnotesjson --userName myusername --noteFile exported-notes.json
exportbookmarks stack exec migration -- exportbookmarks --userName myusername --bookmarkFile exported-bookmarks.json
exportnetscapebookmarks stack exec migration -- exportnetscapebookmarks --userName myusername --bookmarkFile exported-bookmarks.html
exportnotesjson stack exec migration -- exportnotesjson --userName myusername --noteFile exported-notes.json
printmigratedb stack exec migration -- printmigratedb
runmigratedb stack exec migration -- runmigratedb
showuser stack exec migration -- showuser --userName myusername

importbookmarks Command Notes:

See sample-bookmarks.json, which contains a JSON array, each line containing a FileBookmark object.

Example:

[
  {
    "href": "http://raganwald.com/2018/02/23/forde.html",
    "description": "Forde's Tenth Rule, or, \"How I Learned to Stop Worrying and \u2764\ufe0f the State Machine\"",
    "extended": "",
    "time": "2018-02-26T22:57:20Z",
    "shared": "yes",
    "toread": "yes",
    "tags": "raganwald"
  },
  ,
  {
    "href": "http://downloads.haskell.org/~ghc/latest/docs/html/users_guide/flags.html",
    "description": "7.6. Flag reference \u2014 Glasgow Haskell Compiler 8.2.2 User's Guide",
    "extended": "-fprint-expanded-synonyms",
    "time": "2018-02-26T21:52:02Z",
    "shared": "yes",
    "toread": "no",
    "tags": "ghc haskell"
  }
]