Vectrum
Sign In
178 lines · 11.8 KB

Vectrum

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 <host>/<owner>/<repo> (or <host>/<owner>/<repo>/<name> 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
  • 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
  • Light and dark mode

Getting started

Requires Node.js 22+ (24.7+ for post-quantum SSH key exchange) and git on the PATH.

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. 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

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). To close registration, set SIGNUPS_DISABLED: "1" before you deploy, and create accounts from the command line instead:

docker compose exec -it vectrum node scripts/create-user.mjs <username> <email> [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:
    git remote add origin ssh://git@localhost:2222/<you>/<repo>.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):

git clone http://localhost:3000/<you>/<repo>.git
# Username: <you>   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:

docker login localhost:3000 -u <you>         # Password: vec_…
docker build -t localhost:3000/<you>/<repo>:latest .
docker push localhost:3000/<you>/<repo>:latest
docker push localhost:3000/<you>/<repo>/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;).

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:
    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:
    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.

Configuration

VariableDefaultPurpose
PUBLIC_URLrequest hostCanonical 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./dataWhere repositories, the database and the SSH host key live
PB_PORT8090Port of the PocketBase server Vectrum runs
PB_URL—Use this PocketBase server instead of running one
PB_SUPERUSER_EMAIL, PB_SUPERUSER_PASSWORDgeneratedSuperuser login; required with PB_URL
PB_BINARYbin/pocketbasePocketBase executable to run
SSH_PORT2222Port of the built-in SSH server
SSH_LISTEN_HOST0.0.0.0Interface the SSH server binds to
SSH_HOSTrequest hostHost 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, 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/<owner>/<repo>.git      bare git repositories
  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

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.
  • 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/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/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