How to Publish a Haskell Package on Hackage the Right Way
You have a library that works on your machine, a few people have asked for it, and now you want to publish a Haskell package without embarrassing yourself on day one. The upload itself takes thirty seconds.

You have a library that works on your machine, a few people have asked for it, and now you want to publish a Haskell package without embarrassing yourself on day one. The upload itself takes thirty seconds. Getting the metadata, bounds, and documentation right is the part that decides if anyone trusts the release.
This guide walks through the exact sequence I use when I publish a Haskell package to Hackage, from account setup to the first patch release. It assumes you build with cabal and already have a project that compiles cleanly with a recent GHC, say 9.6 or 9.8.
Before you publish a Haskell package, get the account sorted
Hackage accounts are not instantly able to upload. You register with a username and email, confirm the email, and then request upload rights. A Hackage administrator grants that permission by hand, and in my experience it has taken anywhere from a few hours to four days. Do this first, while you are still polishing code, so the wait does not land on release day.
Once approved, store your credentials somewhere cabal can find them. The cleanest option is an API token generated from your Hackage account page, which you can revoke later without changing your password. Put it in your cabal config rather than typing it at every upload, and keep that file out of any dotfiles repository you share publicly.
Write a cabal file that strangers can read
Your .cabal file is the storefront. Hackage renders most of it on the package page, and tools like cabal and Stack read it to decide whether your library fits into someone else's build. These are the fields that matter most:
- cabal-version: use 3.0 unless you have a reason to support very old tooling. It allows common stanzas, which keep shared warnings and extensions in one place.
- synopsis: one line, under about 80 characters, saying what the package does. "Fast CSV parsing with streaming support" beats "A library for working with data".
- description: two or three short paragraphs with a tiny usage example. This is what people see before deciding to click through to the docs.
- license and license-file: pick deliberately. If you have not decided yet, our guide to choosing an open source license for a Haskell library covers the trade-offs between BSD-3-Clause, MIT, and the copyleft options.
- category, maintainer, homepage, bug-reports: fill all four. An empty bug-reports field tells users you do not want to hear from them.
- extra-doc-files: list README.md and CHANGELOG.md so they ship in the tarball and show on Hackage.
- tested-with: list the GHC versions your CI actually runs, for example GHC 9.4.8, 9.6.6, and 9.8.4. Do not list versions you have never built against.
Version bounds on dependencies
Every build-depends entry should carry a lower bound you have tested and, for most libraries, an upper bound that follows the Package Versioning Policy. A typical line looks like text bounded at 2.0 or higher and below 2.2. If you are fuzzy on what a major bump means, read how PVP versioning works for library maintainers before picking your own version number. Start at 0.1.0.0, not 1.0.0.0, unless the API really is settled.
Run the checks that catch boring mistakes
Three commands catch most first release problems, and they take under five minutes combined on a small library:
- cabal check reads your .cabal file and complains about missing fields, unbounded dependencies, and flags Hackage will reject. Treat every warning as a bug until you can explain why it does not apply.
- cabal sdist builds the source tarball that will actually be uploaded. Unpack it into a temporary folder and build from there. This is how you discover the module you forgot to list in exposed-modules or other-modules, which compiles fine in your working tree because the file is sitting right there.
- cabal haddock with the option for Hackage-ready output builds documentation the way the Hackage server expects. Broken links and missing module headers show up here instead of on your public page.
If your CI matrix does not already include a job that builds from the sdist tarball, add one. It costs a minute per run and catches the missing module problem permanently.
Why you should not upload straight to the public index
The usual advice is to ship early and let real users find the rough edges. For application code, fine. For a Hackage release, I disagree, and the reason is permanence. Hackage does not let you delete a published version. You can deprecate it, and you can make metadata revisions to adjust bounds, but the tarball stays in the index forever and anyone with a pinned index state can still resolve to it.
That is why package candidates exist. Running cabal upload without the publish flag sends your tarball to a candidate page instead of the live index. The candidate page renders exactly like the real one: description, module list, changelog, and (after you upload them) the docs. Spend ten minutes reading it as if you were a stranger. I almost always find something, usually a description that formats badly or a README image path that breaks.
When the candidate looks right, run the same upload command again with the publish flag. Same tarball, now public.
Documentation that actually appears
Hackage tries to build documentation for you, but its build machines do not always have your system libraries, and the queue can lag by hours. If your package binds to a C library or needs an unusual GHC, the automatic build may simply fail and leave your page with no docs at all.
The reliable fix is to build docs locally with the Hackage-ready haddock option, then upload the resulting docs tarball yourself using cabal upload with the documentation flag (short form -d) plus the publish flag. It takes about two minutes and removes a whole class of "why are there no docs" issues from your tracker.
Write at least one module header comment with a short example. A user scanning the Haddock index decides in roughly fifteen seconds if your library solves their problem.
After the first release
Publishing once is easy. Staying publishable is the actual job. A few habits keep that manageable:
- Keep CHANGELOG.md updated in the same pull request as each change, not reconstructed from git log on release day.
- When a new major version of a dependency lands, test it, then use a Hackage metadata revision to relax the upper bound instead of cutting a new release for a one-character change.
- Tag every release in git with the exact version number so bug reporters can point at a commit.
- Answer the first issue someone files within a week, even if the answer is "not planned". Early responsiveness shapes whether contributors stick around.
If people start sending patches, our piece on what a first open source contribution to a Haskell project looks like is worth reading from the other side, so you know what newcomers expect from you.
Frequently asked questions
Can I delete a package version from Hackage?
No. Published versions are permanent so that existing builds keep resolving. You can mark a version as deprecated, which tells the solver to avoid it, and you can edit bounds through metadata revisions. That permanence is the main reason to use package candidates before publishing.
How long does it take to get Hackage upload rights?
Upload permission is granted manually by an administrator after you register and confirm your email. It is often done within a day or two, but plan for up to a week. Request it before your code is finished so it does not delay your release.
Should my first Haskell package start at version 1.0?
Usually not. Starting at 0.1.0.0 signals that the API may still change and gives you room for breaking major bumps without alarming anyone. Move to 1.0 once a handful of real users depend on the library and the core types have stopped moving.
More in Open Source
Open Source
How to Set Up Haskell CI for an Open Source Library
Your library builds on your laptop, a stranger opens a pull request, and three days later someone files an issue saying the...
Open Source
Haskell PVP Versioning Explained for Library Maintainers
You release version 1.4.2 of your library on a Friday, and by Monday three downstream packages fail to build because you...
Open Source
First Open Source Contribution to a Haskell Project
The first pull request I ever sent to a Haskell library changed eleven characters in a Haddock comment.