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:
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:
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:
| Action | Data Provider method |
|---|---|
| Get a page of records | getList |
| Get one record | getOne |
| Get several by id | getMany |
| Get related records | getManyReference |
| Create a record | create |
| Update a record | update |
| Update several | updateMany |
| Delete a record | delete |
| Delete several | deleteMany |
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:
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>:
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.