Your book highlights, film lines and favourite quotes, in one place you host yourself.
Keep them, find them in a second, and actually remember them.
Try the demo Β· Roadmap Β· Design log Β· UI glossary
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
Shot from a real library with its notes and tags replaced. The demo is always the latest interface.
![]() |
A short Daily Quiz brings quotes back just before you would forget them, using a real spaced-repetition model, and leads with the ones you have not been asked yet. Five kinds of question, including fill-in-the-blank, and a Practice mode whenever you want more. How it works. |
![]() |
Type a title, pick the right match, and the cover, cast and blurb arrive from TMDB, IGDB, Google Books and others. Nothing to type by hand. |
Search every title, person, quote, note and tag at once. It forgives typos, and typing
author:, tag: or colour: suggests words from your own library.
|
![]() |
![]() |
Gather quotes into your own reading order, write between them, and export the result as Markdown or EPUB. |
| Tippani draws a quote card on your device, in your theme, with the speaker's portrait behind it, and sends it to your phone's share sheet. Plain text, Markdown, WhatsApp and Reddit formats too. | ![]() |
| English and Bengali ship in the box, and a quote keeps its own script and translation. Adding another interface language is one text file, with no rebuild. | ![]() |
And also: several accounts on one server, sign-in through your own identity provider (Authelia, Authentik, Keycloakβ¦), Pushover notifications, a gethomepage widget, detailed stats, full export, and encrypted backups.
- One ~29 MB binary with the interface built in. No Node, no separate database server.
- About 30 MB of memory when idle. Nothing runs unless somebody asked for it, and nothing wakes on a timer.
- Covers are stored on your own disk. Metadata lookups are optional, and nothing is fetched on a timer.
TIPPANI_OFFLINE=1stops every outside connection except sign-in to your own identity provider. Every call that does go out is kept in its job's log, or in the system log an admin reads, and both are read and exported in Settings β Jobs.- Tippani was written with AI assistance and contains no AI: no model calls, nothing sent anywhere. How this was written.
docker run -d --name tippani --restart unless-stopped -p 8080:8080 -v tippani-data:/data ghcr.io/aaronified/tippani:latestOr with Compose:
services:
tippani:
image: ghcr.io/aaronified/tippani:latest
container_name: tippani
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- tippani-data:/data
volumes:
tippani-data:Open http://<host>:8080 and create your admin account straight away: until the first account exists,
whoever reaches the page first becomes admin. The admin adds everyone else from Profile.
The image runs on linux/amd64 (tested) and linux/arm64 (published, not yet tested).
Metadata keys (TMDB, TheTVDB, Google Books) are set in the app, under Metadata βΊ Sources. The published
images include built-in TMDB and TheTVDB credentials, so films and shows work with nothing configured; a key you
save always wins. A binary you build yourself has no built-in key until you pass
make build TMDB_TOKEN=β¦ TVDB_TOKEN=β¦.
| Setting | Default | What it does |
|---|---|---|
/data volume |
tippani-data |
Everything Tippani keeps: the database (your library, and 30 days of jobs and logs), covers, translations and backups. Must be writable by uid 65532. |
TIPPANI_BIND |
0.0.0.0:8080 in the image |
Listen address. Publish 127.0.0.1:8080:8080 to keep it local behind a proxy or VPN. |
TIPPANI_TLS_CERT / TIPPANI_TLS_KEY |
unset | A PEM certificate and key. Tippani then serves HTTPS itself and picks up renewals automatically. |
TIPPANI_COOKIE_SECURE |
0 |
Set 1 when a proxy in front handles HTTPS. |
TIPPANI_TRUSTED_PROXY |
0 |
Set 1 to trust X-Forwarded-* headers from your proxy. |
TIPPANI_OFFLINE |
0 |
Set 1 to block every outside connection except sign-in to your own identity provider. Your library still works. |
TIPPANI_OIDC_* |
unset | Single sign-on. See below. |
TIPPANI_PUSHOVER_TOKEN |
unset | A shared Pushover app token, so each reader only needs their own user key. |
TIPPANI_DOCKER_HOST |
unset | Docker Engine address for one-click updates, e.g. tcp://dockerproxy:2375. |
TIPPANI_LOG_LEVEL |
info |
debug for detailed logs. Every TIP-* code is in Troubleshooting. |
Commands (docker exec -i tippani /tippani β¦): user add <name>, user passwd <name>, user del <name>
(passwords read from stdin), notify daily, healthcheck, version.
More settings
| Setting | Default | What it does |
|---|---|---|
TIPPANI_DATA |
/data in the image |
Data directory. |
TIPPANI_DOCKER_SOCK |
/var/run/docker.sock |
Where a mounted Docker socket is. |
TIPPANI_UPDATER_IMAGE |
nickfedor/watchtower |
The one-shot image an update uses to recreate the container. |
TIPPANI_OIDC_REDIRECT_URL |
derived | Set it if your proxy rewrites the host and is not trusted. |
TIPPANI_OIDC_SCOPES |
profile email |
Extra scopes beside openid. |
GOMAXPROCS Β· GOMEMLIMIT Β· GOGC |
Go defaults | Runtime limits for a busy NAS. |
TheTVDB's free key needs your subscriber PIN beside it; both fields are in Metadata βΊ Sources.
Plain-file backup. On the host: sqlite3 tippani.db "VACUUM INTO 'backup.db'" against the /data folder.
One-click updates
Settings βΊ Server can pull the new image and restart the container. Give it Docker access through a socket proxy rather than the socket itself:
services:
dockerproxy:
image: tecnativa/docker-socket-proxy
restart: unless-stopped
environment:
CONTAINERS: 1
IMAGES: 1
POST: 1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks: [tippani-internal]
tippani:
image: ghcr.io/aaronified/tippani:latest
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- tippani-data:/data
environment:
TIPPANI_DOCKER_HOST: tcp://dockerproxy:2375
networks: [default, tippani-internal]
networks:
tippani-internal:
internal: true
volumes:
tippani-data:Or mount the socket directly: -v /var/run/docker.sock:/var/run/docker.sock:ro plus
group_add: ["<your docker group id>"] (stat -c %g /var/run/docker.sock prints it), since the image does not run as root.
One-click updates only work on a moving tag such as :latest.
[!WARNING] Updating means creating and starting containers, which is powerful access to your Docker host. The proxy narrows it (no socket in Tippani's container, most endpoints blocked) but does not remove it. Only turn it on if you want one-click updates.
Single sign-on (Authelia example)
Any OpenID Connect provider works: Authelia, Authentik, Keycloak, Pocket ID, Zitadel.
-
Register Tippani with your provider, with the redirect URI
https://<your tippani>/api/auth/oidc/callback, PKCES256, and client authclient_secret_basic. For Authelia:identity_providers: oidc: clients: - client_id: 'tippani' client_name: 'Tippani' client_secret: '$pbkdf2-sha512$310000$β¦' # the digest, see below public: false authorization_policy: 'two_factor' require_pkce: true pkce_challenge_method: 'S256' redirect_uris: - 'https://tippani.example.com/api/auth/oidc/callback' scopes: ['openid', 'profile', 'email'] response_types: ['code'] grant_types: ['authorization_code'] token_endpoint_auth_method: 'client_secret_basic'
authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986prints a password (for Tippani) and its digest (forclient_secret). -
Point Tippani at it:
environment: TIPPANI_OIDC_ISSUER: "https://auth.example.com" TIPPANI_OIDC_CLIENT_ID: "tippani" TIPPANI_OIDC_CLIENT_SECRET: "<the password from step 1>" TIPPANI_OIDC_NAME: "Authelia" TIPPANI_COOKIE_SECURE: "1" TIPPANI_TRUSTED_PROXY: "1"
-
Each reader links their account once: sign in with your password, open Profile βΊ Single sign-on, and press Link Authelia. After that, "Sign in with Authelia" works. Your password keeps working too.
Optional: TIPPANI_OIDC_AUTO_CREATE=1 creates accounts for new identities, and TIPPANI_OIDC_LINK_USERNAME=1
links an identity to the account with the same username. Only use the second if people can't choose their own
username at the provider. A failed sign-in logs TIP-AUTH-001 with the reason.
Pushover notifications
- Create an app at pushover.net/apps/build and copy its token. Set it once as
TIPPANI_PUSHOVER_TOKEN, or let each reader paste their own. - In Profile βΊ Notifications, paste your Pushover user key, save, and send a test.
- Choose what reaches you: the daily review, large imports, long metadata fetches, and (for admins) backups.
The daily message needs one line in the host's cron, since Tippani has no timer of its own:
0 8 * * * docker exec tippani /tippani notify dailyDashboard widget (gethomepage)
Shows four numbers from your library: works, quotes, forgotten and mastered. In Profile βΊ Dashboard widget press
Make a key, then copy the YAML it shows into gethomepage's services.yaml. The key is shown once, and it can
only read those four numbers.
- Tippani:
href: https://tippani.example.com
widget:
type: customapi
url: https://tippani.example.com/api/widget
headers:
X-API-Key: tpw_β¦
mappings:
- { field: works, label: Works }
- { field: quotes, label: Quotes }
- { field: forgot, label: Forgot }
- { field: mastered, label: Mastered }Without Docker
Go 1.26+ builds it. Node is only needed to rebuild the interface.
make build # -> bin/tippani
./bin/tippani serve # http://127.0.0.1:8080
printf '%s\n' 'a-long-password' | ./bin/tippani user add alice # or create the admin from the CLIdeploy/tippani.service is a hardened systemd unit, and
deploy/Caddyfile.example puts HTTPS in front. To build, change or fork it, see
Developing. Release history is in CHANGELOG.md.
Caution
Amazon cookie (optional, at your own risk). An admin can paste an Amazon session cookie in Metadata βΊ Sources to fetch book descriptions and genres. It is stored write-only, but it gives access to your Amazon account, and scraping is against Amazon's terms. Covers and Kindle import work without it.
An Android app that turns a photographed page into a highlight, more imports (Kobo, Apple Books, Readwise), collections, passkeys and 2FA, and opt-in AI summaries. The roadmap has the full list in priority order. Request a feature or report a bug.
Book details come from Google Books and Open Library, book covers and author photos from Amazon, and people's links from Wikidata. IMDb pages are read on request for game casts and quote imports. Why only two logos: see Provider marks.
Built with pretext (text flowing around stickers), Phosphor, Tabler and Atlas icons (MIT), CC0 Textures, and eighteen open font families via Fontsource (SIL OFL 1.1), all bundled rather than fetched. Thanks to Bookcision and Readest for making highlights portable.
MIT. See LICENSE.














