Skip to content

add deployment-wide user directory for user search - #474

Open
maxwellpeterson wants to merge 2 commits into
mainfrom
mpeterson/user-directory
Open

maxwellpeterson wants to merge 2 commits into
mainfrom
mpeterson/user-directory

Conversation

@maxwellpeterson

@maxwellpeterson maxwellpeterson commented Sep 10, 2026

Copy link
Copy Markdown
Member

Adds a central user directory to support platform-wide user search. This is implemented as a singleton DO in the workshop backend that stores a copy of user metadata from each user DO. When user metadata is updated in the user DO, the user DO propagates those changes through to the directory DO by calling the syncUser RPC method. The directory is incrementally backfilled with existing user records on next login.

The backend implementation is as simple as possible. DO SQLite supports FTS5 for full-text search, but this didn't seem worth the complexity to use here. Local benchmarks showed that it was sometimes slower than a full table scan with substring search, and simple substring search is fine for our use case.

I would recommended reviewing packages/workshop-backend/src/user.ts and packages/workshop-backend/src/user-directory.ts first, since they contain the bulk of the backend changes. This PR is also split into two independently reviewable commits. The first adds the user directory, and the second adds an admin toggle for controlling access to the user directory:

image

@github-actions github-actions Bot added workshop/frontend Changes to the Workshop frontend kernel Changes to the Workshop kernel delivery Changes to CI or release delivery workshop/shared Changes to shared Workshop APIs labels Sep 10, 2026
@github-actions

Copy link
Copy Markdown

Preview: pr474-mpeterson-use-97fd7f6b

https://pr474-mpeterson-use-97fd7f6b-router.cloudflare-os-previews.workers.dev

Dashboard · deleted when this PR closes

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 10, 2026

Copy link
Copy Markdown

Posted 2 actionable inline findings.

github run

@maxwellpeterson
maxwellpeterson marked this pull request as ready for review September 10, 2026 19:02
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

ask-bonk[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

ON CONFLICT (id) DO UPDATE SET name = excluded.name, search_text = excluded.search_text,
rev = excluded.rev
WHERE excluded.rev > users.rev`,
record.id, record.name, `${record.id}\n${record.name}`.toLowerCase(), rev);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Bound user-controlled profile fields before indexing them. setOwnDisplayName() and password signup accept arbitrary-length strings, and this line copies the id/name into the one deployment-wide singleton. A user can persist megabytes into name/search_text; every substring scan then processes that row and a matching search returns it over RPC, so repeated accounts can exhaust singleton storage/CPU and disrupt search for everyone. Enforce shared server-side limits on ids/display names, and reject oversized records defensively here.

Comment thread packages/workshop-backend/src/user-directory.ts
const handleAddCollaborator = async () => {
const username = addUsername.trim()
if (!username || addingRef.current) return
const userId = selectedUser?.id ?? (canInviteUser ? directoryQuery : '')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Make the primary Invite action honor the highlighted directory result. After typing a display name such as Ada, the first match is rendered highlighted and aria-selected, but selectedUser remains null until the option itself is clicked, so Invite submits raw Ada; an email-backed account such as ada@example.com then reports no account found. Submit the active match here, or expose raw canonical-id submission as a separate explicit option so the lazy-index fallback remains available.

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown

Posted 3 actionable inline findings: one P1 and two P2 issues.

github run

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown

No additional findings beyond the existing inline review comments.

github run

const directoryQuery = addUsername.trim()
// Everyone already on the workspace: the caller, the owner (absent from listCollaborators()
// when the caller is a collaborator), and every collaborator.
const directoryExcludeIds = useMemo(() => [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep exclusions within the directory RPC contract. This array grows with every collaborator, but UserDirectoryDurableObject.searchUsers() rejects more than 1,000 distinct IDs and there is no corresponding collaborator cap. Once a workspace has 1,000 collaborators (plus its owner), every lookup fails with the temporary-unavailable state, so display-name search stops working for that workspace. Either avoid sending the entire membership list while still filtering results correctly, or make the RPC support the workspace size.

user’s next connection.
</p>
</div>
<Switch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Give this switch an accessible name. With no label, aria-label, or aria-labelledby, Kumo falls back to aria-label="Switch"; the Access tab therefore exposes this and the sign-up control under the same generic name, so a screen-reader user cannot tell which deployment policy they are changing. Associate it with the heading or provide an explicit accessible label.

@ask-bonk

ask-bonk Bot commented Sep 11, 2026

Copy link
Copy Markdown

Posted 2 actionable inline findings.

github run

devin-ai-integration[bot]

This comment was marked as resolved.

@ask-bonk

ask-bonk Bot commented Sep 14, 2026

Copy link
Copy Markdown
  • [P2] Admin search policy is misreported after upgrade (packages/workshop-backend/src/admin-settings.ts:314): For legacy configs with sign-ups disabled, KV parsing defaults user search to enabled, while #config() defaults it to disabled. The admin UI therefore shows search off while searches still work; any unrelated admin update then silently disables it. Apply the same dependent default in both config paths.

github run

const directoryQuery = addUsername.trim()
// Everyone already on the workspace: the caller, the owner (absent from listCollaborators()
// when the caller is a collaborator), and every collaborator.
const directoryExcludeIds = useMemo(() => [

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Wait for the membership list before searching. collaborators starts empty while loadData() is in flight, so on a slow workspace lookup this search can return an existing collaborator and let the user select it. When listCollaborators() later fills the exclusions, selectedUser keeps directorySearching false, so that stale selection is never invalidated; Invite can then add or upgrade a direct permission edge for someone who was already present via a share link. Gate directory search on the initial membership load, and clear any selected result when the exclusions change.

@ask-bonk

ask-bonk Bot commented Sep 14, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

...collaborators.map(({ profile }) => profile.id),
], [collaborators, currentUser, metadata.owner])
const directorySearching = userSearchEnabled && selectedUser === null && directoryQuery !== ''
const directoryOpen = directorySearching && !directoryDismissed

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Keep previous-query results closed. After results for one query settle, changing the input commits while directory still contains those old results; the effect clears them only in a later update. Because directoryOpen remains true, a rapid Enter can select the old highlighted account while the field shows the new query, and aria-activedescendant also references that stale option. Require directory.query === directoryQuery here so mismatched results cannot render or enter the keyboard path.

@ask-bonk

ask-bonk Bot commented Sep 14, 2026

Copy link
Copy Markdown

Posted 1 actionable inline finding.

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

delivery Changes to CI or release delivery kernel Changes to the Workshop kernel workshop/frontend Changes to the Workshop frontend workshop/shared Changes to shared Workshop APIs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant