Skip to content
sakuga-softwarePublic

Latest commit

Β 

History

287 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ“ Fruitback

CI Licence: MIT + AGPL-3.0

Visual feedback on a live site, without a backend to host.

A client opens their staging site, clicks the element that bothers them, types a note. It lands as a triaged issue carrying the CSS selector, the React component and the source file behind that element. When they come back to the page, their pins are still there, coloured by the issue's status: new, to do, in progress, done.

A pin anchored on a button of a live page, coloured by its issue's status

The guides are on sakuga-software.github.io/fruitback, and they are the files in docs/.

Fruitback runs no service that holds your feedback. It lands in the tracker your team already uses β€” or, with FRUITBACK_STORE=sqlite, in a file on a volume you own. No database of ours, no dashboard of ours, no account to create anywhere.

It comes in three modes, and which one you want is the first thing to settle. They differ on what the site has to ship: public, where the site embeds the widget and every visitor can leave a note; private, where the site embeds nothing and a browser extension puts the widget on it for one reviewer; and team, where the site embeds a dormant widget that wakes up for a reviewer the extension has signed in. One page each way β€” docs/modes.md.

Install, in public mode

Two lines on a site with no build step. client is the name this site reports under, and endpoint is your own worker β€” the piece that holds the tracker's API key, because that key cannot ship in client-side JavaScript. It is one container: docs/self-hosting.md.

The other two modes need the same worker and a browser extension on top; the reviewer's side of both is docs/reviewing.md.

<script
  src="https://cdn.acme.dev/fruitback.iife.js"
  data-fruitback-endpoint="https://feedback.acme.dev"
  data-fruitback-client="acme"
  defer
></script>

Or as an import, mounted after your app has rendered the elements it points at:

import { init } from 'fruitback';

const widget = init({ endpoint: 'https://feedback.acme.dev', clientId: 'acme' });

As a tag, for a page that prefers markup to a call (@fruitback/element):

<script src="https://cdn.acme.dev/fruitback-element.iife.js" defer></script>
<fruitback-widget endpoint="https://feedback.acme.dev" client-id="acme"></fruitback-widget>

Or as a component in a React application (@fruitback/react):

import { Fruitback } from '@fruitback/react';

<Fruitback endpoint="https://feedback.acme.dev" clientId="acme" />;

The packages are not on npm yet. The lines above are the shape of the install, not something that resolves today. The worker image is published β€” see docs/self-hosting.md.

The worker is three commands and one line to edit, and none of them needs a clone of this repository:

curl -fsSLO https://raw.githubusercontent.com/sakuga-software/fruitback/main/docker-compose.yml
curl -fsSL https://raw.githubusercontent.com/sakuga-software/fruitback/main/.env.example -o .env
# set ALLOWED_ORIGINS in .env to the site that embeds the widget
docker compose up -d --wait

It keeps the notes in SQLite on a Docker volume, with no account anywhere. CI runs this file from an empty directory, plants a pin, recreates the container and reads the pin back. To update, run docker compose pull first: Compose does not pull a tag that is already on the machine, and edge moves on every merge.

While this repository is private, the raw files and the image are private too. Until then, download the two files with the GitHub CLI, logged in with gh auth login, and log in to ghcr.io with a token that has read:packages β€” see docs/self-hosting.md:

gh api repos/sakuga-software/fruitback/contents/docker-compose.yml -H 'Accept: application/vnd.github.raw' > docker-compose.yml
gh api repos/sakuga-software/fruitback/contents/.env.example -H 'Accept: application/vnd.github.raw' > .env

The full walk-through β€” Linear setup, per-client routing, identified reporters β€” is in docs/install.md.

The note popover, anchored to the element it is about

Three ways to run it

The first question a reader has is whether their reviewers need the site to ship anything. All three answers exist, and docs/modes.md is the page that picks between them.

Public Private Team
The site embeds the widget nothing the widget, dormant
Delivered as <script> tag or npm a browser extension <script> tag or npm
Published not yet β€” build it from this repo not yet β€” load it unpacked not yet β€” both of the above
Who is shown the pins every visitor the reviewer who switched the site on reviewers who are signed in
Who can fetch them with no credential anyone anyone nobody, under authenticated
Who supplies the credential the host's backend, or nobody nobody can the reviewer, by pairing
Good for a public "report a problem" reviewing a client's site, invisibly a team reviewing its own staging
Built yes yes, MV3 on Chromium and Firefox yes

