# Add a protected app page Add a product-review page to an embedded app using the Admin client returned by authentication. The page will list product names and statuses without exposing credentials to the browser. ## Before you begin Complete [Build an embedded app](/guides/apps/getting-started) and ensure the active app version permits product reads. Keep the template's root provider and authentication boundary integration. ## 1. Add the route module Create `app/routes/product-review.tsx` with this module: Protected route ```tsx import { authenticate } from "~/thor.server"; import type { Route } from "./+types/product-review"; export async function loader({ request }: Route.LoaderArgs) { const { admin } = await authenticate.admin(request); const response = await admin.graphql(/* GraphQL */ ` query AppProductReview { products(first: 20) { nodes { id name status } pageInfo { hasNextPage endCursor } } } `); const result = await response.json(); if (!response.ok || result.errors?.length || !result.data) { throw new Response("Unable to load products. Please try again.", { status: 502 }); } return { products: result.data.products.nodes }; } export default function ProductReview({ loaderData }: Route.ComponentProps) { return (

Product review

{loaderData.products.length === 0 ?

No products yet.

: ( )}
); } ``` Authentication happens before the Admin call and is not swallowed by a catch-all handler. An empty catalog is a successful state; a failed query is not converted into an empty table. The example shows the first 20 products. Add [pagination](/concepts/pagination) before presenting it as the full catalog. ## 2. Register the route and generate types Add `route("product-review", "routes/product-review.tsx")` to the existing route list in `app/routes.ts`. Then run: Verify the route ```bash pnpm codegen pnpm typecheck pnpm build ``` Code generation picks up the named GraphQL document and produces its types. Do not hand-edit the generated files under `app/types`. ## 3. Add an action when writes are needed Keep writes out of the loader. A protected action must call `authenticate.admin(request)` once, validate submitted values, and inspect the mutation's typed business errors. Use the authenticated project context instead of accepting a different project or token in form data. Request only the extra scope the write needs and update the app's published scope configuration. ## Verify in the dashboard Open the route through the installed app and confirm product names and statuses. Test an empty catalog, missing product-read permission, and an expired session. Keep the operator's retry path and sanitized error handling consistent with the existing app boundary. See [App authentication and sessions](/guides/apps/authentication).