Skip to content

Developer Procedures

Tamas Vince Kornel edited this page Dec 1, 2025 · 3 revisions

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.).

iris - The Frontend

Adding a new component

  1. 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.

Adding a Frontend Page

Note

All example paths are relative to apps/iris, the project root.

  1. Create the route file under src/routes. Use createFileRoute so the TanStack plugin can discover it.
  2. Export the Route constant and pass a component, loader, or beforeLoad as needed.
  3. The Vite plugin regenerates src/route-tree.gen.ts automatically. Committed code should not touch that file manually.
  4. 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>;
}

Checklist

  • New file under src/routes exports Route = createFileRoute(...)
  • Page uses translations and typed API calls instead of literals whenever possible
  • UI pieces (sidebar/nav) link to the new route when applicable

Handling State & Data Fetching

  • Authentication-aware components should call authClient.useSession() to get session/user and pending state. Guarded layouts (see routes/_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 PermissionGuard component to wrap sensitive UI parts.
  • Server data should flow through React Query:
    • Derive a stable queryKey per resource.
    • Wrap Hono calls with parseResponse(api.<feature>.<path>.$get()) to get typed results.
    • Use enabled to defer queries until required props (e.g., selectedCohortId) are ready, as seen in components/timetable-view.tsx.
  • 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 or useNavigate (see the cohort selector in TimetableView).
  • Mutations should go through useMutation and invalidate related queries upon success. If the API toggles persisted data (feature flags, timetable imports, etc.), also trigger toast notifications via sonner.

Important

Do not call REST endpoints directly with fetch. Always go through api so you retain full typing derived from the server router.

Handling i18n

  1. Add or update keys in both public/locales/en/translation.json and public/locales/hu/translation.json. Keep the nesting consistent ("featureFlags.toggle", "doorlock.openDoor", etc.).
  2. Use the hook: const { t } = useTranslation(); inside components, then render t('doorlock.openDoor').
  3. Server-side rendering already injects the correct language into <html lang> via RouterContext. If a component needs direct access to the i18n instance (e.g., to change the language), consume it from useTranslation or via the context exposed in route loaders.
  4. 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.

chronos - The Backend

Adding a New API Endpoint

Note

All example paths are relative to apps/chronos, the project root.

  1. Scaffold the feature folder under src/routes/<feature> if it does not exist yet.
  2. Factory: create _factory.ts exporting createFactory<Context>(). The shared Context lives in src/utils/globals.ts and already carries user & session variables.
  3. Handlers: write one file per endpoint (or group) using <feature>Factory.createHandlers(...). Inside:
    • Describe the route with describeRoute(...) so /api/doc/openapi.json stays accurate.
    • Chain middlewares such as requireAuthentication / requireAuthorization('permission').
    • Validate input with zValidator or manual Zod parsing.
    • Return SuccessResponse/ErrorResponse payloads.
  4. Router: in _router.ts, wire HTTP verbs to handlers via featureFactory.createApp().get(...).post(...).
  5. Register: mount the router once inside src/index.ts using api.route('/my-feature', myFeatureRouter);. Development-only routes should stay behind the env.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.

Consuming the New API from the Frontend

Note

All example paths are relative to apps/iris, the frontend project root.

  1. 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),
    };
  2. Call it inside React Query (see the example in the frontend section) or inside a mutation hook.

  3. Handle auth: endpoints that need a session rely on credentials: 'include' provided via dOpts. Avoid overriding it unless you know what you are doing.

  4. Error surfacing: wrap failures with toast.error or display localized error messages using t('error.generic', { message }).

Working Agreements & Verification

  • Run bun install once after pulling new dependencies.
  • Use bun run dev to get both the API and frontend (via Vite + Hono dev server) with auto-regenerated routes.
  • Before sending a PR:
    • bun run lint
    • bun 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 ❤️

Clone this wiki locally