Session
The session is resolved server-side. It is then hydrated into the page, injected into handlers, and sent to remote Fronds.
useCurrentUser
const { user, refresh } = useCurrentUser();
// user: Ref<User | null> — resolved during SSR, hydrated with the page: zero extra requests
// refresh: () => Promise<void> — re-read after sign-in/out
<template>
<UButton v-if="user" :to="localePath('/blog/new')" :label="$t('nav.write')" />
<UButton v-else :to="localePath('/login')" :label="$t('nav.signIn')" />
</template>
Route protection is an ordinary Nuxt middleware reading the same session — Fougere does not add a parallel guard system.
The auth surface
auth: betterAuth({ … }) in fougere.config.ts (reference)
mounts better-auth under basePath — the endpoints your pages call directly:
await $fetch('/auth/sign-up/email', { method: 'POST', body: { name, email, password } });
await $fetch('/auth/sign-in/email', { method: 'POST', body: { email, password } });
await $fetch('/auth/sign-out', { method: 'POST', body: {} });
// then: await refresh()
Your User entity extends AuthUser — better-auth's fields plus yours:
import { AuthUser } from '@fougere/auth-better';
import { oneOf, optional } from '@fougere/schema';
export default class User extends AuthUser.extend({
role: optional(oneOf('admin', 'editor', 'reader')),
}) {}
The named subclass matters: the scanner registers the entity as User, and DI matches
user: User | null by that name.
Do not place pages under the auth
basePath: the server catch-all uses/auth/**. This site serves its forms at/loginand/registerfor that reason.
One resolution, three readers
| Reader | How identity arrives |
|---|---|
| the page | hydration — useCurrentUser(), zero requests |
| a handler | invocation state → a Collector fills user: User | null |
| a remote Frond | state is sent with the call — its collectors receive the same user |
The browser cannot write this state: it is built server-side from the request context (Queries & commands).
Next: invoke — calling an operation server-side.