Skip to Content
Documentation

Users

Current user

Read the signed-in VitNode user in Hono API handlers, React components and AdminCP screens, and see which fields the user record carries.

VitNode resolves the session cookie before your code runs, so the signed-in user is one call away on the server and in React. Use this page when a route or a component needs to know who is asking, or whether anybody is signed in at all.

In an API route

c.get("user") returns the signed-in user, or null for a guest. This route answers 401 for guests and returns a few fields for everybody else:

plugins/site-notes/src/api/modules/hello/routes/me.route.ts
import { z } from "@hono/zod-openapi";
import { HTTPException } from "hono/http-exception";

import { buildRoute } from "@/api/builders";

export const meRoute = buildRoute({
  route: {
    method: "get",
    path: "/me",
    responses: {
      200: {
        content: {
          "application/json": {
            schema: z.object({ id: z.number(), name: z.string() }),
          },
        },
        description: "The signed-in user.",
      },
      401: { description: "Nobody is signed in." },
    },
  },
  handler: c => {
    const user = c.get("user");
    if (!user) {
      throw new HTTPException(401, { message: "Unauthorized" });
    }

    return c.json({ id: user.id, name: user.name }, 200);
  },
});

Add meRoute to the hello module's routes array, and the route answers at GET /api/site-notes/hello/me. API routes shows how a route joins a module. The user object carries the columns in the user record below, with the avatar and cover already resolved to avatarUrl and coverUrl.

In a React component

useSessionQuery() from @vitnode/core/tanstack/auth returns the session. data.user is null for a guest:

plugins/site-notes/src/pages/user-greeting.tsx
import { useSessionQuery } from "@vitnode/core/tanstack/auth";

export const UserGreeting = () => {
  const { data } = useSessionQuery();
  const user = data?.user;

  if (!user) return <span>Welcome, guest!</span>;

  return <span>Welcome back, {user.name}!</span>;
};

useSessionQuery() never suspends, so data is undefined on the first render of a page that did not load the session in its loader. A route that needs the session before paint calls ensureAuthState(queryClient) (same import) in its loader.

In the AdminCP

The AdminCP session is a second cookie with its own query. isAdmin on the public session means "may be offered the AdminCP", not "is signed in to it", so an admin screen always reads the admin session:

plugins/site-notes/src/admin/current-admin.tsx
import { useAdminSessionQuery } from "@vitnode/core/tanstack/admin";

export const CurrentAdmin = () => {
  const { data } = useAdminSessionQuery();

  if (data.status !== "granted") return null;

  return <span>Signed in as {data.session.user.name}</span>;
};

useAdminSessionQuery() suspends instead of returning undefined, and the AdminCP layout loads it, so every screen below the layout reads it from the cache. Two shorter hooks from the same import sit on top of it:

HookReturns
useAdminAccess()The whole access state: granted, or why access was refused
useAdminUser()The signed-in administrator, or null when access is refused

Auth and sessions explains why there are two sessions and how long each lasts.

The user record

Users live in the core_users table. These are the fields c.get("user") and the session carry:

ColumnTypeDescription
idserialPermanent identifier
namevarchar(255)Display name (the nickname)
nameCodevarchar(255)URL-safe handle for the profile URL /users/[nameCode]
emailvarchar(255)Unique sign-in email
emailVerifiedbooleanWhether the email is confirmed
firstNamevarchar(128)Given name, set by the user (nullable)
lastNamevarchar(128)Family name, set by the user (nullable)
phonevarchar(32)Contact number (nullable)
headlinevarchar(100)One line about the user (nullable)
showRealNamebooleanShow the real name publicly instead of name
birthdaytimestampBirthday, edited in the AdminCP (nullable)
newsletterbooleanNewsletter opt-in
roleIdintegerPrimary role (foreign key to core_roles)
avatarColorvarchar(6)Hex colour for the letter avatar
avatarIdintegerUploaded avatar, a core_files row (nullable)
coverIdintegerUploaded profile cover, a core_files row (nullable)
languagevarchar(32)Preferred UI locale (defaults to en)
timeZonevarchar(64)IANA time zone (nullable)
createdAttimestampWhen the account was created
lastSeenAttimestampLast activity, set by the presence heartbeat (nullable)
hideOnlineStatusbooleanBrowse anonymously - hide presence from other members

Every user payload the API hands out (the session, the public profile, the AdminCP list and detail, search hit authors) carries the two images as URLs, avatarUrl and coverUrl, instead of the file ids. Both are null until someone uploads an image. The Avatar component then draws a letter avatar from the first character of nameCode, never of the displayed name, so a member who switches to showing their real name keeps the same face everywhere.