5.9 KiB
Chitai frontend
SvelteKit web app for the eBook library. See the repo-root AGENTS.md for the overall picture and
dev-environment setup.
Stack: SvelteKit 2 with adapter-node · Svelte 5 (runes) · Tailwind v4 · Zod v4 ·
epubjs · mode-watcher (dark mode) · svelte-sonner (toasts) · pnpm.
Two experimental flags are on in svelte.config.js and the codebase depends on both:
kit.experimental.remoteFunctions and compilerOptions.experimental.async (await in components).
Tailwind v4 has no config file — the theme, oklch colour tokens and @custom-variant dark all
live in src/app.css.
Talking to the backend
There are two mechanisms; pick deliberately.
1. Remote functions — the default. src/lib/api/*.remote.ts export query / command / form
functions from $app/server. Each takes a Zod schema from $lib/schema as its validator and runs
on the server, reaching the API through locals.api (the ApiClient in src/lib/server/api.ts):
export const getBook = query(stringCoerce, async (id): Promise<Book> => {
const { locals } = getRequestEvent();
const response = await locals.api.get(`/books/${id}`);
if (!response.ok) error(response.status === 404 ? 404 : 500, '…');
return await response.json();
});
Conventions: build query strings with createQueryParams from $lib/utils; on a failed response
throw SvelteKit's error(status, message); multipart uploads go through postMultipart /
putMultipart. Re-export new modules from src/lib/api/index.ts.
2. The catch-all proxy at src/routes/api/[...path]/+server.ts forwards GET/POST/PATCH/DELETE
to the backend with the auth header attached. Use it only where the browser itself must fetch
the backend — e.g. streaming a book file into the EPUB/PDF reader. It is not the general-purpose
path.
Auth
src/hooks.server.ts is a sequence of two handles: the first reads the authToken cookie,
constructs an ApiClient, validates it with GET /access/me and fills locals.user /
locals.authToken / locals.api (clearing the cookie if invalid); the second redirects any route
outside /login to the login page when there is no user.
The cookie is set in src/lib/api/auth.remote.ts (login) — httpOnly, secure, sameSite strict, one
week — and deleted by logout. The backend JWT never reaches client-side JS.
Schemas
src/lib/schema/*.ts— hand-written Zod schemas, used as remote-function input validators and as the source of the exported TS types. Mirrorbackend/src/chitai/schemas/when the API changes.src/lib/schema/common.ts— shared building blocks:stringCoerce/arrayCoercecoercion helpers,PaginatedResponse<T>, and the pagination / search / order query schemas that most list endpoints compose from.src/lib/schema/openapi/schema.d.ts— generated from the backend's OpenAPI document withopenapi-typescript. Never hand-edit; regenerate after backend API changes.
State
Client state lives in classes in src/lib/state/*.svelte.ts using $state / $derived, shared via
Svelte context with a module-level Symbol key and a setXState / getXState pair:
const LIBRARY_KEY = Symbol('LIBRARY');
export function setLibraryState(libraries: Library[]) { return setContext(LIBRARY_KEY, new LibraryState(libraries)); }
export function getLibraryState() { return getContext<ReturnType<typeof setLibraryState>>(LIBRARY_KEY); }
Follow that pattern rather than introducing stores. library.svelte.ts is the reference — including
its optimistic-delete-with-rollback and toast handling. bookCollection / bookSelection /
bookOperations split list data, selection and mutations across three cooperating classes.
Components
src/lib/components/ui/— vendored shadcn-svelte (components.json) plus jsrepo blocks from@ieedan/shadcn-svelte-extras(jsrepo.json). Treat as generated: add components with the CLIs rather than hand-writing them, and prefer wrapping over editing.- App components live in
forms/,layout/,view/(browser, grid/list/table, filters, sort) andreader/(epub reader + chapter sidebar). cn()from$lib/utilsmerges Tailwind classes; theWithElementRef/WithoutChildhelpers there are the shadcn prop-typing conventions.
Routing
Route groups carry the layout structure:
(root)— the authenticated shell (sidebar, header);+layout.server.tsloads the libraries.(root)/(library)— library-scoped pages:library/[libraryId]/view,book/[bookId], edit, and the readers atbook/[bookId]/read/{epub,pdf}/[fileId].+layout@.sveltebreakouts reset to the root layout for the login page and the full-screen reader.
Conventions
Prettier (.prettierrc): tabs, single quotes, no trailing commas, 100 columns, with the Svelte and
Tailwind plugins. Run pnpm check (svelte-check) and pnpm lint before considering work done.
Known rough edges
Observed in the current tree — don't mistake these for intentional patterns to copy:
src/lib/schema/openapi/schema.d.tsis stale — it predates theBookProgressrework and has noBookProgressRead, sobook.progresstypes as{}. This is the source of most of the ~104 errorspnpm checkreports on a clean tree; regenerating it should clear them. Get a baseline before assuming an error is yours.src/routes/api/[...path]/+server.ts— all four handlers are annotatedRequestHandlerwhile the import of that type is commented out at line 4.src/app.d.ts—App.Locals["user"]is typed fromlucide-svelte'sUsericon component rather than theUserinterface in$lib/server/auth.- Uncommitted work in progress (as of 2026-08-10): library icons, spanning
components/ui/icon-picker/, the newly vendoredcomponents/ui/popover/,forms/library-create-form.svelte,layout/library-switcher.svelteandschema/library.ts. Prefer not to refactor those files mid-flight.