Self-hosted service
jonschoning/espial avatar
jonschoning/espial

Espial: a self-hosted bookmark server in Haskell and SQLite

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

919 stars32 forksHaskellAGPL-3.0

At a glance

What is it?
Espial stores bookmarks in SQLite, serves them over a small HTTP API, and ships a bookmarklet for one-click saving. It is aimed at people who want their own bookmark database rather than a hosted service, and the Docker path is the one the README recommends.
Who is it for?
Espial fits a self-hoster who wants a multi-user bookmark database with a small API surface and does not mind SQLite as the single storage engine. It is a poor fit if you need a hosted service, a sync protocol across many devices, or a database other than SQLite.
Can I use it commercially?
Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
Is it still maintained?
Yes. The repository last received commits 45 days ago.
What is it written in?
Mainly Haskell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem Espial solves: bookmarks you own, in one file

Browser bookmark sync ties your saved links to a vendor account. Espial takes the opposite position. The README describes it as an open-source, web-based bookmarking server that supports multiple user accounts and is primarily intended for self-hosted deployments. Bookmarks live in a SQLite database, which the README justifies on the grounds of keeping setup and maintenance straightforward. That single choice drives most of the project's character: there is no database server to run, no connection string to configure, and no separate migration service to keep alive. The cost is that SQLite is the only storage option the material describes, so anyone who wants PostgreSQL for operational reasons is looking at the wrong project. The intended user is someone running a small server for themselves, a household, or a small group, who wants a web UI plus a scriptable way to add links. The presence of a demo instance with the credentials demo/demo suggests the project expects people to look before installing, which is a reasonable signal for a tool whose value is mostly in the interface.

How the pieces fit: SQLite, a migration binary, and one HTTP endpoint

Two executables appear in the source instructions. The first is migration, which owns the database lifecycle: it creates the database, creates users, creates API keys, and imports bookmark files. The second is espial, which runs the server. That split matters because it means schema and user management happen out of band, not through a first-run web wizard. The web application serves the bookmark list, the settings page, and the import/export page. A bookmarklet provides the fastest capture path for a logged-in user: the README says it opens a small popup with the URL, title, and any selected text pre-filled as the description, and the user adds tags or edits fields before saving. Programmatic capture goes through POST /api/add, authenticated with an API key in an Authorization: ApiKey header. Only url is required in the JSON body. The response is 201 Created with the new bookmark id on a create, or 204 No Content when an existing bookmark was updated instead. The match rule is explicit: by bid if given, otherwise by the (userid, url) pair. That is a small detail with real consequences, because it means re-posting the same URL without a bid silently updates rather than duplicates. Fields include title, description, a space-separated tags string, private, toread, bid, and slug, where slug is described as the URL-friendly identifier used in bookmark links of the form u:<user>/<slug> and is auto-generated when omitted.

Getting it running: the docker run line and the migration commands

The README calls Docker the recommended method and points to a separate repository, jonschoning/espial-docker, for the fuller setup. For a trial, it gives a single command: MSYS_NO_PATHCONV=1 docker run --name espial -p 9090:3000 -v espial-data:/app/data -d jonschoning/espial:espial. The port mapping is host 9090 to container 3000, and the named volume espial-data is mounted at /app/data, which is where the SQLite database is stored. The MSYS_NO_PATHCONV prefix is a Windows/MSYS detail; on Linux it is unnecessary. After the container is up, user creation runs through the same binary: docker exec espial ./migration createuser --userName myusername --userPassword myuserpassword. From there you either use the bookmarklet or import from the settings page, and the README suggests sample-bookmarks.json as a test file. The source path is longer. Install Stack or GHCup, run stack build, then stack exec migration -- createdb, then createuser with the same flags, then optionally importbookmarks --userName myusername --bookmarkFile sample-bookmarks.json or importfirefoxbookmarks for a Firefox export, and finally stack exec espial for a production server. API keys can be generated either from the settings page under API Key, where the README warns the key is shown only once, or from the CLI with createapikey --userName myusername.

The bookmarklet and the API are two different capture paths with different failure modes

The bookmarklet is a browser bookmark whose target is a URL on your Espial instance. It works while you are logged in and viewing another page, which is the moment you actually decide to save something. Its limitation is structural rather than a bug: it is a bookmark, so it depends on a logged-in session cookie and on the popup being permitted. The API path has the opposite profile. It needs no session, only a key, so it suits scripts and the companion Android app that the README links for share-intent capture. But the key is a bearer credential with no scopes described in the material, and the README notes it is displayed only once at creation, with a Reset API Key action to replace it. Anyone who leaks the key and does not reset it has handed over the ability to add and update bookmarks for that user. The update-by-URL behaviour amplifies this slightly: a leaked key lets someone overwrite the title, description, tags, or private flag of any existing bookmark whose URL they know, not just append new ones. That is worth knowing before exposing /api/add beyond a trusted network.