Who may read is read β€” FRUITBACK_READ worker-wide, or per client in FRUITBACK_CLIENTS β€” and the mode decides who can satisfy it. A public-mode site can run read: 'authenticated' if it mints identity tokens of its own β€” init({ identityToken }) is the seam, and the credential then lives in that site's page. Team mode is the only one where the reviewer supplies it and the page never holds it, attached in the extension's background. Private mode can supply nothing at all: the widget it mounts has no token and no relay, so on an authenticated worker its reads answer 401, and left at the public default its pins are readable by anyone who can build the URL β€” it changes who is shown the feedback, never who may fetch it. docs/modes.md is the page that lays this out. The four lines a team-mode site adds are in docs/install.md; what the relay refuses is in SECURITY.md.

The extension asks for no host permission at install: its content scripts are registered at runtime, per origin, when somebody switches that site on.

Where your feedback lives

FRUITBACK_STORE picks the connector. It defaults to linear, so a deployment that sets nothing keeps the behaviour it has. Each connector reads only its own variables β€” a worker on SQLite is never asked for a Linear key β€” and an unknown name is refused at boot rather than quietly defaulted.

FRUITBACK_STORE Needs Runs on Good for
linear (default) LINEAR_API_KEY, LINEAR_TEAM_ID anywhere the worker runs A team already triaging in Linear. Dashboard, API, MCP and integrations come for free.
sqlite FRUITBACK_SQLITE_PATH a persistent filesystem only Self-hosting with no third party at all. One file on a volume.
github FRUITBACK_GITHUB_APP_ID, FRUITBACK_GITHUB_PRIVATE_KEY, FRUITBACK_GITHUB_REPOSITORY anywhere the worker runs A team whose issues are already on GitHub. Three stages instead of five: New while open, Done when closed as completed, Closed when closed as not planned or as a duplicate.
memory nothing the dev loop Refused under NODE_ENV=production.

SQLite is one file through node:sqlite β€” no dependency, no native module, and the schema migrates itself on open, so a self-hoster starts one container rather than two. What it gives up is the dashboard: with no tracker behind it, the pins on the page are the interface. The compose snippet, the one-line backup and what else you trade are in docs/self-hosting.md.

GitHub Issues signs in as a GitHub App, never with a personal token: the worker mints a token that expires after an hour and reaches one repository. Setting up the App is in docs/self-hosting.md. Every connector plugs into apps/worker/src/store.ts.

Security, and what is open by default

Fruitback puts a widget in somebody else's page and a worker in front of somebody's issue tracker. SECURITY.md says where those boundaries are, and it is worth reading before you deploy rather than after. Two things surprise people most often:

  • GET /feedback answers anyone who can build the URL, unless you set FRUITBACK_READ=authenticated. Every note, its reporter's name and address, and the team's replies are readable by any visitor of the client's site β€” and by curl. The default stays open so an upgrade never blanks a working deployment; the boot log names every client it applies to.
  • clientId is asserted by the browser, never authenticated. origins makes the claim checkable against the browser's own header β€” the trust level CORS gives, and no more.

It also says how to report a vulnerability.

Documentation

docs/modes.md The three modes, what each protects, and which one you want
docs/install.md Putting the widget on a site, end to end
docs/translating.md Adding a language to the widget's bundle
docs/reviewing.md The reviewer's side: the extension, switching a site on, pairing
docs/self-hosting.md Running the worker: the image, the tags, a deployment
docs/privacy.md What a note carries, where it goes, how to delete it. Not legal advice
docs/architecture.md Why this shape, the seed contract, the layout, the commands
docs/decisions/ Per-subject rules and histories: what was measured, what failed first
SECURITY.md The threat model, stated rather than implied
CONTRIBUTING.md Running the project, what must pass, the conventions, adding a connector
CODE_OF_CONDUCT.md The Contributor Covenant, and where to report a breach
CLAUDE.md What no lint rule and no test catches, for anyone β€” human or agent β€” writing code here

Work is tracked in Linear on the Fruitback project (team Fruitback, key FRU).

Licence

Two licences, split where the client/server boundary is (FRU-22).

packages/widget, packages/shared, packages/fruitback, packages/element, packages/react MIT
apps/worker AGPL-3.0-only

The five client-side packages are MIT because they are compiled into someone else's site: a copyleft licence on code that ships inside a client's own bundle is one nobody can adopt, and the widget is worth nothing unadopted. (They are the three meant for npm β€” not three that are on it; see the install note above.) The worker is AGPL-3.0-only β€” it is the server, the only place copyleft actually bites, so anyone who hosts a modified version publishes their modifications. Copyright (C) 2026 Sakuga Software; the full text is in apps/worker/LICENSE.

@fruitback/widget compiles react-grab and zod into its dist rather than asking a client site to install them. Both are MIT, and MIT requires their notices to travel with the code, so the tarball ships THIRD-PARTY-NOTICES.md β€” checked by the suite, not by hand. apps/playground is not published; it inherits the repository's MIT licence.

Releases

Packages

Contributors

Languages