Guest links
Verified against v0.10.0 · internal/api/links.go, internal/api/router.go, internal/store/links.go, internal/api/link_routes_test.go, web/src/components/LinkPanel.tsx, web/src/pages/LinkPage.tsx, web/src/lib/links.ts
A contractor needs to size two stories. Somebody from another team wants to vote on the one story that touches their service. Neither of them should have to be added to your space, hold your passcode, or become a user the instance remembers afterwards.
A guest link is the answer: one URL, bound to one room, that expires by itself. Whoever opens it types a name and sits down at that table. They are not a member of the space, and when the link runs out they are not anything at all.
Minting one
Section titled “Minting one”The facilitator of a room gets a Guest links button in its header. Nobody else does — the panel is not offered to a member, and the server refuses the request even if it is asked directly.


Create a guest link returns the URL once, in a field you copy. Nothing stores the token: Parley keeps only a hash of it, so if you lose that field before copying it, the link is unrecoverable and the only thing to do is revoke it and mint another.
Rooms that have ended cannot mint new links. Existing links to an ended room still open it, read-only — the same view a member gets, minus the CSV export, which is never a guest’s to take.
What the guest gets, and what they do not
Section titled “What the guest gets, and what they do not”The link is a capability for one room. Redeeming it creates an ordinary user — a name and a seat colour, present on the roster, attributed in the CSV export the room’s members can take — flagged as link-bound. They cannot pick a portrait, because that is one of the identity routes they are refused.
That flag is the whole of their authorization. A guest may read the room the link names, take part in it, and read their own identity. Every other route refuses them, including routes anonymous callers are welcome on: renaming themselves (the initial redemption name is not checked, so the roster marks every guest seat regardless), the space page, the CSV export of the room they are sitting in, the facilitator controls, the spectator toggle, and the guest-link panel itself. They never see the links to their own room, minted or otherwise.
What the room shows them is trimmed to match: a guest sees the people taking part in the meeting — other guests included, and their own seat from the first load — not the rest of the space’s members, and never the space’s join code or slug. The room’s own members see the guest on the roster, seated as a participant rather than a spectator, and marked as a guest beside their name. Nothing stops a guest typing a name a member already uses, so the mark is set by the server from how the seat was obtained — never guessed from the name — and it shows on every seat the room draws, the guest’s own copy included.
A guest can lose their browser storage — a private window, a cleared site, a second device the link was forwarded to — without losing their seat. The cookie is the credential, and the room asks the server who they are when nothing local answers, so they land back in the meeting rather than at a name prompt they would not be allowed to answer.
A guest is never the facilitator, and never becomes one — the abandoned-seat
claim is refused for them at the middleware and again in the UPDATE behind it.
Both kinds of room work the same way for a guest. In poker they vote, and the table waits on their vote before it reveals. In a standup they write an update the room sees, take a place in the round-robin order like anybody else, and get the turn when it comes round to them. What a guest never gets in either is the facilitator’s controls: starting, advancing and skipping the round stay with the facilitator, as does selecting and revealing a story.
The URL is the credential
Section titled “The URL is the credential”There is no second factor and no invitation to accept. Anyone holding the link is the holder — forward it and you have handed the seat on. Share it the way you would share a password, to one person, over a channel you would send a password over.
Two things reduce the blast radius, and neither of them changes that sentence:
- It expires in 24 hours, from the moment it is minted. The expiry lives on the session the redemption issues, not on a sweep, so a lapsed link drops the guest’s WebSocket mid-meeting rather than leaving them seated until something gets round to noticing.
- It can be redeemed 25 times, and no more. That is a cap on how far a leaked link can spread, not an invitation to hand one link to a whole department — one guest, one link, is what revocation is designed around.
The token rides in the URL fragment — …/link#t=… — never the query
string. A fragment is not sent to the server and never appears in a Referer
header, so a link pasted into a chat window does not scatter the credential
through access logs, proxy history, or the analytics of whatever site the next
click lands on. Parley reads it once on arrival and wipes it out of the address
bar, so it does not survive into a bookmark, the back button, or a screenshot.
It is still a secret in transit.
Revoking
Section titled “Revoking”Revoke beside a link kills it immediately, and revocation is not merely a stop on future redemptions: the sessions of everyone who already redeemed that link are deleted and their sockets are closed on the spot. Someone let in by mistake is out of the room in seconds, not at the end of the day.
Deleting the room does the same to its guests.
A guest can also let themselves out. The banner across the top of the room carries Leave room, which ends that guest’s session on the spot and drops them on a dead-link screen — the seat is gone, and the cookie behind it stops working, rather than sitting valid in the browser until the link expires. That matters most in the case guest links are for: a contractor or stakeholder on a borrowed or shared machine, where walking away should not leave the next person holding the seat. It is the guest’s own session only; nobody else who redeemed the same link is affected, and coming back means a fresh link.
Leaving keeps the same promise revocation does, below: votes, standup entries and CSV attribution all survive it.
Closing the tab is not the same thing, and it is worth being precise about
what it does. A guest’s cookie is a session cookie — no Max-Age, no
Expires — so the browser drops it when the browsing session ends. A
browsing session is the browser, not the tab. Close the room tab while any
other tab or window of that browser is still open and the cookie is untouched,
so the next person to open the room URL is put back into that guest’s seat,
able to vote, write standup entries and watch the room live. The name and hue
do vanish with the tab — they are held in sessionStorage — but the room asks
the server who this browser is and seats it again from the cookie.
Quitting the browser altogether does end the seat, with one exception: a browser configured to restore its last session — “continue where you left off” — restores session cookies with the tabs, so that guest can come back seated until the link expires.
Leave room and Revoke both delete the token server-side, and are the only two things proof against all of it. On a borrowed or shared machine, use one of them. Both are recorded under known limitations.
A refresh is untouched throughout: the cookie survives every navigation inside the browsing session, so a guest who reloads, or navigates away and comes back mid-meeting, is still seated. That is deliberate — a seat torn down on page hide would strand every guest who merely refreshed.
What revocation does not do is unpick the meeting. The guest’s user row stays, and so do their votes and their standup entries — they belong to the session that happened, and to any CSV exported from it afterwards.
Revoking is idempotent, so a retried click is a success rather than an error,
and a room may hold 20 live links at a time by default
(LINK_LIMIT_PER_SESSION). Revoked and expired links do not count against it.
Redeeming a link mints a guest account, so it is rate limited by address like
any other signup — but against its own hourly bucket
(LINK_REDEMPTION_IP_HOURLY_LIMIT, 50 by default) rather than the
open-signup one, so a whole team sharing one office address can reach a link’s
25-redemption cap. The instance-wide hourly ceiling still applies, which is what
keeps a leaked link from minting accounts without bound.
What is not here
Section titled “What is not here”| Capability | Status | What to do instead |
|---|---|---|
| Choosing an expiry or labelling a link | Out of scope | One lifetime and one cap are server constants. A facilitator handing out a link has no better idea of the right number than the server does, and a chooser in the UI would be a second, softer copy of a rule the server already owns. |
| A link to a whole space | Out of scope | A link names one room and grants nothing beyond it. To let somebody into everything a space holds, add them to the space with its passcode. |
| Spectating as a guest | Not built | Spectator mode is a flag on a space membership, which a guest has none of, so the toggle is refused rather than silently accepted. |
| Turning a guest into an account | Not built | A guest cannot rename itself or sign in. When the link expires the identity stops working; what they contributed stays. |
| Seeing who redeemed a link | Not built | The panel reports a count, not a list. Who is in the room is on the roster; which link seated them is not recorded anywhere you can read. |