Architecture

API-centric architecture

Why Prood routes all commerce operations through apps/api and how callers authenticate.

Prood uses an API-centric architecture: all commerce reads and writes go through apps/api, and client applications use @prood/api-client.

Why an API layer?

ConcernDirect import (@prood/commerce)API-centric (apps/api)
AuthEach app implements tenant resolutionSingle resolveCaller() with JWT, API key, session, Host
ContractImplicit function signaturesOpenAPI 3.1 spec → typed client, MCP tools, Agent Auth
AgentsNot possible without custom wiringEvery operationId → agent capability
CachingPer-app cache tag managementAPI controls cache invalidation via revalidateTag
DeploymentApps need DB accessOnly API needs DATABASE_URL for commerce
TestingMock @prood/commerce in each appMock HTTP or test API routes directly

The storefront and dashboard are thin clients. They render UI and call HTTP endpoints. Business logic lives in @prood/commerce, invoked only by the API.

Request flow

Drag to pan · Scroll to zoom

When the storefront uses a BFF

Not every storefront request goes directly to the API. Local BFF routes exist where browser UX requires them:

BFF routeWhy not direct API?
/api/commerce/cart/*Cart ID stored in HTTP-only cookie; BFF reads cookie and forwards to API
/api/commerce/searchClient-side search palette needs a lightweight JSON endpoint
/api/commerce/countriesStatic-ish data for address forms; avoids exposing API URL to client
/api/auth/[...all] (storefront only)Better Auth handler must run on the storefront origin (*.prood.app / custom domains)

Dashboard does not run Better Auth handlers directly. Browser auth calls go to a same-origin BFF at /api/auth/*, which proxies to apps/api so session cookies work when the dashboard and API are on different hostnames (e.g. separate *.vercel.app deploys). With custom domains, AUTH_COOKIE_DOMAIN=.prood.com on the API project also shares cookies across dashboard.prood.com and api.prood.com.

Everything else (products, categories, store info, orders) is fetched server-side in React Server Components via @prood/api-client.

API client configuration

Both storefront and dashboard create an API client that forwards authentication context:

ts
// apps/storefront/lib/commerce-api.ts
import { createCommerceApiClient } from '@prood/api-client'

export function getCommerceApi() {
  return createCommerceApiClient({
    baseUrl: process.env.COMMERCE_API_URL!,
    headers: async () => {
      const h = await headers()
      return {
        cookie: h.get('cookie') ?? '',
        host: h.get('host') ?? '',
      }
    },
  })
}

The API uses the x-storefront-host header for tenant resolution on storefront-scoped routes and the session cookie for dashboard admin routes.

Scopes

The API enforces three scopes:

ScopeAccess
storefrontCatalog reads, cart CRUD, place order, customer orders
staffRead admin data, for every member of a store
adminReads and changes catalog, inventory, orders and store settings, and reads customers. Payment accounts, domains, team and keys stay in the dashboard. Starts on Grow.

Route handlers call requireCaller('storefront'), requireCaller('staff') or requireCaller('admin') to enforce scope.

OpenAPI as the contract

The API generates OpenAPI 3.1 from Zod schemas in apps/api/lib/schemas.ts:

Drag to pan · Scroll to zoom

The docs site syncs this spec at build time (pnpm sync:openapi) and renders it via Fumadocs OpenAPI at /docs/api/*.

@prood/api-client is generated from the same spec, ensuring type safety between server and client.

MCP and Agent Auth

The API also exposes:

SurfacePathPurpose
MCP server/mcpModel Context Protocol tools mirroring REST endpoints
Agent Auth/api/auth/* + /.well-known/agent-configurationAI agent registration, capability grants, JWT access
API keysx-api-key headerProgrammatic access with org + scope metadata

See Agent Auth and MCP server.

Dashboard exception

The dashboard uses @prood/commerce directly for two operations that do not go through the REST API:

  1. Integration config — encrypt/decrypt provider credentials in integration_config
  2. Domain management — CRUD on tenant_domain table

All product, order, and customer management goes through admin-api.ts → API /v1/admin/*.

Using commerce from apps

PatternRecommended approach
import { getProducts } from '@prood/commerce' in an appgetCommerceApi().GET('/products')
Client-side composable for catalog dataServer component + commerce-data.ts fetchers
createCommerce({ adapter })Only in apps/api/lib/commerce-service.ts
Custom commerce endpointsExplicit routes in apps/api/app/v1/