A server-side package registry for hica, inspired by
crates.io and written in hica itself using the http
(server), sqlite, and json libraries.
The registry is both a real service and a flagship dogfooding project: if hica can host its own package registry, it can build real web services.
Today pkg.hica.dev is a static file host: packages are published over FTP
as three flat artefacts (latest, <version>.json, and a source tarball). It
works, but it has no index (so search only scans the local cache), no ownership
model, no download stats, no yank, and publishing requires shared FTP
credentials.
There are no production users yet, so this project replaces that wholesale with a clean, dynamic HTTP API backed by SQLite.
- Clean, coherent API — a single versioned surface (
/api/v1/…) modelled on crates.io, with no legacy paths to carry. - Searchable — a real server-side search endpoint (
GET /api/v1/search?q=), backed by SQLite FTS5 (with aLIKEfallback for portability). - Self-hosted in hica — the server is built on the
httprouter +sqlitejsonlibraries.
- Authenticated publish — token-based
hica publish, replacing shared FTP credentials. - Immutable versions — once
foo@1.2.3is published it never changes; hiding is done via yank. - Auditable — download counts, publish timestamps, and yank state live in the database.
Non-goals (v1): semver range resolution / SAT solving, a web UI, multi-region CDN, and user-to-user permissions beyond per-package owners.
┌─────────────────────────────┐
hica CLI (deps.kk) │ registry server │
────────────────► │ (hica + http + json) │
GET /api/v1/… │ │
GET …/download │ router.hc (FastAPI-style)│
PUT …/{name}/{ver} │ │ │
(multipart publish) │ ▼ │
│ handlers ──► sqlite ──► registry.db
│ │ │
web browser ─────────►│ └──► tarball store │
GET /api/v1/search │ │
└─────────────────────────────┘
httpserver provides typed routes, path/query params, middleware, and automatic404/500handling.sqlitestores all metadata and the full-text search index in oneregistry.dbfile (WAL mode for concurrent readers).jsonparses publish metadata and emits every API response.- Tarball store — starts on the local filesystem, streamed by the download route (which increments the counter).
One versioned JSON API under /api/v1 (all responses JSON except the tarball
download):
| Method | Path | Auth | Purpose |
|---|---|---|---|
GET |
/health |
— | Service health: {"status":"ok","db":"ok"} (HTTP 503 if DB unreachable) |
GET |
/api/v1/packages/{name} |
— | Package detail: metadata, latest, every version |
GET |
/api/v1/packages/{name}/{version} |
— | Single version metadata |
GET |
/api/v1/packages/{name}/{version}/download |
— | 302 redirect to the tarball; increments per-version download counter |
GET |
/api/v1/search?q=<term>&limit=<n> |
— | Full-text search |
GET |
/api/v1/index |
— | Lightweight catalogue for offline seeding |
PUT |
/api/v1/packages/{name}/{version} |
token | Publish a version (multipart) |
DELETE |
/api/v1/packages/{name}/{version}/yank |
owner | Hide a version from resolution |
PUT |
/api/v1/packages/{name}/{version}/unyank |
owner | Restore a yanked version |
GET/PUT/DELETE |
/api/v1/packages/{name}/owners |
owner | List / add / remove owners |
| Phase | Deliverable | Status |
|---|---|---|
| 0 | RFC + schema agreed | ✅ |
| 1 | Read API (packages, download, search, index) + one-time seed from the existing static files; retire FTP |
✅ |
| 2 | Authenticated multipart publish (hica publish / hica login); move publish-pkg.yml off FTP |
🟡 server done; CLI open |
| 3 | Yank / unyank, owners, download counts surfaced in pkg info |
✅ |
| 4 | (Optional) static web UI over the JSON API; semver ranges | — |
hica build # compile to binary
hica run # compile and run
hica fmt # format according to hica style guide
hica check # type-check without emitting
hica clean # remove generated files
# test suites (73 tests total)
hica test tests/test_db.hc # db helpers, schema, auth primitives (10)
hica test tests/test_read.hc # read-only GET routes (26)
hica test tests/test_publish.hc # authenticated publish ( 5)
hica test tests/test_downloads.hc # download counter (10)
hica test tests/test_yank.hc # yank / unyank (10)
hica test tests/test_owners.hc # owners API (17)Dependencies (json, sqlite, http) are declared in hica.hml.
hica run src/main.hcOn first boot the server creates registry.db, applies the schema, and inserts
a dev admin user with token hica-admin-CHANGEME. Rotate this token before
any production deployment.
| Variable | Default | Purpose |
|---|---|---|
HICA_REGISTRY_ADMIN_TOKEN_HASH |
SHA-256 of hica-admin-CHANGEME |
SHA-256 of the seed admin token, prefixed with sha256:. Set this to a secret value in production. |
HICA_DB_PATH |
registry.db |
Path to the SQLite database file. |
HICA_TARBALL_DIR |
./tarballs |
Directory where published tarballs are stored. |
Generate a token hash for production:
echo -n "your-secret-token" | sha256sum | awk '{print "sha256:" $1}'Then pass it to the server at startup:
HICA_REGISTRY_ADMIN_TOKEN_HASH=sha256:<hash> hica run src/main.hcTarballs are written to ./tarballs/<name>/ by default. Override with:
HICA_TARBALL_DIR=/var/hica/tarballs hica run src/main.hccurl -X PUT http://localhost:8080/api/v1/packages/mylib/1.0.0 \
-H "Authorization: Bearer hica-admin-CHANGEME" \
-F 'metadata={"description":"My library","license":"MIT"}' \
-F tarball=@mylib-1.0.0.tar.gzThe server recomputes the SHA-256 of the received tarball and stores only the
hash — the declared checksum field in metadata is optional but verified if
present. The raw token is never stored; only its SHA-256 reaches the database.
The registry ships as a Docker image published to
ghcr.io/cladam/hica-registry. The database and tarballs must be mounted from
host storage so they survive container restarts and upgrades.
# 1. Copy the example env file and fill in your values
cp .env.example .env
# 2. Generate the admin token hash
echo -n "your-secret-token" | sha256sum | awk '{print "sha256:" $1}'
# Paste the result into .env as HICA_REGISTRY_ADMIN_TOKEN_HASH
# 3. Set the host paths where persistent data will live
# REGISTRY_DATA_DIR=/mnt/registry/data
# REGISTRY_TARBALL_DIR=/mnt/registry/tarballs
# 4. Start
docker compose up -dThe server will be reachable on port 8080. Put your HTTPS terminator in front
of it; no other ports are needed.
docker compose pull
docker compose up -dNo data is lost — the database and tarballs live on the host paths outside the container.