The first question a reader has is whether their reviewers need the site to ship anything. There are three answers, and picking one is the only decision this page asks for.
| 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 |
| What wakes it | nothing, it is there | the extension’s popup | the extension announcing itself |
| Who sees pins on the page | every visitor | the reviewer who switched the site on | reviewers who are signed in |
| Who can fetch them, unauthenticated | anyone | anyone | nobody, under authenticated |
| Who supplies the credential | the host’s own backend, or nobody | nobody can | the reviewer, by pairing |
| The reporter is | anonymous, or verified by the host’s token | self-declared | verified, by the session |
| Good for | a public “report a problem” | reviewing a client’s site, invisibly | a team reviewing its own staging |
Those middle rows are the ones worth reading twice, and the next section is why.
read, and the mode decides who can satisfy itAccess is not the mode. FRUITBACK_READ, or read on one client, is what decides whether a read
needs a credential; it is public by default, and under authenticated every read wants an HS256
identity token. What the three modes differ on is who can produce one.
init({ identityToken }) is a function the embedding
site supplies, and the widget sends what it returns on reads as well as writes. A site with its own
login can therefore run read: 'authenticated' and get verified reporters — the credential is
minted by that site’s backend and lives in its page’s JavaScript.identityToken and no transport,
so it has nothing to attach and the site embeds nothing that could supply one. On an authenticated
worker those reads answer 401, and the page shows no pins. The popup says why: This worker
answers a signed-in reader only, and private mode carries no session. Left at the public default, the pins are the same pins on the same open path: private
mode changes who is shown the feedback, never who may fetch it.public default buys a tidier page and
nothing more.A credential is a credential in every mode: somebody holding a valid token reads that worker with
curl too. That is what the token is for. The row above is about the callers who have none.
So:
read: 'authenticated' and an identityToken;FRUITBACK_READ=authenticated.A private-mode reviewer is not verified either: verification comes from the session the extension
holds, and only the relay carries it. Their name and address on a note are self-declared, exactly as
in a public-mode site that supplies no token — reporter.verified is the worker’s word and it
withholds it.
Team mode covers a team reviewing its own product, which private mode served awkwardly: it asked a team to deploy nothing when the code is theirs. Private mode keeps the case team mode cannot reach — a client’s site that will never install the package. One origin, switched on in a popup, and nothing to ask anybody to ship.
Both extension modes are one field on the site’s entry, chosen in the popup, and an entry with no mode reads as private.
| Public | Private | Team | |
|---|---|---|---|
| A worker | yes | yes | yes |
A <script> tag or init on the site |
yes | no | yes, dormant |
| The extension installed | no | yes | yes |
| A pairing code from an operator | no | no | yes |
FRUITBACK_READ=authenticated |
optional | it would lock the widget out | the point |
Every mode needs the worker: it is what holds the tracker’s API key, and that key cannot ship in client-side JavaScript. One container — self-hosting.md.
FRUITBACK_READ=authenticated under private mode is worth spelling out: the widget the extension
mounts sends no token, so the read answers 401 and the page shows no pins — the same page as a
worker that is down. The popup tells the two apart: it asks the worker the same read, and on a 401
it says that the worker wants a session that private mode does not carry.
One worker can serve team mode for several clients, grouped in workspaces (FRU-95). Give each
client of FRUITBACK_CLIENTS a workspace, and mint each code for one of them:
pair --subject alice --workspace acme. The session of that code reads and writes on the clients of
acme and on no other. A client keeps its own read, so a private-mode client can stay public beside
a team-mode one on authenticated. A map in which no client declares a workspace, with
FRUITBACK_SESSION_PATH set, is refused at boot: a session there would reach no client.
showComments is an editorial switch, not an access controlThe team’s replies come back inside the pin by default, and FRUITBACK_HIDE_COMMENTS — or
showComments: false on one client — turns them off.
Under read: 'public' that flag is the only thing between an issue thread and every visitor, so it
is worth a thought. Under read: 'authenticated' the access question does not arise: the reader is
somebody this worker checked, and the replies are already only reaching people entitled to them.
What is left there is editorial — whether a reviewer should see the team talking about their note —
and it stays the operator’s call rather than being forced on. Nothing in the worker couples the two.
| install.md | The site’s and the operator’s side: Linear, the worker, the widget, per-client routing |
| reviewing.md | The reviewer’s side: the extension, switching a site on, pairing |
| self-hosting.md | Running the worker: the image, the tags, a deployment |
| ../SECURITY.md | What each boundary actually holds, and what it does not |