# hypothesis/h: the server behind the Hypothes.is annotation API

> hypothesis/h is the Pyramid web app that serves hypothes.is and its public annotation API. It is a self-hosting project for teams that need to run the annotation store themselves, and the setup path is a Makefile plus Docker Compose, not a single binary.

**hypothesis/h** — Annotate with anyone, anywhere.

- Repository: https://github.com/hypothesis/h
- Website: https://hypothes.is/
- Stars: 3,184 · Forks: 460
- Language: Python
- License: BSD-2-Clause
- Published: 2026-09-24 · Updated: 2026-09-24 · Language: en
- Canonical page: https://hysenlabs.com/projects/hypothesis-h

## What hypothesis/h actually is, and who it is for

The README opens by saying h "is the web app that serves most of the https://hypothes.is/ website, including the web annotations API at https://hypothes.is/api/". That sentence defines the boundary of the project. h is the server side. The browser annotator that users see on a page is a different repository, hypothesis/client, which the README describes as "a browser-based annotator that is a client for h's API".

So the audience is narrow. If you are an individual who wants to highlight text on a web page, you do not need this repository. If you are an institution, a research group or a platform team that wants annotation data to live in your own PostgreSQL instance, with your own Elasticsearch index behind search, then h is the thing you install. The project's own framing is community oriented: the README points contributors at a Slack invite, a development mailing list and a contributor's guide, and it asks people to work on "issues that are ready for work". That is a contributor funnel, not a customer funnel.

The licence is BSD-2-Clause, which the README also surfaces as a badge. That is permissive, and it is the reason a company can fork h and run it internally without the copyleft obligations a GPL server would impose. It is not legal advice, and the LICENSE file is the authority.

## The architecture visible in docker-compose.yml and the Dockerfile

h is not a single process. The docker-compose.yml file in the repository root declares three backing services, each bound to 127.0.0.1 rather than 0.0.0.0: postgres on port 5432, elasticsearch on port 9200, and rabbit on ports 5672 and 15672. The images are pinned: postgres:15.10-alpine, hypothesis/elasticsearch:elasticsearch7.10 and rabbitmq:3.12-management-alpine.

The Elasticsearch image is a project-published one rather than upstream, and the version is 7.10. That is a deliberate pin, and it is the kind of detail that decides whether a h deployment can share an existing Elasticsearch cluster with other applications. The compose file also expects an external Docker network named dbs, with a comment explaining that the network "allows FDW connections between H, LMS and report DBs" and that it is created by make services in each project. If that network does not exist, the compose file will not be able to attach to it.

The Dockerfile is a two-stage build. Stage one is node:25.2-alpine, runs yarn install --immutable and yarn build, and produces the frontend bundle. Stage two is python:3.11.11-alpine3.19, installs nginx and libpq, copies conf/nginx.conf to /etc/nginx/nginx.conf, installs requirements/prod.txt, and copies the build directory from stage one. The application is exposed on port 5000 and runs as an unprivileged hypothesis user. nginx sits in front of it inside the same container, which is a design choice worth noticing: there is no separate reverse proxy service in the compose file.

## Installing h and running it for the first time

The README lists prerequisites before any command: Git, GNU Make, pyenv, Docker Desktop, Node with npm, and Yarn. The Yarn install line given is sudo npm install -g yarn. pyenv matters because the repository pins a Python version in .python-version, and the README notes you can use pyenv without shims.

The setup sequence in the README is four commands. Clone the repository, change into it, bring up the services, and load development data.

```bash
git clone https://github.com/hypothesis/h.git
cd h
make services
make devdata
```

make services is what creates the external dbs network and starts the three containers from docker-compose.yml. Expect PostgreSQL on 5432, Elasticsearch on 9200 and RabbitMQ on 5672 with its management interface on 15672. The Elasticsearch healthcheck has a start_period of one minute, so the service is not considered ready immediately. make devdata then loads sample data so the app has something to show. The README recommends running make help afterwards to see what other targets exist; that is the honest way to discover the rest of the workflow, because the README does not enumerate every target.

The README's own note about contributing points to the Contributor's guide at h.readthedocs.io for "further instructions on setting up a development environment". That guide, not this README, is where the full local run instructions live.

## Dependency management is delegated to pip-compile and Dependabot

