The lean, fast artifact repository that guards every publish.
Run it
docker run -d -p 8080:8080 -v telesphorepo-data:/data ghcr.io/telesphorepo/telesphorepo:latestThen open port 8080. The first start prints the admin password to the container log. The image runs on linux/amd64 and linux/arm64.
What you get
Section titled “What you get”A repository should hold your builds and stay out of your way. These are the parts that make it do that.
Publishing a leaked password cannot touch
- A new session for every publishAn upload needs a publish session: a token for one repo that ends after 15 minutes. The Gradle plugin and the Maven extension get one and close it on every publish. A password or a read token only reads.
- A second factor for every new deviceA laptop enrolls its own key once. You approve it with an authenticator code or a passkey. A stolen password alone cannot add a device.
- CI with no stored secretGitHub Actions and Jenkins prove who they are with their own OIDC token. A trust policy names the repo, the branch and the workflow that may publish.
- Single sign-on for adminsAdmins sign in through your identity provider with OpenID Connect: Keycloak, Microsoft Entra, Google, Okta or any other. The provider decides who is an admin, and its MFA applies.
- IP allow-lists per repoLimit who may read and who may publish to a repo, each with its own list of address ranges. A GitHub Actions preset stays current by itself; for other runners, add their fixed addresses as ranges.
Nothing extra to run
- One image, one binaryThe server, the web UI and the database run in one process. SQLite is inside, so there is no database server or search cluster to keep alive.
- Any storage, moved liveKeep files on local disk or in any S3-compatible bucket: Cloudflare R2, SeaweedFS, AWS S3. Change the storage in Settings and the server copies every file while reads and uploads continue.
- Settings in the UIEvery setting is on the Settings page and applies without a restart. Secret settings are stored encrypted.
- Real client IPs behind Caddy and CloudflareWhen requests arrive through a proxy, Settings offers to trust it in one click. For Cloudflare, pick the built-in Cloudflare preset. Its ranges refresh every day. Rate limits, IP rules and the audit log see the real client address, with no proxy tuning.
- Drop a folder to uploadDrop a whole build folder, a Maven-layout tree or a folder of .deb files. The server finds the artifacts, checks each one strictly, and shows a preview. Nothing is stored until you confirm.
Fast at any size
- The same speed at a million filesEvery request uses an index, and lists page by key. In the scale test, no p95 moved by more than 21 µs from 100k to 1M assets.
Your server, your rules
- Delete what you wantDelete a file, a version or a whole folder from the UI. A deleted Debian package leaves the apt indexes by itself.
- IP addresses only in the audit logFile lists show who uploaded, by username. Client IP addresses appear only in the admin audit log.
- Open sourceThe server is AGPL-3.0. The Gradle plugin and the Maven extension are MIT.
An easy way off Nexus
Every publish gets its own ticket
Section titled “Every publish gets its own ticket”In many setups, a CI job holds a deploy password that never changes. Here, no long-lived credential can write. The client proves who it is, and the server issues a publish session: a token for one repo that ends after 15 minutes.
The Gradle plugin and the Maven extension do this on every publish, so you do not see it. GitHub Actions and Jenkins use their own identity tokens, so a CI job has no secret to leak. The publish log keeps every session for 30 days after it ends.
A publish has four steps. First, the client proves who it is in one of three ways: an enrolled device key signs a one-time nonce (POST /api/publish/sessions), a CI system sends its OIDC token and a trust policy must match it (POST /api/publish/sessions/oidc), or a person enters a fresh second-factor code in the web UI (POST /api/publish/sessions/manual). Second, the server answers with a token that starts with trs_. The token works for one repository and lives at most publish.session_ttl, 15 minutes by default. Third, the client uploads with the token as the HTTP Basic password. Fourth, the client closes the session with DELETE /api/publish/sessions/{id}, or the session expires by itself. In the example, a publish takes 90 seconds and the client closes the session, so the token is dead for the other 13 and a half minutes.
1. Prove who you are
Device key
The Gradle plugin or the Maven extension signs a one-time nonce with its Ed25519 key.
POST /api/publish/sessionsCI identity token
GitHub Actions or Jenkins sends its OIDC token. A trust policy must match it.
POST /api/publish/sessions/oidcBy hand
A person signs in to the web UI and enters a fresh second-factor code.
POST /api/publish/sessions/manual
2. Get a session for one repo
trs_9c1e4b7a02f3_...- repo
- releases
- ends
- in 15 min
The limit is
publish.session_ttl. One hour at most.3. Upload
PUT /maven/releases/...The token is the Basic password. Any user name works.
The token also reads this repo. No other repo takes it.
4. Close
DELETE /api/publish/sessions/{id}closed
Or it expires by itself.
One session, from open to close
A Gradle publish of 90 seconds, then the client closes the session. The token is dead for the other 13.5 minutes.
Read how publishing works, then set up the Gradle plugin, the Maven extension or GitHub Actions.
As fast at a million files as at a hundred thousand
Section titled “As fast at a million files as at a hundred thousand”Every query on a request path uses an index, and lists page with keys, not offsets. So a request costs the same in a large repo as in a small one. The scale test checks this on every run of go tool task test scale. It seeds 100k synthetic assets, measures, seeds up to 1M, and measures again.
The scale test measures the p95 latency of four operations at 100k and at 1M assets. Maven GET: 586 µs at 100k, 607 µs at 1M. Asset list page: 1.49 ms at 100k, 1.51 ms at 1M. Search: 3.38 ms at 100k, 3.39 ms at 1M. Token auth: 1.03 ms at 100k, 1.03 ms at 1M. An operation fails the test when its p95 at 1M exceeds the larger of twice its p95 at 100k and its p95 at 100k plus 2 ms.
One image. Nothing else to run.
Section titled “One image. Nothing else to run.”There is no database server and no search cluster. Search runs on an SQLite full-text index. The server, the web UI and the database run in one process. Settings live in the database and apply without a restart. To move the files from local disk to a bucket, change the storage on the Settings page. The server copies every file in the background while reads and uploads continue.
The whole install is three things. The binary, /telesphorepo, serves the web UI, the JSON API and the format routes on one port, 8080. One SQLite file, /data/telesphorepo.db, holds users, repos, settings and an index of every stored file. One blob store holds the file contents: /data/blobs/ on local disk, or any S3-compatible bucket such as Cloudflare R2, SeaweedFS or AWS S3.
- The binary
/telesphorepo - The server, the web UI, the JSON API and the format routes, on one port (8080).
- The database
/data/telesphorepo.db - One SQLite file: users, repos, settings, and an index of every stored file.
- The blobs
/data/blobs/ or s3://bucket - The file contents, on local disk or in any S3-compatible bucket: Cloudflare R2, SeaweedFS, AWS S3.
Start the image with a volume for /data:
docker run -d --name telesphorepo -p 8080:8080 -v telesphorepo-data:/data ghcr.io/telesphorepo/telesphorepo:latestOn the first start, the server prints a generated admin password once to the container log. Read it, then open http://localhost:8080 and sign in.
docker logs telesphorepoBinaries for Linux, macOS and Windows are on the GitHub Releases page. The install guide has the other ways to run it.
Maven, Gradle plugins, Debian and raw today
Section titled “Maven, Gradle plugins, Debian and raw today”Each format is a plugin with its own routes and its own rules. More formats will come over time.
- Maven
/maven/{repo}/Artifacts with their checksums. A release file never changes. Metadata and SNAPSHOT files can. - Gradle plugins
/maven/{repo}/Builds and Gradle plugins use a maven repo. The Gradle plugin gets its own publish sessions. - Debian
/debian/{repo}/A signed apt repository. Upload a .deb and the server builds the indexes. - Raw
/raw/{repo}/Plain files at any path. Each repo allows or denies overwrites.
Leaving Nexus? Bring your repos with you.
Section titled “Leaving Nexus? Bring your repos with you.”Nexus 3 needs a JVM and a database to run, and some of its upgrades are projects of their own. The move from OrientDB to H2 is one example: its issue was opened in 2024 and is still open.
The move takes three steps, and your builds keep working the whole time.
Moving off Nexus takes three steps. First, add your Nexus server in Telesphorepo and pick the repos. Second, Telesphorepo copies them, then keeps copying new uploads every 15 minutes, while your builds still publish to Nexus. Third, when you are ready, switch your builds to Telesphorepo and press Cut over.
1Add your Nexus server in Telesphorepo and pick the repos.
Nexus 3
- maven-releases
- maven-snapshots
- apt-hosted
- raw-archive
2Telesphorepo copies them, then keeps copying new uploads every 15 minutes, while your builds still publish to Nexus.
Your builds
publishNexus
Nexus
every 15 minTelesphorepo
3When you are ready, switch your builds to Telesphorepo and press Cut over.
Your builds
publishTelesphorepo
The Nexus migration guide has each step in detail.