-
Notifications
You must be signed in to change notification settings - Fork 1
Developer Procedures
This page collects the most common day-to-day workflows in the Chronos stack.
Tip
When unsure where something should live, mirror the structure of an existing feature (doorlock, timetable, feature-flags, etc.).
- Search basecn if an existing component is available on basecn.dev.
cd apps/iris
bunx shadcn@latest add @basecn/<component>Note
If the component is not available on basecn, use the shadcn/ui version of it, but open an issue to track replacing it.
Why?
shadcn/ui is built on Radix UI, which is in maintenance mode. Basecn built on Base UI by the MUI team with bug fixes, new components, and better accessibility.
Note
All example paths are relative to apps/iris, the project root.
-
Create the route file under
src/routes. UsecreateFileRouteso the TanStack plugin can discover it. -
Export the
Routeconstant and pass acomponent,loader, orbeforeLoadas needed. - The Vite plugin regenerates
src/route-tree.gen.tsautomatically. Committed code should not touch that file manually. - Add navigation (sidebar, navbar, deep link) if the page needs to be reachable through the UI.
// src/routes/_private/admin/example.tsx
import { createFileRoute } from '@tanstack/react-router';
import { useQuery } from '@tanstack/react-query';
import { parseResponse } from 'hono/client';
import { api } from '~/utils/hc';
export const Route = createFileRoute('/_private/admin/example')({
component: AdminExamplePage,
});
function AdminExamplePage() {
const widgetsQuery = useQuery({
queryKey: ['widgets'],
queryFn: async () => {
const res = await parseResponse(api.widgets.index.$get());
if (!res.success) throw new Error('Failed to load widgets');
return res.data;
},
});
if (widgetsQuery.isLoading) return <p>Loading…</p>;
return <pre>{JSON.stringify(widgetsQuery.data, null, 2)}</pre>;
}- New file under
src/routesexportsRoute = createFileRoute(...) - Page uses translations and typed API calls instead of literals whenever possible
- UI pieces (sidebar/nav) link to the new route when applicable
-
Authentication-aware components should call
authClient.useSession()to getsession/userand pending state. Guarded layouts (seeroutes/_private/route.tsx) should handle the loading spinner so child routes can assume a session exists. -
Components which require role-based access control should use the
PermissionGuardcomponent to wrap sensitive UI parts. -
Server data should flow through React Query:
- Derive a stable
queryKeyper resource. - Wrap Hono calls with
parseResponse(api.<feature>.<path>.$get())to get typed results. - Use
enabledto defer queries until required props (e.g.,selectedCohortId) are ready, as seen incomponents/timetable-view.tsx.
- Derive a stable
-
Local UI state sticks to React primitives (
useState,useMemo,useReducer). Keep derived state memoized and push URL-derived state into query params via the TanStack router oruseNavigate(see the cohort selector inTimetableView). -
Mutations should go through
useMutationand invalidate related queries upon success. If the API toggles persisted data (feature flags, timetable imports, etc.), also trigger toast notifications viasonner.
Important
Do not call REST endpoints directly with fetch. Always go through api so you retain full typing derived from the server router.
-
Add or update keys in both
public/locales/en/translation.jsonandpublic/locales/hu/translation.json. Keep the nesting consistent ("featureFlags.toggle","doorlock.openDoor", etc.). -
Use the hook:
const { t } = useTranslation();inside components, then rendert('doorlock.openDoor'). -
Server-side rendering already injects the correct language into
<html lang>viaRouterContext. If a component needs direct access to the i18n instance (e.g., to change the language), consume it fromuseTranslationor via the context exposed in route loaders. - Validation or toast strings should reference translation keys, not inline literals, so they get localized in both SSR and CSR.
Quick test: run bun run dev, switch between English/Hungarian, and confirm the new keys render. Missing keys show up as the raw key path in dev mode.
Note
All example paths are relative to apps/chronos, the project root.
-
Scaffold the feature folder under
src/routes/<feature>if it does not exist yet. -
Factory: create
_factory.tsexportingcreateFactory<Context>(). The sharedContextlives insrc/utils/globals.tsand already carriesuser&sessionvariables. -
Handlers: write one file per endpoint (or group) using
<feature>Factory.createHandlers(...). Inside:- Describe the route with
describeRoute(...)so/api/doc/openapi.jsonstays accurate. - Chain middlewares such as
requireAuthentication/requireAuthorization('permission'). - Validate input with
zValidatoror manual Zod parsing. - Return
SuccessResponse/ErrorResponsepayloads.
- Describe the route with
-
Router: in
_router.ts, wire HTTP verbs to handlers viafeatureFactory.createApp().get(...).post(...). -
Register: mount the router once inside
src/index.tsusingapi.route('/my-feature', myFeatureRouter);. Development-only routes should stay behind theenv.mode === 'development'block.
// src/routes/widgets/index.ts
import { describeRoute, resolver } from 'hono-openapi';
import { zValidator } from '@hono/zod-validator';
import z from 'zod';
import { widgetsFactory } from '~/routes/widgets/_factory';
import { db } from '~/database';
import { widget } from '~/database/schema/widget';
import { requireAuthentication } from '~/utils/middleware';
import type { SuccessResponse } from '~/utils/globals';
const bodySchema = z.object({ name: z.string().min(1) });
export const createWidget = widgetsFactory.createHandlers(
describeRoute({
description: 'Create a dashboard widget',
requestBody: {
content: {
'application/json': { schema: resolver(bodySchema) },
},
},
responses: { 201: { description: 'Widget created' } },
tags: ['Widgets'],
}),
requireAuthentication,
zValidator('json', bodySchema),
async (c) => {
const { name } = c.req.valid('json');
const [result] = await db.insert(widget).values({ name }).returning();
return c.json<SuccessResponse<typeof result>>(
{ data: result, success: true },
201
);
}
);Note
describeRoute + ensureJsonSafeDates keep the OpenAPI spec and Swagger UI working. Always wrap responses that include Date objects.
Note
All example paths are relative to apps/iris, the frontend project root.
-
Expose a typed client by extending
src/utils/hc.ts:import type { widgetsRouter } from '~/routes/widgets/_router'; export const apiClient = { ..., widgets: hc<typeof widgetsRouter>('/api/widgets', dOpts), };
-
Call it inside React Query (see the example in the frontend section) or inside a mutation hook.
-
Handle auth: endpoints that need a session rely on
credentials: 'include'provided viadOpts. Avoid overriding it unless you know what you are doing. -
Error surfacing: wrap failures with
toast.erroror display localized error messages usingt('error.generic', { message }).
- Run
bun installonce after pulling new dependencies. - Use
bun run devto get both the API and frontend (via Vite + Hono dev server) with auto-regenerated routes. - Before sending a PR:
bun run lintbun run typecheck-
bun run build(if you touched bundler config, routers, or server entrypoints)
- Keep commits focused per feature (e.g., one commit for "Add admin reports route").
- Double-check translations, query invalidations, and OpenAPI output when modifying endpoints.
Built with ❤️