Frontend · How-to guide
Auth pages
Add application-owned sign-in, verification, password reset, and two-factor routes around the packaged forms.
The SDK provides the auth forms, but your application owns their routes, layout, metadata, and navigation. This keeps the experience consistent with the rest of your product while Krakstack Auth owns credentials and sessions.
Required provider
Mount KrakstackAuthProvider above every auth page. For the recommended proxy setup, omit baseUrl so the browser client uses the application's own origin.
import { Outlet, createRootRoute } from "@tanstack/react-router";
import { KrakstackAuthProvider } from "@krak-stack/auth/components";
import { KrakstackProvider } from "@krak-stack/registry/krakstack-provider";
import { getLocale, locales } from "@/paraglide/runtime";
export const Route = createRootRoute({
component: () => (
<KrakstackProvider locale={getLocale()} locales={locales}>
<KrakstackAuthProvider
projectId={import.meta.env.VITE_KRAKSTACK_AUTH_PROJECT_ID}
>
<Outlet />
</KrakstackAuthProvider>
</KrakstackProvider>
),
});The provider loads project branding and enabled authentication methods. Supply the active Paraglide locale through the enclosing KrakstackProvider, not a prop on KrakstackAuthProvider, and keep the project ID public in VITE_KRAKSTACK_AUTH_PROJECT_ID. The auth provider also syncs that context locale to the signed-in user's account; see locale syncing for public-page boundaries and failure handling.
Auth layout
Create a pathless _auth layout for shared page chrome and to redirect users who are already signed in. Preserve a validated redirect query parameter so users return to their original destination.
import { useAtomSuspense } from "@effect/atom-react";
import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router";
import { authSessionAtom } from "@krak-stack/auth/components";
import { Schema } from "effect";
const AuthSearch = Schema.toStandardSchemaV1(
Schema.Struct({
redirect: Schema.optional(Schema.String),
}).annotate({ identifier: "AuthSearch" }),
);
const safeReturnPath = (value: string | undefined) => {
if (!value?.startsWith("/") || value.startsWith("//")) return "/";
const url = new URL(value, "https://app.invalid");
if (url.origin !== "https://app.invalid") return "/";
return `${url.pathname}${url.search}${url.hash}`;
};
export const Route = createFileRoute("/_auth")({
validateSearch: AuthSearch,
component: AuthLayout,
});
function AuthLayout() {
const search = Route.useSearch();
const session = useAtomSuspense(authSessionAtom(), {
suspendOnWaiting: true,
}).value;
if (session) {
return <Navigate href={safeReturnPath(search.redirect)} reloadDocument />;
}
return (
<main className="mx-auto flex min-h-screen max-w-md items-center px-4">
<Outlet />
</main>
);
}The schema validates the search shape; safeReturnPath enforces the security boundary. Never send an untrusted absolute or protocol-relative URL to <Navigate href>.
Add the form routes
Create these public paths. The components link and redirect to one another using these names:
| Route | Component | Purpose |
|---|---|---|
/sign-in | Signin | Password, email-code, and social sign-in |
/verify-email | VerifyEmail | Email verification code |
/reset-password | ResetPassword | Request or complete a password reset |
/2fa | TwoFactor | TOTP, email-code, or backup-code challenge |
A minimal TanStack Router page follows the same pattern for each component:
import { Signin } from "@krak-stack/auth/components";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_auth/sign-in")({
component: () => <Signin />,
});Repeat this route with VerifyEmail, ResetPassword, and TwoFactor. There is no /sign-up route or Signup component. Email OTP and Google create an account when the identity does not exist, directly from the sign-in flow; password authentication is sign-in only. Use the exact public paths above unless you also customize every link and callback that refers to them.
Forgot-password entry
Signin links to /reset-password. Without a token, ResetPassword collects the email address and requests a reset link. When the user returns from that email with a token, the same component displays the new-password form. The request response stays generic so it does not disclose whether an account exists.
Protected application routes
Use a parent route guard to redirect unauthenticated visitors for navigation and user experience:
import { useAtomSuspense } from "@effect/atom-react";
import { Navigate, Outlet, createFileRoute } from "@tanstack/react-router";
import { authSessionAtom } from "@krak-stack/auth/components";
export const Route = createFileRoute("/_protected")({
component: ProtectedLayout,
});
function ProtectedLayout() {
const location = Route.useLocation();
const session = useAtomSuspense(authSessionAtom(), {
suspendOnWaiting: true,
}).value;
if (!session?.user) {
return <Navigate to="/sign-in" search={{ redirect: location.href }} />;
}
return <Outlet />;
}Route guards are not authorization. A user can call backend endpoints without visiting the page, so every protected API operation must also use AuthService.requireUser(), requireOrganization(), or requireUserOrganization().
Organization access
Use MemberRequired when a page should explain missing organization access and allow a pending invitation to be accepted:
import { MemberRequired } from "@krak-stack/auth/components";
<MemberRequired
organizationId={organizationId}
contactEmail="[email protected]"
>
<OrganizationDashboard />
</MemberRequired>;This is a UI boundary only. Repeat membership and role checks in backend services.