Vibes DIY
Vibes DIY / Docs / Reference
creator docs · reference · generated

useViewer Hook

This is the same document the Vibes code generator reads when it builds apps — rendered from prompts/pkg/llms/use-viewer.md on every deploy, so it can't drift from what the platform actually does. Read it from your terminal any time with npx vibes-diy skills.

useViewer() is a read-only window into viewer identity. The platform owns the rules — who's the owner, who has been granted read or write — and useViewer() lets your app see who's signed in and render identity. You cannot grant or revoke access from code; you can only reflect the runtime's verdict in your UI.

The signed-in viewer's own pill and the Sign in button are system chrome — you don't build them. The platform shows the current user (and a "Sign in" button when anonymous) inside the Vibes Switch, the panel that opens when you click the logo. A visitor always has a way to see who they are and sign in from there, so your app does not need to add a header pill or a login button for the current viewer — don't render a no-prop <ViewerTag /> just to show "who's signed in" or to offer sign-in. To find the login button, click the logo.

Use useViewer() to render other people's identity — comment authors, rosters, "added by" labels — with <ViewerTag userHandle={...} />, and to read viewer/can when your UI needs to branch on who's looking. Write surfaces are gated with useVibe(dbName).can (see use-vibe docs), not with viewer/access.*.

Basic Usage

Start with a minimal component that reads the viewer identity. You don't render a sign-in button or a current-user pill — those live in the Vibes Switch (click the logo). Just branch on viewer for any welcome/empty copy your app needs:

App.jsx

jsximport React from "react";
import { useFireproof } from "use-fireproof";
import { useViewer } from "use-vibes";

export default function App() {
  const { viewer, isViewerPending } = useViewer();

  if (isViewerPending) return null;

  return (
    <div>
      {/* No sign-in button here — the logo opens the Vibes Switch, which shows
          the current user and a "Sign in" button as system chrome. */}
      {viewer ? <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p> : <p>Sign in from the logo menu to get started.</p>}
    </div>
  );
}

What you get

  • viewer{ userHandle, displayName? } or null for anonymous visitors. Avatars are not on the payload — render them with <ViewerTag userHandle={...} />, which resolves the avatar from the handle. Don't build avatar URLs yourself.
  • isViewerPendingtrue while the platform is still resolving the viewer identity (e.g. on first render before the parent shell has pushed the identity update). Gate any auth-dependent UI on !isViewerPending to avoid flashing the wrong state. Once it becomes false, viewer is either populated or definitively null.
  • can(action) — membership boolean for "read"/"write"/"delete": is the viewer through the door? Access functions enforce per-document and per-database rules server-side. Prefer useVibe(dbName).can.create/edit/delete for write gating; it runs the app's access function and returns a reason.
  • ViewerTag — ready-made user pill; see the ViewerTag section below.
  • HandleInput — ready-made handle-lookup text input; see the HandleInput section below.

Gating UI

