Core concepts

Contract layers

The core, application and client contracts compiled from one schema, how the root and its two overlays combine, and why the frontend only ever sees the client layer.

Read as Markdown

A contract is plain data that says which Resources exist, which of their fields can be read, filtered, ordered or written, and which operations are available. Aventara compiles three of them from the same Prisma schema and your configuration, so one API can show a trusted server job more than it shows a browser.

The layers

LayerWho uses itOver HTTP
CoreThe baseline: the schema as the adapter can faithfully perform it, with your root configuration applied. The other two layers start from it.No
ApplicationYour own server-side code: framework.application.*. Trusted code.No
ClientRemote callers: the HTTP protocol and the generated frontend client.Yes, at GET <api>/_contract
AgentAI agents, through an MCP server. It narrows the client layer by default, or the application or core layer it extends.To the MCP server that serves it

Only the client contract is served over the HTTP protocol; the agent contract is what an MCP server builds its tools from. The other two never leave the process. Guards, hooks, pipes and other callbacks are never part of any contract; they run on the server. Field validators are declarative data rather than callbacks, so they are part of the contract of each layer that exposes their field.

The compiled contracts are on the framework instance: framework.contracts.core, .application, .client and .agent. See The agent layer and MCP.

Root and overlays

The root of your configuration is the core layer, and the starting point of the other two. application and client are overlays on it: an application call sees the root plus application, a remote call the root plus client. Each key combines by its own rule (Configuration): exposure only narrows, limits and transactionsStyle override, pipelines and validators add up.

Code
// src/aventara.config.ts
export const aventaraConfig = {
  api: "/api",

  // client: remote callers only
  client: {
    exposed: true,
    exemptions: {
      resources: {
        User: {
          fields: {
            internalNotes: true,                           // not in the client layer at all
            passwordHash: ["select", "filter", "order"],   // write-only for the client
          },
          operations: { delete: true },                    // no deletes
        },
      },
    },
    limits: { maxListLimit: 100 },
  },
} as const;

The same schema, seen through each layer (the User Resource, from framework.contracts):

ApplicationClient
internalNotesreadable and writableabsent: no filter, order, projection or write
passwordHashreadable and writablewrite-only: settable, never selected, filtered or ordered
delete operationsall offerednone
maxListLimit250 (the default)100

An overlay can only take away from what the root exposes. Something only your server code may reach is exposed at the root and hidden in the client overlay, as here. Hiding something at the root hides it from both overlays, and no overlay can bring it back. See Exposure.

What the difference looks like

Server-side code runs through the application layer and can read the field the client layer hides:

Code
const users = await framework.application.User.find.many({
  select: ["id", "email", "internalNotes"],
  limit: 2,
});
// [{ id: 1, email: "ada@example.com", internalNotes: null }, ...]

The same request through the client layer is refused before any query runs:

Code
// the field is not in the client layer's types either, so this needs a cast to compile
await framework.client.User.find.many({ select: ["id", "internalNotes" as never] });
// throws AvValidationError: code "A2004", issue V1005: Field "internalNotes" is not available on Resource "User".

A remote caller gets the same answer over HTTP, and the generated frontend client does not offer the field or the operation at all, so it is a compile error before it is a runtime one:

Code
await avClient.User.find.many({ select: ["passwordHash"] });  // type error; at runtime: A2004 / V1008 "not selectable"
avClient.User.delete.unique;                                  // type error: the operation is not in the client contract

Calling a removed operation by hand over HTTP answers 404 A2002: Operation "delete.unique" is not available on Resource "User".

Limits follow the same rule. With the configuration above, limit: 200 is accepted from application code and refused to a remote caller with A2009 / V1014 (limit exceeds maxListLimit (100)).

Hiding and write-only fields

Exemption, under exposed: trueEffect in that layer
<field>: trueThe field does not exist: no input, output, filter, ordering, identifier or nested projection.
<Resource>: trueThe Resource does not exist. Relations pointing at it must be hidden too, or the framework refuses to start (COMPILER_DANGLING_RELATION).
<field>: ["select", "filter", "order"]Write-only: can be written on create and update, never read back.
operations: { <family>: true } or { <family>: [<variants>] }Switches operations off. Operations can only be switched off, never redefined.

A unique operation needs a complete identifier: hiding every identifier while find.unique stays exposed is refused at startup (COMPILER_MISSING_IDENTIFIER). In the same way, a layer that can still create a Resource cannot hide a field every create must write unless a compute.create stage fills it (COMPILER_HIDDEN_REQUIRED_FIELD). See Exposure for every option, and the Pick style (exposed: false) for exposing only what a frontend calls.

Why this matters for the frontend

The client generator reads only the client contract. An exemption therefore removes things from the generated client's types as well as from what the server answers. Hiding a field in the application layer does not change the client; hiding it in the client layer, or at the root, does. An exemption under client changes the client contract and its hash, so regenerate the client afterwards.

See also