Skip to content

Configuration

Aperture is configured via Spring Boot's application.yml. All properties under aperture.* are Aperture-specific. Standard Spring Boot properties (spring.datasource.*, spring.jpa.*, spring.liquibase.*) work as normal.

Database

yaml
spring:
  datasource:
    url: ${DB_URL:jdbc:postgresql://localhost:5432/aperture}
    username: ${DB_USER:aperture}
    password: ${DB_PASS:password}
    driver-class-name: org.postgresql.Driver
  jpa:
    open-in-view: false
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml

Aperture requires PostgreSQL. spring.liquibase.change-log must point to the root changelog generated by the build.

OneOf request validation (aperture.oneof-validation)

OneOf relationship writes are validated before JSON:API processing. The validator buffers matching JSON:API and Atomic Operations request bodies so it can reject undeclared member types and missing required relationships consistently.

PropertyTypeDefaultDescription
aperture.oneof-validation.max-request-body-bytesinteger1048576Maximum buffered request body size in bytes. Larger validated requests receive HTTP 413. Increase this for intentionally large Atomic Operations batches.

JWT authentication (aperture.auth.jwt)

PropertyTypeDefaultDescription
aperture.auth.jwt.secretstringNoneRequired. HMAC signing key. Use ${APERTURE_JWT_SECRET}. Minimum 32 bytes.
aperture.auth.jwt.issuerstringNoneJWT iss claim value
aperture.auth.jwt.audiencestringNoneJWT aud claim value
aperture.auth.jwt.access-durationISO 8601 durationPT15MAccess token lifetime
aperture.auth.refresh-durationISO 8601 durationPT720HRefresh token lifetime (30 days)
aperture.auth.simple.enabledbooleantrueSet false to disable all simple-auth endpoints (when using a custom CredentialValidator)

Production warning: never use a hardcoded jwt.secret. Always read from an environment variable or secrets manager.

yaml
aperture:
  auth:
    jwt:
      secret: ${APERTURE_JWT_SECRET}
      issuer: my-app
      audience: my-api
      access-duration: PT15M
    refresh-duration: PT720H

CORS (aperture.cors)

PropertyTypeDefaultDescription
aperture.cors.enabledbooleanfalseEnable CORS support
aperture.cors.allowed-originslist of strings[]Exact origins (e.g. https://app.example.com)
aperture.cors.allowed-origin-patternslist of strings[]Wildcard patterns (e.g. https://*.example.com)
aperture.cors.max-agelong (seconds)3600Preflight cache duration

Note: aperture.cors.enabled=true requires at least one origin or pattern. Aperture will refuse to start if CORS is enabled with no origins configured.

yaml
aperture:
  cors:
    enabled: true
    allowed-origins:
      - https://app.example.com

Rate limiting (aperture.rate-limit)

PropertyTypeDefaultDescription
aperture.rate-limit.enabledbooleantrueSet false to skip rate limiting entirely
aperture.rate-limit.backendstringmemoryRate-limit provider backend (memory or valkey)
aperture.rate-limit.ip.capacityinteger100IP bucket maximum burst size
aperture.rate-limit.ip.refillTokensinteger100IP bucket refill amount per window
aperture.rate-limit.ip.windowSecondsinteger60IP bucket refill window
aperture.rate-limit.user.capacityinteger50User bucket maximum burst size
aperture.rate-limit.user.refillTokensinteger50User bucket refill amount per window
aperture.rate-limit.user.windowSecondsinteger60User bucket refill window
aperture.rate-limit.tenant.capacityinteger500Tenant bucket maximum burst size
aperture.rate-limit.tenant.refillTokensinteger500Tenant bucket refill amount per window
aperture.rate-limit.tenant.windowSecondsinteger60Tenant bucket refill window
aperture.rate-limit.valkey.hoststring127.0.0.1Valkey host used by the demo/provider
aperture.rate-limit.valkey.portinteger6379Valkey port
aperture.rate-limit.valkey.libraryNamestringaperture_rate_limitLoaded Lua function library name
aperture.rate-limit.valkey.functionNamestringconsumeFunction name invoked for each rate-limit check
aperture.rate-limit.valkey.keyPrefixstringaperture:rate-limit:Prefix used for bucket keys in Valkey
yaml
aperture:
  rate-limit:
    enabled: true
    backend: memory
    ip:
      capacity: 100
      refillTokens: 100
      windowSeconds: 60
    user:
      capacity: 50
      refillTokens: 50
      windowSeconds: 60
    tenant:
      capacity: 500
      refillTokens: 500
      windowSeconds: 60
    valkey:
      host: localhost
      port: 6379

Field encryption (aperture.encryption.local)

PropertyTypeDefaultDescription
aperture.encryption.local.keystring (Base64)demo keyMust be overridden in production. 32-byte AES-256 key, Base64-encoded.

Generate a key: openssl rand -base64 32

yaml
aperture:
  encryption:
    local:
      key: ${APERTURE_ENCRYPTION_KEY}

Hooks (aperture.hooks)

PropertyTypeDefaultDescription
aperture.hooks.secretstringdefault-hook-secretShared secret sent in X-Hook-Secret header
aperture.hooks.base-urlstring(empty)Override the host portion of all hook URLs
aperture.hooks.timeout.commitduration string5sTimeout for the phase-gated validate (PRECOMMIT) and trigger (POSTCOMMIT) hook calls
aperture.hooks.timeout.asyncduration string30sTimeout for guard (PRESECURITY) hook calls and, hardcoded regardless of phase, every mutate call
aperture.hooks.timeout.connectduration string2sTCP connect timeout
yaml
aperture:
  hooks:
    secret: ${APERTURE_HOOKS_SECRET}
    timeout:
      commit: 10s
      async: 10s
      connect: 3s

MCP (aperture.mcp / spring.ai.mcp)

PropertyTypeDefaultDescription
aperture.mcp.enabledbooleanfalseActivates ApertureMcpAutoConfiguration. With this unset or false, no MCP beans (adapter, tool callback provider, sanitization filter, tools/list filter) are registered at all, regardless of what else is on the classpath
aperture.mcp.tool-list-scopePRINCIPAL | STATICPRINCIPALPRINCIPAL scopes tools/list to the calling principal's roles and principal-only ABAC policies. STATIC restores the pre-phase-2 behavior: every generated tool is listed to every caller regardless of role
spring.ai.mcp.server.protocolstringSTATELESSMCP server mode. Aperture currently supports stateless HTTP
spring.ai.mcp.server.streamable-http.mcp-endpointstring/mcpHTTP endpoint for MCP JSON-RPC requests
spring.ai.mcp.server.namestringNoneMCP server name shown to clients
spring.ai.mcp.server.versionstringNoneMCP server version shown to clients

aperture.mcp.enabled controls Aperture's generated tool registration. Spring AI's spring.ai.mcp.server.* properties control the transport. For the current stateless HTTP transport, direct HTTP clients should send both Content-Type: application/json and Accept: application/json, text/event-stream.

There is no aperture.mcp.transport property. The manifest's spec.mcp.transport records intent and is validated during generation, but the transport the server actually speaks comes from spring.ai.mcp.server.protocol.

yaml
aperture:
  mcp:
    enabled: true
    tool-list-scope: PRINCIPAL # or STATIC

spring:
  ai:
    mcp:
      server:
        protocol: STATELESS
        name: aperture-api
        version: 1.0.0
        streamable-http:
          mcp-endpoint: /mcp

Principal-scoped tools/list

With tool-list-scope: PRINCIPAL (the default), a caller only sees the generated tools their domainRoles and principal-only ABAC policies (expressions referencing only #user, decidable with no row in hand) could plausibly succeed at. For example, a ReadOnly-role API key sees list_*/get_* tools but not create_*/update_*/delete_*. Superadmin principals always see every tool, and a TenantAdmin principal sees every tool of a tenant-scoped (or scopedBy) entity regardless of their own domainRoles, mirroring the TenantAdminCheck bypass Elide already grants those entities.

This is not an authorization boundary. It exists purely so an agent-facing tool list, which functions like a prompt on every conversation, does not tempt a model with tools its principal would never actually succeed at calling. tools/call is still authorized for real by Elide on every invocation, completely independent of what tools/list returned; a bug in this filtering can leak a tool name to a caller who shouldn't see it, but it can never leak data and can never let a call through that Elide would otherwise reject. See McpToolListFilter's javadoc in aperture-simple-mcp.

An ABAC policy expression referencing #record or #input is row-scoped and cannot be decided without a row in hand; such policies never participate in tools/list filtering. If a role also grants the same operation, the tool they apply to stays listed for that role, unfiltered by the row-scoped policy; Elide alone enforces it on the actual call. But if the operation is granted only by a row-scoped policy (no role grants it), the tool has no role to be listed under, so it is hidden from every caller except SuperAdmin and (for tenant-scoped/scopedBy entities) TenantAdmin. McpToolContribution tools have no entity or operation and are never registry-governed. They are always listed, regardless of tool-list-scope.

See Extending MCP for the two ways to add behavior beyond the generated per-entity tools: build-time tool contributions (McpToolContribution) and the runtime McpRequestAdapter bean seam.

GraphQL (elide.graphql)

Aperture's entity dictionary is also queryable over GraphQL, with the same entities, permissions, and manifests over a different transport. This is a native Elide/Spring property, not an aperture.* one, and it isn't manifest-driven: there's no per-entity or per-field GraphQL config, just a single on/off switch for the whole app.

PropertyTypeDefaultDescription
elide.graphql.enabledbooleanfalseEnable the GraphQL endpoint
elide.graphql.pathstring/graphqlBase path; the API version is appended, e.g. /graphql/v1
yaml
elide:
  graphql:
    enabled: true
    path: /graphql

GraphQL requires the path-based API versioning strategy (elide.api-versioning-strategy.path.enabled: true; see API versioning); there is no unversioned /graphql route. Query and mutation field names are the plural JSON:API resource path (customers, invoices, lineitems), not the singular entity name, and results come back as Relay-style connections ({ edges { node { ... } } }).

oneof relationship fields are omitted from the generated GraphQL schema in this release. Their owning entities and ordinary relationships remain available. Use JSON:API, the generated CLI, or generated MCP tools to read and write the selected one-of member.

To disable GraphQL entirely, omit the elide.graphql block or set elide.graphql.enabled: false. This is also the default if the block is absent.

Tracing (management.tracing)

These are standard Spring Boot / Micrometer Tracing properties, not aperture.* ones. Aperture only supplies a default for the sampling probability.

PropertyTypeDefaultDescription
management.tracing.sampling.probabilitydouble0.1Fraction of requests traced. The 0.1 default is set by ApertureDefaultsEnvironmentPostProcessor; override to sample more (e.g. 1.0, as demos/aperture-demo does, for full tracing in a demo/test environment)
management.otlp.tracing.endpointstringNoneOTLP/HTTP trace exporter endpoint, e.g. http://jaeger:4318/v1/traces. Requires the OTel exporter dependency on the classpath
yaml
management:
  tracing:
    sampling:
      probability: 1.0
  otlp:
    tracing:
      endpoint: "http://jaeger:4318/v1/traces"

See Observability for the full tracing/metrics picture, including the spans and metrics Aperture emits once tracing is configured.

Security (aperture.server)

PropertyTypeDefaultDescription
aperture.server.https-onlybooleanfalseReject HTTP requests and redirect to HTTPS

OpenAPI / Swagger

Aperture includes Springdoc integration. Enable the Swagger UI:

yaml
springdoc:
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
  api-docs:
    enabled: true
    path: /v3/api-docs

When enabled, the generated OpenAPI spec includes all entity endpoints, auth and management endpoints, and request/response schemas derived from the manifests.

Bootstrap admin

There is no aperture.bootstrap.* property in Aperture, and the runtime/starter do not read APERTURE_BOOTSTRAP_ADMIN_PASSWORD. Automatic superadmin provisioning is not currently a general Aperture feature, despite what earlier revisions of this page implied. Setting aperture.bootstrap.admin-password in your own application.yml has no effect; nothing binds it.

What exists today is demo-specific: demos/aperture-demo ships its own DemoBootstrap component (active only when aperture.profile=demo) that creates a fixed superadmin@aperture.local user on ApplicationReadyEvent, reading the raw APERTURE_BOOTSTRAP_ADMIN_PASSWORD environment variable directly, not through any aperture.* configuration property. If your own application needs a bootstrap superadmin, write an equivalent ApplicationReadyEvent listener; see demos/aperture-demo/src/main/java/com/itsjool/aperture/demo/DemoBootstrap.java for the pattern.

Complete example (application.yml)

yaml
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USER}
    password: ${DB_PASS}
    driver-class-name: org.postgresql.Driver
  jpa:
    open-in-view: false
  liquibase:
    change-log: classpath:db/changelog/db.changelog-master.xml

aperture:
  cors:
    enabled: true
    allowed-origins:
      - ${FRONTEND_URL}
  auth:
    jwt:
      secret: ${APERTURE_JWT_SECRET}
      issuer: my-app
      audience: my-api
      access-duration: PT15M
    refresh-duration: PT24H
  encryption:
    local:
      key: ${APERTURE_ENCRYPTION_KEY}
  hooks:
    secret: ${APERTURE_HOOKS_SECRET}

APERTURE_BOOTSTRAP_ADMIN_PASSWORD is not read by this configuration. See Bootstrap admin above.