Identity: guests, review links, and your own users
Nobody signs up to leave a comment. That is the point of the product, and it is also the thing that needs explaining, because "no account" and "no access control" are not the same sentence.
The three kinds of person
| Kind | How they got in | Named |
|---|---|---|
| guest | Typed a name, or nothing | By what they typed |
| link | Opened a review link you sent | By the name on the link |
| verified | Your own application vouched for them | By your user record |
Plus you — the studio. That is a real account with a password, on the dashboard, and it is a completely separate system: different table, different credential, different origin, different lifetime. A reviewer credential can never become a dashboard credential, because there is no code path between them.
Who is allowed to comment
A per-site setting, and the default changes after a day:
write_mode | Who can write |
|---|---|
open | Anyone who can load the page |
link-required | Only somebody holding a review link |
verified-only | Only users your application vouches for |
A new site starts open for 24 hours, then falls back to link-required on its own. That window exists so the first pin needs no set-up — paste the tag, click, type — and it closes on its own so that a site you set up and forgot is not a public comment box a year later.
SNAGSET_WRITE_MODE_GRACE_HOURS changes the window. Set it to 0 for link-required from the first second.
Review links
The link is the access control. There is no separate permission screen, because a second system to keep in step with the links is a second system to get wrong.
A link carries:
- A name, so comments are attributed without anyone typing one
- What the holder may do — six permissions, of which three are on by default
- An expiry — 14 days unless you say otherwise
- Optionally a round, so the link lands on the round you meant
snagset link prj_… "Ana at Acme"
| Permission | Default |
|---|---|
thread:create | ✅ |
comment:create | ✅ |
thread:resolve | ✅ |
thread:delete:own | — |
thread:private | — |
thread:assign | — |
Revoking is immediate. It bumps the project's session epoch, so every session minted from that link stops verifying on its next request — including a tab that was already open, which is told it was signed out rather than quietly dropping to read-only.
An unknown permission is refused at creation, not stored and ignored. A permission that is silently dropped reads to the operator as "I granted it and it did nothing".
Sessions
A reviewer's credential is a short-lived signed token: 15 minutes sliding, 12 hours absolute. It lives in memory on the client and is exchanged again when it lapses. It is not a cookie, because the widget runs on somebody else's origin where a cookie would be both useless and dangerous.
The link itself lives only in the URL fragment — after the # — so it is never sent to your server, never lands in an access log, and never appears in a Referer header. It is taken out of the address bar on arrival.
Your own users (verified)
If your application already knows who is looking at the page, tell Snagset and skip the link entirely. Your server signs a short HMAC over the user's id and name with a shared secret; the widget presents it; a verified reviewer is created or matched.
That is the tier with the moderation bypass, and the only one — which is why an agent writing through MCP is never verified, whatever name its token carries. It posts as a guest, flagged as an agent, traceable to the token that made it.
What a reviewer can never do
- See a site they were not given a link to
- See a thread marked team-only
- Reach the dashboard
- Add a site, mint a link, or change a setting
- Act on another project, even holding a valid credential for this one
Every one of those is checked on the server, per request. Absent CORS headers stop a browser on another origin from reading a response; they stop nothing else, which is a lesson this codebase learned by minting a review link with curl against its own port.