Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Tabkeep

A small full-stack reading-list app. Save a link, tag it, search it, and highlight it once you've read it. Built because my "read later" plan was 90 open browser tabs and a bookmarks bar I never opened.

It's deliberately small and finished rather than large and half-done. The interesting parts to talk through are the tag system (a real many-to-many relationship), search, roll-your-own auth, and the test suite.

Tabkeep main view

Features

  • Email + password accounts (passwords hashed, sessions are server-side)
  • Save a link with an optional title, note, and tags — paste a bare domain and it adds https://
  • Tag links and filter by tag; tags are scoped per user
  • Search across titles, notes, and URLs
  • Filter by All / To read / Read, with live counts
  • Mark links read/unread (read links get a highlighter swipe across the title)
  • Edit and delete links
  • Each user only ever sees their own data
  • Responsive down to mobile; keyboard focus styles; reduced-motion respected

Tech stack

  • Backend: Python + FastAPI
  • Database: SQLite via Python's built-in sqlite3 module — no ORM, just SQL
  • Auth: bcrypt password hashing + server-side sessions in an httpOnly cookie
  • Frontend: vanilla JavaScript (no framework, no build step)
  • Tests: pytest

The only runtime dependencies are fastapi, uvicorn, httpx, and bcrypt.

Running it locally

Requires Python 3.11 or newer.

pip install -r requirements.txt
python main.py
# open http://localhost:8000

Create an account on the sign-in screen and start saving links. The database is created automatically at data/tabkeep.db.

To run the tests:

pytest

There are 22 tests covering auth, validation, CRUD, tags, search, the read/unread filter, and — importantly — that one user can't read or modify another user's bookmarks.

How it's put together

main.py          FastAPI app + all routes. create_app() builds the app without
                 starting it, so tests can run against an in-memory database.
database.py      Opens SQLite, sets pragmas, and defines the schema.
auth.py          Password hashing and session management.
public/          The frontend (index.html, styles.css, app.js).
test/            The API test suite.

Data model

users ──< bookmarks ──< bookmark_tags >── tags
  └──────< sessions

bookmark_tags is the join table that makes the many-to-many relationship between bookmarks and tags work. Tags are stored per user and pruned when they're no longer attached to anything.

Decisions I made (and why)

  • Server-side sessions instead of JWTs. A session is a random token stored in the database; logging out deletes the row. That means a session can actually be revoked server-side, which a stateless JWT can't do without extra machinery. For an app like this that's the simpler, safer default.
  • 404, not 403, for other people's bookmarks. Asking for a bookmark that isn't yours returns "not found" rather than "forbidden", so the API never even confirms that someone else's bookmark exists.
  • Python's built-in sqlite3. No native dependency, no ORM, no migration framework — just SQL and Python's standard library. check_same_thread=False and WAL mode give safe concurrent access without a connection pool.
  • No frontend framework. The app is small enough that plain DOM rendering is easy to read and has zero build tooling. If it grew, I'd reach for a framework.

Known limitations / what I'd do next

  • Search uses LIKE. It's fine at this scale, but the obvious next step is SQLite's FTS5 full-text index for ranked results and better matching.
  • bcrypt blocks the thread. Fine for a personal-scale app; under real load I'd move hashing to a thread pool with asyncio.to_thread.
  • No password reset or email verification yet. The auth is intentionally minimal; these are the next things I'd add for a real product.
  • No pagination. Every bookmark loads at once. I'd add cursor-based paging once a pile gets into the thousands.

Deploying

The database is a file on disk, so deploy somewhere with a persistent disk (Render, Railway, and Fly all offer this) and point DB_FILE at it. Set ENV=production so the session cookie is marked Secure. A Procfile is included.

This setup is not a fit for fully serverless/static hosts (e.g. Vercel), whose filesystem is read-only and ephemeral — the SQLite file wouldn't persist.

About

A minimal reading-list web app to track books with notes, ratings, and read status.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages