Skip to main content
The frontend is a React SPA with client-side routing, built-in authentication, and pre-built system pages for admin, settings, monitoring, and docs. You write page components and route definitions — the framework handles everything else.

App entry point

Every app extends BaseBrowserApp. For the task tracker, register routes for the dashboard, task list, and task detail pages:
// src/web/App.tsx
import "@synthetiq/app-framework/web/styles";
import "./index.css";
import {
  BaseBrowserApp, configureFramework, initTrpc,
  systemRoutes, ProtectedRoute,
} from "@synthetiq/app-framework/web";
import { APP_ID, BASE_PATH, API_BASE } from "../constants";
import { DashboardPage } from "./pages/Dashboard";
import { TasksPage } from "./pages/Tasks";
import { TaskDetailPage } from "./pages/TaskDetail";

configureFramework({ basePath: BASE_PATH, appId: APP_ID, apiBase: API_BASE });
initTrpc();

class App extends BaseBrowserApp {
  constructor() {
    super({ basePath: BASE_PATH, displayName: "Task Tracker" });

    this.useRouter({
      ...systemRoutes(BASE_PATH, "Task Tracker"),
      "/": {
        title: "Dashboard",
        description: "Overview of tasks and recent activity.",
        metaTitle: "Dashboard | Task Tracker",
        metaDescription: "View your task dashboard",
        render: () => (
          <ProtectedRoute>
            <DashboardPage />
          </ProtectedRoute>
        ),
      },
      "/tasks": {
        title: "Tasks",
        description: "List of all tasks with filtering by status and priority.",
        metaTitle: "Tasks | Task Tracker",
        metaDescription: "View and manage your tasks",
        render: () => (
          <ProtectedRoute>
            <TasksPage />
          </ProtectedRoute>
        ),
      },
      "/tasks/:id": {
        title: "Task Detail",
        description: "Detailed view of a single task with edit and status actions.",
        metaTitle: "Task | Task Tracker",
        metaDescription: "View task details",
        render: (routeParams) => (
          <ProtectedRoute>
            <TaskDetailPage id={routeParams?.id ?? ""} />
          </ProtectedRoute>
        ),
      },
    });
  }
}

new App();
Routes wrapped in ProtectedRoute require authentication — unauthenticated users are redirected to /login. The title and description fields are used by the build pipeline to generate pages.json, which gives the AI agent awareness of the app’s page structure.

Page components

The routes above reference TasksPage and TaskDetailPage — these are page components that live in src/web/pages/. Create the tasks list page:
// src/web/pages/Tasks.tsx
import { useAuth } from '@synthetiq/app-framework/web';
import { trpcReact } from '../trpc';

export function TasksPage() {
  const { scopes } = useAuth();
  const isAdmin = scopes.includes('tasks:viewAll');

  const { data: tasks, isLoading } = isAdmin
    ? trpcReact.tasks.getAllTasks.useQuery()
    : trpcReact.tasks.getMyTasks.useQuery();

  if (isLoading) return <div>Loading...</div>;

  return (
    <div>
      <h1>Tasks</h1>
      {tasks?.map(task => (
        <a key={task.id} href={`/tasks/${task.id}`}>
          {task.title} — {task.status}
        </a>
      ))}
    </div>
  );
}
And the task detail page:
// src/web/pages/TaskDetail.tsx
import { useAuth } from '@synthetiq/app-framework/web';
import { trpcReact } from '../trpc';

export function TaskDetailPage({ id }: { id: string }) {
  const { scopes } = useAuth();
  const isAdmin = scopes.includes('tasks:viewAll');

  const { data: tasks } = isAdmin
    ? trpcReact.tasks.getAllTasks.useQuery()
    : trpcReact.tasks.getMyTasks.useQuery();

  const utils = trpcReact.useUtils();
  const task = tasks?.find(t => t.id === id);

  const updateMutation = trpcReact.tasks.updateTask.useMutation({
    onSuccess: () => {
      utils.tasks.getMyTasks.invalidate();
      utils.tasks.getAllTasks.invalidate();
    },
  });

  if (!task) return <div>Task not found</div>;

  return (
    <div>
      <h1>{task.title}</h1>
      <p>Status: {task.status}</p>
      <p>Priority: {task.priority}</p>
      <button onClick={() => updateMutation.mutate({ id, status: 'completed' })}>
        Mark Complete
      </button>
    </div>
  );
}
Page components live in src/web/pages/ and correspond to routes registered in App.tsx. Reusable UI elements shared across pages (headers, cards, layouts) go in src/web/components/ instead.

Authentication

Use the useAuth hook to access user info and scopes:
import { useAuth } from "@synthetiq/app-framework/web";

function Header() {
  const { user } = useAuth();
  return <span>Welcome, {user?.name || user?.email}</span>;
}

UserMenu

Include the UserMenu component in your header. It provides the user avatar, organization switcher, admin links, and sign out:
import { UserMenu } from "@synthetiq/app-framework/web";

function Header() {
  return (
    <header className="flex h-14 items-center justify-between border-b border-border px-4">
      <span className="font-medium">Task Tracker</span>
      <UserMenu />
    </header>
  );
}

System pages

systemRoutes() provides pre-built pages for login, admin, settings, monitoring, and docs — no app code required. For the full list, see the Routing reference. For RouteConfig properties, dynamic routes, programmatic navigation, and scope-based route protection, see the Routing reference.