TurboJet
Authentication

User and session

Read the session on the API and with the Better Auth client.

TurboJet exposes the session through the Hono API helpers and the Better Auth React client. There is no @auth/hooks or getSession from a SaaS-only module in this host.

Session on the API (server)

Product API handlers use getSessionFromHeaders from packages/api/src/lib/session.ts. It calls auth.api.getSession, loads hasOnboarded from Prisma, and returns a validated SafeSession (or null):

packages/api/src/lib/session.ts
import { auth } from "@repo/auth";
import { db } from "@repo/database";
import { safeSessionSchema, type SafeSession } from "@repo/validations/user";

export async function getSessionFromHeaders(
	headers: Headers,
): Promise<SafeSession | null> {
	const session = await auth.api.getSession({ headers });
	if (!session) return null;

	const userData = await db.user.findUnique({
		where: { id: session.user.id },
		select: { hasOnboarded: true },
	});
	if (!userData) return null;

	// …build SafeSession and parse with safeSessionSchema
}

Authenticated routes should prefer requireAuth and then c.get("session") — see Protect API endpoints.

SafeSession shape

From @repo/validations/user:

type SafeUser = {
	id: string;
	name: string | null;
	email: string | null;
	emailVerified: boolean | null;
	image: string | null;
	hasOnboarded: boolean | null;
	role: "USER" | "ADMIN"; // Prisma Role enum
	createdAt: string; // ISO datetime
	updatedAt: string;
	isBanned: boolean;
};

type SafeSession = {
	expiresAt: string; // ISO datetime
	user: SafeUser;
};

Session on the client

The shared client lives in packages/auth/src/client.ts:

packages/auth/src/client.ts
import { createAuthClient } from "better-auth/react";
import {
	organizationClient,
	magicLinkClient,
	emailOTPClient,
	inferAdditionalFields,
} from "better-auth/client/plugins";
import { clientEnv } from "@repo/env/client";

export const authClient = createAuthClient({
	baseURL: clientEnv.NEXT_PUBLIC_APP_URL,
	plugins: [
		organizationClient(),
		magicLinkClient(),
		emailOTPClient(),
		inferAdditionalFields({
			user: {
				hasOnboarded: { type: "boolean" },
				role: { type: "string" },
				isBanned: { type: "boolean" },
				mobile: { type: "string", required: false },
			},
		}),
	],
});

In a client component on the SaaS host, use Better Auth’s session hook from the client:

const { data: session, isPending } = authClient.useSession();

if (isPending) return <div>Loading...</div>;
if (!session) return <div>You need to be logged in</div>;

return <div>{session.user.name}</div>;

After mutating the user (for example renaming via /api/me), refetch the client session with Better Auth’s session APIs (authClient.getSession() / invalidate the session query). This package does not export a separate reloadSession helper.

Protecting pages and APIs

  • API: requireAuth / requireAdmin — Protect API endpoints
  • SaaS pages: In the full kit, protect dashboard routes in that app’s proxy/layout. This marketing+docs host has no session gates.

On this page