Internationalization, imports, and what the README does not describe

The README lists internationalization as a supported feature and lists i18n and multilingual among the repository topics, but it does not enumerate the available locales or explain how translations are contributed. If locale coverage matters to you, that is something to check on the demo instance rather than infer from the topic tags. Import support is better documented: Pinboard-format files through importbookmarks, Firefox exports through importfirefoxbookmarks, and a settings page with Import / Export in the web UI. Export is named in that page title, and the API add endpoint is documented field by field, but the material does not document a corresponding export endpoint or a full API reference beyond /api/add. The README table also appears truncated mid-row at the slug entry, so treat the field list as the documented set rather than a guarantee that no other fields exist. None of this is unusual for a project at version 0.0.42, and the release cadence in the supplied data (v0.0.40, v0.0.41, v0.0.42 across three consecutive weeks in July 2026, with a push in early August) suggests active but pre-1.0 development, where interface details can still move.

Where Espial is the wrong tool

Three cases stand out. First, if you want automatic capture of everything you browse, Espial does not do that. Saving is an explicit act: click the bookmarklet, or POST to /api/add. There is no browser extension described in the material, only a bookmarklet and a separate Android app. Second, if you need bookmarks to sync bidirectionally with a browser's native bookmark store, Espial is a destination, not a peer. The Firefox path is an import command, which reads a file; nothing in the README describes ongoing two-way sync. Third, if your operational requirements include a database other than SQLite, or a multi-node deployment with a shared database, the storage choice rules Espial out. The SQLite decision that makes setup easy is the same decision that constrains scaling and concurrent-write behaviour, and the README does not discuss either. There is also the ordinary self-hosting cost: you own the container, the volume, and the backups, and the README's single docker run command creates a named volume whose contents you must copy yourself.

The realistic alternative, and how the approach differs

The obvious comparison is a hosted bookmark service, and the difference is not just who runs the server. A hosted service typically owns identity, storage, and the sync protocol, and gives you a client on every platform plus a shareable public page as part of the product. Espial gives you the server and a web UI, and leaves clients to a bookmarklet, an API, and a third-party Android app. What you gain is control of the data file and the ability to script writes against a documented JSON endpoint on your own host. What you give up is the polished multi-platform client story and any guarantee about uptime other than your own. A second comparison is a plain browser profile with sync enabled, which is genuinely less work and covers the common case of one person on two devices. Espial becomes the better answer when the requirement is multiple user accounts on one instance, or a bookmark database you can query and back up as a file, or an endpoint that other tools can POST to. If none of those three applies, the browser's built-in sync is the cheaper choice and there is no shame in it.

Licence, maintenance, and what to check before you commit

Espial is AGPL-3.0. That is a copyleft licence with a network-use clause, which in practice means the obligations attach to modified versions you let other people interact with over a network, not just to distributed binaries. Running an unmodified container for your household is a different situation from forking the code and offering it as a service. This is a description of the licence's general shape, not legal advice; if you plan to modify and expose Espial, read the licence text or ask someone qualified. On maintenance, the version numbering is the honest signal. Releases sit at v0.0.x, and the supplied data shows three releases in three weeks followed by a push a week later. That pattern means upgrades can arrive often and interfaces are not frozen, so pinning a specific image tag rather than tracking a moving tag is the safer default. The upgrade procedure itself is not described in the README beyond the Docker and source paths, and no migration-on-upgrade step is documented, so the thing to verify first is how schema changes are applied between versions. Check the espial-docker repository for that, since the README defers the recommended deployment to it.

Editorial conclusion

Espial fits a self-hoster who wants a multi-user bookmark database with a small API surface and does not mind SQLite as the single storage engine. It is a poor fit if you need a hosted service, a sync protocol across many devices, or a database other than SQLite. Before adopting it, verify two things in your own environment: that the AGPL-3.0 licence is acceptable for how you intend to run and modify it, and that your backup routine actually copies the file inside the espial-data volume, since the README puts the database there and the container is the only thing writing to it.

Official sources

  1. Issues
  2. jonschoning/espial on GitHub
  3. License: AGPL-3.0
  4. README
  5. Releases
Community notes

Community notes