A catalog of retro software for Soviet home computers: Vector-06C, BK, Radio-86RK, and more.
Content lives in the storage/ git tree (markdown + files), is indexed in SQLite, and is served via a Go API and React SPA.
Content repo can be found here.
Inspired by caglrc.cc/scalar.
WARNING: Neuroslop ahead.
| Layer | Technologies |
|---|---|
| Backend | Go 1.27, chi, SQLite (FTS5), goldmark, bluemonday |
| Frontend | React 19, TypeScript, Vite, Ant Design |
| Emulator | vector06js — vendored in frontend/emulator-src/, bundled at build time |
| Containers | Podman/Docker, Alpine 3.24.3; Debian 13 (E2E); Node.js 24.22.1 LTS; reflex (dev) |
cmd/server/ — HTTP server entry point
internal/
config/ — config (YAML + env)
delivery/http/ — REST API
parser/ — README.md and frontmatter parsing
repository/ — SQLite + FTS
sync/ — storage sync from git remote
staticfiles/ — storage, frontend/dist, and emulator static files
frontend/
src/ — React SPA
emulator-src/ — vector06js sources (vendored, no nested .git)
scripts/ — emulator bundle build and vendor updates
public/emulator/ — build output (gitignored)
dist/ — production SPA + emulator build
storage/ — content (platforms, authors, ROM/FDD/zip)
build/
dev/ — dev image, reflex
prod/ — production Containerfile, compose example
- Docker or Podman (with Compose)
- git
- make
Go, Node.js, and CGO toolchains run inside containers — nothing extra on the host.
Run make help for all targets.
make setupmake dev
# or: make setup && make up-build- Backend: http://localhost:8080 (reflex restarts Go on changes)
- Frontend: http://localhost:5173 (Vite HMR)
Health check: curl http://localhost:8080/ping → pong.
make prod # copies build/prod/config.yaml.example if needed, then compose up --buildCompose file: build/prod/docker.compose.example.yaml. Example config: build/prod/config.yaml.example.
Production uses a persistent SQLite database in the warehouse06-data Docker volume (file:/app/data/warehouse06.db). Content is mounted from storage/ at the repository root.
The image is built from build/prod/Containerfile: Node frontend build → Go server build → Alpine runtime.
Go stays on the 1.27 release line without a patch pin. Make build targets pull the latest base images; CI also checks for the latest Go 1.27 patch.
Example YAML config: build/dev/config.yaml.example.
| Parameter | Description |
|---|---|
storage_dir |
Path to the content directory (default storage) |
storage_url |
Git remote for periodic sync |
server.port |
HTTP port (8080) |
gopher.enabled |
Enable the read-only Gopher TCP service (default true, env GOPHER_ENABLED) |
gopher.listen_addr |
Internal TCP bind address (default :7070, env GOPHER_LISTEN_ADDR) |
gopher.host |
Public hostname advertised in menus (default localhost, env GOPHER_HOST) |
gopher.port |
Public port advertised in menus (default 7070, env GOPHER_PORT) |
database.dsn |
SQLite DSN (:memory: or file path) |
sync.interval_hours |
Sync interval in hours; 0 — startup only |
Precedence: defaults → YAML → environment variables (env wins).
To load a YAML file, set CONFIG_PATH to its path. If CONFIG_PATH is unset or points to a missing file, the app uses only defaults + environment variables.
Gopher is enabled by default in development and production. Set gopher.enabled: false or GOPHER_ENABLED=false to disable it.
Connect to gopher://localhost:7070/ in the development stack.
Menus provide platforms, authors, tags, search, entry cards, full text and original file downloads.
The service shares the current library catalog with HTTP and follows the same file extension restrictions.
Menus and generated text use UTF-8; menus are paginated and limited to 32 KiB, 800 lines, 255 bytes per line and 72 display columns per label.
Full texts larger than 24 KiB after conversion open directly on the first page of a text menu, with previous/next page and entry-card links at the bottom.
Shorter texts retain the plain-text view.
The threshold counts UTF-8 text after normalization and wrapping, including CRLF line endings but excluding dot-stuffing and the protocol terminator.
Pages preserve paragraphs where possible, then sentences; a long sentence may exceed the 24 KiB target, but the complete menu must still fit the limits above.
When a hard limit requires splitting a sentence, pages split at word boundaries, or at UTF-8 character boundaries for an oversized word.
Page links use a zero-based page parameter and read the current catalog content on each request; updating a document can change its page boundaries.
Legacy /text?path=... selectors follow the same rules, so a long document now returns a menu rather than plain text; reopen its entry card if an old bookmark declares text type 0.
For Lynx in a UTF-8 terminal, explicitly set the assumed document and display charsets:
lynx -assume_charset=UTF-8 -display_charset=UTF-8 gopher://localhost:7070/Lynx's default assumed charset can garble Cyrillic Gopher menus even when the terminal uses UTF-8.
For a public deployment, set gopher.host in build/prod/config.yaml to the hostname clients can reach. Production Compose publishes 70:7070 by default, and the sample config advertises port 70 while keeping gopher.listen_addr: ":7070".
The advertised public address is independent of the listener address.
Long search queries use compact page links backed by an in-memory FIFO cache of 512 distinct long queries.
Those links expire on restart or after eviction; the client receives an explicit search-expired error and can re-enter the query.
Short search queries remain embedded in their page links.
Requests must fit within 4096 bytes including CRLF and arrive within 60 seconds.
Transfers refresh a 60-second write timeout for each chunk of at most 512 bytes, allowing downloads to continue beyond one minute.
An enabled Gopher listener that cannot start causes the backend startup to fail.
Gopher closes its connections during backend shutdown.
This is a native Gopher TCP service; it does not expose Gopher menus over HTTP(S). Acceptance is covered by automated TCP tests; testing on a physical Vector-06C is outside the scope of this feature.
Interactive documentation (schemas, parameters, try-it-out) is served at /api/docs when the server is running, generated from the OpenAPI spec at internal/delivery/http/docs/openapi.yaml.
All routes are under the /api prefix:
| Method | Path | Description |
|---|---|---|
| GET | /api/status |
Sync status |
| GET | /api/entries |
Entry list (filters, pagination) |
| GET | /api/entries/search |
Full-text search (FTS) |
| GET | /api/entries/{path} |
Single entry by path |
| GET | /api/authors |
Author list |
| GET | /api/authors/{dir} |
Author page |
| GET | /api/tags |
Tags |
| GET | /api/platforms |
Platforms |
Static files:
/— React SPA fromfrontend/dist/emulator/— vector06js (build bundle)/{platform}/...— files fromstorage/(ROM, FDD, zip, images)
Each entry is a directory with a README.md (frontmatter + markdown). Top-level platforms include vector06c, bk, rk86, authors, …
The syncer can pull updates from storage_url on startup (and on a schedule).
The built-in emulator is vendored upstream code without a nested git repo:
- Sources in
frontend/emulator-src/, pinned versions invendor.lock.json. npm run build:emulator(viamake devormake ci) — concatenates JS and copies zip.js (Web Workers) and assets intopublic/emulator/.- Vite copies
public/emulator/todist/emulator/.
make vendor-emulator # use refs from vendor.lock.json
make vendor-emulator REF=<sha> # bump vector06js to a commitVendoring applies the local iframe/input and zip.js integration patch from
frontend/scripts/emulator.patch, and generates bin2wav types from
frontend/scripts/bin2wav-tape.d.ts. The check compares the complete generated
contents, including stale files, against the pinned upstream sources.
Commit vendored sources, the lock and adaptation changes together. Do not commit
public/emulator/.
make test # Go tests in dev backend container
make ci # frontend build + tests (same as CI)Go tests require CGO and the sqlite_fts5 build tag (enabled automatically via make test / make ci):
CGO_ENABLED=1 go test -tags sqlite_fts5 ./...Running bare go test ./... without these flags will fail on FTS5 schema initialization.
CI (.github/workflows/ci.yml) runs Go and frontend checks on GitHub runners,
Playwright E2E, vendor verification, production image build, Trivy scan and smoke test.
Successful pushes to master publish the Docker image. make ci runs the same
build and test checks locally in containers; vulnerability checks run separately
with make vuln, npm audit and Trivy.
| Target | Description |
|---|---|
make help |
List all targets |
make setup |
Copy dev .env if missing |
make dev |
Start dev stack (docker compose up --build) |
make build |
Build production Docker image (warehouse06:local) |
make test / make ci |
Go tests / CI pipeline (in containers) |
make up / make prod |
Docker dev / production compose |
make vendor-emulator |
Refresh emulator sources (in container) |
make clean |
Remove build artifacts on bind mounts |
- vector06js — BSD-style, Viacheslav Slavinsky (svofski/vector06js, README)
- i8080-js — Alexander Demin (begoon/i8080-js)
- zip.js — Gildas Lormeau (gildas-lormeau/zip.js)
- bin2wav — Viacheslav Slavinsky (svofski/bin2wav)