โ All docs ยท Home
Deployment
One-command demo (Docker Compose)
docker compose up --build
Brings up the full stack:
| Service | Port | Role |
|---|---|---|
feedback |
8080 | publish + serve + /sync |
discovery |
8090 | announce + resolve |
collection |
8100 | index + equivalence + polite cache |
The collection server is pre-pointed at the feedback server
(FREEDBACK_SERVERS=http://feedback:8080). Try it:
curl localhost:8080/.well-known/freedback
curl 'localhost:8100/index?target=https://example.com/item/1'
Single server
docker build -t freedback .
docker run -p 8080:8080 -e FREEDBACK_BASE_URL=https://feedback.example.org \
freedback freedback-feedback-server
Environment
| Var | Default | Applies to |
|---|---|---|
FREEDBACK_BIND |
0.0.0.0:8080 (image) |
all servers |
FREEDBACK_BASE_URL |
http://<bind> |
all servers (mints ids / links) |
FREEDBACK_SERVERS |
โ | collection (comma-separated upstreams) |
FREEDBACK_STORE_PATH |
โ | feedback (JSON-Lines snapshot file; in-memory backend) |
FREEDBACK_ROCKSDB_PATH |
โ | feedback (durable RocksDB dir; needs the rocksdb build) |
FREEDBACK_OAUTH_TOKEN / _APP / _USER |
โ | feedback (one demo bearer token) |
FREEDBACK_DEFAULT_LICENSE |
โ | feedback (license IRI advertised as "license" in /.well-known/freedback; annotations without an explicit rights fall under it โ ADR 0022) |
Storage
This image uses Oxigraph's in-memory backend โ the workspace pins
oxigraph with default-features = false, so RocksDB and Clang are not needed
and the build is a clean single binary.
Durable demo storage (snapshots). Set FREEDBACK_STORE_PATH to a file on a
mounted volume and the feedback server loads it on boot, re-snapshots every 60 s,
and snapshots on graceful shutdown (Ctrl-C / SIGTERM). The compose stack does
this on a named volume, so feedback survives restarts. The format is
JSON-Lines (one annotation per line), backend-agnostic and put-idempotent.
This is snapshot โ not transactional โ persistence (ADR 0008): a crash
between snapshots can lose up to ~60 s of writes. Leave FREEDBACK_STORE_PATH
unset for the old ephemeral behavior.
Durable RocksDB backend (rocksdb feature). For real transactional
persistence, build the feedback server with the on-disk Oxigraph/RocksDB backend
and point it at a directory:
# Native:
cargo run -p freedback-feedback-server --features rocksdb
# with FREEDBACK_ROCKSDB_PATH=/var/lib/freedback/db
# Docker (durable image โ the rust image has the C/C++ toolchain RocksDB needs):
docker build --build-arg FEEDBACK_FEATURES=rocksdb -t freedback:durable .
docker run -p 8080:8080 -v freedback-db:/data \
-e FREEDBACK_ROCKSDB_PATH=/data/db freedback:durable freedback-feedback-server
When FREEDBACK_ROCKSDB_PATH is set on a rocksdb build, every write is
persisted directly (no snapshot loop) and survives a hard restart. On a build
without the feature the variable is ignored (with a warning) and the in-memory +
snapshot path is used. RocksDB needs Clang/LLVM (or g++) at build time, which is
why it is opt-in rather than the default demo image.
Releases
Per-package releases (the primary model)
Every releasable unit is versioned, tagged, and released independently. The
set of packages is declared once in .github/packages.json:
| Package | Path | Tag |
|---|---|---|
protocol-lib, storage, feedback-server, cli-client, discovery-server, collection-server, advanced-client |
crates/* |
<name>-v<version> |
widgets (@freedback/widgets) |
widgets/ |
widgets-v<version> |
mobile (the Tauri app) |
apps/mobile/ |
mobile-v<version> |
firefox-extension |
firefox-extension/ |
firefox-extension-v<version> |
Two rules make this run without anyone cutting tags by hand:
- Bump-on-touch (enforced at PR time).
.github/workflows/versions.ymlfails a pull request that changes a package's code without bumping that package's version (docs-only changes are exempt). Each crate carries its ownversionin itsCargo.toml(no longer inherited from the workspace);widgetsuses itspackage.jsonversion; the mobile app uses its workspace version; the extension uses itsmanifest.jsonversion. - Tag + release on merge.
.github/workflows/tag-and-release.ymlruns on every push tomain: for each package whose current version has no tag yet, it creates<name>-v<version>and a GitHub Release. It's idempotent โ a merge that bumped onlyfeedback-serverreleases onlyfeedback-server.- binary crates (the servers,
freedback,freedback-sync) attach a staticx86_64-unknown-linux-muslbinary (the build also gates the release โ broken code can't publish); - library crates (
protocol-lib,storage) and the extension get a notes-only Release; widgetsadditionally runsnpm publish(guarded onNPM_TOKEN);mobilehands off tomobile-release.yml, which builds the signed APK/AAB and publishes behind its ownapp-ci.ymlgate.
- binary crates (the servers,
The changes job in ci.yml also uses the package layout to run only the
suites affected by a change (a docs- or ontology-only PR skips the Rust and
browser matrices).
The binary crates' Releases each carry a fully static
x86_64-unknown-linux-musl binary (built with cargo-zigbuild โ zig as the
C cross-compiler for ring; no glibc, no OpenSSL, no RocksDB, runs on any
x86-64 Linux) plus its .sha256. Library crates and the extension get a
notes-only Release. (There is no aggregate all-in-one bundle any more โ each
package ships on its own tag.)
Publishing @freedback/widgets to npm (guarded)
tag-and-release.yml publishes the drop-in widgets as
@freedback/widgets (the
scope reserved in docs/naming.md) as part of the widgets package's release,
so React/any apps can npm add @freedback/widgets. It builds the ESM + UMD
bundles and the bundled .d.ts from the canonical widgets/freedback-widgets.js
via the package's prepublishOnly hook, then npm publish --access public.
It is guarded: it runs only when an NPM_TOKEN repo secret is present.
Until the owner enables it, the step is skipped cleanly โ the tag + GitHub
Release still go out, just no npm publish. To turn it on (one-time, outside
the repo):
- Create the
@freedbacknpm org/scope at https://www.npmjs.com/org/create (or reserve the scope under your user). The package name@freedback/widgetsneeds the@freedbackscope to exist and be owned by the publishing account. (The unscoped namefreedbackis taken by an unrelated dormant package โdocs/naming.md.) - Mint an automation token (npm โ Access Tokens โ Granular/Automation,
with publish rights to
@freedback/*). - Add it as a repo secret named
NPM_TOKEN(GitHub โ Settings โ Secrets and variables โ Actions โ New repository secret).
Then bumping widgets's version and merging publishes it at that version. npm
rejects re-publishing an already-published version, so the version must be new
(the bump-on-touch gate enforces that). The <script>/CDN path
(https://freedback.net/widgets/freedback-widgets.js, served by Pages) keeps
working independently of npm.
TLS
The servers speak HTTP/1.1. Terminate TLS at a reverse proxy (Caddy/Traefik) in front of them. Browser/WASM clients have TLS managed by the browser.
Static artifacts (GitHub Pages)
.github/workflows/pages.yml publishes the protocol artifacts at the stable
freedback.net URLs that protocol-lib::context pins:
https://freedback.net/ns/context.jsonldโ the JSON-LD@contexthttps://freedback.net/ns/freedback.ttlโ the vocabulary (also at/ns)https://freedback.net/ns/shapes.ttlโ the SHACL shapeshttps://freedback.net/profile/1โ the validation profile (dcterms:conformsTo)https://freedback.net/widgets/freedback-widgets.jsโ the drop-in widget script
Pages is static-only and never runs a server. The workflow writes a CNAME
of freedback.net into the artifact, so once the apex DNS points at GitHub Pages
the site serves on the custom domain with auto-provisioned HTTPS.
Attaching freedback.net (one-time, registrar + GitHub UI)
These steps are outside the repo and must be done by the domain owner:
- DNS at your registrar โ point the apex
freedback.netat GitHub Pages:Aโ185.199.108.153,185.199.109.153,185.199.110.153,185.199.111.153AAAAโ2606:50c0:8000::153,2606:50c0:8001::153,2606:50c0:8002::153,2606:50c0:8003::153- (optional)
CNAMEwwwโ<owner>.github.io
- Repo โ Settings โ Pages: set Source = GitHub Actions (the
pages.ymlworkflow), set Custom domain =freedback.net, then enable Enforce HTTPS once the certificate is issued. - (Recommended) Verify the domain under GitHub Settings โ Pages โ "Verified
domains" (add the
TXT _github-pages-challenge-โฆrecord it shows) to prevent takeover.
freedback.dev is not available (taken by an unrelated, dormant npm homonym
โ docs/naming.md); freedback.net is the owned, canonical base.
CI validation
ci.ymlโ fmt, clippy, native tests, wasm32 builds, thex86_64-unknown-linux-muslstatic build, widgets unit + headless E2E, and ontology parse checks.container.ymlโ builds this image (on deploy-config / lockfile changes or on demand), so the Dockerfile can't silently rot.pages.ymlโ publishes the static artifacts frommain.versions.ymlโ PR gate: a package touched without a version bump fails.tag-and-release.ymlโ on merge tomain, tags + releases each package whose version is new (per-package musl binary, notes, or npm publish), and dispatches the mobile release. See "Per-package releases" above.app-ci.yml/mobile-release.ymlโ the mobile app's own CI and release pipeline (apps/mobile/README.md);mobile-release.yml's Release publish is gated onapp-ci.ymlhaving succeeded for the released commit.