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

add deployment-wide user directory for user search#474
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. 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.

@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

const directoryOpen = selectedUser === null && directoryQuery !== ''
const canInviteUser = selectedUser !== null || (
directoryQuery !== '' &&
directory.status === 'ready' &&

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 exact-ID invites available when lookup fails. This condition permits a raw query only after a successful empty result. When searchUsers() rejects, the status becomes failed, so both Enter and Invite become no-ops even though overseer.addCollaborator() can still resolve a known username/email without the directory. A transient failure of the new singleton therefore regresses the existing direct-invite flow; allow raw submission in the failed state as the fallback.

...(currentUser ? [currentUser.id] : []),
...collaborators.map(({ profile }) => profile.id),
], [collaborators, currentUser])
const directoryOpen = selectedUser === null && 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] Close the result popover when the combobox loses focus. directoryOpen depends only on a nonempty query and no selection, so after typing, the list remains rendered when the user tabs or clicks into the role picker, share-link controls, or the rest of the modal. It can keep overlaying those following controls indefinitely, especially at the full mobile width. Track combobox focus/outside interaction (and Escape) so leaving the search dismisses the popover while retaining the query.

@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 devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

3 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)

Devin Review

Comment thread packages/workshop-backend/src/user.ts
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.

🟡 Search crosses profile field boundaries

searchUsers can match a newline-containing query across the stored id and name. It returns users whose individual fields contain no such substring.

Learn more

The directory contract searches for a substring of either the user id or display name. Joining both fields with a separator creates additional substrings spanning the join. The RPC accepts arbitrary strings, so the separator itself does not prevent such queries.

Example: For id ada@example.com and name Grace, query com\ngra matches search_text. Neither ada@example.com nor Grace contains that query, so the user must not be returned.

Recommended fix: Store normalized id and name in separate columns and apply instr to each column independently. Derive ranking from the valid per-field positions rather than a concatenated offset.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +130 to +132
searchUsers(query: string, excludeIds: string[]): Promise<UserDirectoryRecord[]> {
return retryOnDoReset(() => this.ctx.exports.UserDirectoryDurableObject.getByName("")
.searchUsers(query, [this.#userId.name!, ...excludeIds]));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟨 Directory search lacks input bounds

Any authenticated user can send oversized query and excludeIds values to the singleton directory. Repeated requests can exhaust its CPU or memory and disrupt deployment-wide search.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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