- HTML 66.8%
- Elixir 24.9%
- CSS 5.8%
- JavaScript 1.9%
- Dockerfile 0.4%
- Other 0.1%
Bookpile
Enter an ISBN, get a list of bookshops and libraries near you. No affiliate links, no cookies. AGPL-3.0-or-later.
How it works
bookpile.org/b/<isbn> looks up book metadata, detects the visitor's location
from their IP address (server-side, using a local MaxMind database), and returns
nearby shops and libraries. The URL works for everyone - each visitor sees
results for their own location.
The list of shops and libraries lives in
priv/websites.json. Pull requests to add or fix
entries are welcome.
websites.json format
{
"regions": {
"DE": {
// ISO 3166-1 alpha-2 country code
"shops": [
/* shown for every city in this country */
],
"libraries": [
/* shown for every city in this country */
],
"cities": {
"Berlin": {
"lat": 52.52, // used for nearest-city matching
"lng": 13.405,
"shops": [
/* city-specific shops */
],
"libraries": [
/* city-specific libraries */
],
},
},
},
},
"everywhere": {
// shown to all visitors regardless of location
"shops": [],
"libraries": [],
"trackers": [], // book-tracking sites (Goodreads, StoryGraph, ...)
},
}
The country code is used to look up the display name (e.g. "Germany") and flag emoji
at runtime via the browser's Intl.DisplayNames API.
Each entry in any list follows the same shape:
{
"name": "Buchbox Berlin",
"note": "Short description shown in the UI",
"url": "https://example.com/search?q={{ISBN}}", // {{ISBN}} or {{TITLE}}
"tags": ["Indie"], // optional, see below
}
URL placeholders: {{ISBN}} is replaced with the book's ISBN; use {{TITLE}}
for services that don't support ISBN search.
Tags control sort order within the national/everywhere shop list:
| Tag | Meaning |
|---|---|
"Indie" |
Independent shop — sorted to the top |
"International" |
Ships worldwide — sorted to the bottom |
The full structure is defined in priv/websites_schema.json
and is validated against it automatically when running mix test.
Dependencies
- Elixir ≥ 1.14 and Erlang/OTP ≥ 25
- PostgreSQL ≥ 14
- Node.js (for esbuild asset bundling)
- A Google Books API key
- A MaxMind GeoLite2-City database file (free, requires registration)
Running locally
# 1. Install Elixir dependencies
mix deps.get
# 2. Copy the environment template and fill in your values
cp .env.example .env
# Set BOOKPILE_GOOGLE_API_KEY in .env
# 3. Place the MaxMind database
mkdir -p priv/geo
cp /path/to/GeoLite2-City.mmdb priv/geo/
# 4. Create and migrate the database
mix ecto.setup
# 5. Start the server
mix phx.server
Visit localhost:4000.
S3 cover image caching (optional)
Set these environment variables to have book cover images downloaded and stored in S3 instead of served from external sources:
| Variable | Description |
|---|---|
BOOKPILE_S3_BUCKET |
Bucket name — setting this enables the feature |
BOOKPILE_S3_REGION |
Region (default: us-east-1) |
BOOKPILE_S3_ENDPOINT |
S3 API host for non-AWS providers (e.g. fsn1.your-objectstorage.com) |
BOOKPILE_S3_PUBLIC_URL |
Public base URL for stored files (e.g. https://my-bucket.fsn1.your-objectstorage.com) — defaults to the AWS URL pattern |
AWS_ACCESS_KEY_ID |
Access key |
AWS_SECRET_ACCESS_KEY |
Secret key |
Covers are stored as covers/<isbn>.<ext> with public-read ACL. If the bucket
variable is not set the feature is disabled and external image URLs are used
as-is. Works with any S3-compatible storage (AWS, Hetzner, etc.).
Development
mix test # run tests
mix check # tests + credo + dialyzer + formatter (run before committing)
mix format # format code
Assets are bundled with esbuild - no separate Node build step needed during development.