Skip to main content

RouteConfig

PropertyTypeDescription
titlestringShort page title (used in docs and AI agent context)
descriptionstringWhat the page contains and what users can do on it
metaTitlestringBrowser tab title
metaDescriptionstringMeta description tag
render(routeParams?, searchParams?) => JSX.ElementRender function

Route protection

// Authentication required
<ProtectedRoute>
  <MyPage />
</ProtectedRoute>

// Scope-based (user must have at least one of the listed scopes)
<ProtectedRoute requiredScopes={["users:viewAll", "roles:view"]}>
  <AdminPage />
</ProtectedRoute>
Frontend route protection is a UX convenience — the actual security enforcement happens on the backend via scopedProcedure and database-level RLS.

Dynamic routes

"/tasks/:id": {
  render: (routeParams) => <TaskPage id={routeParams?.id ?? ""} />,
},

"/projects/:owner/:repo": {
  render: (routeParams, searchParams) => (
    <ProjectPage owner={routeParams?.owner ?? ""} repo={routeParams?.repo ?? ""} />
  ),
},

System routes

systemRoutes(basePath, displayName) provides:
RoutePageProtection
/loginLogin pagePublic
/admin/*Admin pages (users, roles, orgs, scopes, services, monitoring, logs)Admin scopes
/settings/*User settings (OAuth apps)Authenticated
/docs/*API docs, data models, workflowsAuthenticated
Do not redefine /login when using systemRoutes(). Standard <a> tags with relative paths are automatically intercepted for client-side navigation:
<a href="/tasks">Tasks</a>
<a href="/tasks/123">Task 123</a>

Programmatic navigation

import { useApp } from "@synthetiq/app-framework/web";

function MyComponent() {
  const { basePath } = useApp();

  const navigateTo = (path: string) => {
    window.history.pushState({}, "", basePath.concat(path));
    window.dispatchEvent(new PopStateEvent("popstate"));
  };

  return <button onClick={() => navigateTo("/settings")}>Settings</button>;
}

Active navigation state

import { useApp } from "@synthetiq/app-framework/web";

function Layout({ children }: { children: React.ReactNode }) {
  const { basePath } = useApp();
  const currentPath = window.location.pathname.replace(basePath, '') || '/';

  return (
    <div className="flex">
      <nav>
        <a href="/" className={currentPath === '/' ? 'bg-background-secondary' : ''}>Dashboard</a>
        <a href="/tasks" className={currentPath.startsWith('/tasks') ? 'bg-background-secondary' : ''}>Tasks</a>
      </nav>
      <main>{children}</main>
    </div>
  );
}

useAuth

import { useAuth } from "@synthetiq/app-framework/web";

const {
  user,            // SynthetiqUser | null
  scopes,          // string[] — user's permission scopes
  loading,         // boolean — auth initialization in progress
  isAuthenticated, // boolean
  hasAppAccess,    // boolean — whether user can access this app
  currentOrgId,    // string | null — selected organization
  organizations,   // { id: string; name: string }[] — user's orgs
  setCurrentOrgId, // (orgId: string) => void — used by built-in UserMenu org switcher
  login,           // () => Promise<void>
  logout,          // () => void
} = useAuth();

User properties

PropertyTypeDescription
user.id / user.substringUser ID (same value)
user.emailstring | undefinedEmail address
user.email_verifiedboolean | undefinedEmail verification status
user.namestring | undefinedDisplay name
user.picturestring | undefinedProfile picture URL

Conditional rendering with scopes

const { scopes } = useAuth();
const hasViewAll = scopes.includes('tasks:viewAll');

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