Skip to content
aaronifiedPublic

About

Self-hosted, multi-user store for book annotations: a single static Go binary (SQLite + FTS5) built for low-powered NAS boxes.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

2,153 Commits

Folders and files

Repository files navigation

tippani Β· টিΰ¦ͺ্ΰ¦ͺনী β€” αΉ­ippaαΉ‡Δ« Β· ΰ€Ÿΰ€Ώΰ€ͺΰ₯ΰ€ͺΰ€£ΰ₯€, a note in the margin

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.

Release Container Go License

Try the demo Β· Roadmap Β· Design log Β· UI glossary


The Library: a shelf of book covers sorted by author, with genre, state and series filters The Catalogue in a dark theme: film, show and game posters with line counts
Search correcting the misspelling athiest to atheist, with a book and a highlight in the results Stats in a dark theme: library counts, a calendar of saves, and where every quote stands in memory
An anthology on a phone: an introduction, then quotes from Bhagat Singh and Dostoyevsky with notes between them The Daily Quiz on a phone: a fill-in-the-blank question from Subhas Chandra Bose

Shot from a real library with its notes and tags replaced. The demo is always the latest interface.

What it does

A Daily Quiz card asking which word completes a quote by Einstein, with four choices

🧠 It helps you remember

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.

πŸ“š One library for everything you read and watch

Books keep their chapter and page. Films and shows keep their timestamp and episode. Games keep their act and quest. A quote from anywhere else (a speech, a letter, something a friend said) keeps its speaker and, if you want, its translation.
Favourites on Home: a book highlight, a film line, a game line and a standalone quote side by side
The Idiot's page: its cover, year, genre and author portrait, beside its highlights

πŸ“‘ Covers and details fill themselves in

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.

πŸ”Ž Find anything instantly

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.
The search box after typing tag:, suggesting the library's four tags
An anthology: Practise, Edit, Export and EPUB, an introduction, and quotes with notes written between them

πŸ“– Anthologies

Gather quotes into your own reading order, write between them, and export the result as Markdown or EPUB.

πŸ“¨ Share a quote as a picture

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.
A quote by Bhagat Singh drawn as a picture in the dark theme, his portrait behind the text
The import queue: five Pride and Prejudice highlights from a Kindle file, waiting to be approved or discarded

πŸ“₯ Bring your highlights with you

Kindle (Bookcision, the notebook, or My Clippings.txt), Readest and Tippani Markdown, saved Goodreads and Hardcover pages, IMDb quote pages. Imports wait for your approval, and the same file never adds anything twice.

🌐 Any language

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.
A board of Bengali proverbs in Bengali script, most with an English translation under them

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.

Light on your server

  • 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=1 stops 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.

Quick start

docker run -d --name tippani --restart unless-stopped -p 8080:8080 -v tippani-data:/data ghcr.io/aaronified/tippani:latest

Or 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).

Configuration

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.

  1. Register Tippani with your provider, with the redirect URI https://<your tippani>/api/auth/oidc/callback, PKCE S256, and client auth client_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 rfc3986 prints a password (for Tippani) and its digest (for client_secret).

  2. 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"
  3. 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
  1. 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.
  2. In Profile β€Ί Notifications, paste your Pushover user key, save, and send a test.
  3. 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 daily
Dashboard 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.

The Tippani widget on a gethomepage dashboard, marked healthy: 41 works, 863 quotes, 39 forgot, 19 mastered

- 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 CLI

deploy/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.

Coming next

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.

Credits

TMDB Film, show and game metadata, posters and cast. This product uses the TMDB API but is not endorsed or certified by TMDB.
TheTVDB Metadata provided by TheTVDB. Please consider adding missing information or subscribing. Show and film records, episode data, and character images.

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.

License

MIT. See LICENSE.

About

Self-hosted, multi-user store for book annotations: a single static Go binary (SQLite + FTS5) built for low-powered NAS boxes.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages