# Vectrum [vectrum.cc](https://vectrum.cc) · A self-hosted, multi-user GitHub built with Next.js. Repositories are real bare git repositories on disk, and you push to them with plain git over **SSH** or **HTTPS**. ## Features - **Accounts**: sign up, sign in, profiles, password change (PocketBase auth: bcrypt-hashed passwords, token sessions) - **Repositories**: public or private, rename, delete, optional README on creation - **Collaborators**: give other users push access to a repository - **Git over SSH**: built-in SSH server (default port 2222). Add public keys under *Settings → SSH keys* - **Git over HTTPS**: smart HTTP protocol (v0 and v2). Authenticate with API tokens - **Container registry**: every repository is also a Docker/OCI registry, like ghcr.io. `docker push //` (or `///` for more images per repository) with the same visibility, collaborators and API tokens as the code. Multi-platform images, cross-repository blob mounts, chunked uploads and the referrers API (signatures, SBOMs) are supported. Images are listed under the repository's *Packages* tab, where writers can delete versions - **Releases**: publish a version from a tag, with markdown notes and files to download (binaries, installers, checksums) of any size, plus the source code as zip and tar.gz. Drafts and pre-releases; a release made for a new tag creates it and starts the workflows a push of it would. Files can be uploaded from scripts and CI with `curl`, and `/releases/latest/download/` always links the newest version - **Wikis**, like GitHub's: Markdown pages written in the browser (with preview, page history, old revisions and their diffs) or cloned and pushed as a git repository of their own, `.wiki.git`. `[[Page Name]]` links, a custom `_Sidebar` and `_Footer`, and images and other files from the wiki's repository. Collaborators edit, readers read; the owner can turn the wiki off under *Settings* - **API tokens**: read-only or read & write, for all repositories or selected ones, with expiry. Only a hash is stored - **Web editor**: CodeMirror with syntax highlighting and markdown preview. Create, edit, rename, upload and delete files, committing to the current branch or a new one - **Code browsing**: file tree, highlighting (Shiki), rendered READMEs (sanitized HTML), raw files, image previews - **History**: commit log per branch or file, commit diffs, branches overview, recent-activity dashboard - **Issues as a Linear-style project tracker**, per repository: - Workflow statuses (Backlog → Todo → In Progress → In Review → Done / Canceled), priorities, assignees, labels, estimates and due dates - List view grouped by status, priority or assignee, and a kanban board with drag and drop - Filters (assignee, priority, label, "My issues"), scopes (All / Active / Backlog) and orderings, kept in the URL so views can be shared - Quick-create dialog and keyboard shortcuts: `C` new issue, `J`/`K` move, `Enter` open, `S`/`P`/`A`/`L` set status, priority, assignee, labels - Inline editing of every property, with an activity feed of changes next to the markdown comments - Anyone who can read a repository can open issues and comment, but only collaborators and the owner can change an issue (title, description, status and all other properties). Only an issue's creator and the repository owner can delete it. Labels are managed under *Settings → Labels* or created from the label menu - **Actions with self-hosted runners**: workflows in `.vectrum/workflows/*.yml` (a subset of GitHub Actions syntax) run on push or by hand, on runners you register for one repository or for all of yours. Live logs, cancel and re-run, and CI status marks in the commit list - **MCP server for AI agents**: agents like Claude Code connect to `/mcp` and sign in with OAuth 2.1. You approve each one, read-only or read & write, for all repositories or selected ones. They can read and commit code, manage branches, issues, labels and releases, create repositories, and run and debug workflows. Manage connected apps under *Settings → Connected apps* - Light and dark mode ## Getting started Requires Node.js 22+ (24.7+ for post-quantum SSH key exchange) and `git` on the `PATH`. ```bash bun install # also downloads the PocketBase server into bin/ bun dev ``` Open http://localhost:3000, sign up, and create a repository. The data lives in [PocketBase](https://pocketbase.io). Vectrum starts `bin/pocketbase` itself on `127.0.0.1:8090`, with its data in `data/pb_data` and the schema from `pb_migrations/`; its superuser login is generated into `data/pocketbase-superuser.json` (also good for the PocketBase dashboard at http://127.0.0.1:8090/_/). Run `bun run pocketbase:install` if the binary is missing. To use a PocketBase server of your own instead, set `PB_URL` and the superuser credentials; it needs the migrations in `pb_migrations/` (`pocketbase serve --migrationsDir pb_migrations`). ### Running with Docker ```bash docker compose up -d --build ``` The image runs the web UI on port 3000 and the SSH server on port 2222, with all data in the `vectrum-data` volume (mounted at `/data`). Settings go under `environment:` in `docker-compose.yml` (see [Configuration](#configuration)). To close registration, set `SIGNUPS_DISABLED: "1"` before you deploy, and create accounts from the command line instead: ```bash docker compose exec -it vectrum node scripts/create-user.mjs [name] ``` It asks for the password (or reads `VECTRUM_PASSWORD`). Outside Docker, run `node scripts/create-user.mjs` the same way while Vectrum runs. Clone URLs show `SSH_PORT`, so publish SSH on the same port on the host, and set `SSH_HOST` to your domain. Put a TLS reverse proxy in front of port 3000 if the server is reachable from the internet, and set `PUBLIC_URL` to its address. ### Pushing over SSH 1. Add your public key (`~/.ssh/id_ed25519.pub`) under **Settings → SSH keys**. 2. Check the connection: `ssh -T -p 2222 git@localhost` 3. Push: ```bash git remote add origin ssh://git@localhost:2222//.git git push -u origin main ``` ### Pushing over HTTPS Generate a token under **Settings → API tokens** and use it as the password when git asks (any username works): ```bash git clone http://localhost:3000//.git # Username: Password: vec_… ``` Anonymous users can clone public repositories without a token. ### Pushing container images Log in with an API token as the password (read & write to push), then tag and push images under a repository's name. Image names are lowercase: ```bash docker login localhost:3000 -u # Password: vec_… docker build -t localhost:3000//:latest . docker push localhost:3000//:latest docker push localhost:3000///worker:1.0 # more images in one repository ``` Anyone who can read the repository can pull; anonymous users can pull images of public repositories. The repository has to exist before you push. Docker only talks plain HTTP to `localhost`; anywhere else, serve Vectrum over HTTPS (or add the host to Docker's `insecure-registries`). A reverse proxy in front of Vectrum must pass large request bodies through (e.g. nginx `client_max_body_size 0;`). ### Writing the wiki Start the first page under a repository's **Wiki** tab, or clone the wiki and push Markdown files to it (the first push creates it): ```bash git clone ssh://git@localhost:2222//.wiki.git cd .wiki && echo "# Hello" > Home.md git add Home.md && git commit -m "Add Home" && git push ``` A page is named after its file: `Getting-Started.md` is *Getting Started* at `///wiki/Getting-Started`, in whichever folder it is. `Home` is the wiki's front page, `_Sidebar.md` and `_Footer.md` are shown around every page, and `[[Getting Started]]` or `[[the guide|Getting Started]]` link to a page. Pushes to a wiki don't start workflows. Repository names can't end in `.wiki`, since that's where wikis are cloned from. ### Publishing releases Draft one under a repository's **Releases** tab, or upload files from a script with a read & write API token. Uploading to a pushed tag that has no release yet publishes one for it; uploading a file again replaces it: ```bash git tag v1.0.0 && git push origin v1.0.0 curl -fT dist/app-linux-x64.tar.gz -u :vec_… \ http://localhost:3000///releases/download/v1.0.0/app-linux-x64.tar.gz curl -X DELETE -u :vec_… http://localhost:3000///releases/download/v1.0.0/app-linux-x64.tar.gz ``` Downloads are `GET ///releases/download//` (or `/releases/latest/download/` for the newest release that isn't a pre-release) and support ranges, so interrupted downloads resume. Source code is at `///archive/refs/tags/.zip` (or `.tar.gz`; branches and commits work too). Private repositories need a token (`-u :vec_…` or `-H "Authorization: Bearer vec_…"`). To publish from a workflow, give the runner's environment a token (workflows have no secrets) and `curl` the files in a job that runs `on: push: tags`. ### Running workflows 1. Register a runner under **Settings → Runners** (for all your repositories) or under a repository's **Settings → Runners**, and copy its token. 2. Start it on any machine with Node.js 18+ and git: ```bash curl -fsSLO http://localhost:3000/vectrum-runner.mjs node vectrum-runner.mjs --url http://localhost:3000 --token vrn_… ``` 3. Add `.vectrum/workflows/ci.yml` and push: ```yaml name: CI on: push: branches: [main] workflow_dispatch: # adds a "Run workflow" button jobs: test: runs-on: self-hosted # labels the runner must have steps: - run: npm ci - name: Test run: npm test ``` Supported: `on: push` (with `branches`, `tags`, `paths` and their `-ignore` forms) and `workflow_dispatch`; `env` at every level; jobs with `runs-on`, `needs`, `timeout-minutes`; steps with `run`, `name`, `shell`, `working-directory`, `env`, `if: success()/failure()/always()`, `continue-on-error` and `timeout-minutes`. The repository is checked out before the first step (`uses: actions/checkout` is accepted and skipped). Other `uses:` actions, `${{ }}` expressions, matrices, services and secrets are not supported; steps get `VECTRUM_*` environment variables (`VECTRUM_SHA`, `VECTRUM_REF_NAME`, `VECTRUM_REPOSITORY`, …) instead. Runners poll the server over HTTPS (`/api/runner/*`), so they need no inbound port. Every runner has the labels `self-hosted`, its OS (`linux`, `macos`, `windows`) and architecture (`x64`, `arm64`), plus any you give it. Each job gets a short-lived, read-only token that can only clone its own repository. Steps run as the user that started the runner, with no sandbox, so only run runners for repositories whose workflows you trust. ### Connecting AI agents (MCP) Vectrum is a remote [MCP](https://modelcontextprotocol.io) server at `/mcp` (Streamable HTTP). Add it to your agent, e.g. Claude Code: ```bash claude mcp add --transport http vectrum http://localhost:3000/mcp ``` Then run `/mcp` in Claude Code and choose *Authenticate*. Your browser opens Vectrum, where you sign in and choose what the agent may do: read-only or read & write, for all repositories or selected ones. Other MCP clients work the same way: they find the OAuth endpoints from the `401` answer, register themselves, and send you to approve them. *Settings → Connected apps* lists what you approved, and disconnecting cuts access right away. Clients without OAuth can send an API token instead (`Authorization: Bearer vec_…`). The tools, which check the same permissions as the web UI: | Area | Tools | | ---- | ----- | | Account and repositories | `get_me`, `list_repositories`, `search_repositories`, `get_repository`, `create_repository`, `update_repository` | | Code | `list_branches`, `create_branch`, `list_files`, `read_file`, `commit_files` (several files in one commit, optionally on a new branch, with an optimistic `expected_head` check), `list_commits`, `get_commit` (with its diff) | | Issues | `list_issues`, `get_issue`, `create_issue`, `update_issue`, `comment_on_issue`, `list_labels`, `create_label` | | Releases | `list_releases`, `get_release`, `create_release` (creates a missing tag at a branch or commit), `update_release` (also publishes drafts), `delete_release`, `upload_release_asset`, `create_release_upload_url`, `delete_release_asset` | | Wiki | `list_wiki_pages`, `read_wiki_page`, `write_wiki_page` (creates, replaces or renames a page) | | Actions | `list_workflows`, `list_workflow_runs`, `get_workflow_run`, `get_job_logs`, `run_workflow`, `cancel_workflow_run`, `rerun_workflow_run` | Read-only connections only see the reading tools. Commits made by an agent are authored as you and start workflows like a push would. Nothing deletes repositories or issues; `delete_release` removes a release and its files but keeps the tag. Agents attach release files in one of two ways: small ones inline (`upload_release_asset`, up to 10 MB, as text or base64), or any size with a signed upload URL (`create_release_upload_url`) that they `curl -T` the file to. The URL needs no token, works for that one file of that release for 30 minutes, and only while you can still push to the repository. It's signed with a key generated into `data/release-upload-key`. ## Configuration | Variable | Default | Purpose | | ----------------- | ----------- | --------------------------------------------------------- | | `PUBLIC_URL` | request host | Canonical address, e.g. `https://vectrum.cc`. Used for clone URLs; `https` also makes session cookies `Secure`. Set it in production | | `SECURE_COOKIES` | — | Set to `1` to mark session cookies `Secure` without `PUBLIC_URL` | | `DATA_ROOT` | `./data` | Where repositories, the database and the SSH host key live | | `PB_PORT` | `8090` | Port of the PocketBase server Vectrum runs | | `PB_URL` | — | Use this PocketBase server instead of running one | | `PB_SUPERUSER_EMAIL`, `PB_SUPERUSER_PASSWORD` | generated | Superuser login; required with `PB_URL` | | `PB_BINARY` | `bin/pocketbase` | PocketBase executable to run | | `SSH_PORT` | `2222` | Port of the built-in SSH server | | `SSH_LISTEN_HOST` | `0.0.0.0` | Interface the SSH server binds to | | `SSH_HOST` | request host | Host name shown in SSH clone URLs, e.g. `vectrum.cc` | | `SSH_DISABLED` | — | Set to `1` to not start the SSH server | | `SIGNUPS_DISABLED` | — | Set to `1` to close registration (add users with `scripts/create-user.mjs`) | Data layout: ``` data/ pb_data/ PocketBase: users, keys, tokens, repos, issues, releases, workflow runs and logs pocketbase-superuser.json the generated PocketBase superuser login ssh_host_ed25519_key generated on first start hooks/ server-side git hooks for every push, rewritten on start repos//.git bare git repositories wikis/.git each repository's wiki, named by its record id registry.db container image manifests, tags and blob records (SQLite) registry/blobs/sha256/… container image layers, shared by all repositories registry/uploads/ image uploads in progress releases///… release files, named by their record id ``` The registry keeps its records in SQLite rather than PocketBase so that storing a blob and pruning unused ones never interleave. A blob is deleted once no manifest of any repository references it (blobs pushed in the last 24 hours are kept so pushes in progress don't lose their layers). ## Security notes - Serve the web UI over HTTPS if it's reachable beyond localhost, and set `PUBLIC_URL`. `Host` and `X-Forwarded-*` headers are client-controlled, so they never decide cookie security, and only stand in for `PUBLIC_URL` in development. - Ref names are never passed to git where they could be read as options: pushes of refs with a component starting with `-` are refused by a pre-receive hook, such refs are ignored when reading, and every revision argument follows `--end-of-options`. - Private repositories show a 404 page to anyone without access, so their existence isn't revealed. - OAuth for agents follows the MCP authorization spec: public clients only, PKCE (S256) required, exact redirect URIs (any port for `localhost`), one-time codes that expire in 5 minutes, access tokens that last an hour, and refresh tokens that rotate on every use (presenting an old one revokes the grant). Only hashes of codes and tokens are stored. The consent page can't be framed, and it shows where it will send you, because anyone can register an application under any name. - Release files are always served as `application/octet-stream` attachments with `nosniff` and a sandboxing CSP. Uploads signed in with the browser session must come from the site's own origin; scripts use tokens. - Raw files are served with `nosniff` and a sandboxing CSP, so repository HTML/SVG can't run scripts in the app's origin. ## Layout - `lib/db.ts`: starting and connecting to PocketBase; `pb_migrations/`: the schema - `lib/users.ts`, `lib/auth.ts`: accounts and sessions (PocketBase auth records and tokens) - `lib/repos.ts`: repository records, permissions (`repoAccess`), collaborators - `lib/issues.ts`, `lib/issue-meta.ts`: issue tracker data and its shared vocabulary (statuses, priorities); UI in `components/issues/` - `lib/keys.ts`, `lib/tokens.ts`: SSH keys and API tokens - `lib/oauth.ts`, `lib/oauth-rules.ts`: the OAuth 2.1 authorization server for agents (`app/oauth/*`, `app/.well-known/*`); `lib/mcp/*`: the MCP endpoint (`app/mcp`) and its tools - `lib/git.ts`, `lib/git-write.ts`: reading from and committing to bare repositories - `lib/git-http.ts`, `lib/ssh-server.ts`: git transports (started from `instrumentation.ts`) - `lib/registry.ts`, `lib/registry-http.ts`: container registry storage and the OCI distribution API, served under `/v2` - `lib/wiki.ts`, `lib/wiki-links.ts`: wiki pages in the wiki's git repository, and page names and `[[links]]`; UI in `app/[owner]/[repo]/wiki/` and `components/wiki/` - `lib/releases.ts`, `lib/release-http.ts`: releases and their files, and the download, upload and source archive routes - `lib/workflows.ts`, `lib/ci.ts`, `lib/runs.ts`, `lib/runners.ts`: workflow parsing, starting runs on push, the job queue and runner registration; `app/api/runner/*` is the runner API and `public/vectrum-runner.mjs` the runner - `app/actions/*`: server actions, each re-checking the session and permissions