truth-web · Developer Onboarding

Welcome to Truth Social.

This guide covers your internship plan, the technologies we use to build and ship the Truth Social web client, and the workflows you'll follow as a contributor. Start with the internship overview, then work through the technical modules in order.

TypeScript
React 18
Tailwind CSS
React Query
Zod
Vite
Vitest
GitLab CI

Internship Plan

What this internship is designed to accomplish, and how your time will be structured.

This internship is built around genuine participation — not shadowing, not busy work. You'll be in real meetings, shipping real code, and learning the tools that define how modern software gets built. The goal is for you to leave with hands-on experience across the full development lifecycle and a concrete understanding of where the industry is headed.

Team Web Meetings

You'll be included in the web team's regular standups and planning sessions. Pay attention to how work gets scoped, how decisions get made, and how engineers communicate blockers and progress. These meetings are part of the job — not optional context.

Product Meetings (Where It Makes Sense)

You'll be brought into product discussions when the topics are relevant to what you're working on or learning. Seeing how features move from idea to specification to implementation is something you can't learn from documentation — take notes, ask questions, and observe how product thinking translates into engineering decisions.

TypeScript & React Learning

You'll work through structured learning materials covering the core technologies in this codebase — TypeScript, React, and the libraries we've built on top of them. The technical modules in this document are your curriculum. Don't just read them; work through the exercises, follow the file references, and ask questions when something isn't clicking. The goal is fluency, not familiarity.

Contributing to truth-web

You'll make real contributions to the Truth Social web client — starting with the scoped starter tasks in this guide and growing from there. You'll go through the same workflow every engineer on the team uses: branch, code, pre-push checks, MR, code review. Your code will be reviewed seriously, your feedback will be real, and your changes will ship.

Help Center Improvements

Alongside your work on truth-web, you'll contribute to improvements on the Truth Social Help Center. This is a great opportunity to develop a full-stack perspective — understanding not just how the product is built, but how it's documented and supported. It's also a lower-stakes environment to get comfortable shipping independently.

Spark Team — AI-Forward Development

You'll spend time with our Spark team, which is leading the charge on AI-forward development across the company. This isn't a trend we're watching — it's a shift we're actively driving. The future of software development is increasingly about prompting: knowing how to work with AI systems effectively, how to structure problems for them, how to verify their output, and how to integrate them into a real engineering workflow.

You're entering the industry at exactly the right moment to build these skills from the start, rather than having to unlearn old habits later. Lean into it.

1

The Browser Environment

Goal: Understand what the browser gives you and what constraints it imposes.

The app is a React 18 single-page application. The entire UI runs in the browser — there are no server-rendered pages and no page reloads between views. The Truth Social API is a separate back-end server; this repo is the front end only.

Key constraints

ConstraintWhat it means in practice
No file systemPersistence happens through localStorage, IndexedDB (we use localforage), or the API. Auth tokens live in IndexedDB — never cookies.
Single threadJavaScript is single-threaded. async/await is cooperative, not parallel. Long synchronous work blocks the entire UI.
DOM = the UIReact manages the DOM for you. You rarely touch it directly.
Network can failAuth expiry, CORS, offline — every fetch needs error handling. Never assume a call will succeed.
No runtime build stepWhat ships is the compiled output of npm run build. Vite handles bundling and tree-shaking at build time.
✦ Exercises
  1. Open DevTools (F12) → Application → IndexedDB and find the stored auth tokens.
  2. Open the Network tab, filter to Fetch/XHR, then take an action (like/follow) and inspect the request and response.
  3. Open the Console and run localStorage.getItem('soapbox') to see stored UI preferences.
  4. Open the React Query DevTools panel (bottom-right corner in dev) and navigate between pages — watch the cache populate.
2

TypeScript

Goal: Read and write TypeScript confidently before touching project code.

TypeScript adds a static type system on top of JavaScript. The compiler catches type errors before the code runs, and editors use the types for autocomplete and inline documentation. Strict null checks are enabled — null and undefined are separate and must be handled explicitly.

Syntax primer

// Types describe the shape of data
type Status = "public" | "self" | "group";   // union — one of these values

// Interfaces describe object shapes (used for component props)
interface ButtonProps {
  label: string;
  onClick: () => void;
  disabled?: boolean;     // ? = optional
}

// Generics work like templates
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

