Leave Localhost logoLeave LocalhostDocs
Recipes

Add a Dashboard Page

Add a new authenticated page to the product app, wired into the localized dashboard layout, sidebar, and Convex data.

Add a new authenticated screen to the product app (apps/app). Dashboard pages live under the localized (dashboard) route group and inherit its sidebar, header, and auth guard.

1. Create the route

Add a folder and page.tsx under apps/app/src/app/[locale]/(dashboard)/. For example, a "Reports" page:

apps/app/src/app/[locale]/(dashboard)/reports/page.tsx

The (dashboard) layout already enforces authentication and renders the shell, so the page only needs its own content.

2. Load data the route needs

Prefer a page-level Convex query that returns the route's initial view model, then preload it in the Server Component and pass its Preloaded result to the client component. Read it there with usePreloadedAuthQuery, following the loading UX standard. Include ordinary permission and capability state in that view model when the route needs it.

import { convexAuth } from "@/lib/convex-auth-server";
import { api } from "@leavelocalhost/backend/convex/_generated/api";

export default async function ReportsPage() {
  const preloaded = await convexAuth.preloadAuthQuery(api.reports.pageView, {});
  return <ReportsClient preloaded={preloaded} />;
}

Add a loading.tsx next to the page that renders a skeleton matching the final layout.

Use a preliminary server authorization decision only when a protected query would otherwise throw. The billing settings page first fetches api.permissions.getCurrentPermissions, then preloads its independent plan and billing-state queries in parallel. This is intentionally not a one-query aggregate.

Use a client useQuery only for data that genuinely needs a live, paginated, or interaction-driven subscription. Existing route-level examples are api.users.getDashboardLayoutContext in the dashboard layout and the page-level queries in packages/backend/convex/settings.ts.

Dashboard shell data ownership

api.users.getDashboardLayoutContext is the dashboard shell's single initial read model. It owns the current user display data, organization switcher data, selected-organization permissions and billing summary, super-admin access, and the notification-bell summary. Do not add page-specific records to this query: give each dashboard route its own authorized page view instead. The shell read model composes domain helpers directly, while the individual public queries remain independently authorized for live or route-specific use.

3. Add navigation and labels

4. Gate it (optional)

If the page should be role- or plan-restricted, enforce the permission in its backend query. Use the route's preloaded permission snapshot to conditionally render navigation and controls; the backend check remains the real boundary.

On this page