TimeTagger logo Pricing Support Log in Sign up
by Almar Klein | published 01-02-2022 | last edited 01-02-2022

← all articles

How to self-host your time tracker

In this post I'll take you through the steps to setup the TimeTagger time tracker application on your own server.

The TimeTagger library

TimeTagger is an open source time tracker - the source is on Github. The server is written in Python, making it easy to setup and tinker with. It is implemented a bit like a framework, giving you a lot of flexibility to tweak the server to your liking and e.g. implement your own authentication mechanism. More on that later.

TimeTagger on GitHub

The application

When you have the server up and running, it will serve a simple website and the TimeTagger application. The app's interface is based around interactive timeline, and is aimed at individuals who want a simple time tracker with powerful features such as easy navigation (also with the keyboard) and solid reporting. You could try the demo to get an idea.

TimeTagger screenshot

Setting things up

TimeTagger needs Python 3.6 or higher. The easiest way to install it (and its dependencies) is to use the Python installer:

$ python -m pip install timetagger

Creating the server script

You will need to create a script that uses the timetagger library to create a running server. The run.py from Github is a good starting point that just works. You can then later edit it if you want.

The reason why there is not a simple timetagger.start() is because this way you have much more freedom to e.g. embed TimeTagger in a larger website, or implement your own authentication flow.

Once you have your own version of run.py, you can start the server with:

$ python run.py

You can then open a browser tab at http://localhost to get started. That's it, really! The remainder of this post explains things in more details.


You can tweak the behaviour of the server using environment variables and CLI arguments. For example, by default it binds to making it available to outside connections, but you can change that as follows:

$ python run.py --bind=localhost:80

For a full list of configuration options see the api docs.

Understanding the server script

Let's dive a bit deeper into the script to see what it does. We'll not cover each line, but highlight the most important bits. The functions discussed below are explained in detail in the api docs.

Let's start with these lines:

common_assets = create_assets_from_dir(resource_filename("timetagger.common", "."))
apponly_assets = create_assets_from_dir(resource_filename("timetagger.app", "."))
image_assets = create_assets_from_dir(resource_filename("timetagger.images", "."))
page_assets = create_assets_from_dir(resource_filename("timetagger.pages", "."))

Here, the assets required to run TimeTagger are loaded into Python dicts. These are later combined into just two dicts. Here you could insert your own assets to tweak the hosted website. The dicts are then wrapped in a handler using asgineer:

app_asset_handler = asgineer.utils.make_asset_handler(app_assets, max_age=0)
web_asset_handler = asgineer.utils.make_asset_handler(web_assets, max_age=0)

This creates an object that can very efficiently handle http requests. This brings us to the main handler. We use asgineer, a micro web framework that allows writing handlers as simple async functions:

async def main_handler(request):
    if request.path == "/":
        return 307, {"Location": "/timetagger/"}, b""  # Redirect
    elif request.path.startswith("/timetagger/"):
        if request.path.startswith("/timetagger/api/v2/"):
            path = request.path[19:].strip("/")
            return await api_handler(request, path)
        elif request.path.startswith("/timetagger/app/"):
            path = request.path[16:].strip("/")
            return await app_asset_handler(request, path)
            path = request.path[12:].strip("/")
            return await web_asset_handler(request, path)
        return 404, {}, "only serving at /timetagger/"

You can see how depending on the path, this handler will either respond/redirect directly, or will delegate the request to the asset handlers we saw above.


TimeTagger authentication occurs via a webtoken that the server generates itself. This token expires, but the client will automatically exchange it for a new one before this happens. But how does the client obtain the first token? This happens by establishing - one way or another - that the client can be trusted, and then handing out the token.

The default run.py script performs this authentication bootstrapping by checking whether the client is on the same machine as the server:

async def webtoken_for_localhost(request):
    # Establish that we can trust the client
    if request.host not in ("localhost", ""):
        return 403, {}, "forbidden: must be on localhost"
    # Return the webtoken for the default user
    token = await get_webtoken_unsafe("defaultuser")
    return 200, {}, dict(token=token)

You can replace this with your own authentication mechanism, e.g. a username plus password form, email authentication, OAuth, to name a few. On timetagger.app we use the auth0 service to get the user's email address in a trusted manner.

Running in the cloud

You could run your own TimeTagger server in the cloud, but if you do this, you should:

At this point, you may want to consider taking a subscription at timetagger.app, because we take care of all these things for you. Plus you'd support the ongoing work of improving TimeTagger :)


TimeTagger is subject to the GPL-3.0 License, you can find more details in the and README.md and the LICENSE.