// async/await — returns Promise<T>
async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/v1/accounts/${id}`);
  return res.json();
}

Project type conventions

// Use `type` for unions, intersections, and computed shapes
type ReadonlyAccount = Readonly<Account>;

// Use `interface` for prop bags
interface ChatItemProps {
  isRead: boolean;        // booleans: always is* / has* / can* prefix
  hasAttachments: boolean;
  canDelete: boolean;
}

// Derive types from Zod schemas — never write both by hand
import z from "zod";
const schema = z.object({ id: z.string(), name: z.string() });
type Account = z.infer<typeof schema>;   // type comes from the schema
✦ Exercises
  1. Read src/schemas/account.ts end to end. Notice how the TypeScript type is derived from the Zod schema rather than written separately.
  2. Find a hook in src/api/chats/hooks/ and trace its generic type parameters from the call site back to the definition.
  3. Open src/types/api.ts and study how API error types are structured as a union.
3

React Fundamentals

Goal: Understand the component model and hook-based state before touching project code.

A React component is a function that takes props and returns UI (JSX). React calls it again whenever its props or state change. The framework diffs the result against the previous render and only updates what changed in the DOM.

Component anatomy

interface UserBadgeProps {
  username: string;
  isVerified: boolean;
}

const UserBadge = ({ username, isVerified }: UserBadgeProps) => (
  <span>
    @{username}
    {isVerified && <VerificationBadge />}
  </span>
);

Hooks you'll use most

HookPurpose
useStateLocal component state — triggers a re-render on change
useEffectSide effects that run after render (subscriptions, timers)
useRefMutable value that does not trigger a re-render
useContextRead a value from a React context (our dependency injection)
useMemo / useCallbackMemoize values/functions — avoid unless profiling shows a need

Never mutate state directly. Always return a new object or array. Effects run after paint — don't use them to derive state from props; compute it inline instead.

If data should be shared across many components, it belongs in a context, not passed through props down multiple levels.

✦ Exercises
  1. Read src/components/status.tsx. Map each prop to what it renders on screen.
  2. Find a useState call in any feature component. Trace what triggers the change and what re-renders as a result.
  3. Read src/contexts/composer-context.tsx. Identify what state it holds and why it lives in context rather than in a single component.
4

Project Architecture

Goal: Know where things live and how the layers connect.
src/
├── api/              # Data layer: hooks, schemas, React Query integration
│   ├── core/         # useEntity, useEntities, useEntityMutation
│   └── <feature>/   # Feature-specific hooks (e.g. api/chats/, api/groups/)
├── components/       # Shared UI components reusable across features
│   └── ui/           # Primitive UI kit: Button, Modal, Select, Input…
├── contexts/         # React contexts (global shared state)
├── features/         # Full page / screen implementations
│   └── ui/           # App shell, routing, layout
├── hooks/            # Shared custom hooks
├── schemas/          # Zod schemas — single source of truth for API shapes
├── types/            # Global TypeScript types (non-schema)
├── utils/            # Pure utility functions
└── styles/           # Tailwind config and global CSS

The three-layer rule

Every feature follows the same structure. A component should never call fetch or axios directly — always go through the hook layer.

LayerLocationResponsibility
Schemasrc/schemas/Defines what an API entity looks like; validates at runtime
API hooksrc/api/<feature>/Fetches, caches, and mutates entities via React Query
Feature componentsrc/features/<feature>/Renders the entity; calls the API hook

Always use useApi() for authenticated requests — it handles token injection and refresh automatically.

5

The API Contract

Goal: Understand exactly how the client talks to the Truth Social API — authentication, request lifecycle, pagination, and error handling.

The Truth Social API is a Mastodon-compatible REST API. Every feature in this repo is ultimately a wrapper around HTTP calls to that API. Understanding how those calls are made, authenticated, paginated, and validated is fundamental to working in this codebase.

Authentication

The app supports two auth states: logged in (bearer token) and logged out (visitor token). The correct headers are applied automatically by the HTTP client — you never attach them manually.

StateHeader sentWhere it comes from
Logged inAuthorization: Bearer <token>Token retrieved from IndexedDB via getCachedAccountToken()
Logged outX-Visitor-Token: <token>Persisted in localStorage under truth:visitor-token, refreshed from response headers

Always use useApi() to get the authenticated axios instance. It reads the cached token and the correct baseURL at call time — calling baseClient() directly will bypass this.

The HTTP client

The client is built on axios, configured in src/api/index.ts. It sets the baseURL to VITE_BACKEND_URL and attaches the auth header. In development, /api/*, /oauth/*, /pleroma/*, and /socket paths are proxied to the backend via Vite — no CORS issues locally.

// The correct way to make an authenticated request
const api = useApi();                         // axios instance with auth header

// GET
const response = await api.get("/api/v1/accounts/verify_credentials");

// POST with body
const response = await api.post("/api/v1/statuses", {
  status: "Hello, Truth!",
  visibility: "public",
});

// DELETE
const response = await api.delete(`/api/v1/statuses/${id}`);

Pagination via Link headers

List endpoints are paginated using cursor-based pagination — never offset-based. The API returns a Link HTTP response header containing URLs for the next and previous pages. The client parses these with the http-link-header library.

// Response headers from a timeline request:
// Link: <https://truthsocial.com/api/v1/timelines/home?max_id=123>; rel="next",
//       <https://truthsocial.com/api/v1/timelines/home?since_id=456>; rel="prev"

import { getNextLink, getPrevLink } from "@/api";

const nextUrl = getNextLink(response);   // pass to next page fetch
const prevUrl = getPrevLink(response);   // pass to prev page fetch

Never use offset pagination. Always follow the next link from the response header. useEntities handles this automatically — you only need to call fetchNextPage().

Some endpoints use an X-Total-Count response header to communicate total result counts without sending all the data. useEntities exposes a getTotalCount callback for this.

Request → Response lifecycle

Here is what happens end-to-end when a component triggers a data fetch:

StepWhereWhat happens
1. Component mountsFeature componentCalls useEntity or useEntities with an endpoint and schema
2. Cache checkReact QueryIf a fresh cached result exists for the query key, return it immediately — no network call
3. HTTP requestuseApi() → axiosGET to the API with auth header attached
4. Response receivedaxios interceptorVisitor token in response headers is persisted if present
5. Zod parseuseEntity / useEntitiesResponse body validated against the schema; unknown fields stripped, bad fields defaulted via .catch()
6. Cache writeReact QueryParsed entity stored under the query key; all subscribed components re-render

Error handling

The API returns errors as JSON. Two error shapes exist — standard API errors and Pepe (internal service) errors — both defined in src/types/api.ts.

// Standard API error shape
interface ApiError {
  error: string;
  error_message: string;
  error_code: string;
  message: string;
  errors: {
    error_code: string;
    error_field: string | null;
    error_message: string;
  }[];
}

The mutation layer in useEntityMutation handles two status codes automatically — you don't need to handle these yourself:

StatusAutomatic behaviour
403 (SMS re-verification required)Opens the REVERIFY_SMS modal if the current account has sms_reverification_required set
429 (rate limited)Shows a toast with the error message from the response body, falling back to the default "Too many requests" message

For validation errors with field-level details (e.g. form submissions), use buildErrorMessage(errors) from src/utils/errors.ts to convert the errors object into a human-readable string.

The Zod boundary

Zod sits at the exact point where raw API data enters the app. Nothing upstream of useEntity / useEntities ever touches unvalidated data. This means:

Relationships as a second request

Account and group list endpoints don't embed relationship data (follow state, membership role, etc.) in the response — that comes from a separate /api/v1/accounts/relationships or /api/v1/groups/relationships call. useEntities handles this automatically when you pass a relationshipConfig option — it batches the IDs from the current page, fetches relationships, and merges them into the result.

✦ Exercises
  1. Open src/api/index.ts. Find where the Authorization header is attached and trace where the token comes from.
  2. Open the Network tab in DevTools and load a timeline. Find the Link response header and identify the max_id cursor value.
  3. Open src/api/core/hooks/useEntityMutation.ts and find the onError handler. Identify the two HTTP status codes it handles automatically.
  4. Trigger a 429 in a dev environment (or read the handler) and trace what the user sees.
6

Data Fetching with React Query

Goal: Understand how server state is fetched, cached, and updated.

React Query manages all async server data. It caches responses, deduplicates in-flight requests, and re-fetches when data goes stale — you don't manage loading/error state by hand.

Reading data

import { useEntity, QueryKeys } from "@/api/core";
import { accountSchema } from "@/schemas/account";

const { entity: account, isLoading } = useEntity({
  id,
  queryKey: QueryKeys.accounts.account(id),
  endpoint: `/api/v1/accounts/${id}`,
  schema: accountSchema,   // response is validated + typed automatically
});

Mutating data

const { mutate: followUser } = useEntityMutation({
  queryKey: QueryKeys.accounts.account(id),
  endpoint: `/api/v1/accounts/${id}/follow`,
  method: "POST",
  onMutate: async () => { /* snapshot cache for optimistic update */ },
  onError: (_, __, context) => { /* roll back from snapshot */ },
});

Optimistic updates

When a user takes an action (follow, like, etc.), the UI should feel instant:

StepWhat happens
onMutateSnapshot the current cache; write the expected new value immediately
Server succeedsDo nothing — the cache is already correct
onErrorRestore the snapshot

All query keys come from QueryKeys in src/api/core/query-keys.ts. If a key is invalidated from more than one folder, it belongs in core, not in a feature folder.

✦ Exercises
  1. Open a mutation hook in src/api/statuses/. Identify the optimistic update pattern.
  2. In the React Query DevTools (dev mode), watch the cache as you navigate between pages.
  3. Trace a query key from a component → hook → QueryKeys definition.
7

Zod — Runtime Validation

Goal: Understand why we validate API responses and how to write schemas.

The API can return unexpected shapes — missing fields, wrong types, extra nulls. Zod validates the response at the boundary so the app never crashes on malformed data, and gives us a strongly-typed object to work with.

import z from "zod";

const statusSchema = z.object({
  id:          z.string(),
  content:     z.string().catch(""),                              // safe default on failure
  favourited:  z.boolean().catch(false),
  visibility:  z.enum(["public", "self", "group"]).catch("public"),
  created_at:  z.string().datetime().optional(),
});

type Status = z.infer<typeof statusSchema>;   // type derived from schema

Never hand-write a type for an API entity. Always derive it with z.infer<>. Add new schemas to src/schemas/ before writing any hook or component that consumes the entity. Prefer .catch() over .optional() for API fields.

8

Styling with Tailwind CSS

Goal: Apply styling using utility classes without writing custom CSS.

Tailwind is a utility-first CSS framework. Instead of writing a CSS class with properties, you apply small, single-purpose utility classes directly on the element.

import { cn } from "@/utils/cn";

<div className={cn(
  "rounded-lg border border-border px-4 py-2",
  isActive   && "bg-primary text-primary-foreground",
  isDisabled && "opacity-50 cursor-not-allowed",
)} />

Project rules

RuleDetail
Semantic tokensUse text-muted-foreground, bg-primary, border-border — never text-gray-700 dark:text-gray-600
Class compositionUse cn() from project utilities, not clsx. The migration is done; new clsx imports are regressions.
Dark modeUse the dark: Tailwind variant. Never use CSS prefers-color-scheme media queries.
✦ Exercises
  1. Toggle dark mode in the app (Settings → Appearance) and observe which dark: classes activate in DevTools.
  2. Find a component using raw text-gray-* classes — note this is legacy; new code should not follow this pattern.
9

Routing

Goal: Understand how URLs map to feature components.

All routes are declared in src/features/ui/index.tsx using WrappedRoute. Lazy-loaded components are registered in src/features/ui/util/async-components.ts.

<WrappedRoute
  path="/notifications"
  page={BundleContainer}
  component={Notifications}
  requiresAuth
/>

requiresAuth redirects unauthenticated users to login automatically — you don't need to handle that in the feature component. Auth routes (login, register, OTP) live in src/features/auth-layout/.

10

Internationalization (i18n)

Goal: Add user-facing text correctly.

Every string visible to users must go through React Intl. No hardcoded string literals in JSX.

import { defineMessages, useIntl } from "react-intl";

const messages = defineMessages({
  follow: { id: "account.follow", defaultMessage: "Follow" },
});

const FollowButton = () => {
  const intl = useIntl();
  return <button>{intl.formatMessage(messages.follow)}</button>;
};

After adding any message, run npm run i18n — it extracts all messages into src/locales/en.json. Never hand-edit en.json.

Terminology rule: User-facing copy says "Truth" / "Truths" — not "post" / "posts". The API uses status internally — keep that in code, but not in anything the user sees.

11

Testing

Goal: Read, run, and write tests.

We use Vitest — Jest-compatible and Vite-native. Tests live alongside their subjects (src/features/compose/__tests__/) or in src/__tests__/. Mock modules are in src/__mocks__/.

What to testHow
Utility functionsPure input → output unit tests
Custom hooksrenderHook from @testing-library/react
Complex component interactionsComponent tests — forms, modals, multi-step flows

Test observable behavior, not implementation details. If a refactor doesn't change what the user sees, the tests shouldn't break.

12

Git Workflow & CI

Goal: Understand how code gets from your machine to production.

Branching

We work on GitLab. The main branch is main. For any piece of work, create a feature branch off main with a descriptive name (e.g. fix/chat-unread-count, feat/group-join-button). Make focused, atomic commits — each commit should represent one logical change.

Opening a Merge Request

When your branch is ready, push it and open an MR against main on GitLab. Fill out the MR template completely:

FieldWhat to write
SummaryWhat changed and why — be specific
ScreenshotsRequired for any visual change, using the Before/After table format
Jira IssueLink to the relevant ticket

Code review

Reviews are formal but not adversarial — the goal is shared understanding and code quality. Expect comments on correctness, convention adherence, and whether an existing utility could be reused. Respond to every comment, either with a change or a brief explanation. Mark threads resolved once addressed. Don't merge your own MR without approval.

CI pipeline

Every push triggers the pipeline automatically. All three test-stage jobs must pass before your MR can merge.

Test
lint
typecheck
test
Build
build
Deploy
pages
opensource

Jobs only run when relevant files change — a docs-only commit won't trigger the full test suite. If CI fails, click the failing job in GitLab to see the full log, fix locally with the relevant command, then push.

Pre-push checklist

npm run typecheckNo type errors
npm run lintNo lint violations
npm testAll tests pass
13

Conventions & Gotchas

Goal: Know the project-wide rules that will trip you up if you don't see them coming.
AreaRule
API callsAlways use useApi() — never call fetch or axios directly in a component
API errorsExtend the error union in src/types/api.ts before introducing a new error shape
Blocking actionsUse openModal("CONFIRM", { … }) — not a toast — for any action the user must explicitly resolve
Transient feedbackUse toast.success() / toast.error() for completed actions and network failures only
Disabled vs hiddenUse the disabled prop for unavailable actions — don't conditionally remove the element. Disable while isPending.
New UI primitivesCheck src/components/ui/ first — Button, Modal, Select, Input and more already exist
PerformanceDefault to no useMemo / useCallback / React.memo. Add only when a profiler shows a real problem.
Real-time dataNew Truths and notifications arrive via WebSocket through useUserStream. They patch the React Query cache directly — never manually manage unread counts.
Chat stateShared chat state belongs in ChatContext (src/contexts/chat-context.tsx), not feature-local component state
Groupssrc/features/groups/ (plural) = discovery/list. src/features/group/ (singular) = single group. Always check the user's role before rendering moderation controls.
AnalyticsAlways use useAnalytics() from src/contexts/analytics-context.tsx — never call mixpanel directly

How to Read a Feature End-to-End

A repeatable method for tracing any feature from URL to rendered UI to API call.

When you encounter an unfamiliar screen or need to understand how something works, always start at the route and work inward. Here's the pattern to follow — use the notifications screen as a concrete example.

Step 1 — Find the route

Open src/features/ui/index.tsx and search for the URL path. Every screen in the app is registered here with WrappedRoute.

// Search for the path string, e.g. "/notifications"
<WrappedRoute
  path="/notifications"
  component={Notifications}
  page={BundleContainer}
  requiresAuth
/>

Step 2 — Find the component

The component prop is a lazy-loaded reference. Open src/features/ui/util/async-components.ts and search for the component name to find the actual file path.

// In async-components.ts
export const Notifications = lazy(() =>
  import("@/features/alerts/components/alerts-index")
);

Step 3 — Open the feature component

Navigate to the resolved path (src/features/alerts/components/alerts-index.tsx in this case). Read the component top-down:

Look forWhat it tells you
Hook importsWhich API hooks supply the data for this screen
useEntities / useEntity callsThe endpoint and query key being fetched
Mutation callsWhat actions the user can take and how they hit the API
Context usage (useContext)What shared state this screen depends on
Child componentsWhere the rendering is delegated — follow these if needed

Step 4 — Find the API hook

The hook imported by the component lives in src/api/<feature>/hooks/. Open it and note:

Step 5 — Verify in the Network tab

Load the screen in the browser with DevTools open. Filter the Network tab to Fetch/XHR and confirm the request matches the endpoint you found in code. Check the response shape against the schema. This closes the loop between what the code expects and what the API actually returns.

The same method works in reverse — if you see an API call in the Network tab and want to find the code driving it, search the codebase for the endpoint path string. It will appear in exactly one hook.

✦ Practice
  1. Walk through all five steps for the Home Timeline screen (/home).
  2. Walk through all five steps for the Chats screen (/messages).
  3. Pick any network request you see in the Network tab while browsing the app and trace it back to the hook and schema that own it.

Starter Tasks

Real, self-contained contributions to make in the codebase. Each one is small enough to complete in a single MR.

These tasks are designed to be approachable first contributions — each one touches a real problem in the codebase and follows the full workflow: branch → code → pre-push checklist → MR → review. Ask your team lead to point you to a specific file for each one if you're not sure where to start.

01

Fix a hardcoded user-facing string

  • Find a string literal rendered directly in JSX that isn't wrapped in <FormattedMessage> or intl.formatMessage()
  • Wrap it correctly using defineMessages and the appropriate hook
  • Run npm run i18n to extract it into src/locales/en.json
  • Verify the string still renders correctly in the UI
02

Replace every clsx import with cn()

  • First, read up on the project's cn utility and how it wraps tailwind-merge — unlike clsx, cn de-dupes conflicting Tailwind classes (e.g. px-2 px-4 resolves to px-4)
  • Search the codebase for import.*clsx to find all legacy usages, not just one
  • Replace each import with cn and update the call sites
  • Be careful: because cn collapses conflicting utilities, swapping it in can change the rendered output where call sites were (knowingly or not) relying on clsx keeping both classes. Review each conversion for visual changes
  • Confirm every touched component still renders and behaves correctly
  • Run npm run lint and npm test before opening the MR
03

Convert a conditionally hidden menu item to disabled

  • Find a dropdown or menu where an action is conditionally rendered (&& or ternary returning null) based on some state
  • Replace conditional rendering with the disabled prop so the item is always visible but non-interactive when unavailable
  • Check the component renders correctly in both the enabled and disabled states
04

Write a unit test for an untested utility function

  • Browse src/utils/ and find a function without a corresponding test file
  • Create a __tests__/ file next to it and write tests covering the main cases and at least one edge case
  • Run npm run test:coverage and confirm coverage for that file increased
  • Make sure the test file follows the existing naming and structure conventions in the codebase

Suggested Progression

Work through these phases in order. Each builds on the last.
1

Environment & Language

  • Read AGENTS.md in full — the single most important document in the repo
  • Get the app running locally: cp .env.example .env → set VITE_BACKEND_URLnpm installnpm run dev
  • Complete Modules 1 and 2. Spend real time in DevTools.
2

React & Architecture

  • Complete Modules 3 and 4
  • Read src/components/status.tsx, src/features/home-timeline/, and src/features/compose/ end to end
  • Map where state lives, where data is fetched, and where it's rendered for a single user action (e.g. liking a Truth)
3

Data Layer

  • Complete Modules 5 and 6
  • Trace a full data flow: user action → mutation → API call → cache update → re-render
  • Write a small read-only hook using useEntity against a simple endpoint
4

Styling, i18n, Routing, Testing

  • Complete Modules 7–10
  • Make a small UI change (copy, spacing, color) and verify it in both light and dark mode
  • Run the full test suite and ensure it passes cleanly
5

Git Workflow & First MR

  • Complete Modules 11 and 12
  • Pick a well-scoped bug or small feature. Follow the three-layer rule: schema → hook → component.
  • Run the pre-push checklist, open an MR with a completed template, and go through a full review cycle
>_

Quick Reference: Commands

npm run devStart local dev server at localhost:3000
npm run typecheckTypeScript check (no build output)
npm run lintLinting with oxlint
npm testRun full test suite
npm run i18nExtract / update translation strings
npm run buildProduction build → static/

Files to Bookmark

AGENTS.md
Full project conventions — read this first
src/api/core/index.ts
useEntity, useEntities, useEntityMutation exports
src/api/core/query-keys.ts
All query key definitions
src/schemas/account.ts
Example of a complete Zod schema
src/contexts/auth/useAuth.ts
Auth context hook
src/features/ui/index.tsx
All route declarations
src/features/ui/util/async-components.ts
Lazy-loaded feature registry
src/styles/globals.css
Semantic color token definitions
src/components/ui/
Shared UI primitive library
.gitlab-ci.yml
Full CI pipeline definition
.gitlab/merge_request_templates/
MR templates
.claude/rules/
Feature-specific coding rules

External Resources

Recommended reading alongside the codebase.