# Getting started (https://session-kit.pages.dev/docs/getting-started)

In this guide, you build a small React app that signs in, stays signed in
across reloads and tabs, calls an API, and signs out. The web router guides
build on this app. Server-rendered apps create their client differently, so
start from [Server rendering](https://session-kit.pages.dev/docs/server-rendering) instead.

The code signs in against
[DummyJSON's auth API](https://dummyjson.com/docs/auth), a public test
backend, so it runs as written.

## Before you begin

In a React app, install Session kit and Zod, as
[Installation](https://session-kit.pages.dev/docs/installation) shows.

## 1. Talk to your backend

Session kit never calls your backend for you, so start with the two requests
you would write anyway: one that signs in, and one that exchanges a refresh
token for new tokens. Nothing in this file imports Session kit:

```ts title="src/backend.ts"
import { z } from "zod";

export const API = "https://dummyjson.com";

export const userSchema = z.object({
  id: z.number(),
  username: z.string(),
  email: z.string(),
  firstName: z.string(),
});

const tokensSchema = z.object({
  accessToken: z.string(),
  refreshToken: z.string(),
});

const post = (path: string, body: unknown, init?: RequestInit) =>
  fetch(API + path, {
    ...init,
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });

/** Checks the password, and resolves the tokens and the user. */
export async function login(username: string, password: string) {
  const response = await post("/auth/login", { username, password });
  if (!response.ok) throw new Error(`Sign-in failed (${response.status})`);
  // DummyJSON returns the user's fields beside the tokens.
  const body: unknown = await response.json();
  return { ...tokensSchema.parse(body), user: userSchema.parse(body) };
}

/**
 * Exchanges a refresh token for new tokens. Resolves `null` when the backend
 * rejects the token, and throws for any other failure.
 */
export async function refresh(refreshToken: string, signal: AbortSignal) {
  const response = await post("/auth/refresh", { refreshToken }, { signal });
  // DummyJSON answers 403 for an invalid token and 401 for a missing one.
  if (response.status === 401 || response.status === 403) return null;
  if (!response.ok) throw new Error(`Refresh failed (${response.status})`);
  return tokensSchema.parse(await response.json());
}
```

The schemas do two jobs. They check what the backend sent, and they give the
rest of the app its types. Requests omit `credentials: "include"`, because
Session kit keeps tokens out of cookies and attaches them itself.

## 2. Create the client

The client is the one object that holds the session. Tell it what a user is,
where to save the session, and how to refresh:

```ts title="src/auth.ts"
import { createAuth, webStorage } from "@monarcode/session-kit";

import { login, refresh, userSchema } from "./backend.js";

export const auth = createAuth({
  // Keeps this app's saved session apart from any other on the same origin.
  name: "my-app",
  // Every user is checked against this schema, and typed by it.
  user: userSchema,
  // Where the session is saved: `localStorage`.
  storage: webStorage(),
  // How expired access is replaced.
  refresh: ({ refreshToken, signal }) => refresh(refreshToken, signal),
});

/** Signs in with the backend, then hands the result to Session kit. */
export async function signInWithPassword(username: string, password: string) {
  const tokens = await login(username, password);
  await auth.signIn(tokens);
}

export type AppAuth = typeof auth;
```

`refresh` tells two failures apart. `null` means the backend rejected the
refresh token, so the user is signed out. A throw means something else failed,
so the session is kept and the error reported. That split is why an outage
doesn't sign anyone out. [Refresh](https://session-kit.pages.dev/docs/configuration/refresh) has the
details, and [Storage](https://session-kit.pages.dev/docs/configuration/storage) lists the other places a
session can be saved.

Create the client once, at module level, as this file does. Give each client
in an app its own `name`.

DummyJSON's tokens are JWTs, so their expiry comes from the `exp` and `iat`
claims. [Expiry](https://session-kit.pages.dev/docs/configuration/expiry) covers backends that say it
another way.

## 3. Add a sign-in form

`signInWithPassword` is all a form needs. The form has no Session kit code
of its own:

```tsx title="src/sign-in.tsx"
import { useState, type FormEvent } from "react";

import { signInWithPassword } from "./auth.js";

export function SignIn() {
  const [error, setError] = useState("");
  const [pending, setPending] = useState(false);

  async function submit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const form = new FormData(event.currentTarget);
    setError("");
    setPending(true);
    try {
      await signInWithPassword(
        String(form.get("username")),
        String(form.get("password")),
      );
    } catch (cause) {
      setError(cause instanceof Error ? cause.message : "Could not sign in");
    } finally {
      setPending(false);
    }
  }

  return (
    <form onSubmit={submit}>
      <h1>Sign in</h1>
      {/* DummyJSON's documented test user. */}
      <label>
        Username
        <input name="username" defaultValue="emilys" autoComplete="username" />
      </label>
      <label>
        Password
        <input
          name="password"
          type="password"
          defaultValue="emilyspass"
          autoComplete="current-password"
        />
      </label>
      {error && <p role="alert">{error}</p>}
      <button disabled={pending}>{pending ? "Signing in…" : "Sign in"}</button>
    </form>
  );
}
```

The form doesn't have to do anything when it succeeds. Signing in changes the
client's state, and the next step renders from that state. With a router, the
sign-in page's guard runs again and sends the user on.

## 4. Read the session in React

The hooks come from `@monarcode/session-kit/react`. Register your client's
type once, where you render `AuthProvider`, and they carry your user.

Then render by `status`. Each case is one of the states in
[How it works](https://session-kit.pages.dev/docs#how-it-works). By the last case, TypeScript has narrowed
`user` to a value that's set:

```tsx title="src/app.tsx"
import {
  AuthProvider,
  useAuth,
  useAuthClient,
} from "@monarcode/session-kit/react";

import { auth } from "./auth.js";
import { SignIn } from "./sign-in.js";

// Types `AuthProvider` and the hooks with this client.
declare module "@monarcode/session-kit" {
  interface Register {
    auth: typeof auth;
  }
}

export function App() {
  return (
    <AuthProvider client={auth}>
      <AuthError />
      <Screen />
    </AuthProvider>
  );
}

function Screen() {
  const { status, user } = useAuth();
  const client = useAuthClient();

  switch (status) {
    case "initializing":
      return <p>Restoring your session…</p>;
    case "unauthenticated":
      return <SignIn />;
    case "unavailable":
      // Access couldn't be restored or refreshed: offer another try.
      return (
        <button onClick={() => void client.retry().catch(() => {})}>
          Try again
        </button>
      );
    default:
      // Signed in: "authenticated", or "refreshing" with the user on screen.
      return (
        <main>
          <h1>Welcome, {user.firstName}</h1>
          <button onClick={() => void client.signOut().catch(() => {})}>
            Sign out
          </button>
        </main>
      );
  }
}

/** The latest failure, such as a refresh the backend couldn't answer. */
function AuthError() {
  const error = useAuth((state) => state.error);
  return error ? <p role="alert">{error.message}</p> : null;
}
```

Each part of this file has a job:

- `AuthProvider` mounts the client. It restores the saved session, starts the
  refresh timer, and follows other tabs.
- `useAuth()` returns the whole state. `useAuth(selector)` returns a part of
  it, and re-renders only when that part changes, as `AuthError` does.
- `useAuthClient()` returns the client, for actions.
- With more than one client in an app, create hooks for each with
  `createAuthHooks<typeof client>()` instead.
- `isSignedIn(state)` is `true` while `authenticated` or `refreshing`. Pass it
  to `useAuth` to read only that: `useAuth(isSignedIn)`. It comes from
  `@monarcode/session-kit`.
- `retry` and `signOut` reject when they fail, and publish the same failure in
  `state.error`. `AuthError` shows it, so the handlers have nothing left to
  do.

With [`locking`](https://session-kit.pages.dev/docs/client/locking), give `locked` a case of its own. It
has a user too, but no usable access.

That's a working app. To check it:

1. Sign in. The welcome message appears.
2. Reload the page. You're still signed in.
3. Open the app in a second tab, and sign out in one of them. Both tabs show
   the sign-in form.

## 5. Call your API

`createAuthFetch` makes a `fetch` that sends the access token to one origin,
and to no other:

```ts title="src/api.ts"
import { createAuthFetch } from "@monarcode/session-kit/http";

import { auth } from "./auth.js";
import { API, userSchema } from "./backend.js";

export const authFetch = createAuthFetch(auth, { baseUrl: `${API}/` });

/** The signed-in user, fresh from the backend. */
export async function fetchProfile() {
  const response = await authFetch("auth/me");
  if (!response.ok) throw new Error(`Profile failed (${response.status})`);
  return userSchema.parse(await response.json());
}
```

When a request gets a 401, `authFetch` refreshes, then sends a GET or HEAD
again. Other methods are never sent twice.
[Authenticated requests](https://session-kit.pages.dev/docs/client/requests) covers the rest of its
behaviour and its options.

Using ky, axios or openapi-fetch? Pass `authFetch` as the client's `fetch`:
see [Other HTTP clients](https://session-kit.pages.dev/docs/client/other-http-clients).

## 6. Protect your routes

The client is the same everywhere. What changes is how each router keeps
signed-out users away from private pages. To continue, follow the guide for
your router:

- [TanStack Router](https://session-kit.pages.dev/docs/setups/tanstack-router), and
  [TanStack Start in SPA mode](https://session-kit.pages.dev/docs/setups/tanstack-router#tanstack-start-in-spa-mode).
- [React Router](https://session-kit.pages.dev/docs/setups/react-router), in declarative, data, or
  framework mode with `ssr: false`.
- [Expo Router](https://session-kit.pages.dev/docs/setups/expo), for iOS and Android. Its guide starts from
  scratch.
- Without a router, you are done. Render by `status`, as `Screen` does.

With Effect, the same client becomes a service: see
[Effect and effetch](https://session-kit.pages.dev/docs/effect).

## Next steps

- [Using the client](https://session-kit.pages.dev/docs/client): what else the client does, such as
  updating the user and locking.
- [Configuring the client](https://session-kit.pages.dev/docs/configuration): the rest of its options.