Skip to content

Core Concepts

The manifest-driven model

YAML manifests are the single source of truth for everything Aperture generates. Every entity class, permission annotation, database migration, and hook wiring derives from the manifests. You never edit generated code; you edit the manifest and rebuild.

Manifests live under the manifests/ directory of your project:

manifests/
  domain/
    billing/
      invoice.yaml
      line-item.yaml
    identity/
      customer.yaml
  aperture/
    config.yaml
  roles/
    abac-policies.yaml
    roles.yaml
  migrations/
    backfill-phone-number.yaml

Manifest kinds

For a by-example tour of every kind and field-level feature, including scopedBy, tenantScoped, relationships, hooks, permissions, and MCP, see Manifest Authoring.

KindPurpose
EntityDeclares a domain entity with fields, permissions, policies, and hooks
ApertureConfigSets tenancy mode, default roles, and MCP configuration
ApiVersionConfigDeclares API versions and their status (ACTIVE or SUNSET)
AbacPolicyDefines an attribute-based access control rule by name
RoleDefinitionDeclares the named domain roles available in the system
PrincipalAttributeDefinitionDeclares security attribute keys for ABAC and API key delegation
MigrationDefines a manual SQL migration with rollback SQL

A complete entity manifest

The billing demo's invoice.yaml shows all the sections:

yaml
apiVersion: aperture.itsjool.com/v1
kind: Entity
metadata:
  name: Invoice
spec:
  description: "A billing invoice linking a customer to line items and payments"
  tenantScoped: true           # every row gets aperture_tenant_id
  optimisticLocking: false     # set true to require ETag/If-Match
  softDelete: false            # set true for soft-delete (deleted_at column)
  fields:
    amount:
      type: decimal
      required: true
    status:
      type: string
      enum: [DRAFT, ISSUED, PAID]
    customer:
      type: ref
      target: Customer
      relation: ManyToOne
      required: true
    lineItems:
      type: ref
      target: LineItem
      relation: OneToMany
      mappedBy: invoice
  permissions:
    Admin:      [read, delete]
    Accountant: [create, read, update]
    Viewer:     [read]
  policies:
    FinanceTeamOnly: [read, update]
    EuRegionOnly:    [read, update]
  hooks:
    ValidateInvoice:
      type: validate
      on: [create, update]
      url: http://hook-service:8080/hooks/validate-invoice

Field types

TypeJava typeDB column
stringStringVARCHAR(255)
decimalBigDecimalDECIMAL(19,2)
integerIntegerINTEGER
booleanBooleanBOOLEAN
uuidUUIDUUID
datetimeLocalDateTimeTIMESTAMPTZ
ref@ManyToOne / @OneToManyFK column (ManyToOne only)
oneofmarker interface association{field}_type + {field}_id

oneof fields point at a named OneOf manifest, not at a single entity. They are for cases where one field may reference one of a closed set of entity types. The generated database stores the concrete resource type and ID in separate columns; JSON:API clients send the concrete member type in the relationship data, for example { "type": "products", "id": "…" }.

The build pipeline

When you run mvn verify, the aperture-maven-plugin runs during the generate-sources phase:

  1. Parse: reads all YAML files from manifests/ into the domain model (EntityDef, OneOfDef, FieldDef, HookDef, etc.)
  2. Diff: reads snapshot JSON from .aperture.lock/ and computes a diff against the current manifests
  3. Breaking change check: if fields were removed or types changed without an API version bump, the build fails
  4. Code generation: writes Java source files to target/generated-sources/aperture/
  5. Changeset generation: writes Liquibase XML to target/generated-resources/db/changelog/
  6. Lock file update: writes updated JSON snapshots to .aperture.lock/

Spring Boot then compiles the generated Java, and Liquibase runs the changesets on startup.

What gets generated

For each entity, the code generator produces:

  • A Spring @Entity class (one per API version if versioning is configured)
  • Elide controller registration
  • Permission annotations (@ReadPermission, @CreatePermission, @UpdatePermission, @DeletePermission)
  • ABAC policy check classes for any policies: declared
  • MCP tool class (if mcp.enabled: true in Aperture config)

Optionally (when <cli><enabled>true</enabled></cli> is set in the plugin config):

  • A standalone Maven project at target/generated-cli/aperture-cli/ containing a Picocli-based CLI with CRUD commands for every entity, auth commands, and a native image build profile

The generation system uses an ApertureGenerationTarget SPI internally. Custom generation targets are not yet configurable via the plugin, but this is planned for a future release.

CLI generation is extensible through <cli><extensions>. Put an implementation of CliAuthExtension (custom auth commands) and/or CliCommandContribution (additional top-level commands) on the Maven plugin classpath and list its class name in the plugin configuration. See Generated CLI → Custom auth extensions and Custom commands.

MCP generation has the same kind of seam via <mcp><extensions> and McpToolContribution. See Extending MCP.

For the changeset generator:

  • aperture-schema.xml: full DDL (used when deploying to a fresh database)
  • aperture-incremental.xml: diff-only delta (used for rolling upgrades to existing databases)
  • Manual migration files are copied from manifests/migrations/ and included in the root changelog

The lock files

.aperture.lock/ contains one JSON file per entity, committed to the repository:

.aperture.lock/
  1-Invoice.json
  1-Customer.json
  1-LineItem.json
  1-domain-model.json
  ...

These are JSON snapshots of each entity's state at the time of the last successful build. The 1- prefix is the API version number. 1-domain-model.json records model-level constructs such as OneOf declarations so the diff engine can detect member-set changes separately from individual entity field changes.

Example (1-Invoice.json):

json
{
  "name": "Invoice",
  "tenantScoped": true,
  "fields": {
    "amount": { "type": "decimal", "required": true, ... },
    "status":  { "type": "string",  "required": false, ... },
    "customer": { "type": "ref", "relation": "ManyToOne", "target": "Customer", ... }
  },
  "permissions": {
    "Admin":      ["read", "delete"],
    "Accountant": ["create", "read", "update"]
  }
}

Commit lock files alongside manifest changes. They are the schema migration record that the diff engine uses to understand the current database shape. Deleting them forces Aperture to treat every entity as new and regenerate all changesets from scratch.

Generated code is never hand-edited

The target/generated-sources/aperture/ directory is regenerated on every build. Any changes made directly to files there are overwritten. The correct workflow is:

  1. Edit the manifest
  2. Run mvn verify
  3. The generated code reflects the new manifest
  4. Commit: the manifest change + the updated lock files

If you need logic that goes beyond what manifests can express, use hooks to attach your own code over HTTP at the right point in the request lifecycle.