- Go 55.5%
- TypeScript 30.6%
- CSS 8.2%
- Shell 5.2%
- Makefile 0.3%
- Other 0.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Reported from a real machine: upgrading an existing server asked all the questions again and then refused, saying port 8080 was in use — by the very service it was about to upgrade. The detection looked for the binary where *this* version puts it, so anything installed by an earlier one was invisible. It is the unit that says an installation is here, and that has been in the same place all along. The old layout kept its settings as Environment= lines inside that unit rather than in a configuration file, so they are read out of it: an upgrade keeps the port somebody chose instead of quietly moving their server to 8080. The service is now stopped before the port is looked at, which is what makes the check mean "somebody else has this port". And the binary the previous layout left in /usr/local/bin is removed, because leaving an outdated one on the PATH under the same name is how somebody debugging at midnight runs the version before last. The look is the house's now: the banner, the welcome line, the questions in the order the others ask them, and the same three closing lines. The installed command is universal again. It carried LASTHELP_CHANNEL=beta, which is a thing to remember and to get wrong, when the installer asks which channel you want — the variable stays for the case that has no terminal to ask on. Verified against a machine set up by the previous installer, with something really listening on the port it had chosen: the upgrade is recognised, port 9001 and the proxy setting survive, the database is backed up, the old binary goes, and the hardened unit replaces the inline one. |
||
| .forgejo/workflows | ||
| assets | ||
| cmd | ||
| deploy | ||
| docs | ||
| internal | ||
| scripts | ||
| web | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| bun.lock | ||
| CHANGELOG.md | ||
| CONTRIBUTING.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| go.mod | ||
| go.sum | ||
| install.sh | ||
| lasthelp.key | ||
| LICENSE | ||
| Makefile | ||
| NOTICE | ||
| package.json | ||
| README.md | ||
| SECURITY.md | ||
| tsconfig.json | ||
📖 Read the Documentation for detailed guides on installation, your first repository, connecting a forge, notifications and keeping the queue readable.
Status
Beta. One release is published and signed, and this section says what is actually behind it — a README describing a server nobody wrote is the first thing that makes people stop trusting one.
Working end to end, each of them checked by driving a real browser before it ships: organisations and boards, reporting without an account, the secret follow link, the moderation queue, publishing, voting, comment threads, similar-item hints while you type, the roadmap, the panel — overview, the cross-board listing and triage from it — the anti-abuse layers, notifications in both directions, the forge bridge for GitHub, GitLab, Gitea and Forgejo, installing, and updating in place.
Not there yet: Bitbucket, and a stable release — the only release is a
prerelease, so answer beta when the installer asks which channel you want.
Why LastHelp
Self-hosting your forge usually means turning registration off, and that closes the only door outsiders had for telling you something is broken. Opening registration to the world is not the answer; a separate front door is. The open-source feedback tools do not talk to any forge, and the ones that do are closed, paid and hosted by somebody else — so nobody serves the person who self-hosts their own forge, which is almost always the same person who would self-host a feedback portal. LastHelp was built with a simple rule: anybody can tell you something without an account, and nothing reaches a public page until a person says so.
- Reporting without an account — a bug or an idea, in one form, from anybody. What they get back is a secret link: the only way to their own report, and the reason no sign-up is needed at all.
- A bridge to your forge — GitHub, GitLab, Gitea and Forgejo, hosted anywhere including your own network. A triaged report becomes an issue, or is attached to one that already exists — which is what a project with four thousand open issues actually needs — and when that issue closes or reopens, the report follows.
- A roadmap nobody maintains — three columns built from the state of each report, so it cannot be out of date. Ideas gather votes, which is what turns "somebody asked for this once" into an order of work.
- A queue that stays readable — nothing is published without a person, and the decision sits on the row: publish, reject, spam. One listing covers every board an account has a role in, filtered by state, kind, status, board or words, with the filters in the address so a narrowed view can be sent to somebody else.
- An overview worth opening — the last thirty days as a chart you can ask about day by day, the most wanted ideas, the newest reports and the work per board. Every number on it leads to the listing it counts.
- Anti-abuse without a third party — a signed single-use form token, a timing window, a honeypot, rate limits keyed on the address a caller cannot choose, duplicate and link heuristics, an operator word list, and a breaker that closes a board rather than hand somebody a queue they will not open. Every refusal is counted by layer, because a filter nobody can measure is a filter quietly turning away people who never complain.
- Notifications in both directions — the team is told through ntfy, Gotify, a webhook or mail, configured per repository. Whoever reported something follows an Atom feed that needs no address, no account and no configuration, or leaves an email if a relay is set up.
- One binary, one file — no database server, no runtime, no dependencies. SQLite and the whole interface are compiled in, and a backup is a copy of one file — or a button in the panel.
- Twenty languages, translated by hand — not machine-filled — with right-to-left laid out properly for Arabic, Persian and Urdu. English and Spanish are complete; about a hundred strings from the newest screens are still waiting in the other eighteen and fall back to English until they are done.
- Reproducible builds —
-trimpath,CGO_ENABLED=0and a frontend stamped with a digest of its own output, so two builds of the same commit produce the same bytes and you can check ours against yours.
Bitbucket is not bridged yet. Everything else above works end to end, and is checked by driving a real browser rather than by hoping.
Security & Privacy
Security
Passwords are hashed with Argon2id. Sessions are opaque tokens in an HttpOnly, SameSite=Lax cookie, marked Secure over HTTPS, and CSRF is a double-submit token checked alongside Sec-Fetch-Site — a header the browser writes from the real origins and a page cannot forge. Every response carries a Content-Security-Policy with script-src 'self', so an injected script does not run, and Referrer-Policy: no-referrer, because the address of a report carries the token that opens it. Rate limits are keyed on the address our own proxy added rather than the first one the caller sent, because a proxy appends to X-Forwarded-For and reading the left-hand entry lets anybody choose their own identity. Forge tokens are stored encrypted and never shown again, request bodies are bounded per handler, and every release is signed.
For a detailed breakdown, including what the threat model does not cover, see the Security Policy.
Privacy
Reporting is anonymous by design. No account, no tracking, no analytics, and no telemetry of any kind: nothing about you or your instance is ever reported to us. The email address a reporter may leave is optional, asked for only when a mail relay exists to use it, stored encrypted, visible only to the team, and deleted when the report closes.
And no AI. Nothing anybody writes here is sent to a third party — the whole point is that it reaches one database and nowhere else.
Installation Methods
Quick Install (Linux)
Run this command to install or update LastHelp:
curl -fsSL https://git.codigosh.com/CodigoSH/LastHelp/raw/branch/main/install.sh | sudo sh
It asks which port to use, which release channel to follow and whether a reverse proxy is in front, and remembers all three the next time you run it. Answer beta to the channel question today: there is no stable release yet, and the default only ever picks a stable one, so nobody following this command lands on a prerelease by accident. Without a terminal to ask on — a container, a cron job — every answer is its default and LASTHELP_CHANNEL=beta supplies that one.
It verifies the signature over the checksums before anything reaches the disk, installs a systemd service, keeps the database in /var/lib/lasthelp and serves on port 8080 unless you answer otherwise when it asks. The first account created is the administrator: open the address it prints and claim the instance — a server with somebody in it cannot be claimed again.
Docker
The repository ships a Dockerfile and a docker-compose.yml, and the image is built from source: there is no published image yet.
git clone https://git.codigosh.com/CodigoSH/LastHelp.git
cd LastHelp
docker compose up -d
services:
lasthelp:
image: lasthelp:latest
build: .
container_name: lasthelp
restart: unless-stopped
ports:
- "8080:8080"
volumes:
- lasthelp-data:/data
environment:
LASTHELP_PORT: "8080"
LASTHELP_DB_FILE: "/data/lasthelp.db"
# Turn this on only when a reverse proxy really is in front. With it on
# and no proxy there, the caller chooses the address we attribute their
# requests to, and the rate limits stop meaning anything.
LASTHELP_TRUST_PROXY: "false"
volumes:
lasthelp-data:
The image runs as its own unprivileged user and the binary inside refuses to update itself, because updating a container is pulling a new image.
Every setting the binary reads is listed in .env.example and explained under Settings.
Updates
Updates are delivered directly from our own Forgejo instance — no proxy, no intermediary and no telemetry.
The updater checks the signature over the checksum list before it downloads anything, then the binary's own checksum, then makes the candidate prove it runs and report its schema, then refuses any binary older than the database, takes a backup, and only then replaces anything. None of those has a switch. In a container it says to pull an image instead of offering a button that would silently revert.
For detailed instructions see the Updating guide, or the Security Policy for how the signing works.
Build
make build # frontend + binaries into bin/
make run # build and run
make test # go test, the architecture rules and the frontend tests
make drive # walk the whole journey in a real browser
make live-forge # and again against a real Forgejo, Gitea or GitLab in a container
make shots # regenerate the images this repository shows
Support & Recognition
We are an open-source initiative managed through Open Collective Europe.
Financial Support
If you find our tools useful, consider supporting our infrastructure through an unconditional donation.
Acknowledgments
Special thanks to everyone supporting our mission.
Organizations
We appreciate the organizations that support us as a gesture of goodwill to help maintain our infrastructure.
Credits
- Everything drawn here: the mark, the icons and the charts are drawn by hand in this repository — no icon library, no chart library, no frontend framework. The whole frontend is TypeScript compiled by bun, and its only dependency is the type definitions.
Full attribution, including the Go modules compiled into the binaries, lives in NOTICE.
A project by CodigoSH — built to last.
