Protocol User
Accessing the current authenticated user with useProtocolUser.
useProtocolUser()
The primary hook for reading the current viewer. It must be called inside RisingTideProviders (or the lower-level ProtocolUserProvider).
import { useProtocolUser } from "@rising-tide/ui/auth";
function UserMenu() {
const {
user, // ProtocolUser | null
isLoading, // boolean - the protocol user fetch is in flight
authenticated, // boolean
authResolved, // boolean - false until the stored session has been read back
hasEntitlement, // (name: string) => boolean
signOut, // () => Promise<void>
} = useProtocolUser();
if (!authResolved) return null;
if (!authenticated || !user) return <SignInButton />;
return <div>Hello, {user.displayName || user.stxAddress}</div>;
}authenticated and authResolved are two different facts. On the first render of every cold load authenticated is false because nothing has been read back yet, not because the viewer is signed out. Anything that acts on a visitor being signed out - a redirect, a marketing panel in place of their data - gates on authResolved first. isLoading is not that signal: it tracks the user fetch and goes false as soon as there is no address to fetch for, which is exactly the un-restored state.
What the hook returns
| Field | Type | Meaning |
|---|---|---|
user | ProtocolUser | null | The protocol user row, once loaded |
isLoading | boolean | The user fetch is in flight |
error | Error | null | The last failure from loading or creating the user |
authMethod | "privy" | "wallet" | null | Which arrival path this session used |
stxAddress | string | null | The viewer's Stacks address, available before user is |
authenticated | boolean | A session exists |
authResolved | boolean | The session state has been established, one way or the other |
authTokenReady | boolean | The bearer token has been attached to the SDK client. Reads that resolve the caller from the token wait on this |
requiresInvite | boolean | This app's App.requireInviteForSignup, read from GET /api/apps/{id}/config |
byowEnabled | boolean | This app's App.byowEnabled - whether the sign-in surface offers the bring-your-own-wallet path. Defaults true while the config loads |
createFeeMicro | bigint | null | What creating costs in this app, from App.createFeeMicro. Null means no server answer yet, never zero |
showAuthDialog / setShowAuthDialog | The shared sign-in dialog's open state | |
showWelcomeDialog / pendingUserAddress | First-signup state the app's welcome dialog is wired to | |
handleWelcomeComplete / handleWelcomeSkip | Create the protocol user from the welcome dialog, or with a fallback name | |
inviteBindingMismatch / clearInviteBindingMismatch | Set when a stashed invite is bound to a different email, X handle or address than the one that signed in | |
hasEntitlement | (name: string) => boolean | Whether user.entitlements holds the name. False when signed out |
signOut | () => Promise<void> | Ends the Privy or wallet session and clears local state |
ProtocolUser shape
import type { ProtocolUser } from "@rising-tide/ui/auth";
interface ProtocolUser {
id: number;
stxAddress: string;
displayName: string;
email?: string;
thumbnailUri?: string;
bio?: string;
authMethod: "privy" | "wallet" | null;
entitlements: string[];
preferredCurrency?: "STX" | "SBTC" | null;
}Roles are not on the client object. The server resolves the viewer's roles to a flat entitlements list at load time, and that list is what hasEntitlement reads. A display name is always present: signup falls back to User <last four of the address> when the welcome form is skipped.
Checking entitlements
const { hasEntitlement } = useProtocolUser();
if (hasEntitlement("COLLECT_EDITIONS")) {
// the viewer may collect an edition
}
if (hasEntitlement("VIEW_ADMIN_PANEL")) {
// render the admin link
}useEntitlement("COLLECT_EDITIONS") from the same entry point is shorthand for the same check. See Entitlements Reference for every name; the six verbs are CREATE_*, MANAGE_*, VIEW_*, COLLECT_*, SUBMIT_* and RSVP_*.