Next.js
App Router (Next.js 15 and 16). The session lives in an HttpOnly cookie on your domain: browser JavaScript never sees the token, server components read it, and a proxy route forwards calls to Tausi.
Install
@tausi-auth/js on npmRequires React 18+ for the React helpers, Next.js 15+ for the Next.js helpersnpm install @tausi-auth/jsTAUSI_URL=https://auth.tausi.app
TAUSI_PUBLISHABLE_KEY=pk_…
TAUSI_SECRET_KEY=sk_… # no NEXT_PUBLIC_ prefix: it must stay on the serverAdd your app's origin (for example http://localhost:3000) to the project's allowed origins.
Server setup
One server-only module configures everything:
import "server-only";
import { createTausiNext } from "@tausi-auth/js/next";
export const tausi = createTausiNext({
baseUrl: process.env.TAUSI_URL!,
publishableKey: process.env.TAUSI_PUBLISHABLE_KEY!,
secretKey: process.env.TAUSI_SECRET_KEY!,
signInUrl: "/sign-in",
});The proxy route holds the session cookie. The browser calls /api/tausi/*; the route adds the session and forwards to Tausi, and strips tokens from the responses:
import { tausi } from "@/lib/tausi";
export const { GET, POST, PATCH, DELETE } = tausi.proxyHandlers;Client provider
"use client";
import { TausiNextProvider } from "@tausi-auth/js/next/client";
export function Providers({ baseUrl, publishableKey, children }: { baseUrl: string; publishableKey: string; children: React.ReactNode }) {
return (
<TausiNextProvider baseUrl={baseUrl} publishableKey={publishableKey}>
{children}
</TausiNextProvider>
);
}import { Providers } from "./providers";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers baseUrl={process.env.TAUSI_URL!} publishableKey={process.env.TAUSI_PUBLISHABLE_KEY!}>
{children}
</Providers>
</body>
</html>
);
}The provider refreshes server components by itself when someone signs in, signs out or switches account. In client components, use useAuth() (status, user, isSignedIn, signOut), useUser(), <SignedIn>, <SignedOut> and useTausi() for the client itself.
Sign-in and sign-up
"use client";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useTausi } from "@tausi-auth/js/next/client";
import { isTausiError } from "@tausi-auth/js";
export default function SignIn() {
const tausi = useTausi();
const router = useRouter();
const [error, setError] = useState<string | null>(null);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
const f = new FormData(e.currentTarget);
try {
await tausi.signIn({ email: String(f.get("email")), password: String(f.get("password")) });
router.push(new URLSearchParams(location.search).get("redirect_url") ?? "/dashboard");
} catch (err) {
if (!isTausiError(err)) throw err;
setError(
err.code === "INVALID_CREDENTIALS" ? "Wrong email or password."
: err.code === "EMAIL_NOT_VERIFIED" ? "Check your inbox to verify your email first."
: err.message, // ACCOUNT_BANNED, ACCOUNT_SUSPENDED, RATE_LIMITED, ...
);
}
}
return (
<form method="post" onSubmit={onSubmit}>
<input name="email" type="email" required />
<input name="password" type="password" required />
<button>Sign in</button>
{error && <p role="alert">{error}</p>}
</form>
);
}method="post" falls back to a GET submit and puts the password in the URL. In development, open the app at localhost; for other hosts (127.0.0.1, your LAN IP) add them to allowedDevOrigins in next.config.ts, or the page never becomes interactive.Sign-up accepts optional profile attributes. If the email already has an account you get EMAIL_ALREADY_REGISTERED, and Tausi emails the owner:
try {
const { verificationRequired } = await tausi.signUp({
name, email, password,
username, phoneNumber, // optional
locale: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
redirectTo: `${location.origin}/welcome`, // where the verification link opens (optional)
});
// verificationRequired: tell them to check their inbox
} catch (err) {
if (isTausiError(err) && err.code === "EMAIL_ALREADY_REGISTERED") {
// offer "Sign in" and "Reset password"
}
}Protect pages and APIs
Server components and route handlers ask Tausi once per request, so a revoked session or a ban takes effect immediately:
import { tausi } from "@/lib/tausi";
export default async function Dashboard() {
const { user } = await tausi.requireAuth({ returnTo: "/dashboard" }); // redirects to /sign-in when signed out
return <h1>Hello {user.firstName ?? user.name}</h1>;
}import { tausi } from "@/lib/tausi";
// Works with the session cookie (your pages) and with Authorization: Bearer (mobile apps).
export async function GET() {
const session = await tausi.auth();
if (!session) return Response.json({ error: "Unauthorized" }, { status: 401 });
return Response.json(await ordersFor(session.user.id));
}For a fast redirect before rendering, add the proxy guard. It only checks that the cookie exists (no network call), so pages must still call auth() or requireAuth():
import { tausi } from "@/lib/tausi";
export const proxy = tausi.proxy({ protect: ["/dashboard", "/settings"] });
export const config = { matcher: ["/((?!_next/static|_next/image|favicon.ico|api/tausi).*)"] };| Helper | Returns |
|---|---|
tausi.auth() | { user, session } or null. Throws if Tausi can't be reached (your error.tsx handles it). |
tausi.currentUser() | The user or null. |
tausi.requireAuth({ returnTo }) | { user, session }, or redirects to signInUrl?redirect_url=… |
tausi.proxy({ protect }) | A proxy.ts function that redirects visitors without a session cookie. |
Email links
Tausi emails link to your App URL's /verify-email and /reset-password, or to the redirectTo your app passed (how it works). Read the token from the URL and redeem it on a click, not on page load, so email link scanners can't use it up:
"use client";
import { useTausi } from "@tausi-auth/js/next/client";
export default function VerifyEmail() {
const tausi = useTausi();
const verify = () => tausi.verifyEmail(new URLSearchParams(location.search).get("token") ?? "");
return <button onClick={verify}>Verify my email</button>;
}
// app/reset-password/page.tsx: same idea with
// tausi.resetPassword(token, newPassword)
// and a "forgot password" form that calls
// tausi.forgotPassword(email, { redirectTo: `${location.origin}/reset-password` })Profile and sessions
const tausi = useTausi();
await tausi.updateUser({ firstName: "Asha", username: "asha", timezone: null }); // null clears a field
await tausi.changePassword(current, next); // signs out other devices
const sessions = await tausi.listSessions(); // each with userAgent, ipAddress, lastActiveAt, current
await tausi.revokeSession(sessions[1].id);
await tausi.revokeOtherSessions();
await tausi.signOut();Manage users from Next.js
For admin pages, use the backend client from @tausi-auth/node in server code (it uses the secret key). See Managing users for every operation.
import "server-only";
import { TausiBackend } from "@tausi-auth/node";
export const admin = new TausiBackend({ baseUrl: process.env.TAUSI_URL!, secretKey: process.env.TAUSI_SECRET_KEY! });
// const { items } = await admin.users.list({ access: "banned" });
// await admin.users.suspend(userId, { durationHours: 24, reason: "Chargeback" });Token mode (no cookie)
If you'd rather keep the token in the browser (for example to call another API directly), use token mode. The token lives in localStorage, where any script on the page can read it, so prefer cookie mode when you can.
- Pass
mode="token"andsessionSyncUrl="/api/tausi/session"toTausiNextProvider. - Create
app/api/tausi/session/route.tswithexport const { POST, DELETE } = tausi.handlers;so server components still see the session. - Call other APIs with
Authorization: Bearer ${await tausi.getToken()}.
Social sign-in
Enable providers in the project's Social sign-in tab, then:
redirectTomust be on one of the project's allowed origins.