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:
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:
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:
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:
| Hook | Returns |
|---|---|
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:
| Column | Type | Description |
|---|---|---|
id | serial | Permanent identifier |
name | varchar(255) | Display name (the nickname) |
nameCode | varchar(255) | URL-safe handle for the profile URL /users/[nameCode] |
email | varchar(255) | Unique sign-in email |
emailVerified | boolean | Whether the email is confirmed |
firstName | varchar(128) | Given name, set by the user (nullable) |
lastName | varchar(128) | Family name, set by the user (nullable) |
phone | varchar(32) | Contact number (nullable) |
headline | varchar(100) | One line about the user (nullable) |
showRealName | boolean | Show the real name publicly instead of name |
birthday | timestamp | Birthday, edited in the AdminCP (nullable) |
newsletter | boolean | Newsletter opt-in |
roleId | integer | Primary role (foreign key to core_roles) |
avatarColor | varchar(6) | Hex colour for the letter avatar |
avatarId | integer | Uploaded avatar, a core_files row (nullable) |
coverId | integer | Uploaded profile cover, a core_files row (nullable) |
language | varchar(32) | Preferred UI locale (defaults to en) |
timeZone | varchar(64) | IANA time zone (nullable) |
createdAt | timestamp | When the account was created |
lastSeenAt | timestamp | Last activity, set by the presence heartbeat (nullable) |
hideOnlineStatus | boolean | Browse 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.