5. Auth & Production

Add authentication and connect Chelone to a real API.

Two things stand between the tutorial and a real deployment: authentication, and a data provider that talks to your actual backend.

Adding authentication

Chelone makes no assumption about your auth strategy (basic auth, OAuth, a custom route). You supply an authProvider object and pass it to <Admin> with requireAuth to gate pages. Chelone also calls it to react to 401 / 403 responses from your API.

An authProvider exposes four required async methods:

src/auth-provider.ts
import type { AuthProvider } from "@chelone/core";
 
export const authProvider: AuthProvider = {
  // Called when the user submits the login form.
  async login({ username }) {
    // Accept everyone for the tutorial; call your auth API here instead.
    localStorage.setItem("username", String(username));
  },
  // Called when the user clicks logout.
  async logout() {
    localStorage.removeItem("username");
  },
  // Called on every navigation to confirm the user is still authenticated.
  async checkAuth() {
    if (!localStorage.getItem("username")) {
      throw new Error("Authentication required");
    }
  },
  // Called when the data provider returns an error — sign out on 401/403.
  async checkError(error) {
    const status = (error as { status?: number })?.status;
    if (status === 401 || status === 403) {
      localStorage.removeItem("username");
      throw new Error("Session expired");
    }
  },
};

Wire it into the App from Setup. Keep its dataProvider, queryClient, resources, layout, resource views, and surrounding <BrowserRouter> in these App.tsx excerpts:

src/App.tsx
import { Admin } from "@chelone/react";
import { authProvider } from "./auth-provider";
 
<Admin
  authProvider={authProvider}
  requireAuth
  dataProvider={dataProvider}
  queryClient={queryClient}
  resources={resources}
>
  {/* …resources… */}
</Admin>;

With requireAuth, the admin waits for checkAuth and redirects unauthenticated users to login. <LoginPage> is the default login page, so no loginPage prop is needed. See Security for getIdentity, getPermissions, canAccess, and OAuth handleCallback.

Connecting to a real API

The tutorial ran on fakeDataProvider, an in-memory adapter. For production you swap in a Data Provider that maps Chelone's method calls to your API's HTTP dialect. Chelone calls one method per action and expects a Promise back:

ActionData Provider method
Get a page of recordsgetList
Get one recordgetOne
Get several by idgetMany
Get related recordsgetManyReference
Create a recordcreate
Update a recordupdate
Update severalupdateMany
Delete a recorddelete
Delete severaldeleteMany

This example targets a REST API with JSON request and response bodies, _page / _limit pagination, and an x-total-count response header. It uses page pagination (defaulting to page 1) and the first sort key (defaulting to id ASC). Adapt these conventions to your backend:

src/data-provider.ts
import { type DataProvider, HttpError } from "@chelone/core";
 
const apiUrl = "https://my.api.url";
 
async function json(input: string, init?: RequestInit) {
  const res = await fetch(input, {
    headers: { "Content-Type": "application/json" },
    ...init,
  });
  if (!res.ok) throw new HttpError(res.statusText, res.status);
  return res.json();
}
 
export const dataProvider: DataProvider = {
  async getList(resource, params) {
    const { pagination, sort } = params;
    const { page, perPage } = pagination && "page" in pagination
      ? pagination
      : { page: 1, perPage: 10 };
    const { field, order } = (Array.isArray(sort) ? sort[0] : sort)
      ?? { field: "id", order: "ASC" };
    const query = new URLSearchParams({
      _sort: field,
      _order: order,
      _page: String(page),
      _limit: String(perPage),
      ...(params.filter as Record<string, string>),
    });
    const res = await fetch(`${apiUrl}/${resource}?${query}`);
    if (!res.ok) throw new HttpError(res.statusText, res.status);
    const data = await res.json();
    const total = Number(res.headers.get("x-total-count") ?? data.length);
    return { data, total };
  },
  async getOne(resource, params) {
    return { data: await json(`${apiUrl}/${resource}/${params.id}`) };
  },
  async getMany(resource, params) {
    const query = params.ids.map((id) => `id=${id}`).join("&");
    return { data: await json(`${apiUrl}/${resource}?${query}`) };
  },
  async getManyReference(resource, params) {
    const query = new URLSearchParams({ [params.target]: String(params.id) });
    const data = await json(`${apiUrl}/${resource}?${query}`);
    return { data, total: data.length };
  },
  async create(resource, params) {
    const data = await json(`${apiUrl}/${resource}`, {
      method: "POST",
      body: JSON.stringify(params.data),
    });
    return { data };
  },
  async update(resource, params) {
    const data = await json(`${apiUrl}/${resource}/${params.id}`, {
      method: "PUT",
      body: JSON.stringify(params.data),
    });
    return { data };
  },
  async updateMany(resource, params) {
    await Promise.all(
      params.ids.map((id) =>
        json(`${apiUrl}/${resource}/${id}`, {
          method: "PUT",
          body: JSON.stringify(params.data),
        }),
      ),
    );
    return { data: params.ids };
  },
  async delete(resource, params) {
    const data = await json(`${apiUrl}/${resource}/${params.id}`, {
      method: "DELETE",
    });
    return { data };
  },
  async deleteMany(resource, params) {
    await Promise.all(
      params.ids.map((id) =>
        json(`${apiUrl}/${resource}/${id}`, { method: "DELETE" }),
      ),
    );
    return { data: params.ids };
  },
};

Replace the fake provider's import in src/App.tsx with the production provider. Keep authProvider and requireAuth on <Admin>:

src/App.tsx
import { Admin } from "@chelone/react";
import { authProvider } from "./auth-provider";
import { dataProvider } from "./data-provider";
 
<Admin
  dataProvider={dataProvider}
  queryClient={queryClient}
  resources={resources}
  authProvider={authProvider}
  requireAuth
>
  {/* …resources… */}
</Admin>;

See Data Providers for the full method signatures and for wrapping a provider with lifecycle callbacks.

Conclusion

You've built a Zod-schema-backed CRUD admin, added search, auth, and a real API adapter. Because the UI lives in your own project (via the shadcn registry), any view, field, or input is yours to reshape.

Continue with Guides and Concepts, or browse the Page Components reference.