CLI tool
almarklein/timetagger avatar
almarklein/timetagger

TimeTagger tracks time with tags instead of projects, and ships as one pip package

Tag your time, get the insight

1,807 stars169 forksPythonGPL-3.0

At a glance

What is it?
TimeTagger is a self-hostable time tracker for individuals and freelancers, built on uvicorn, asgineer and SQLite via itemdb, with a client written in Python and compiled to JavaScript by PScript. It is easy to start locally and awkward to use as a team server without extra configuration.
Who is it for?
TimeTagger fits a single person or a freelancer who wants an interactive timeline, PDF and CSV reports and no account with a vendor. It is the wrong choice for a team that needs per-user roles, a shared project hierarchy, or an approval workflow, since the documented multi-user path is just credentials plus sync.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 25 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

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

Editorial analysis

Tags replace projects, which is the whole modelling idea

Most time trackers make you create a project, then log hours against it. TimeTagger takes the other route. The model is built around tags, and the README calls that choice a lightweight feel by use of tags rather than projects.

The interface follows from that. It is an interactive timeline, so what you see is a continuous band of time you either label or leave blank, and reporting in PDF and CSV is layered on top of the tags you have created. Daily, weekly and monthly targets exist, and there is an integrated Pomodoro method that the README marks as experimental.

The stated audience is individuals and freelancers. That is not a throwaway line; it is the clearest signal about scope in the whole document. There is no mention of teams, roles, approval, or billing clients. The web app is responsive, working on small and large screens, and sync between devices is built in, so one person moving between a laptop and a phone is the assumed usage pattern.

There is also a hosted version at https://timetagger.app with a demo at https://timetagger.app/demo, and a separate command line tool at https://github.com/almarklein/timetagger_cli that is not part of this repository.

One pip install gives you a working single-user server

TimeTagger is packaged as a Python library that requires Python 3.6 or higher, and `setup.py` states `python_requires=">=3.6.0"`. Dependencies are read straight from `requirements.txt` at build time, so what pip resolves for you is exactly that file:

code
uvicorn
asgineer>=0.8.0
itemdb>=1.1.1
pscript>=0.7.6
pyjwt
jinja2
markdown
bcrypt
iptools

Install and run it like this:

bash
pip install -U timetagger
python -m timetagger

That is the entire first-run experience. If the server is on your own machine you can use single-user mode out-of-the-box, which means no account, no password, no configuration. `python -m timetagger` starts an ASGI server and you open the URL it prints.

The version is not hardcoded in the setup script. It is read out of the package with a regular expression:

python
with open("timetagger/__init__.py") as fh:
    VERSION = re.search(r"__version__ = \"(.*?)\"", fh.read()).group(1)

So the version lives in one place in the source tree, and a mismatch between `__init__.py` and the packaging metadata is structurally impossible.

Two Docker variants, and the difference is the user the process runs as

For anything beyond a local experiment, the project publishes images through the GitHub container registry. Two compose files sit in `deploy/`:

- `docker-compose.yml`, which runs the server as root inside the container - `docker-compose.nonroot.yml`, which runs it as user 1000

That distinction is the entire difference the README calls out, and it matters more than it looks. The nonroot variant exists for people who do not want the server process holding root in a container that is exposed to a network.

The project also points at a self-hosting article at https://timetagger.app/articles/selfhost2/ for the details it does not repeat in the README.

One thing the documentation does not spell out is where the SQLite file ends up in either variant, or how to back it up. Since itemdb stores everything in SQLite, that file is the whole dataset, and a backup plan is something you have to work out for your own deployment. The README does not document a backup or restore command.

Multi-user means BCrypt credentials you have to generate yourself

The moment the server is not on localhost, or more than one person needs it, you need credentials. TimeTagger takes them as a username and a BCrypt hash, either as a command line argument or through an environment variable:

bash
# Using command-line args
python -m timetagger --credentials=test:$2a$08$0CD1NFiIbancwWsu3se1v.RNR/b7YeZd71yg3cZ/3whGlyU6Iny5i
bash
# Using environment variables
export TIMETAGGER_CREDENTIALS='test:$2a$08$0CD1NFiIbancwWsu3se1v.RNR/b7YeZd71yg3cZ/3whGlyU6Iny5i'
python -m timetagger

The format is `<username>:<hash>`, salted BCrypt, and the README suggests generating them at https://timetagger.app/cred.

Two observations. First, passing a password hash on a command line puts it in your shell history and in the process list for any user on the machine; the environment variable is the better of the two. Second, there is no documented admin interface for adding a user after the fact, so this looks like a config-file or restart operation rather than something an end user does. The details live in the config documentation at https://timetagger.readthedocs.io/en/latest/libapi/, which the README links rather than reproduces.

Reverse proxy auth trusts a header and only from an address you list

If you already run something like Authelia in front of your services, TimeTagger can take the identity from a header instead. Three settings control it, available as flags or environment variables:

bash
python -m timetagger --proxy_auth_enabled=True --proxy_auth_trusted=127.0.0.1 --proxy_auth_header=X-Remote-User
bash
export TIMETAGGER_PROXY_AUTH_ENABLED=True TIMETAGGER_PROXY_AUTH_TRUSTED=127.0.0.1 TIMETAGGER_PROXY_AUTH_HEADER=X-Remote-User
python -m timetagger