The README is unusually explicit that requirements/*.txt files are compiled artifacts. Dependencies are declared in requirements/*.in files, and make requirements regenerates the .txt outputs. The reason given is that the same .in file can compile to different .txt files under different Python versions. That is a real constraint: if you edit a .txt file by hand, the next make requirements run discards your change.

Adding a dependency means editing the .in file and running make requirements. Removing one means the same. For version surgery the README gives three invocations, all of which pass arguments through to the underlying tool:

```bash
make requirements --always-make args='--upgrade-package <FOO>'
make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>'
make requirements --always-make args=--upgrade
```

The first upgrades a single package to latest, the second pins it to a specific version, and the third upgrades everything. The README states that routine updates are handled by Dependabot through automated pull requests, and that manual upgrades are the fallback. Changing the Python version itself is a four-step process: edit python_version in .cookiecutter/cookiecutter.json, run make template, run make requirements, then commit and open a pull request. The cookiecutter template is pyramid-app from hypothesis/cookiecutters, which the README shows as a badge. That means the project's structure is generated, and files you edit outside the template may be overwritten by make template.

## Where h is the wrong tool

The clearest limitation is that h is a service, not a library. There is no pip package described in the README that you install into an existing Python application to get annotation storage. You run the app, you point a client at its API, and you operate three stateful backing services. Teams that wanted a small annotation feature inside an existing product will find the operational surface disproportionate.

The release history reinforces this. The recent releases listed are v0.39.0 from 2019-11-05, v0.38.0 from 2016-08-08 and v0.36.0 from 2016-07-27, and the v0.39.0 note says "Find the latest on Docker Hub". So the GitHub releases page is not the distribution channel, and anyone reading it for version guidance will be misled. The repository is not archived and the last push was on 2026-09-23, so code is still landing, but the tagged release line has been quiet for years. Plan to track the main branch or a container image, not a release tag.

A second failure mode is version coupling. Elasticsearch is pinned to 7.10 through a project-specific image. If your organisation already runs a newer Elasticsearch cluster, h will not simply point at it. The same applies to PostgreSQL 15.10, which is pinned in compose but not necessarily in production. Neither the README nor the compose file documents an upgrade path between these versions.

## The alternative: run the client against the hosted API

The realistic alternative is not another self-hosted annotation server. It is using hypothesis/client, the browser annotator, against the public hypothes.is API and not running h at all. The README draws exactly this line: the client "is a browser-based annotator that is a client for h's API".

The difference in approach is operational, not functional. With the client alone you get the annotation interface and you depend on someone else's API, storage and search. With h you own the data, the PostgreSQL schema, the Elasticsearch index and the message queue, and you also own the backups, the version pins and the Elasticsearch 7.10 constraint. For a course or a research project with a fixed lifetime, the hosted route removes an entire category of maintenance. For an institution with data residency requirements, h is the only option in this pair, and the cost is the three services in docker-compose.yml. There is no middle setting described in the README: you either run the server or you do not.

## Maintenance cost and what the licence does and does not settle

The BSD-2-Clause licence lets you modify and redistribute h, including in a commercial product, provided the copyright notice and licence text are retained. It does not require you to publish your changes. That is the practical difference from a copyleft server licence, and for a team embedding h in an internal platform it removes a legal review step. It does not settle anything about the data you store in PostgreSQL or the annotations users create, and it does not cover the hypothesis/elasticsearch image, which may carry its own terms. Read the LICENSE file and the image's own metadata rather than relying on the badge.

The maintenance burden is front-loaded into the environment. You are responsible for three stateful services, a Python 3.11 runtime, a Node build stage and a pinned Elasticsearch. The README's dependency workflow reduces drift for Python packages, and Dependabot handles routine bumps, but neither of those touches the Elasticsearch pin. The README does not document rollback, backup or a production deployment procedure. It is written for developers setting up a local environment, and the Contributor's guide is where the project sends you for more. Treat that as the real documentation boundary: if a topic is not in the README or the contributor's guide, the project has not committed to an answer.

## Conclusion

Adopt hypothesis/h if you want the annotation API and web app running on your own infrastructure, and you are prepared to operate PostgreSQL, Elasticsearch and RabbitMQ alongside it. Do not adopt it if you want a drop-in annotation widget with no server: that is the separate Hypothesis client repository. Before committing, verify that make services brings up the dbs network and that make devdata loads sample data, because the README does not document a rollback path and the repository has no production deployment guide.

## FAQ

### What is hypothesis/h and what does it serve?

The README states that h is the web app that serves most of the hypothes.is website, including the web annotations API at https://hypothes.is/api/. The browser annotator users interact with lives in a separate repository, hypothesis/client, which is a client for h's API.

### How do you install hypothesis/h for local development?

The README lists Git, GNU Make, pyenv, Docker Desktop, Node with npm, and Yarn as prerequisites, then gives the sequence git clone, cd h, make services, make devdata. make services creates the external dbs network and starts PostgreSQL, Elasticsearch and RabbitMQ from docker-compose.yml.

### Which backing services does hypothesis/h require?

The docker-compose.yml file declares postgres:15.10-alpine on 5432, hypothesis/elasticsearch:elasticsearch7.10 on 9200, and rabbitmq:3.12-management-alpine on 5672 and 15672. All three bind to 127.0.0.1 and attach to an external network named dbs.

### How do you add or upgrade a Python dependency in hypothesis/h?

The README says to add the package to the appropriate requirements/*.in file and run make requirements. To move a single package to a specific version it gives make requirements --always-make args='--upgrade-package <FOO>==<X.Y.Z>', and it notes that routine updates arrive through Dependabot pull requests.

### What licence does hypothesis/h use?

The README shows a BSD-2-Clause badge and links to the LICENSE file in the repository root. That licence permits modification and redistribution provided the copyright notice and licence text are kept.

## Sources

- [hypothesis/h on GitHub](https://github.com/hypothesis/h)
- [License: BSD-2-Clause](https://github.com/hypothesis/h/blob/main/LICENSE)
- [Project website](https://hypothes.is/)
- [README](https://github.com/hypothesis/h/blob/main/README.md)
- [Releases](https://github.com/hypothesis/h/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/hypothesis-h