Gate the comment form on useVibe("comments").can — not on viewer. The current viewer never needs a pill here (that's in the Vibes Switch); just render the gated form and its reason when denied:

App.jsx

jsx<<<<<<< SEARCH
import { useViewer } from "use-vibes";
=======
import { useViewer, useVibe } from "use-vibes";
>>>>>>> REPLACE

App.jsx

jsx<<<<<<< SEARCH
  const { viewer, isViewerPending } = useViewer();
=======
  const { viewer, isViewerPending } = useViewer();
  const { can, ready, me } = useVibe("comments");
>>>>>>> REPLACE

App.jsx

jsx<<<<<<< SEARCH
      {viewer ? <p>Welcome back, {viewer.displayName ?? viewer.userHandle}!</p> : <p>Sign in from the logo menu to get started.</p>}
=======
      {/* write gate: useVibe().can, not viewer */}
      {!ready ? null : (() => {
        const v = can.create({ type: "comment", authorHandle: me?.userHandle });
        return v.ok ? (
          <form>
            <input placeholder="Add a comment..." />
            <button type="submit">Post</button>
          </form>
        ) : (
          <p>{v.reason}</p>
        );
      })()}
>>>>>>> REPLACE

Tagging content with the viewer (write/render pattern)

When one user writes content others will see (comments, posts, messages), stamp authorHandle on the doc at write time. That's it — just the handle. Render with <ViewerTag userHandle={doc.authorHandle} /> which resolves display name and avatar automatically. Do not stamp displayName or avatarUrl on docs — ViewerTag handles that from the handle alone.

Wire up a full comment thread with Fireproof and viewer attribution:

App.jsx

jsx<<<<<<< SEARCH
import { useViewer, useVibe } from "use-vibes";
=======
import { useViewer, useVibe } from "use-vibes";
import { useFireproof } from "use-fireproof";
>>>>>>> REPLACE
jsx<<<<<<< SEARCH
  const { viewer, isViewerPending } = useViewer();
  const { can, ready, me } = useVibe("comments");
=======
  const { viewer, isViewerPending, ViewerTag } = useViewer();
  const { can, ready, me } = useVibe("comments");
  const { useLiveQuery, database } = useFireproof("comments");
  const { docs: comments } = useLiveQuery("createdAt");
  const [body, setBody] = React.useState("");
>>>>>>> REPLACE
jsx<<<<<<< SEARCH
      {/* write gate: useVibe().can, not viewer */}
      {!ready ? null : (() => {
        const v = can.create({ type: "comment", authorHandle: me?.userHandle });
        return v.ok ? (
          <form>
            <input placeholder="Add a comment..." />
            <button type="submit">Post</button>
          </form>
        ) : (
          <p>{v.reason}</p>
        );
      })()}
=======
      {/* render OTHER users with userHandle — that's what ViewerTag is for here */}
      <ul>
        {comments.map((c) => (
          <li key={c._id}>
            <ViewerTag userHandle={c.authorHandle} />
            <p>{c.body}</p>
          </li>
        ))}
      </ul>

      {/* write gate: useVibe().can, not viewer */}
      {!ready ? null : (() => {
        const v = can.create({ type: "comment", authorHandle: me?.userHandle });
        return v.ok ? (
          <form onSubmit={(e) => { e.preventDefault(); post(); }}>
            <input value={body} onChange={(e) => setBody(e.target.value)} placeholder="Add a comment..." />
            <button type="submit">Post</button>
          </form>
        ) : (
          <p>{v.reason}</p>
        );
      })()}
>>>>>>> REPLACE

Also add the post handler before if (isViewerPending):

jsx<<<<<<< SEARCH
  if (isViewerPending) return null;
=======
  async function post() {
    if (!body.trim()) return;
    await database.put({
      body: body.trim(),
      createdAt: Date.now(),
      authorHandle: me?.userHandle,
    });
    setBody("");
  }

  if (isViewerPending) return null;
>>>>>>> REPLACE

Key points:

  • Stamp authorHandle at write time — persist the author's handle on the doc. Render with <ViewerTag userHandle={authorHandle} /> which resolves display name and avatar automatically.
  • Avatars are stable — ViewerTag resolves the avatar from the handle; if the author changes their avatar, the URL stays the same and the bytes update. ViewerTag handles this for you.
  • One source of identity — persist authorHandle on the doc. ViewerTag does the rest.

Notes

  • Never use Clerk user IDs. Only userHandle crosses into vibe code.
  • Avatar URLs are stable indirection URLs — when a user changes their avatar, the URL stays the same and the bytes update. Treat them as opaque strings.
  • To reflect a viewer's roles/channels for display, use access from useFireproof(): access.hasRole("moderator"), access.hasChannel("engineering"). To gate a write surface, use useVibe(dbName).can (it runs the same access.js). The access function is the server-side authority either way.

ViewerTag

ViewerTag is a ready-made inline user pill returned alongside viewer from useViewer(). It is not a separate import — you get it from the hook. Use it to render other people — comment authors, roster rows, "added by" labels — by passing userHandle:

jsx{/* Show another user read-only: */}
<ViewerTag userHandle={comment.authorHandle} />
{/* Style override: */}
<ViewerTag userHandle={member.userHandle} style={{ borderRadius: 8, fontSize: 12 }} />

The current viewer's identity and sign-in are system chrome, not app UI. The platform already shows the current user — and offers a "Sign in" button to anonymous viewers — inside the Vibes Switch that opens from the logo. So don't add a no-prop <ViewerTag /> as a header pill or login button just to show who's signed in; reach for userHandle to render someone else instead.

The one in-app reason to render a no-prop <ViewerTag /> is inline avatar self-edit. A no-prop tag shows the signed-in viewer a dashed edit ring that lets them change their own avatar in place, and that works for every signed-in viewer — the Switch's avatar editing is owner-only. So if your app wants any member (not just the owner) to update their photo without leaving the app, render a guarded {viewer && <ViewerTag />} for that — otherwise leave the current-viewer pill to the Switch.

Undefined safety. If userHandle is present in props but falsy (e.g. a missing field from a loop lookup), ViewerTag renders a dim italic placeholder instead of the edit ring. This prevents a broken data source from accidentally granting photo-edit access to an arbitrary pill.

Anonymous & always-safe. ViewerTag never throws regardless of login state. A no-prop <ViewerTag /> would render a "Sign in" button for anonymous viewers, but you don't need to place one for sign-in — the logo's Vibes Switch already offers it.

Theming. ViewerTag reads --accent, --accent-text, --card-bg, --border, --text, and --muted from the app's CSS variables with sensible fallbacks. If your app defines these on :root (which most generated themes do), ViewerTag inherits the theme automatically with no extra props.

Pass <ViewerTag userHandle={...} /> to render other people. The no-prop <ViewerTag /> (current user) covers identity and sign-in too, but the logo's Vibes Switch already does that — reach for it only when you specifically want inline avatar self-edit for any signed-in member.

HandleInput

HandleInput is a ready-made find-a-person input, returned from useViewer() alongside ViewerTag. Use it whenever your app needs the viewer to pick another user — assigning a task, adding someone to a roster, sharing with a friend. Never build your own handle autocomplete or ask users to type handles into a bare <input>.

jsxconst { HandleInput } = useViewer();

// Controlled: store just the handle string on your doc.
const [assignee, setAssignee] = React.useState(null);
<HandleInput value={assignee} onChange={setAssignee} placeholder="Assign to…" />;

How it behaves:

  • Typing opens a dropdown of prefix matches. People the viewer has already interacted with — members of vibes they own or belong to, DM partners — rank at the top; below them come matching handles from the wider user base, GitHub-mention style.
  • Picking one collapses the input into a ViewerTag for that handle (avatar + name, resolved automatically), with an × to clear and pick again.
  • Raw entry always works. Pressing Enter commits the sanitized typed text even when nothing matched, so users can reference someone brand-new.
  • onChange fires with the picked handle string (or null on clear). Persist only the handle on your docs — render it later with <ViewerTag userHandle={...} />, exactly like authorHandle.
  • Works uncontrolled too (omit value) when you only need the onChange events.
  • Theming matches ViewerTag — it reads the same CSS variables (--accent, --card-bg, --border, --text, --muted).
jsx// Typical write pattern: stash the picked handle on a doc.
async function assign(handle) {
  if (!handle) return;
  await database.put({ type: "assignment", taskId: task._id, assigneeHandle: handle, assignedBy: me?.userHandle });
}
<HandleInput onChange={assign} placeholder="Add a teammate…" />;