Rising Tide Developers
UI PackageAuth

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

FieldTypeMeaning
userProtocolUser | nullThe protocol user row, once loaded
isLoadingbooleanThe user fetch is in flight
errorError | nullThe last failure from loading or creating the user
authMethod"privy" | "wallet" | nullWhich arrival path this session used
stxAddressstring | nullThe viewer's Stacks address, available before user is
authenticatedbooleanA session exists
authResolvedbooleanThe session state has been established, one way or the other
authTokenReadybooleanThe bearer token has been attached to the SDK client. Reads that resolve the caller from the token wait on this
requiresInvitebooleanThis app's App.requireInviteForSignup, read from GET /api/apps/{id}/config
byowEnabledbooleanThis app's App.byowEnabled - whether the sign-in surface offers the bring-your-own-wallet path. Defaults true while the config loads
createFeeMicrobigint | nullWhat creating costs in this app, from App.createFeeMicro. Null means no server answer yet, never zero
showAuthDialog / setShowAuthDialogThe shared sign-in dialog's open state
showWelcomeDialog / pendingUserAddressFirst-signup state the app's welcome dialog is wired to
handleWelcomeComplete / handleWelcomeSkipCreate the protocol user from the welcome dialog, or with a fallback name
inviteBindingMismatch / clearInviteBindingMismatchSet when a stashed invite is bound to a different email, X handle or address than the one that signed in
hasEntitlement(name: string) => booleanWhether 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_*.