The request that breaks most tools

A client wants visibility into their project. The easy answer is "create them a normal account" — and now they can browse your team list, see every other client's project in the sidebar, and open the finance tab if nobody thought to lock it down. Most permission systems are built for people inside the company, so the first external account exposes everything nobody explicitly hid.

What "guest" actually means in WKFGo

A guest is a real User row — same login, same table — with one field set: IsGuest = true. That flag does two independent things, and understanding why there are two matters.

First, on invite, the guest gets the same per-project feature-access grants any teammate would get — DASHBOARD_VIEW, TASK_VIEW, KNOWLEDGE_VIEW_OTHERS, DOCUMENTS_VIEW, DESCRIPTION_VIEW for view access, plus TASK_ADD and KNOWLEDGE_ADD if invited as an editor. This is the normal permission system — nothing special, just scoped to one projectId.

Second — and this is the part that actually matters for security — a guard middleware runs on every request and checks isGuest from the auth context. If true, it blocks the request outright for an entire list of organization-level prefixes, no matter what the feature-access table says:

var guestDenyPrefixes = []string{
    "/api/teams", "/api/finances", "/api/saas", "/api/audit", "/api/usage",
    "/api/reports", "/api/project-roles", "/api/feature-accesses", "/api/organization",
    "/api/insight", "/api/portfolio", "/api/git", "/api/email", "/api/packages",
    "/api/goals", "/api/workload", "/api/roles", "/api/digest", "/api/ai/config",
    "/api/ai/usage", "/api/sprints", "/api/scrum",
}

Why two layers instead of one

The obvious question is: if feature-access grants are already scoped, why the second block-list? Because feature-access is a positive permission system — it grants what you explicitly configure, and its correctness depends on every route being wired to the right feature key. If a new endpoint ships without a feature-access check, or an admin fat-fingers a grant meant for teammates, a normal user isn't stopped by anything else. A guest, being external, needs a backstop that doesn't depend on every route being configured correctly — a deny-list that fires purely on "is this an org-level surface," independent of whatever the grants table says.

This is a defense-in-depth pattern: one layer says what a guest can reach, the other says what a guest can never reach, and the second one wins even if the first is misconfigured.

The one deliberate exception

Guests are blocked from /api/feature-accesses — the management endpoint — but allowed a read of their own permissions via /api/feature-accesses/check. Without that carve-out, the guest's own UI couldn't tell whether to show them an "add task" button, because it has no way to ask "what can I do here?" It's a narrow, self-only, read-only exception, not a hole in the deny-list.

What guests never get

There's no email-link invite flow yet — an admin invites by email and project, and the response includes a one-time temporary password to hand to the guest directly. It's deliberately manual: no auto-provisioning email means no risk of an invite landing in the wrong inbox and self-activating.

Common mistakes

Assuming "guest" means read-only. It doesn't — VIEWER and EDITOR are both guest levels; EDITOR can add tasks and knowledge entries on the one project they're scoped to. Read-only is a choice you make at invite time, not a property of the guest flag itself.

Reusing a guest account across two client projects. The invite is scoped to a single projectId at creation. Inviting the same person to a second project is a second invite with its own scoped grants — it doesn't retroactively widen the first one.

Forgetting the username collision check. A guest's username is their email, and it must be free within that organization — the invite fails cleanly if a teammate already used that email as a username, rather than silently merging two accounts.

FAQ

Can a guest see other guests on the same project?

Only what TASK_VIEW and the rest expose about assignees on tasks they can already see — there's no separate "list all guests" surface, and /api/users listing is blocked for guests entirely (they can fetch one user by ID for assignee names, not browse the directory).

What happens if an admin later revokes the project access?

The feature-access grants are just rows — remove them and the guest loses visibility on the next request; the guard middleware keeps blocking org-level surfaces regardless.

Does a guest count against my seat limit?

Check your plan's billing page — guest seats are tracked the same way teammate seats are, since it's the same User table underneath.

Summary

Scoped external access isn't "make an account and hope nobody clicks the wrong link" — it's two independent layers that don't trust each other: explicit per-project grants for what a guest should see, and a hard-coded deny-list for what a guest must never see, regardless of what the first layer says.