The `TIMETAGGER_PROXY_AUTH_TRUSTED` value is the part to get right. It is an address, and `iptools` is in the dependency list precisely so that ranges and netsets can be expressed. If you set this too broadly and the application port is reachable directly, anyone who can open a TCP connection to TimeTagger can claim any username by sending the header themselves. The reverse proxy must be the only path in, and this configuration assumes you have already arranged that.

The trade-off is that you inherit the proxy's authentication, including its session policy, its password rules and its audit trail, and give up the built-in credential path entirely.

The client is Python that PScript compiles down to JavaScript

The server is async Python on uvicorn with asgineer, described by the author as fun and bloody fast, and data lives in SQLite through itemdb. Storage is the reason deployment is easy: there is no database server to run.

The client is the unusual part. It is a mix of HTML, CSS, Markdown and Python, with PScript compiling the Python to JavaScript. The README calls this idiosyncratic, and it is a real constraint rather than a quirk to admire. Nobody who reads JavaScript in the browser devtools is going to find readable source; nobody who wants to fix a frontend bug in this app is going to be editing JavaScript; and the maintainer himself notes the possibility of moving to something that compiles down to Wasm, which is a hint that the current approach has limits he has not resolved.

There is a practical consequence for a team evaluating this: frontend contribution means learning PScript, and any existing frontend build tooling, component library or test harness in your stack does not apply. For a single-user app that nobody intends to modify, this costs nothing.

GPL-3.0 plus a CLA means contributions carry a second condition

The licence is GPL-3.0, confirmed in `setup.py` as `license="GPL-3.0"` with a matching classifier. The README frames its purpose directly: the licence protects the code from being used commercially by other parties, and copyright stays with whoever made a given contribution, inspectable with git blame.

There is a second instrument that matters more for contributors than the licence. `CLA.md` is a Contributor License Agreement, and the README states that making a contribution means agreeing to it, granting Almar the right to use those contributions at services such as TimeTagger.app. In other words, contributing to the GPL project also grants the maintainer a separate permission to run your code commercially through the hosted service.

If your organisation has a policy about CLAs, that is the document to read first, and it is one click from the repository root. If you only intend to run the software unmodified, the GPL obligations attach to redistribution rather than use, and you are not making contributions at all.

Development setup is conventional. Clone, install in editable mode, add the dev tools:

bash
git clone https://github.com/almarklein/timetagger.git
cd timetagger
pip install -e .
pip install invoke black flake8 pytest pytest-cov requests

Day-to-day work goes through invoke tasks: `invoke -l` lists them, `invoke format` runs black, `invoke lint` runs flake8, `invoke tests` runs pytest, and `invoke clean` removes temporary files.

Release versions are dated by year, and the last push was 2026-09-28

The repository is not archived and the last push was on 2026-09-28. Recent tags are v26.1.3 on 2026-02-02, v26.1.2 on 2026-01-29, and v26.1.1 on 2026-01-05, so the numbering scheme is year and sequence rather than semver. A patch release like v26.1.2 followed v26.1.1 within the same month, which is the pattern of someone shipping fixes as they find them.

The gap between the most recent tag in February and the last push in September suggests work has landed on `main` that is not in a tagged release. Self-hosters pulling `main` therefore get something different from users of the PyPI package, and the README does not document a supported upgrade path between the two. That is worth pinning deliberately rather than discovering later.

The package classifier says `Development Status:: 5 - Production/Stable`, and the listed operating systems are MacOS X, Windows and POSIX, so the deployment surface is broad even though the hosting story includes Docker image variants and a local pip install.

Editorial conclusion

TimeTagger fits a single person or a freelancer who wants an interactive timeline, PDF and CSV reports and no account with a vendor. It is the wrong choice for a team that needs per-user roles, a shared project hierarchy, or an approval workflow, since the documented multi-user path is just credentials plus sync. Before committing, run `python -m timetagger` locally and confirm that a reverse proxy in front of it terminates TLS and passes a username header, and read CLA.md, because contributing means granting the maintainer rights to your code.

Frequently asked questions

How do I install and run TimeTagger locally?

Run `pip install -U timetagger` and then `python -m timetagger`. Dependencies in requirements.txt install automatically, and if the server runs on your own machine you get single-user mode with no configuration.

How do I give TimeTagger more than one user?

Provide credentials as `<username>:<hash>` where the hash is a salted BCrypt password hash, using the `--credentials` argument or the TIMETAGGER_CREDENTIALS environment variable. The README suggests generating them at https://timetagger.app/cred.

Can TimeTagger use authentication from a reverse proxy?

Yes. Set proxy auth enabled, the trusted address of the proxy and the header carrying the username, for example --proxy_auth_enabled=True --proxy_auth_trusted=127.0.0.1 --proxy_auth_header=X-Remote-User. The trusted value is what stops clients from forging that header.

Where does TimeTagger store its data?

In SQLite through itemdb, which is why deployment is simple. The README does not document a backup or restore command, so locating and protecting that SQLite file is left to the operator.

What happens if I contribute code to TimeTagger?

By making a contribution you agree to CLA.md, which grants the maintainer the right to use your contributions at services such as TimeTagger.app. The code itself is GPL-3.0 and copyright stays with each contributor.

Official sources

  1. almarklein/timetagger on GitHub
  2. License: GPL-3.0
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/almarklein-timetagger.svg)](https://hysenlabs.com/projects/almarklein-timetagger)

Was this useful?