[Project structure](#project-structure)
[Configuration files](#configuration-files)
[Root layout and providers](#root-layout-and-providers)
[Routing](#routing)
[Middleware](#middleware)
[API routes](#api-routes)
[Server Actions](#server-actions)
[Server vs Client Components](#server-vs-client-components)
[State management](#state-management)
[Internationalization](#internationalization)
[UI layer](#ui-layer)

---

!!!info
This is a guide to help you understand the Quable App template built with **Next.js 16** and **React 19**.
!!!

---

## Project structure

The Quable App template is built with **Next.js App Router**, leveraging React Server Components, Server Actions, and file-based routing to provide a modern, performant architecture.

```
.
├── prisma/
│   ├── schema.prisma              # Database schema definition
│   └── migrations/                # Database migration files
├── scripts/
│   └── banner.js                  # Dev server banner script
├── src/
│   ├── app/                       # Next.js App Router
│   │   ├── layout.tsx             # Root layout with providers
│   │   ├── globals.css            # Global styles
│   │   ├── route.ts               # Main API route (GET/POST /)
│   │   ├── error/
│   │   │   └── page.tsx           # Error page
│   │   ├── readme/
│   │   │   └── page.tsx           # Documentation page
│   │   ├── permission/
│   │   │   └── route.ts           # GET /permission endpoint
│   │   ├── install/
│   │   │   └── route.ts           # POST /install endpoint
│   │   └── [session]/             # Dynamic session routes
│   │       ├── (slots)/           # Grouped slot-based pages
│   │       │   ├── page/
│   │       │   │   └── page.tsx   # Default page slot
│   │       │   ├── document.action.single/
│   │       │   │   └── page.tsx   # Single document action slot
│   │       │   ├── document.action.bulk/
│   │       │   │   └── page.tsx   # Bulk document action slot
│   │       │   └── document.page.tab/
│   │       │       └── page.tsx   # Document page tab slot
│   │       └── product/
│   │           └── page.tsx       # Product listing page
│   ├── components/                # Reusable React components
│   │   ├── SessionDetails.tsx     # Session info display (Server Component)
│   │   └── ToastContainer/
│   │       ├── ToastContainer.tsx # Toast notification wrapper
│   │       └── ToastContainer.scss
│   ├── lib/                       # Shared utilities
│   │   ├── prisma.ts              # Prisma client singleton
│   │   ├── session.ts             # Session management utilities
│   │   └── actions/               # Server Actions
│   │       ├── key-value.ts       # Key-value operations
│   │       └── products.ts        # Product fetching
│   ├── providers/
│   │   └── react-query.tsx        # React Query provider
│   ├── proxy.ts                   # Middleware for session validation
│   └── i18n/
│       └── request.ts             # next-intl configuration
├── translations/
│   ├── en.json                    # English translations
│   └── fr.json                    # French translations
├── .env                           # Environment variables
├── .env.dist                      # Environment template
├── next.config.ts                 # Next.js configuration
├── prisma.config.ts               # Prisma configuration
├── tsconfig.json                  # TypeScript configuration
├── eslint.config.mjs              # ESLint configuration
├── .prettierrc                    # Prettier configuration
├── quable.app.yml                 # Quable App manifest
└── package.json                   # Dependencies and scripts
```

---

## Configuration files

### quable.app.yml

The `quable.app.yml` file at the root of the project is the **Quable App manifest**. It declares the application type and the required PIM permissions:

```yaml
application_type: in_app
quable_pim_scope:
  - full_access
```

- `application_type`: Defines how the app integrates with the PIM (`in_app` means it runs inside the PIM interface).
- `quable_pim_scope`: The permissions your app needs from the PIM.

---

### next.config.ts

The Next.js configuration handles CORS, Server Actions origins, and image optimization:

```ts
import type { NextConfig } from "next";
import createNextIntlPlugin from "next-intl/plugin";

const nextConfig: NextConfig = {
  experimental: {
    serverActions: {
      allowedOrigins:
        process.env.NODE_ENV === "development"
          ? (process.env.DEV_ALLOWED_ORIGINS || "").split(",")
          : [],
    },
  },
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "**",
      },
    ],
  },
  async headers() {
    return [
      {
        source: "/:path*",
        headers: [
          { key: "Access-Control-Allow-Origin", value: "*" },
          { key: "Vary", value: "Origin" },
          { key: "Access-Control-Allow-Methods", value: "*" },
          { key: "Access-Control-Allow-Headers", value: "*" },
          { key: "Access-Control-Max-Age", value: "86400" },
        ],
      },
    ];
  },
};

const withNextIntl = createNextIntlPlugin();
export default withNextIntl(nextConfig);
```

Key points:
- **Server Actions CORS**: In development, allowed origins are read from `DEV_ALLOWED_ORIGINS` env variable.
- **Remote images**: All HTTPS image hosts are allowed (needed for PIM asset thumbnails).
- **CORS headers**: Configured on all routes since the app runs inside the Quable PIM iframe.

---

### Environment variables (.env)

```bash
DATABASE_URL="file:./prisma/dev.db"
NEXT_PUBLIC_APP_URL=https://<your-tunnel>.devtunnels.ms
DEV_ALLOWED_ORIGINS=<your-tunnel>.devtunnels.ms
```

- `DATABASE_URL`: SQLite database file path.
- `NEXT_PUBLIC_APP_URL`: The public URL of your app (tunnel URL during development).
- `DEV_ALLOWED_ORIGINS`: Allowed origins for Server Actions during development.

---

### package.json scripts

```json
{
  "scripts": {
    "dev": "node scripts/banner.js && next dev",
    "build": "node scripts/banner.js && next build",
    "start": "node scripts/banner.js && next start",
    "lint": "eslint",
    "format": "prettier --write .",
    "format:check": "prettier --check ."
  }
}
```

---

## Root layout and providers

The root layout (`src/app/layout.tsx`) wraps the entire application with the necessary providers:

```tsx
import type { Metadata } from "next";
import { Poppins } from "next/font/google";
import { NextIntlClientProvider } from "next-intl";
import { ThemeContextProvider } from "@quable/ui/theme";
import ReactQueryProvider from "@/providers/react-query";
import { ToastContainer } from "@/components/ToastContainer/ToastContainer";

import "@quable/ui/theme/index.css";
import "@quable/ui/index.css";
import "./globals.css";

const poppinsFont = Poppins({
  variable: "--font-poppins-sans",
  subsets: ["latin"],
  weight: ["400", "500", "600", "700"],
});

export const metadata: Metadata = {
  title: "Quable App Template",
  description: "Quable App Template with Next.js and @quable/ui",
};

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body className={`${poppinsFont.className} antialiased`}>
        <ReactQueryProvider>
          <ThemeContextProvider>
            <NextIntlClientProvider>{children}</NextIntlClientProvider>
            <ToastContainer />
          </ThemeContextProvider>
        </ReactQueryProvider>
      </body>
    </html>
  );
}
```

**Provider chain (from outer to inner):**

1. **ReactQueryProvider** — Enables data fetching with `useQuery` and `useMutation` hooks.
2. **ThemeContextProvider** — Applies the Quable UI design system theme.
3. **NextIntlClientProvider** — Provides internationalization to client components.
4. **ToastContainer** — Renders toast notifications (react-toastify).

---

## Routing

The template uses the **Next.js App Router** with file-based routing.

### Session-based dynamic routes

All user-facing pages live under `src/app/[session]/`, where `[session]` is a dynamic parameter containing the **session UUID**:

```
src/app/[session]/
├── (slots)/                          # Route group (no URL segment)
│   ├── page/page.tsx                 # → /:sessionId/page
│   ├── document.action.single/page.tsx  # → /:sessionId/document.action.single
│   ├── document.action.bulk/page.tsx    # → /:sessionId/document.action.bulk
│   └── document.page.tab/page.tsx       # → /:sessionId/document.page.tab
└── product/page.tsx                  # → /:sessionId/product
```

### Slot system

Quable PIM uses a **slot system** to integrate apps into different areas of the interface:

| Slot | Description |
|------|-------------|
| `page` | Full page displayed in the AppStore section |
| `document.action.single` | Action triggered on a single document |
| `document.action.bulk` | Action triggered on multiple selected documents |
| `document.page.tab` | Custom tab on a document's page |

When the PIM calls your app, it sends a POST request with the slot name. Your app creates a session and returns the URL for the appropriate slot page.

---

## Middleware

The middleware file (`src/proxy.ts`) intercepts all requests to session-based routes and validates the session:

```ts
import { NextRequest, NextResponse } from "next/server";
import { getSession } from "./lib/session";

export const config = {
  matcher: [
    "/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js|site.webmanifest).*)",
    "/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)",
    "/(api|trpc)(.*)",
  ],
};

export const proxy = async (request: NextRequest) => {
  const { nextUrl } = request;
  const pathname = nextUrl.pathname + (nextUrl.search || "");

  let response: NextResponse;
  if (pathname.match(/^\/[a-zA-Z0-9-]+\/.+$/)) {
    const sessionId = pathname.split("/")[1];
    const session = await getSession(sessionId);
    if (!session) {
      return NextResponse.redirect(new URL("/error", request.url));
    }

    const requestHeaders = new Headers(request.headers);
    requestHeaders.set("x-session-id", sessionId);

    response = NextResponse.next({
      request: { headers: requestHeaders },
    });

    response.cookies.set("locale", session.interfaceLocale);
    response.cookies.set("sessionId", sessionId, {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
    });
  } else {
    response = NextResponse.next();
  }

  return response;
};
```

For routes matching `/:sessionId/*`, the middleware:

1. **Extracts the session ID** from the URL.
2. **Validates the session** exists in the database and hasn't expired (24h TTL).
3. **Redirects to `/error`** if the session is invalid.
4. **Sets cookies**: `locale` (for i18n) and `sessionId` (httpOnly, secure).
5. **Adds `x-session-id` header** to the request for downstream use.

---

## API routes

### Root route (src/app/route.ts)

Handles two flows for session creation:

**GET /** — AppStore flow (user opens the app from the PIM AppStore):

```ts
export async function GET(request: NextRequest): Promise<NextResponse> {
  const { searchParams } = new URL(request.url);
  const applicationType = searchParams.get("applicationType");
  const quableInstanceName = searchParams.get("quableInstanceName");
  // ... extract interfaceLocale, dataLocale, userId

  if (applicationType === "AppStore" && quableInstanceName && ...) {
    const instanceData = await prisma.quableInstance.findUnique({
      where: { name: quableInstanceName },
    });
    const session = await prisma.session.create({
      data: { userId: parseInt(userId, 10), dataLocale, interfaceLocale, quableInstanceId: instanceData.id },
    });
    return NextResponse.redirect(
      new URL(`${process.env.NEXT_PUBLIC_APP_URL}/${session.id}/page`, request.url)
    );
  }
  return NextResponse.json({ message: "Service is running" });
}
```

**POST /** — Slot flow (PIM triggers an app slot):

```ts
export async function POST(request: NextRequest): Promise<NextResponse> {
  const body = await request.json();
  const { searchParams } = new URL(request.url);
  const slot = searchParams.get("slot");
  const { data } = body;

  // Validate instance, create session with documentIds
  const session = await prisma.session.create({
    data: {
      userId: data.userId,
      dataLocale: data.dataLocale,
      interfaceLocale: data.interfaceLocale,
      documentIds: data.documentIds ?? [],
      quableInstanceId: instanceData.id,
    },
  });

  return NextResponse.json({
    url: `${process.env.NEXT_PUBLIC_APP_URL}/${session.id}/${slot}`,
    err: 0,
  });
}
```

---

### Install route (src/app/install/route.ts)

Called when the app is installed on a Quable PIM instance:

```ts
import prisma from "@/lib/prisma";
import { QuablePimClient } from "@quable/quable-pim-js";
import { NextResponse } from "next/server";

export async function POST(request: Request): Promise<NextResponse> {
  const payload = await request.json();
  const { quableInstanceName, quableAuthToken } = payload.data;

  // Validate the token by making a test API call
  const pimClient = new QuablePimClient({
    apiToken: quableAuthToken,
    instanceName: quableInstanceName,
  });
  await pimClient.API.REST.User.getAll({ limit: 1, type: "api" });

  // Store or update the instance credentials
  await prisma.quableInstance.upsert({
    where: { name: quableInstanceName },
    create: { name: quableInstanceName, token: quableAuthToken },
    update: { name: quableInstanceName, token: quableAuthToken },
  });

  return NextResponse.json(
    { message: `QuableApp installed on ${quableInstanceName}.quable.com` },
    { status: 200 }
  );
}
```

---

### Permission route (src/app/permission/route.ts)

Returns the app's required PIM permissions (read from `quable.app.yml`):

```ts
import { NextResponse } from "next/server";
import { readFile } from "fs/promises";
import * as yaml from "js-yaml";
import { join } from "path";

export async function GET(): Promise<NextResponse> {
  const filePath = join(process.cwd(), "quable.app.yml");
  const yamlContent = await readFile(filePath, "utf8");
  const config = yaml.load(yamlContent) as { quable_pim_scope: string[] };
  return NextResponse.json(config.quable_pim_scope);
}
```

---

## Server Actions

Server Actions replace the traditional controller/service pattern. They are functions marked with `"use server"` that run on the server and can be called directly from client components.

Server Actions are located in `src/lib/actions/`:

```ts
// src/lib/actions/products.ts
"use server";

import { QuablePimClient } from "@quable/quable-pim-js";
import prisma from "../prisma";
import { getCurrentSession } from "../session";

export async function getProducts() {
  const session = await getCurrentSession();
  if (!session) throw new Error("Session not found");

  const quableInstance = await prisma.quableInstance.findUniqueOrThrow({
    where: { id: session.quableInstanceId },
  });

  const quablePimClient = new QuablePimClient({
    apiToken: quableInstance.token,
    instanceName: quableInstance.name,
  });

  const products = await quablePimClient.API.REST.Document.getAll({});
  return products.map((product) => ({
    image: product.mainAssetThumbnailUrl,
    name: product.attributes[`${product.documentType.id}_name`]?.[session.dataLocale] || product.id,
    code: product.id,
    productType: product.documentType.id,
  }));
}
```

Server Actions are then called from client components using React Query:

```tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { getProducts } from "@/lib/actions/products";

const { data: products, isLoading } = useQuery({
  queryKey: ["products"],
  queryFn: () => getProducts(),
});
```

---

## Server vs Client Components

In Next.js App Router, components are **Server Components by default**. Add `"use client"` at the top of a file to make it a Client Component.

| Feature | Server Component | Client Component |
|---------|-----------------|-----------------|
| Runs on | Server only | Server + Client |
| Can access | Database, file system, env vars | Browser APIs, hooks |
| Can use | `async/await` directly | `useState`, `useEffect`, `useQuery` |
| Directive | None (default) | `"use client"` |

**Example Server Component** — `SessionDetails.tsx`:

```tsx
import { getCurrentSession } from "@/lib/session";

export default async function SessionDetails() {
  const session = await getCurrentSession();
  if (!session) return <Typography>No active session</Typography>;

  return (
    <TableContainer component={Paper}>
      {/* Render session data directly — no hooks needed */}
    </TableContainer>
  );
}
```

**Example Client Component** — Form with mutations:

```tsx
"use client";
import { useMutation } from "@tanstack/react-query";
import { useForm } from "react-hook-form";

export default function HomePage() {
  const { register, handleSubmit } = useForm();
  const { mutateAsync } = useMutation({
    mutationFn: (data) => addKeyValue(data.key, data.value),
  });
  // ...
}
```

---

## State management

The template uses **React Query** (TanStack Query) for server state management.

The provider is set up in `src/providers/react-query.tsx`:

```tsx
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useState, type ReactNode } from "react";

function ReactQueryProvider({ children }: { children: ReactNode }) {
  const [client] = useState(new QueryClient());
  return <QueryClientProvider client={client}>{children}</QueryClientProvider>;
}

export default ReactQueryProvider;
```

**Fetching data** with `useQuery`:

```tsx
const { data, isLoading } = useQuery({
  queryKey: ["products"],
  queryFn: () => getProducts(),
});
```

**Mutating data** with `useMutation`:

```tsx
const { isPending, mutateAsync } = useMutation({
  mutationFn: (data: { key: string; value: string }) =>
    addKeyValue(data.key, data.value),
  onSuccess: (data) => {
    if (data.keyValue) toast.success("Success!");
  },
});
```

---

## Internationalization

The template supports multiple languages using **next-intl**.

### Configuration (src/i18n/request.ts)

```ts
import { getRequestConfig } from "next-intl/server";
import { cookies } from "next/headers";

const SUPPORTED_LOCALES = ["en", "fr"];
const DEFAULT_LOCALE = "en";

export default getRequestConfig(async () => {
  const cookieStore = await cookies();
  const sessionLocale = cookieStore.get("locale")?.value;
  const locale = sessionLocale?.split("-")[0] ?? DEFAULT_LOCALE;
  const selectedLocale = SUPPORTED_LOCALES.includes(locale) ? locale : DEFAULT_LOCALE;
  const messages = (await import(`../../translations/${selectedLocale}.json`)).default;
  return { locale: selectedLocale, messages };
});
```

The locale is determined from the `locale` cookie, which is set by the middleware based on the user's `interfaceLocale`.

### Translation files

Translations are stored in `translations/en.json` and `translations/fr.json`:

```json
{
  "common": {
    "app_name": "Quable App Template",
    "save": "Save",
    "key_required": "Key is required",
    "value_required": "Value is required"
  },
  "products_page": {
    "title": "Products",
    "columns": {
      "product_name": "Product Name",
      "product_code": "Product Code"
    }
  }
}
```

### Usage in components

**Client components** use the `useTranslations` hook:

```tsx
"use client";
import { useTranslations } from "next-intl";

export default function MyComponent() {
  const t = useTranslations("common");
  return <Button>{t("save")}</Button>;
}
```

**Server Actions** use `getTranslations`:

```ts
"use server";
import { getTranslations } from "next-intl/server";

export async function myAction() {
  const t = await getTranslations("common");
  return { error: t("toasts.error_message") };
}
```

---

## UI layer

The template uses **@quable/ui** (Quable's design system built on top of MUI) combined with **Tailwind CSS** for styling.

### @quable/ui components

```tsx
import { TextField, Button, DataTable, EmptyState, Chip } from "@quable/ui";
```

### MUI components

```tsx
import { Box, Card, CardHeader, CardContent, Typography, Grid } from "@mui/material";
import { OpenInNew } from "@mui/icons-material";
import { GridColDef } from "@mui/x-data-grid-pro";
```

### Styling options

- **Tailwind CSS**: Utility classes (`className="p-4 flex items-center"`)
- **MUI sx prop**: Inline styles (`sx={{ display: "flex", justifyContent: "center" }}`)
- **Sass**: Component-level styles (`.scss` files)
- **CSS variables**: Quable theme tokens (`var(--q-space-500)`)

!!!success
You now have a complete overview of the Quable App template architecture. In the next steps, we'll dive deeper into database interactions, the Quable PIM API, and webhook events.
!!!
