Genairus logoGenAI-R-Us
Genairus logoGenAI-R-Us

Capacitor Data Modeling Language (DML)

Version 1.0

Introduction

Capacitor is a high-level, declarative Data Modeling Language (DML) designed to decouple data intent from physical storage. It addresses the fundamental "Impedance Mismatch" between application-level object modeling and database-level storage mechanics.

Built with an AI-first philosophy, Capacitor's syntax is optimized for generation by LLM coding agents while remaining perfectly human-readable and writable. The language uses clear, unambiguous structures that both humans and AI can easily understand, making it ideal for modern development workflows where AI assists in code generation.

Capacitor allows architects to model data based on Access Patterns and Semantic Meaning rather than the specific constraints of a database engine. By defining the "What" and "Why" of data, Capacitor can be "woven" into any target database—Relational (SQL), Document (NoSQL), or Key-Value store—automatically generating the optimal physical schemas and access code for each.

The Generator Philosophy

Capacitor follows the principle of universal specification with target-specific implementation. The language specification defines the complete semantic model without database constraints. Generators handle the reality of each database through:

  • Native implementations where the database supports the feature
  • Shim implementations where application code provides the functionality
  • Generation-time warnings when features require shims
  • Generation-time errors when features cannot be implemented

This approach ensures your data model remains portable and future-proof while giving you full transparency about what happens under the hood.


Language Philosophy

Capacitor is built on five core principles:

AI-First Design

Capacitor is specifically designed to be easily generated by LLM coding agents while remaining perfectly human-readable and writable. The syntax prioritizes:

  • Unambiguous structure that LLMs can reliably produce without hallucination
  • Clear semantic intent that reduces AI errors and improves code quality
  • Self-documenting patterns that are obvious to both humans and AI
  • Consistent formatting that makes code generation predictable and maintainable

This AI-first approach doesn't compromise human usability—in fact, the clarity required for AI generation makes the language more intuitive for developers as well.

Contractual Integrity

The data model serves as a formal contract between the storage layer, the application code, and downstream analytics. It ensures that the meaning of data is preserved across stack boundaries.

Access-Pattern Modeling

Unlike standard DDL, Capacitor models are driven by how the application interacts with data. It treats indexes, partitions, and relationships as logical requirements rather than physical implementation details.

Infrastructure Independence

By abstracting the data model, Capacitor eliminates vendor lock-in. A model defined in Capacitor can migrate from a relational structure to a distributed NoSQL cluster without rewriting the application's data access layer.

Progressive Enhancement

Features are defined semantically in Capacitor. Generators implement them using native database capabilities when available, or generate shims when needed. You get clear feedback at generation time about implementation trade-offs.


Namespace and Module System

Overview

Every Capacitor model file must begin with a namespace declaration. Namespaces provide module scoping, prevent naming collisions, and enable dependency management across project boundaries. They are essential for building reusable type libraries and generating properly packaged code for target languages.

Namespace Declaration

Every .capacitor file MUST begin with a namespace declaration using reverse domain notation:

namespace com.acme.data.users

Namespace rules:

  • Minimum two segments required (com.acme, not just acme)
  • Use reverse domain notation (like Java packages, Smithy namespaces)
  • Multiple files MAY share the same namespace (types are merged at validation time)
  • The namespace declaration is authoritative (directory structure is convention, not requirement)

Namespace mapping to target languages:

LanguageNamespace MappingExample
JavaPackagecom.acme.data.users → package com.acme.data.users
TypeScriptnpm scope + modulecom.acme.data.users@acme/data-users
GoModule pathcom.acme.data.usersgithub.com/acme/data/users
C#Namespacecom.acme.data.usersAcme.Data.Users
PythonModulecom.acme.data.usersacme.data.users
RustCratecom.acme.data.usersacme_data_users
SwiftModulecom.acme.data.usersAcmeDataUsers
ScalaPackagecom.acme.data.userscom.acme.data.users

Generator Note: The build system (capacitor-build.json) allows configuring custom namespace-to-package mappings per language. See Build Configuration for details.

Imports

Types from other namespaces are brought into scope with use:

namespace com.acme.data.orders

use com.acme.data.users#User
use com.acme.data.users#UserStatus
use com.acme.data.common#Address
use com.acme.data.common#AuditFields

Import rules:

  • # separates namespace from type name (matches Smithy convention)
  • Each type must be imported explicitly
  • Wildcard imports are NOT supported (explicit is better than implicit for AI generation and human readability)
  • Circular imports between namespaces are a validation error

Grouped Imports

Multiple types from the same namespace can be imported together:

namespace com.acme.data.orders

use com.acme.data.common#{ Currency, Money, Address, ContactInfo }
use com.acme.data.users#{ User, UserStatus, UserPreferences }

Aliased Imports

When type names collide, use aliased imports:

namespace com.acme.data.logistics

use com.acme.data.shipping#Address as ShippingAddress
use com.acme.data.billing#Address as BillingAddress

entity Shipment {
    @identity
    shipmentId: UUID,

    origin: ShippingAddress,
    destination: ShippingAddress,
    billingAddress: BillingAddress
}

Prelude Namespace

Capacitor provides a built-in prelude namespace (capacitor.prelude) that is automatically imported into every file. This includes all scalar types, semantic types, and collection types:

Prelude types (no explicit import needed):

  • Scalars: String, Integer, Long, Float, Double, Decimal, Boolean, Blob, Document
  • Semantic: Email, UUID, Timestamp, Date, Time, Currency, Location, URL, Phone
  • Collections: List<T>, Set<T>, Map<K, V>

Custom types from your models or external dependencies ALWAYS require explicit imports.

File and Directory Convention

While not enforced, the recommended convention is to mirror the namespace structure in your directory layout:

models/
├── com/
│   └── acme/
│       └── data/
│           ├── common/
│           │   ├── types.capacitor        // namespace com.acme.data.common
│           │   └── audit.capacitor        // namespace com.acme.data.common
│           ├── users/
│           │   ├── user.capacitor         // namespace com.acme.data.users
│           │   └── preferences.capacitor  // namespace com.acme.data.users
│           └── orders/
│               ├── order.capacitor        // namespace com.acme.data.orders
│               └── payment.capacitor      // namespace com.acme.data.orders

Note: Multiple files can share a namespace. Types from all files with the same namespace are merged at validation time into a single logical namespace.

External Dependencies

Projects can depend on published Capacitor model packages. Dependencies are declared in capacitor-build.json:

{
  "dependencies": {
    "com.acme.data.common": {
      "version": "1.2.0",
      "repository": "https://repo.acme.com/capacitor"
    },
    "org.capacitor.stdlib.geo": {
      "version": "1.0.0"
    }
  }
}

The dependency resolver:

  • Fetches published .capacitor model packages from configured repositories
  • Makes their types available for use imports
  • Validates version compatibility
  • Supports semantic versioning with range constraints (^1.2.0, ~1.2.0, >=1.0.0 <2.0.0)

Example using an external dependency:

namespace com.acme.data.stores

// Import from external dependency
use org.capacitor.stdlib.geo#GeoPoint
use org.capacitor.stdlib.geo#GeoPolygon

entity Store {
    @identity
    storeId: UUID,

    name: String,

    // Use imported type from external package
    location: GeoPoint,

    // Delivery zone boundary
    deliveryArea: GeoPolygon?
}

Best Practices

  • Use reverse domain notation: Ensures global uniqueness across organizations
  • Organize by domain/bounded context: Group related entities in the same namespace
  • Keep namespaces shallow: 3-4 segments is ideal (com.acme.data.users), avoid deep nesting
  • Explicit imports: Always import types explicitly, never rely on wildcards
  • Namespace per service: Each microservice's data model should have its own root namespace
  • Version external dependencies: Use semantic versioning constraints in dependencies

Generator Note: Namespaces are critical for multi-project codebases and library publishing. Generators use namespace information to produce properly packaged artifacts (JARs, npm packages, Python wheels, etc.) with correct module structures and dependencies.


Core Building Blocks

Entities

Entities are the primary units of Capacitor. They represent stateful objects that persist over time and maintain a unique identity.

entity User {
    @identity
    userId: UUID,

    username: String,
    email: Email,

    createdAt: Timestamp
}

Shapes

Shapes are reusable structures that do not have their own independent identity. They are used to group related fields within an Entity to ensure consistency across the model.

shape Address {
    street: String,
    city: String,
    state: String,
    zipCode: String,
    country: String
}

entity Warehouse {
    @identity
    id: UUID,

    name: String,
    location: Address
}

Enums

Enums define a fixed set of valid values for a field, providing type safety and semantic clarity.

enum OrderStatus {
    PENDING,
    CONFIRMED,
    PROCESSING,
    SHIPPED,
    DELIVERED,
    CANCELLED,
    REFUNDED
}

entity Order {
    @identity
    orderId: UUID,

    status: OrderStatus,
    total: Decimal
}

Generator Note: Implementation varies by database. Native enums in SQL, validation rules in NoSQL. See Generator Guide.

Traits

Traits are metadata modifiers prefixed with @. They allow architects to attach "capabilities" to data fields—such as encryption, indexing, or searchability—without needing to know the underlying database's syntax for those features.


Type System

Capacitor provides a Standard Library (LSL) of semantic types that map to the most efficient underlying platform types while providing built-in validation.

Prelude Types: All scalar types, semantic types, and collection types documented in this section are part of the capacitor.prelude namespace and are automatically imported into every .capacitor file. You never need to explicitly import these built-in types. Custom types (entities, shapes, enums) from your models or external dependencies always require explicit use imports.

Scalar Types

Capacitor TypeDescription
StringUTF-8 encoded text
Integer32-bit signed integer
Long64-bit signed integer
Float32-bit floating point
Double64-bit floating point
DecimalArbitrary precision decimal (for currency, etc.)
BooleanLogical true/false
BlobRaw binary data
DocumentSemi-structured dynamic data (JSON)

Semantic Types

These types include built-in validation rules and formatting requirements:

  • Email: Validated string matching RFC 5322
  • UUID: 128-bit unique identifier (v4 by default)
  • Timestamp: High-precision time (UTC, ISO 8601)
  • Date: Calendar date without time component
  • Time: Time of day without date
  • Currency: Composite of Decimal amount and ISO 4217 currency code
  • Location: Geographic coordinates (Latitude/Longitude)
  • URL: Validated URL string
  • Phone: E.164 formatted phone number

Collection Types

  • List<T>: Ordered collection with duplicates allowed
  • Set<T>: Unordered collection of unique elements
  • Map<K, V>: Key-value pairs with unique keys

Nullable Types

By default, all fields are required (not null). Use ? suffix to make a field optional.

entity Profile {
    @identity
    id: UUID,

    name: String,           // Required
    bio: String?,           // Optional
    avatarUrl: URL?         // Optional
}

Identity and Access Modeling

Capacitor treats identity as an abstract "Key Shape." The generator translates these logical keys into the optimal physical structure.

Primary Keys

Single-field identity:

entity User {
    @identity
    userId: UUID,

    username: String
}

Compound identity:

entity TimeSeriesData {
    @identity @partition
    deviceId: UUID,

    @identity @sort
    timestamp: Timestamp,

    value: Double
}

Key Traits

  • @identity: Marks a field as part of the unique identifier
  • @partition: Defines the horizontal scaling boundary (sharding key)
  • @sort: Defines the physical ordering within a partition

Access Patterns (Indexes)

Instead of manual index management, developers describe the Access Pattern required by the business logic.

Global access (query by non-identity field):

entity Product {
    @identity
    sku: String,

    @access(name: "ByCategory", type: "global")
    category: String,

    name: String,
    price: Decimal
}

Local access (alternate ordering within partition):

entity Task {
    @identity @partition
    projectId: UUID,

    @identity @sort
    taskId: UUID,

    @access(name: "ByPriority", type: "local", sort: "priority")
    priority: Integer,

    title: String,
    status: String
}

Composite access (multi-field index):

entity Customer {
    @identity
    id: UUID,

    @access(name: "ByLocationAndName", type: "global")
    @composite(fields: ["state", "city", "lastName"])
    searchKey: String,

    firstName: String,
    lastName: String,
    city: String,
    state: String
}

Sparse/Partial access (conditional index):

entity Order {
    @identity
    orderId: UUID,

    // Only indexes orders where isFlagged is true
    @access(name: "FlaggedOrders", type: "global")
    @sparse
    isFlagged: Boolean?,

    amount: Decimal
}

Full-text search:

entity Article {
    @identity
    id: UUID,

    @access(name: "SearchContent", type: "fulltext")
    @fulltext(language: "english")
    content: String,

    title: String,
    author: String
}

Generator Note: Full-text implementation varies. PostgreSQL uses native full-text search, MongoDB uses text indexes, DynamoDB requires external search service. See Generator Guide.

Best Practices

  • Limit Global Access: Every global access pattern adds write latency
  • Prefer Local Access: Query within partition boundaries for maximum performance
  • Semantic Naming: Name access patterns after business intent (e.g., ByEmail) not implementation (e.g., Index1)

Constraints and Validation

Capacitor provides semantic constraints that are enforced at the appropriate layer based on the target database.

Unique Constraints

entity User {
    @identity
    userId: UUID,

    @unique
    username: String,

    @unique
    email: Email
}

Default Values

entity Product {
    @identity
    sku: String,

    name: String,

    @default(value: "0")
    inventory: Integer,

    @default(value: "true")
    isActive: Boolean,

    @default(value: "now()")
    createdAt: Timestamp
}

Check Constraints

entity Product {
    @identity
    sku: String,

    @check(condition: "price > 0")
    price: Decimal,

    @check(condition: "inventory >= 0")
    inventory: Integer
}

Length Constraints

entity Post {
    @identity
    id: UUID,

    @length(min: 3, max: 100)
    title: String,

    @length(max: 5000)
    content: String
}

Range Constraints

entity Rating {
    @identity
    id: UUID,

    @range(min: 1, max: 5)
    stars: Integer,

    @range(min: 0.0, max: 100.0)
    confidenceScore: Double
}

Pattern Constraints

entity Product {
    @identity
    id: UUID,

    @pattern(regex: "^[A-Z]{2}\\d{6}$")
    sku: String  // Example: AB123456
}

Generator Note: Check constraints, length, and range validations are implemented natively in SQL databases. For NoSQL databases, they are enforced in the generated SDK. See Generator Guide.


Relationships

Capacitor uses the @link trait to define how entities relate. The implementation is handled by generators based on the target database.

One-to-One Relationships

entity Passport {
    @identity
    passportNumber: String,

    expiryDate: Date,
    issuingCountry: String
}

entity Citizen {
    @identity
    ssn: String,

    name: String,

    @link(to: Passport)
    passport: Passport
}

One-to-Many Relationships

entity Author {
    @identity
    authorId: UUID,

    name: String
}

entity Book {
    @identity
    isbn: String,

    @link(to: Author, on: "authorId")
    author: Author,

    title: String,
    publishedDate: Date
}

Alternative syntax (parent reference):

entity Order {
    @identity
    orderId: UUID,

    @link(to: User, on: "userId")
    customer: User,

    total: Decimal
}

Many-to-Many Relationships

entity Student {
    @identity
    studentId: UUID,

    name: String,

    @link(to: Course, cardinality: "many-to-many")
    enrollments: List<Course>
}

entity Course {
    @identity
    courseId: String,

    title: String,

    @link(to: Student, cardinality: "many-to-many")
    roster: List<Student>
}

Cascading Operations

entity Folder {
    @identity
    id: UUID,

    name: String,

    @link(to: File, onDelete: "cascade")
    files: List<File>
}

entity File {
    @identity
    id: UUID,

    name: String,
    size: Long
}

Cascade options:

  • cascade: Delete related records
  • set_null: Set foreign key to null
  • restrict: Prevent deletion if references exist
  • no_action: Database default behavior

Generator Note: Referential integrity implementation varies. SQL databases use native foreign keys. NoSQL databases implement checks in the SDK. See Generator Guide.


Mixins and Behavioral Composition

Mixins provide a way to compose entity behavior through reusable field and trait combinations. Unlike shapes (which are purely structural), mixins carry semantic meaning that generators understand and implement with framework-specific patterns.

Understanding Mixins vs. Shapes

Shapes are structural reuse — they define a group of fields that can be embedded in multiple entities:

shape Address {
    street: String,
    city: String,
    state: String,
    zipCode: String
}

entity User {
    @identity userId: UUID,
    homeAddress: Address  // Embedded shape
}

Mixins are behavioral reuse — they define fields plus associated behaviors that generators implement:

mixin Auditable {
    @readonly
    @default(value: "now()")
    createdAt: Timestamp,

    @readonly
    createdBy: UUID?,

    @default(value: "now()")
    updatedAt: Timestamp,

    updatedBy: UUID?
}

entity User with Auditable {  // Behavioral composition
    @identity userId: UUID,
    username: String
    // Inherits audit fields plus update behavior
}

Key difference: Generators recognize mixins and generate framework-specific interceptors, triggers, or event listeners to automatically maintain the mixin's behavior.

Defining Mixins

Mixins are defined with the mixin keyword:

namespace com.acme.data.common

/// Adds creation and update tracking to any entity.
/// Generators produce JPA @PrePersist/@PreUpdate listeners,
/// SQLAlchemy event hooks, or equivalent mechanisms.
mixin Auditable {
    @readonly
    @default(value: "now()")
    createdAt: Timestamp,

    @readonly
    createdBy: UUID?,

    @default(value: "now()")
    updatedAt: Timestamp,

    updatedBy: UUID?
}

Common Built-in Mixins

Auditable Mixin

Tracks creation and modification metadata:

mixin Auditable {
    @readonly
    @default(value: "now()")
    createdAt: Timestamp,

    @readonly
    createdBy: UUID?,

    @default(value: "now()")
    updatedAt: Timestamp,

    updatedBy: UUID?
}

Generated behavior:

  • JPA: @PrePersist and @PreUpdate entity listeners
  • SQLAlchemy: before_insert and before_update event listeners
  • TypeORM: @BeforeInsert and @BeforeUpdate decorators
  • Entity Framework: SaveChanges interceptor
  • Prisma: Middleware hooks

SoftDeletable Mixin

Enables logical deletion instead of physical deletion:

mixin SoftDeletable {
    deletedAt: Timestamp?,
    deletedBy: UUID?,

    @computed(expression: "deletedAt == null")
    isActive: Boolean
}

Generated behavior:

  • Generators add WHERE clauses to all queries: WHERE deletedAt IS NULL
  • Repository delete() method sets deletedAt instead of removing the record
  • Generators provide restore() methods on repositories
  • JPA: Hibernate @Where annotation or @FilterDef
  • SQLAlchemy: Query filter
  • Prisma: Middleware to filter soft-deleted records

Versioned Mixin

Enables optimistic concurrency control:

mixin Versioned {
    @version
    version: Long
}

Generated behavior:

  • JPA: @Version annotation triggers automatic version checking
  • SQLAlchemy: Version tracking with version_id_col
  • Entity Framework: IsConcurrencyToken on version property
  • DynamoDB: Conditional updates based on version field
  • Updates fail if version doesn't match (optimistic locking)

TenantScoped Mixin

Adds multi-tenant isolation:

mixin TenantScoped {
    @tenant
    @readonly
    tenantId: UUID
}

Generated behavior:

  • Generators automatically add tenantId filter to all queries
  • Repository methods require tenant context
  • JPA: Hibernate filters based on tenant ID
  • SQLAlchemy: Scoped sessions with tenant filter
  • Prisma: Middleware to inject tenant filter
  • Row-Level Security (PostgreSQL): RLS policies based on tenant ID

Applying Mixins to Entities

Use the with keyword to compose mixins into an entity:

namespace com.acme.data.users

use com.acme.data.common#Auditable
use com.acme.data.common#SoftDeletable
use com.acme.data.common#Versioned

entity User with Auditable, SoftDeletable, Versioned {
    @identity
    userId: UUID,

    username: String,
    email: Email,
    status: UserStatus

    // Inherits from mixins:
    // - createdAt, createdBy, updatedAt, updatedBy (Auditable)
    // - deletedAt, deletedBy, isActive (SoftDeletable)
    // - version (Versioned)
}

Generated Java entity:

@Entity
@Table(name = "users")
@EntityListeners(AuditingEntityListener.class)
@SQLDelete(sql = "UPDATE users SET deleted_at = NOW() WHERE user_id = ? AND version = ?")
@Where(clause = "deleted_at IS NULL")
public class User {
    @Id
    private UUID userId;

    private String username;
    private String email;

    @Enumerated(EnumType.STRING)
    private UserStatus status;

    // From Auditable mixin
    @CreatedDate
    @Column(nullable = false, updatable = false)
    private Instant createdAt;

    @CreatedBy
    @Column(updatable = false)
    private UUID createdBy;

    @LastModifiedDate
    @Column(nullable = false)
    private Instant updatedAt;

    @LastModifiedBy
    private UUID updatedBy;

    // From SoftDeletable mixin
    private Instant deletedAt;
    private UUID deletedBy;

    // From Versioned mixin
    @Version
    private Long version;

    // Computed field from SoftDeletable
    public Boolean getIsActive() {
        return deletedAt == null;
    }
}

Multiple Mixin Composition

Entities can compose multiple mixins:

entity Order with Auditable, SoftDeletable, Versioned, TenantScoped {
    @identity
    orderId: UUID,

    customerId: UUID,
    total: Decimal,
    status: OrderStatus

    // Inherits fields and behaviors from all four mixins
}

Custom Mixins

Define domain-specific mixins for your use cases:

namespace com.acme.data.common

/// Adds approval workflow fields to any entity.
mixin Approvable {
    approvalStatus: ApprovalStatus,

    approvedAt: Timestamp?,
    approvedBy: UUID?,

    rejectedAt: Timestamp?,
    rejectedBy: UUID?,
    rejectionReason: String?
}

enum ApprovalStatus {
    PENDING,
    APPROVED,
    REJECTED
}

Usage:

entity ExpenseReport with Auditable, Approvable {
    @identity
    reportId: UUID,

    employeeId: UUID,
    totalAmount: Decimal,
    description: String

    // Inherits audit tracking and approval workflow
}

Mixin Conflicts

If two mixins define the same field name, you must resolve the conflict explicitly:

mixin MixinA {
    status: String
}

mixin MixinB {
    status: Integer
}

// Validation error: field 'status' conflict
entity MyEntity with MixinA, MixinB {
    @identity id: UUID
}

Resolution:

entity MyEntity with MixinA, MixinB {
    @identity id: UUID,

    // Override to resolve conflict
    @override
    status: String  // Explicitly choose MixinA's definition
}

Mixin Inheritance

Mixins can compose other mixins:

mixin Timestamped {
    @readonly
    @default(value: "now()")
    createdAt: Timestamp,

    @default(value: "now()")
    updatedAt: Timestamp
}

mixin Auditable extends Timestamped {
    // Includes createdAt, updatedAt from Timestamped

    @readonly
    createdBy: UUID?,

    updatedBy: UUID?
}

Best Practices

  • Single Responsibility: Each mixin should represent one behavioral concern
  • Naming Convention: Use adjective names ending in "-able" (Auditable, Versionable, Approvable)
  • Keep Mixins Focused: Limit mixins to 3-5 fields plus their associated behavior
  • Document Behavior: Clearly document what automatic behavior the mixin provides
  • Standard Library: Build a library of standard mixins for your organization
  • Avoid Deep Inheritance: Don't nest mixin inheritance more than 2 levels deep
  • Conflict Resolution: Explicitly resolve any field name conflicts

Generator Note: Mixins are a powerful abstraction that generators translate into framework-specific patterns. The same mixin definition generates JPA entity listeners, SQLAlchemy events, TypeORM decorators, or Entity Framework interceptors depending on the target. This allows you to define behavior once at the semantic level and have it correctly implemented across all platforms. See Generator Guide for implementation details per framework.


Views and Aggregations

Views

Views represent virtual tables derived from queries. They are computed on-demand when queried.

view ActiveUsers {
    source: User,
    filter: "status == 'active' AND lastLogin > now() - 30d",
    fields: [userId, username, email, lastLogin]
}

Complex view with joins:

view OrderSummary {
    source: [Order, Customer],
    join: "Order.customerId == Customer.id",
    fields: [
        Order.orderId,
        Order.total,
        Order.createdAt,
        Customer.name as customerName,
        Customer.email as customerEmail
    ],
    filter: "Order.status != 'CANCELLED'"
}

Materialized Views

Materialized views are pre-computed and stored query results that improve performance for expensive queries.

@materialized(
    refresh: "incremental",  // or "full"
    schedule: "daily"        // or "hourly", "on_change", etc.
)
view CustomerOrderStats {
    source: [Customer, Order],
    join: "Order.customerId == Customer.id",
    aggregate: {
        groupBy: ["Customer.id", "Customer.name"],
        metrics: {
            totalOrders: count(Order.orderId),
            totalRevenue: sum(Order.amount),
            avgOrderValue: avg(Order.amount),
            lastOrderDate: max(Order.createdAt)
        }
    }
}

With filter:

@materialized(refresh: "incremental", schedule: "on_change")
view RecentHighValueOrders {
    source: Order,
    filter: "amount > 1000 AND createdAt > now() - 90d",
    aggregate: {
        groupBy: ["customerId"],
        metrics: {
            orderCount: count(orderId),
            totalSpent: sum(amount)
        }
    }
}

Refresh strategies:

  • full: Rebuild entire view from scratch
  • incremental: Update only changed data
  • on_change: Refresh when source data changes (real-time)
  • scheduled: Refresh at specified intervals

Generator Note: Materialized views are a complex feature with varying support across databases. PostgreSQL and some SQL databases have native support. NoSQL databases require generator shims. See Generator Guide for implementation details and performance considerations.


Documentation and Annotations

Capacitor supports comprehensive documentation annotations that are preserved in generated code and can be used to produce API documentation, user guides, and interactive explorers.

Inline Documentation with Triple-Slash Comments

Use triple-slash line comments (///) to document entities, fields, and other constructs. This is the preferred method for field-level documentation:

/// Represents a registered user in the system.
///
/// Users are created via the registration API and must have
/// a verified email before they can access premium features.
entity User {
    @identity
    /// System-generated unique identifier. Immutable after creation.
    userId: UUID,

    /// Must be 3-50 characters. Alphanumeric and underscores only.
    /// Displayed publicly on the user's profile page.
    @unique
    username: String,

    /// Primary contact email. Must be verified within 24 hours of registration.
    @pii
    email: Email,

    /// Account lifecycle status. Transitions: PENDING → ACTIVE → SUSPENDED → INACTIVE.
    status: UserStatus,

    /// Server-generated timestamp. Cannot be modified after creation.
    @default(value: "now()")
    createdAt: Timestamp
}

Generator Note: Triple-slash comments are converted to target language documentation formats:

  • Java: Javadoc comments (/** ... */)
  • TypeScript: JSDoc comments (/** ... */)
  • Python: Docstrings ("""...""")
  • C#: XML documentation comments (/// <summary>...</summary>)
  • Go: Godoc comments (//...)
  • Swift: Doc comments (/// ...)

Structured Documentation with @documentation Trait

For more structured metadata, use the @documentation trait:

@documentation(
    summary: "Tracks all financial transactions in the system",
    since: "1.0",
    authors: ["data-platform-team"],
    seeAlso: ["com.acme.data.accounting#Ledger"],
    examples: [
        {
            title: "Creating a debit transaction",
            code: """
                Transaction tx = repo.create(
                    Transaction.builder()
                        .type(TransactionType.DEBIT)
                        .amount(Money.of(100, "USD"))
                        .build()
                );
            """
        }
    ]
)
entity Transaction {
    @identity
    transactionId: UUID,

    type: TransactionType,
    amount: Decimal,
    currency: String,
    createdAt: Timestamp
}

Supported metadata fields:

  • summary: Brief description (1-2 sentences)
  • description: Detailed explanation (markdown supported)
  • since: Version when introduced (e.g., "1.0", "2.1.0")
  • deprecated: Deprecation message and migration path
  • authors: List of responsible teams/individuals
  • seeAlso: Related types or documentation (fully qualified names)
  • examples: Array of usage examples with title and code
  • tags: Custom categorization tags

Usage Examples with @example Trait

Attach specific usage examples to fields, queries, or other constructs:

@query(
    name: "findHighValueCustomers",
    entity: Order,
    aggregate: {
        groupBy: ["customerId"],
        having: "SUM(Order.total) > :threshold",
        metrics: {
            totalSpent: sum(Order.total),
            orderCount: count(Order.orderId)
        }
    },
    params: {
        threshold: Decimal
    }
)
@example(
    description: "Find customers who have spent more than $10,000",
    params: { threshold: "10000.00" },
    notes: "Consider running during off-peak hours for large datasets"
)

Documentation Generation

Documentation can be generated in multiple formats via capacitor-build.json:

{
  "documentation": {
    "html": {
      "format": "html",
      "output": "./docs/api",
      "options": {
        "title": "User Service Data Layer API",
        "includeERDiagram": true,
        "includeDatabaseMapping": true,
        "includeAccessPatternDocs": true,
        "includeConstraintDocs": true,
        "includeExamples": true,
        "includeGeneratorNotes": true,
        "searchEnabled": true,
        "theme": "default"
      }
    },
    "markdown": {
      "format": "markdown",
      "output": "./docs/md",
      "options": {
        "singleFile": false,
        "includeTableOfContents": true
      }
    }
  }
}

Generated documentation includes:

  1. Model Overview — All entities, shapes, enums with summaries
  2. Entity Detail Pages — Fields, constraints, relationships, access patterns
  3. ER Diagram — Auto-generated entity-relationship diagram (Mermaid/PlantUML/D2)
  4. Access Pattern Reference — All indexes with types, fields, performance notes
  5. Relationship Graph — Visual map of all @link relationships
  6. Constraint Catalog — All validation rules organized by entity
  7. Query Catalog — Named queries and query builders with code examples
  8. Migration History — Chronological list of schema changes
  9. Generator Compatibility Matrix — Feature support (native vs. shimmed) per target
  10. Type Reference — All scalar, semantic, and custom types with validation
  11. Glossary — Auto-generated from entity and field doc comments

Documentation in Generated Code

All generated code includes comprehensive inline documentation derived from your Capacitor models:

Java (Javadoc):

/**
 * Represents a registered user in the system.
 *
 * <p>Users are created via the registration API and must have
 * a verified email before they can access premium features.</p>
 *
 * <h3>Access Patterns:</h3>
 * <ul>
 *   <li>{@code ByUsername} - Global index on username (unique)</li>
 *   <li>{@code ByEmail} - Global index on email</li>
 * </ul>
 *
 * <h3>Constraints:</h3>
 * <ul>
 *   <li>username: unique, 3-50 characters</li>
 *   <li>email: unique, RFC 5322 validated</li>
 * </ul>
 *
 * @since 1.0
 * @see UserRepository
 * @generated by Capacitor v1.0 — do not edit manually
 */
@Entity
@Table(name = "users")
public class User { ... }

TypeScript (JSDoc):

/**
 * Represents a registered user in the system.
 *
 * Users are created via the registration API and must have
 * a verified email before they can access premium features.
 *
 * @since 1.0
 * @see {@link UserRepository}
 * @generated by Capacitor v1.0 — do not edit manually
 */
export interface User {
  /**
   * System-generated unique identifier. Immutable after creation.
   */
  userId: string;

  /**
   * Must be 3-50 characters. Alphanumeric and underscores only.
   * Displayed publicly on the user's profile page.
   */
  username: string;

  // ...
}

Best Practices

  • Document intent, not implementation: Explain why a field exists, not just what it is
  • Be specific about constraints: Document validation rules, allowed values, and transitions
  • Include examples: Show common usage patterns and edge cases
  • Reference related types: Use seeAlso to link to related entities and documentation
  • Mark deprecations early: Use deprecated to give consumers time to migrate
  • Keep it current: Update documentation when schema changes
  • Document computed fields: Explain the calculation and when it's updated

Generator Note: Documentation generation is available for all target databases and languages. The quality and depth of generated documentation directly correlates with the quality of your inline Capacitor documentation.


Query Definitions

Query definitions allow you to declare reusable, named queries as part of your data model. These queries are translated to framework-specific implementations, generating JPA annotations for Java, SQLAlchemy queries for Python, TypeORM/Prisma for Node.js, Entity Framework for .NET, and Core Data/GRDB for Swift.

Named Queries

Named queries define reusable query logic that can be referenced by name in application code.

entity User {
    @identity
    userId: UUID,

    username: String,
    email: Email,
    status: String,
    lastLoginAt: Timestamp?,
    createdAt: Timestamp
}

@query(
    name: "findActiveUsers",
    entity: User,
    filter: "status == 'ACTIVE'",
    orderBy: ["lastLoginAt DESC"]
)

@query(
    name: "findUsersByEmailDomain",
    entity: User,
    filter: "email LIKE :domain",
    params: {
        domain: String
    }
)

@query(
    name: "findRecentUsers",
    entity: User,
    filter: "createdAt > :since",
    params: {
        since: Timestamp
    },
    orderBy: ["createdAt DESC"],
    limit: 100
)

Generated JPA (Java/Spring):

@Entity
@NamedQueries({
    @NamedQuery(
        name = "User.findActiveUsers",
        query = "SELECT u FROM User u WHERE u.status = 'ACTIVE' ORDER BY u.lastLoginAt DESC"
    ),
    @NamedQuery(
        name = "User.findUsersByEmailDomain",
        query = "SELECT u FROM User u WHERE u.email LIKE :domain"
    ),
    @NamedQuery(
        name = "User.findRecentUsers",
        query = "SELECT u FROM User u WHERE u.createdAt > :since ORDER BY u.createdAt DESC"
    )
})
public class User {
    @Id
    private UUID userId;
    private String username;
    private String email;
    private String status;
    private Timestamp lastLoginAt;
    private Timestamp createdAt;
}

// Repository interface
public interface UserRepository extends JpaRepository<User, UUID> {
    @Query(name = "User.findActiveUsers")
    List<User> findActiveUsers();

    @Query(name = "User.findUsersByEmailDomain")
    List<User> findUsersByEmailDomain(@Param("domain") String domain);

    @Query(name = "User.findRecentUsers")
    List<User> findRecentUsers(@Param("since") Timestamp since, Pageable pageable);
}

Generated SQLAlchemy (Python):

from sqlalchemy import select, and_
from datetime import datetime

class UserQueries:
    @staticmethod
    def find_active_users(session):
        return session.execute(
            select(User)
            .where(User.status == 'ACTIVE')
            .order_by(User.last_login_at.desc())
        ).scalars().all()

    @staticmethod
    def find_users_by_email_domain(session, domain: str):
        return session.execute(
            select(User)
            .where(User.email.like(domain))
        ).scalars().all()

    @staticmethod
    def find_recent_users(session, since: datetime):
        return session.execute(
            select(User)
            .where(User.created_at > since)
            .order_by(User.created_at.desc())
            .limit(100)
        ).scalars().all()

Generated TypeORM (Node.js/TypeScript):

@Entity()
export class User {
    @PrimaryGeneratedColumn('uuid')
    userId: string;

    @Column()
    username: string;

    @Column()
    email: string;

    @Column()
    status: string;

    @Column({ nullable: true })
    lastLoginAt: Date;

    @Column()
    createdAt: Date;
}

export class UserQueries {
    static findActiveUsers(repository: Repository<User>): Promise<User[]> {
        return repository
            .createQueryBuilder('user')
            .where("user.status = :status", { status: 'ACTIVE' })
            .orderBy('user.lastLoginAt', 'DESC')
            .getMany();
    }

    static findUsersByEmailDomain(
        repository: Repository<User>,
        domain: string
    ): Promise<User[]> {
        return repository
            .createQueryBuilder('user')
            .where("user.email LIKE :domain", { domain })
            .getMany();
    }

    static findRecentUsers(
        repository: Repository<User>,
        since: Date
    ): Promise<User[]> {
        return repository
            .createQueryBuilder('user')
            .where("user.createdAt > :since", { since })
            .orderBy('user.createdAt', 'DESC')
            .limit(100)
            .getMany();
    }
}

Generated Entity Framework (.NET/C#):

public class UserQueries
{
    public static IQueryable<User> FindActiveUsers(DbContext context)
    {
        return context.Set<User>()
            .Where(u => u.Status == "ACTIVE")
            .OrderByDescending(u => u.LastLoginAt);
    }

    public static IQueryable<User> FindUsersByEmailDomain(
        DbContext context,
        string domain
    )
    {
        return context.Set<User>()
            .Where(u => EF.Functions.Like(u.Email, domain));
    }

    public static IQueryable<User> FindRecentUsers(
        DbContext context,
        DateTime since
    )
    {
        return context.Set<User>()
            .Where(u => u.CreatedAt > since)
            .OrderByDescending(u => u.CreatedAt)
            .Take(100);
    }
}

Generated Core Data (Swift):

extension User {
    static func findActiveUsers(context: NSManagedObjectContext) -> [User] {
        let request: NSFetchRequest<User> = User.fetchRequest()
        request.predicate = NSPredicate(format: "status == %@", "ACTIVE")
        request.sortDescriptors = [
            NSSortDescriptor(key: "lastLoginAt", ascending: false)
        ]
        return (try? context.fetch(request)) ?? []
    }

    static func findUsersByEmailDomain(
        context: NSManagedObjectContext,
        domain: String
    ) -> [User] {
        let request: NSFetchRequest<User> = User.fetchRequest()
        request.predicate = NSPredicate(format: "email LIKE %@", domain)
        return (try? context.fetch(request)) ?? []
    }

    static func findRecentUsers(
        context: NSManagedObjectContext,
        since: Date
    ) -> [User] {
        let request: NSFetchRequest<User> = User.fetchRequest()
        request.predicate = NSPredicate(format: "createdAt > %@", since as NSDate)
        request.sortDescriptors = [
            NSSortDescriptor(key: "createdAt", ascending: false)
        ]
        request.fetchLimit = 100
        return (try? context.fetch(request)) ?? []
    }
}

Complex Queries with Joins

Queries can span multiple entities using joins.

entity Order {
    @identity
    orderId: UUID,

    @link(to: User, on: "userId")
    customer: User,

    total: Decimal,
    status: String,
    createdAt: Timestamp
}

@query(
    name: "findUserOrderHistory",
    entity: Order,
    join: [
        {entity: User, on: "Order.customer == User.userId"}
    ],
    filter: "User.userId == :userId",
    params: {
        userId: UUID
    },
    orderBy: ["Order.createdAt DESC"],
    select: [
        "Order.orderId",
        "Order.total",
        "Order.status",
        "Order.createdAt",
        "User.username",
        "User.email"
    ]
)

@query(
    name: "findHighValueCustomers",
    entity: Order,
    join: [
        {entity: User, on: "Order.customer == User.userId"}
    ],
    aggregate: {
        groupBy: ["User.userId", "User.username", "User.email"],
        having: "SUM(Order.total) > :threshold",
        metrics: {
            totalSpent: sum(Order.total),
            orderCount: count(Order.orderId),
            avgOrderValue: avg(Order.total)
        }
    },
    params: {
        threshold: Decimal
    },
    orderBy: ["totalSpent DESC"]
)

Generated JPA (Java/Spring):

public interface OrderRepository extends JpaRepository<Order, UUID> {
    @Query("""
        SELECT new com.example.OrderHistoryDto(
            o.orderId, o.total, o.status, o.createdAt,
            u.username, u.email
        )
        FROM Order o
        JOIN o.customer u
        WHERE u.userId = :userId
        ORDER BY o.createdAt DESC
    """)
    List<OrderHistoryDto> findUserOrderHistory(@Param("userId") UUID userId);

    @Query("""
        SELECT new com.example.CustomerValueDto(
            u.userId, u.username, u.email,
            SUM(o.total) as totalSpent,
            COUNT(o.orderId) as orderCount,
            AVG(o.total) as avgOrderValue
        )
        FROM Order o
        JOIN o.customer u
        GROUP BY u.userId, u.username, u.email
        HAVING SUM(o.total) > :threshold
        ORDER BY totalSpent DESC
    """)
    List<CustomerValueDto> findHighValueCustomers(@Param("threshold") BigDecimal threshold);
}

Generated SQLAlchemy (Python):

class OrderQueries:
    @staticmethod
    def find_user_order_history(session, user_id: UUID):
        return session.execute(
            select(
                Order.order_id,
                Order.total,
                Order.status,
                Order.created_at,
                User.username,
                User.email
            )
            .join(User, Order.customer_id == User.user_id)
            .where(User.user_id == user_id)
            .order_by(Order.created_at.desc())
        ).all()

    @staticmethod
    def find_high_value_customers(session, threshold: Decimal):
        return session.execute(
            select(
                User.user_id,
                User.username,
                User.email,
                func.sum(Order.total).label('total_spent'),
                func.count(Order.order_id).label('order_count'),
                func.avg(Order.total).label('avg_order_value')
            )
            .join(User, Order.customer_id == User.user_id)
            .group_by(User.user_id, User.username, User.email)
            .having(func.sum(Order.total) > threshold)
            .order_by(func.sum(Order.total).desc())
        ).all()

Pagination Queries

Define queries with built-in pagination support.

@query(
    name: "findAllUsersPaginated",
    entity: User,
    orderBy: ["username ASC"],
    pagination: {
        type: "offset",  // or "cursor"
        defaultPageSize: 20,
        maxPageSize: 100
    }
)

@query(
    name: "findUsersByCursor",
    entity: User,
    orderBy: ["createdAt DESC", "userId ASC"],
    pagination: {
        type: "cursor",
        cursorFields: ["createdAt", "userId"]
    }
)

Generated Spring Data (Java):

public interface UserRepository extends JpaRepository<User, UUID> {
    @Query("SELECT u FROM User u ORDER BY u.username ASC")
    Page<User> findAllUsersPaginated(Pageable pageable);

    // Cursor-based pagination with Slice
    @Query("SELECT u FROM User u WHERE u.createdAt < :cursor ORDER BY u.createdAt DESC, u.userId ASC")
    Slice<User> findUsersByCursor(
        @Param("cursor") Timestamp cursor,
        Pageable pageable
    );
}

Generated Prisma (Node.js):

export class UserQueries {
    static async findAllUsersPaginated(
        prisma: PrismaClient,
        page: number = 1,
        pageSize: number = 20
    ) {
        const skip = (page - 1) * pageSize;
        const take = Math.min(pageSize, 100);

        const [users, total] = await Promise.all([
            prisma.user.findMany({
                skip,
                take,
                orderBy: { username: 'asc' }
            }),
            prisma.user.count()
        ]);

        return {
            data: users,
            page,
            pageSize: take,
            total,
            totalPages: Math.ceil(total / take)
        };
    }

    static async findUsersByCursor(
        prisma: PrismaClient,
        cursor?: { createdAt: Date; userId: string },
        pageSize: number = 20
    ) {
        return prisma.user.findMany({
            take: Math.min(pageSize, 100),
            ...(cursor && {
                cursor: { userId: cursor.userId },
                skip: 1 // Skip the cursor
            }),
            where: cursor ? {
                createdAt: { lt: cursor.createdAt }
            } : undefined,
            orderBy: [
                { createdAt: 'desc' },
                { userId: 'asc' }
            ]
        });
    }
}

Dynamic Query Builders

For queries that need runtime composition, Capacitor generates type-safe query builders.

@queryBuilder(
    name: "UserSearchBuilder",
    entity: User,
    filters: {
        username: { type: String, operator: ["==", "LIKE"] },
        email: { type: Email, operator: ["==", "LIKE"] },
        status: { type: String, operator: ["==", "IN"] },
        createdAt: { type: Timestamp, operator: [">=", "<=", "BETWEEN"] },
        lastLoginAt: { type: Timestamp, operator: [">=", "<=", "IS NULL"] }
    },
    sortable: ["username", "email", "createdAt", "lastLoginAt"],
    pagination: true
)

Generated TypeScript (TypeORM):

export class UserSearchBuilder {
    private queryBuilder: SelectQueryBuilder<User>;

    constructor(repository: Repository<User>) {
        this.queryBuilder = repository.createQueryBuilder('user');
    }

    whereUsername(value: string, operator: '==' | 'LIKE' = '=='): this {
        if (operator === '==') {
            this.queryBuilder.andWhere('user.username = :username', { username: value });
        } else {
            this.queryBuilder.andWhere('user.username LIKE :username', { username: value });
        }
        return this;
    }

    whereEmail(value: string, operator: '==' | 'LIKE' = '=='): this {
        if (operator === '==') {
            this.queryBuilder.andWhere('user.email = :email', { email: value });
        } else {
            this.queryBuilder.andWhere('user.email LIKE :email', { email: value });
        }
        return this;
    }

    whereStatus(value: string | string[], operator: '==' | 'IN' = '=='): this {
        if (operator === '==') {
            this.queryBuilder.andWhere('user.status = :status', { status: value });
        } else {
            this.queryBuilder.andWhere('user.status IN (:...statuses)', { statuses: value });
        }
        return this;
    }

    whereCreatedAt(
        value: Date | { start: Date; end: Date },
        operator: '>=' | '<=' | 'BETWEEN' = '>='
    ): this {
        if (operator === 'BETWEEN' && typeof value === 'object' && 'start' in value) {
            this.queryBuilder.andWhere(
                'user.createdAt BETWEEN :start AND :end',
                { start: value.start, end: value.end }
            );
        } else if (operator === '>=') {
            this.queryBuilder.andWhere('user.createdAt >= :createdAt', { createdAt: value });
        } else {
            this.queryBuilder.andWhere('user.createdAt <= :createdAt', { createdAt: value });
        }
        return this;
    }

    orderBy(field: 'username' | 'email' | 'createdAt' | 'lastLoginAt', direction: 'ASC' | 'DESC' = 'ASC'): this {
        this.queryBuilder.orderBy(`user.${field}`, direction);
        return this;
    }

    paginate(page: number, pageSize: number): this {
        this.queryBuilder.skip((page - 1) * pageSize).take(pageSize);
        return this;
    }

    async execute(): Promise<User[]> {
        return this.queryBuilder.getMany();
    }

    async executeWithCount(): Promise<[User[], number]> {
        return this.queryBuilder.getManyAndCount();
    }
}

// Usage:
const results = await new UserSearchBuilder(userRepository)
    .whereStatus(['ACTIVE', 'SUSPENDED'], 'IN')
    .whereCreatedAt({ start: new Date('2024-01-01'), end: new Date() }, 'BETWEEN')
    .orderBy('createdAt', 'DESC')
    .paginate(1, 20)
    .executeWithCount();

Best Practices

  • Name queries semantically: Use business-focused names (e.g., findActiveUsers) rather than technical names (e.g., query1)
  • Limit complex joins: Consider materialized views for frequently-executed complex joins
  • Use cursor pagination for large datasets: Cursor-based pagination performs better than offset-based for deep pagination
  • Prefer query builders for dynamic filters: Use named queries for fixed queries, query builders for dynamic composition
  • Index supporting fields: Ensure fields used in WHERE clauses and ORDER BY have appropriate access patterns defined

Generator Note: Query generation varies by framework. JPA uses JPQL/HQL, SQLAlchemy uses the Core/ORM API, TypeORM uses QueryBuilder, Entity Framework uses LINQ, and Core Data uses NSPredicate. The Capacitor generator translates the universal query syntax to the idioms of each platform. See Generator Guide for platform-specific examples and limitations.


Repositories and Data Access Patterns

Repositories define the public API surface of the generated data access layer. They group CRUD operations and custom queries for an entity, providing a type-safe interface for persistence operations.

Repository Declaration

A repository is declared using the repository construct and is associated with a specific entity:

namespace com.acme.data.users

/// Primary data access interface for User entities.
/// Provides CRUD operations and domain-specific queries.
repository UserRepository for User {
    // Repository methods defined here
}

Auto-Generated CRUD Operations

Every repository automatically includes standard CRUD operations without explicit declaration:

  • create(input): Create a new entity
  • findById(id): Find entity by primary key
  • findAll(pagination?): Find all entities (optionally paginated)
  • update(id, input): Update an existing entity
  • delete(id): Delete an entity by primary key
  • exists(id): Check if entity exists by primary key
  • count(): Count total number of entities

These operations can be suppressed if not needed:

@suppress(operations: ["delete"])
repository UserRepository for User {
    // delete() operation will not be generated
}

Custom Query Methods

Define custom query methods using the @query annotation:

repository UserRepository for User {

    /// Find a user by their unique username.
    /// Returns null if no user exists with the given username.
    @query
    findByUsername(username: String): User?

    /// Find a user by their verified email address.
    @query
    findByEmail(email: Email): User?

    /// Find all users with the given status, with pagination.
    @query
    findByStatus(status: UserStatus, page: Pageable): Page<User>

    /// Find users created within a date range.
    @query
    findByCreatedAtBetween(start: Timestamp, end: Timestamp): List<User>

    /// Check if a username is already taken.
    @query
    existsByUsername(username: String): Boolean
}

Generator Note: Query methods are generated using framework-specific patterns:

  • Spring Data (Java): Generates interface methods with proper query derivation or @Query annotations
  • Prisma (TypeScript): Generates findMany, findUnique, findFirst with correct where clauses
  • SQLAlchemy (Python): Generates class methods with session queries
  • Entity Framework (C#): Generates LINQ query methods
  • GORM (Go): Generates query methods with proper where clauses

Custom Mutation Methods

Define custom mutation methods using the @mutation annotation:

repository UserRepository for User {

    /// Deactivate a user account. Sets status to INACTIVE and records timestamp.
    @mutation
    deactivate(userId: UUID): User

    /// Reactivate a previously deactivated account.
    @mutation
    reactivate(userId: UUID): User

    /// Bulk import users from an external system.
    /// Returns a result with success/failure counts.
    @mutation
    bulkImport(users: List<CreateUserInput>): BulkResult<User>

    /// Update the last login timestamp for a user.
    @mutation
    recordLogin(userId: UUID, loginTime: Timestamp): User
}

Aggregation Methods

Define aggregation methods using the @aggregate annotation:

repository UserRepository for User {

    /// Count users grouped by status.
    @aggregate
    countByStatus(): Map<UserStatus, Long>

    /// Get total number of active users.
    @aggregate
    countActive(): Long

    /// Get user statistics by status.
    @aggregate
    getUserStats(): List<UserStatusStats>
}

/// Supporting shape for aggregation results
shape UserStatusStats {
    status: UserStatus,
    count: Long,
    percentOfTotal: Double
}

Repository Traits

Repositories can be annotated with behavioral traits that modify how generated code operates:

/// Cached repository with read-through caching.
@cached(ttl: "5m", strategy: "read-through")
/// All read and write operations are audited.
@audited(operations: ["read", "write"])
/// Soft-delete aware — excludes soft-deleted records by default.
@softDeleteAware
repository UserRepository for User {
    // All methods inherit these behaviors
}

Available repository traits:

TraitDescriptionParameters
@cachedEnable caching for repository operationsttl: Cache duration (e.g., "5m", "1h")
strategy: "read-through", "write-through", "write-behind"
@auditedLog all repository operationsoperations: List of operations to audit (e.g., ["read", "write", "delete"])
@softDeleteAwareAutomatically filter out soft-deleted entitiesNone
@transactionalWrap all operations in transactionsisolation: Transaction isolation level (e.g., "read_committed")
@validatedApply bean validation to inputsNone
@monitoredAdd metrics and monitoringprefix: Metrics prefix for monitoring system

Generator Note: Repository traits generate different implementations per framework:

  • @cached: Spring Cache, Redis clients, or in-memory caching
  • @audited: JPA entity listeners, SQLAlchemy events, or custom interceptors
  • @softDeleteAware: WHERE clause filters or query interceptors
  • @transactional: Spring @Transactional, SQLAlchemy sessions, Prisma $transaction

Repository with Input/Output DTOs

Repositories can specify input and output types for their methods (see Input and Output Shapes for details):

repository UserRepository for User {

    // Create accepts CreateUserInput, returns UserProfile
    @create(input: CreateUserInput, output: UserProfile)
    create(user: CreateUserInput): UserProfile

    // Update accepts partial UpdateUserInput
    @update(input: UpdateUserInput, output: UserProfile)
    update(userId: UUID, changes: UpdateUserInput): UserProfile

    // Finders return output shapes
    @query(output: UserSummary)
    findByStatus(status: UserStatus, page: Pageable): Page<UserSummary>

    // Detail queries return full profile
    @query(output: UserProfile)
    findById(userId: UUID): UserProfile?
}

Transaction Boundaries

Define explicit transaction boundaries for complex operations:

repository AccountRepository for Account {

    /// Transfer funds between two accounts atomically.
    @transaction(isolation: "serializable")
    @mutation
    transferFunds(
        fromAccountId: UUID,
        toAccountId: UUID,
        amount: Decimal,
        reference: String
    ): TransferResult
}

shape TransferResult {
    transactionId: UUID,
    fromBalance: Decimal,
    toBalance: Decimal,
    timestamp: Timestamp
}

Generator Note: Transaction boundaries generate framework-specific transaction management:

  • Spring: @Transactional with specified isolation level
  • SQLAlchemy: Session transaction context manager
  • Prisma: $transaction API
  • Entity Framework: TransactionScope or BeginTransaction

Best Practices

  • Single Responsibility: One repository per entity
  • Semantic Naming: Name methods after business operations (deactivate, recordLogin) not technical operations (updateStatus, setTimestamp)
  • Return Types: Use specific output DTOs for read operations to control what data is exposed
  • Pagination: Always paginate list queries that could return large result sets
  • Transactions: Be explicit about transaction boundaries for multi-step operations
  • Documentation: Document side effects, preconditions, and postconditions for custom operations
  • Error Handling: Let the framework handle constraint violations and not-found cases
  • Testing: Repositories are the ideal boundary for integration testing

Generator Note: Repositories are the primary interface that application code uses to interact with the database. All generated repository code includes comprehensive documentation, type safety, and framework-specific best practices. See Generator Guide for implementation details per target framework.


Input and Output Shapes (DTOs)

Input and Output shapes (Data Transfer Objects) provide clean API boundaries between your persistence layer and application code. They control what data is accepted for writes and what data is returned for reads, preventing exposure of internal fields and providing type-safe interfaces.

Input Shapes for Write Operations

Input shapes define what data is required when creating or updating entities. They automatically exclude auto-generated or system-managed fields:

namespace com.acme.data.users

/// Input for creating a new user.
/// Excludes auto-generated fields (userId, createdAt, status).
input CreateUserInput for User {
    include: [username, email, phone]
    // userId, createdAt, status are excluded — they're auto-generated
}

Generated types vary by language:

Java:

public class CreateUserInput {
    @NotNull
    private String username;

    @NotNull @Email
    private String email;

    private String phone;

    // Getters, setters, builder pattern
}

TypeScript:

export interface CreateUserInput {
    username: string;
    email: string;
    phone?: string;
}

Python:

class CreateUserInput(BaseModel):
    username: str
    email: EmailStr
    phone: Optional[str] = None

Update Inputs with Partial Fields

Update inputs typically make all fields optional to support partial updates:

/// Input for updating an existing user.
/// All fields are optional (partial update semantics).
input UpdateUserInput for User {
    include: [email, phone, status],
    allOptional: true   // Every field becomes nullable for partial updates
}

Generated TypeScript:

export interface UpdateUserInput {
    email?: string;
    phone?: string;
    status?: UserStatus;
}

Field Overrides in Inputs

Override field validation or types in input shapes:

input CreateProductInput for Product {
    include: [name, description, price, category],

    /// Override: price validation is relaxed in input (final validation in entity)
    @override
    price: Decimal  // Different validation rules than entity

    /// Override: category accepts string, converted to enum
    @override
    category: String  // Will be validated and converted to ProductCategory enum
}

Output Shapes for Read Operations

Output shapes control what data is returned, providing different views for different use cases:

/// Compact user representation for list views and search results.
output UserSummary from User {
    include: [userId, username, email, status, createdAt],
    exclude: [ssn, phone]  // Never expose sensitive fields in summaries
}

/// Full user profile with computed fields.
output UserProfile from User {
    includeAll: true,
    exclude: [ssn],  // Exclude only the most sensitive field

    /// Additional computed field not in the entity
    @computed(expression: "createdAt < now() - 365d")
    isVeteranUser: Boolean
}

/// Minimal reference for embedding in other responses.
output UserRef from User {
    include: [userId, username]
}

Generated Java DTOs:

// UserSummary
public class UserSummary {
    private UUID userId;
    private String username;
    private String email;
    private UserStatus status;
    private Instant createdAt;

    // Getters only (immutable DTO)
}

// UserProfile
public class UserProfile {
    private UUID userId;
    private String username;
    private String email;
    private String phone;  // ssn excluded
    private UserStatus status;
    private Instant createdAt;
    private Boolean isVeteranUser;  // computed

    // Getters only
}

Include/Exclude Patterns

Multiple patterns are supported for field selection:

// Explicit include only
output MinimalUser from User {
    include: [userId, username]
}

// Include all except specific fields
output PublicUser from User {
    includeAll: true,
    exclude: [ssn, internalNotes, fraudScore]
}

// Include with additions
output EnrichedUser from User {
    includeAll: true,

    // Add computed fields not in entity
    @computed(expression: "posts.length")
    postCount: Integer,

    @computed(expression: "lastLoginAt > now() - 7d")
    isRecentlyActive: Boolean
}

Nested DTOs

Output shapes can reference other output shapes:

output OrderSummary from Order {
    include: [orderId, total, status, createdAt],

    // Reference another output shape
    customer: UserRef  // Embeds minimal user reference
}

output OrderDetail from Order {
    includeAll: true,

    customer: UserProfile,  // Full customer profile
    items: List<OrderItemDetail>  // List of detailed line items
}

output OrderItemDetail from OrderItem {
    include: [productName, quantity, price, subtotal]
}

DTO Usage in Repositories

Input and output shapes integrate seamlessly with repository methods:

repository UserRepository for User {

    // Create accepts input, returns output
    @create(input: CreateUserInput, output: UserProfile)
    create(user: CreateUserInput): UserProfile

    // Update accepts partial input
    @update(input: UpdateUserInput, output: UserProfile)
    update(userId: UUID, changes: UpdateUserInput): UserProfile

    // Queries can return different output shapes
    @query(output: UserSummary)
    findAll(page: Pageable): Page<UserSummary>

    @query(output: UserProfile)
    findById(userId: UUID): UserProfile?

    @query(output: UserRef)
    searchByUsername(username: String): List<UserRef>
}

DTO Mapping Generation

Generators produce mapping code between entities and DTOs:

Java (MapStruct):

@Mapper(componentModel = "spring")
public interface UserMapper {

    UserProfile toProfile(User user);

    UserSummary toSummary(User user);

    User fromCreateInput(CreateUserInput input);

    @Mapping(target = "userId", ignore = true)
    @Mapping(target = "createdAt", ignore = true)
    void updateFromInput(@MappingTarget User user, UpdateUserInput input);
}

TypeScript (manual or ts-transformer):

export class UserMapper {
    static toProfile(user: User): UserProfile {
        return {
            userId: user.userId,
            username: user.username,
            email: user.email,
            phone: user.phone,
            status: user.status,
            createdAt: user.createdAt,
            isVeteranUser: isOlderThan365Days(user.createdAt)
        };
    }

    static toSummary(user: User): UserSummary {
        return {
            userId: user.userId,
            username: user.username,
            email: user.email,
            status: user.status,
            createdAt: user.createdAt
        };
    }
}

Python (Pydantic):

class UserMapper:
    @staticmethod
    def to_profile(user: User) -> UserProfile:
        return UserProfile(
            userId=user.user_id,
            username=user.username,
            email=user.email,
            phone=user.phone,
            status=user.status,
            createdAt=user.created_at,
            isVeteranUser=is_veteran_user(user.created_at)
        )

Validation in DTOs

Input DTOs inherit validation rules from the entity but can override them:

entity User {
    @identity userId: UUID,

    @length(min: 3, max: 50)
    @pattern(regex: "^[a-zA-Z0-9_]+$")
    username: String,

    @pii
    email: Email
}

input CreateUserInput for User {
    include: [username, email],

    /// Override: relax length constraint during input (will be validated in entity)
    @override
    @length(min: 2, max: 100)  // More lenient
    username: String
}

Generator Note: Validation annotations are translated to framework-specific validators:

  • Java: Bean Validation annotations (@NotNull, @Email, @Size, @Pattern)
  • TypeScript: class-validator decorators
  • Python: Pydantic validators
  • C#: Data Annotations

Best Practices

  • Separate concerns: Use different output shapes for lists (summary) vs. details (profile)
  • Never expose PII in summaries: Mark sensitive fields with @pii and exclude from list views
  • Immutable outputs: Output DTOs should be immutable (read-only)
  • Partial updates: Use allOptional: true for update inputs
  • Computed fields in outputs: Add derived fields that don't need to be stored
  • Minimal references: Use minimal output shapes (like UserRef) when embedding in other DTOs
  • Consistent naming: Use suffixes like Input, Summary, Profile, Detail, Ref

Generator Note: DTO generation is a core feature of all Capacitor generators. The mapping between entities and DTOs is type-safe and validated at generation time. See Generator Guide for language-specific implementation patterns.


Services and Bounded Contexts

The service construct defines the top-level packaging unit in Capacitor. It groups related entities, repositories, shapes, and enums into a cohesive bounded context that becomes a publishable library artifact.

Service Declaration

A service defines the public API surface and versioning for a logical domain:

namespace com.acme.data.users

/// User domain data access layer.
///
/// This service provides all persistence operations for the user domain,
/// including user accounts, preferences, and sessions.
service UserDataService {
    version: "2.1.0",

    // Entities owned by this service
    entities: [
        User,
        UserPreferences,
        Session,
        LoginHistory
    ],

    // Repositories exposed as the public API
    repositories: [
        UserRepository,
        SessionRepository
    ],

    // Shared shapes used in inputs/outputs
    shapes: [
        Address,
        ContactInfo
    ],

    // Enums
    enums: [
        UserStatus,
        SessionType
    ],

    // Streams/CDC (optional)
    streams: [
        UserChanges,
        SessionEvents
    ]
}

Service as Packaging Unit

The service construct maps directly to language-specific package structures:

Java (Maven):

<dependency>
    <groupId>com.acme.data</groupId>
    <artifactId>user-service-dal</artifactId>
    <version>2.1.0</version>
</dependency>

TypeScript (npm):

{
  "name": "@acme/user-service-dal",
  "version": "2.1.0"
}

Python (PyPI):

[project]
name = "acme-user-service-dal"
version = "2.1.0"

Go (module):

module github.com/acme/user-service-dal

go 1.21

require (
    // dependencies
)

Public vs. Private Types

Only types explicitly listed in the service definition are included in the generated package's public API:

namespace com.acme.data.users

// This entity is private (not in service definition)
entity InternalAuditLog {
    @identity
    id: UUID,
    action: String,
    timestamp: Timestamp
}

// This entity is public (listed in service)
entity User {
    @identity
    userId: UUID,
    username: String,
    email: Email
}

service UserDataService {
    version: "1.0.0",

    // Only User is public
    entities: [User],

    repositories: [UserRepository]
    // InternalAuditLog is not exposed
}

Generator Note: Private entities and types are not exported in the generated package. They exist only within the service's internal implementation and cannot be referenced by consumers.

Service Versioning

Services follow semantic versioning (MAJOR.MINOR.PATCH):

service UserDataService {
    version: "2.1.0",  // Breaking.Feature.Fix

    entities: [User, UserPreferences],
    repositories: [UserRepository]
}

Version constraints in consumers:

{
  "dependencies": {
    "com.acme.data.users": {
      "version": "^2.1.0",  // Compatible with 2.x.x (< 3.0.0)
      "repository": "https://repo.acme.com/capacitor"
    }
  }
}

Service Dependencies

Services can depend on other services:

namespace com.acme.data.orders

use com.acme.data.users#User
use com.acme.data.users#UserRef
use com.acme.data.products#Product
use com.acme.data.common#Address

service OrderDataService {
    version: "1.5.0",

    entities: [Order, OrderItem, Payment],

    repositories: [OrderRepository, PaymentRepository],

    // Dependencies are implicit via 'use' imports
    // Generators ensure proper dependency resolution
}

Generated package dependencies:

Java (pom.xml):

<dependencies>
    <dependency>
        <groupId>com.acme.data</groupId>
        <artifactId>user-service-dal</artifactId>
        <version>2.1.0</version>
    </dependency>
    <dependency>
        <groupId>com.acme.data</groupId>
        <artifactId>product-service-dal</artifactId>
        <version>1.0.0</version>
    </dependency>
</dependencies>

TypeScript (package.json):

{
  "dependencies": {
    "@acme/user-service-dal": "^2.1.0",
    "@acme/product-service-dal": "^1.0.0"
  }
}

Multi-Service Projects

Large projects can define multiple services in separate namespaces:

models/
├── com/acme/data/
│   ├── common/
│   │   ├── types.capacitor
│   │   └── service.capacitor         // CommonDataService
│   ├── users/
│   │   ├── user.capacitor
│   │   ├── repositories.capacitor
│   │   └── service.capacitor         // UserDataService
│   ├── orders/
│   │   ├── order.capacitor
│   │   ├── repositories.capacitor
│   │   └── service.capacitor         // OrderDataService
│   └── products/
│       ├── product.capacitor
│       ├── repositories.capacitor
│       └── service.capacitor         // ProductDataService

Build configuration generates multiple artifacts:

{
  "generators": {
    "user-service": {
      "generator": "capacitor:java-jpa",
      "service": "UserDataService",
      "output": "./generated/user-service",
      "artifact": {
        "artifactId": "user-service-dal",
        "version": "2.1.0"
      }
    },
    "order-service": {
      "generator": "capacitor:java-jpa",
      "service": "OrderDataService",
      "output": "./generated/order-service",
      "artifact": {
        "artifactId": "order-service-dal",
        "version": "1.5.0"
      }
    }
  }
}

Service Metadata

Services support additional metadata for documentation and discovery:

@documentation(
    summary: "User domain data access layer",
    description: """
    Provides type-safe persistence operations for user management,
    including accounts, preferences, sessions, and authentication history.
    """,
    authors: ["data-platform-team@acme.com"],
    tags: ["user-management", "authentication"]
)
service UserDataService {
    version: "2.1.0",

    entities: [User, UserPreferences, Session],
    repositories: [UserRepository, SessionRepository]
}

Service as Bounded Context

Services align with Domain-Driven Design (DDD) bounded contexts:

// User bounded context
namespace com.acme.domain.user

service UserDomainService {
    version: "1.0.0",
    entities: [User, UserProfile, UserSettings],
    repositories: [UserRepository]
}

// Order bounded context
namespace com.acme.domain.order

use com.acme.domain.user#User as UserRef  // Reference, not ownership

service OrderDomainService {
    version: "1.0.0",
    entities: [Order, OrderItem, Invoice],
    repositories: [OrderRepository, InvoiceRepository]
    // References User but doesn't own it
}

Best practices:

  • Each service owns its entities exclusively
  • Cross-service references use minimal DTOs (like UserRef)
  • Avoid circular dependencies between services
  • Use event streams for cross-service communication

Best Practices

  • One service per bounded context: Align services with business domains
  • Clear ownership: Each entity belongs to exactly one service
  • Minimal public API: Only expose what consumers need
  • Semantic versioning: Increment major version for breaking changes
  • Documentation: Document the service's purpose, scope, and usage
  • Stable interfaces: Once published, maintain backward compatibility within major versions
  • Internal types: Use @internal trait for implementation details not in the service
  • Dependency management: Keep service dependencies minimal and well-defined

Generator Note: Services are the packaging and publishing unit for Capacitor-generated code. Each service generates a complete, self-contained library with proper dependency management, documentation, and semantic versioning. See Build Configuration for artifact packaging details and Generator Guide for implementation patterns.


Computed Fields

Computed fields derive their value from other fields using an expression. They are calculated, not stored (unless explicitly specified).

entity Order {
    @identity
    orderId: UUID,

    subtotal: Decimal,
    taxRate: Decimal,
    shippingCost: Decimal,

    @computed(expression: "subtotal * (1 + taxRate)")
    totalWithTax: Decimal,

    @computed(expression: "subtotal * (1 + taxRate) + shippingCost")
    grandTotal: Decimal
}

Stored computed fields (calculated once on write):

entity User {
    @identity
    userId: UUID,

    firstName: String,
    lastName: String,

    @computed(expression: "firstName + ' ' + lastName", stored: true)
    fullName: String
}

Computed with database functions:

entity Event {
    @identity
    eventId: UUID,

    startTime: Timestamp,
    endTime: Timestamp,

    @computed(expression: "endTime - startTime")
    duration: Integer  // Duration in seconds
}

Generator Note: Computed fields map to generated columns (SQL), virtual fields (MongoDB), or application-level calculations. See Generator Guide.


Sequences and Identity Generation

Sequences

Sequences provide auto-incrementing numeric identifiers.

sequence InvoiceNumberSequence {
    start: 1000,
    increment: 1,
    cache: 20
}

entity Invoice {
    @identity
    @sequence(InvoiceNumberSequence)
    invoiceNumber: Integer,

    customerId: UUID,
    amount: Decimal,
    createdAt: Timestamp
}

Multiple sequences:

sequence OrderNumberSequence {
    start: 10000,
    increment: 1,
    prefix: "ORD"
}

sequence ReturnNumberSequence {
    start: 5000,
    increment: 1,
    prefix: "RET"
}

Auto-generated Identifiers

entity User {
    @identity
    @default(value: "uuid()")
    userId: UUID,

    username: String
}

entity Log {
    @identity
    @default(value: "ulid()")  // Time-sortable UUID
    logId: String,

    message: String,
    timestamp: Timestamp
}

Available generators:

  • uuid(): UUID v4
  • ulid(): Universally Unique Lexicographically Sortable Identifier
  • cuid(): Collision-resistant ID optimized for horizontal scaling
  • timestamp(): Current timestamp
  • sequence(name): Named sequence

Generator Note: Sequences map to native sequences (PostgreSQL), AUTO_INCREMENT (MySQL), or application-level generators for databases without native support. See Generator Guide.


Advanced Language Features

Capacitor provides advanced features for cross-field validation, transactional operations, test data generation, and access control. These features enable sophisticated data modeling patterns while maintaining portability across databases.

Entity-Level Invariants

Invariants are cross-field validation rules that ensure data consistency beyond single-field constraints. They validate relationships between multiple fields within an entity.

Basic Invariants

entity DateRange {
    @identity
    id: UUID,

    startDate: Date,
    endDate: Date,

    @invariant(
        condition: "endDate > startDate",
        message: "End date must be after start date"
    )
    @invariant(
        condition: "endDate - startDate <= 365",
        message: "Range cannot exceed one year"
    )
}

Exclusive Field Validation

entity Discount {
    @identity
    id: UUID,

    discountPercent: Decimal?,
    discountAmount: Decimal?,

    @invariant(
        condition: "discountPercent != null OR discountAmount != null",
        message: "Either percentage or fixed amount discount must be specified"
    )
    @invariant(
        condition: "NOT (discountPercent != null AND discountAmount != null)",
        message: "Cannot specify both percentage and fixed amount discount"
    )
}

Complex Business Rules

entity Shipment {
    @identity
    shipmentId: UUID,

    weight: Decimal,  // in kg
    volume: Decimal,  // in m³
    hazardousMaterial: Boolean,
    insuranceRequired: Boolean,

    @invariant(
        condition: "(weight > 1000 OR volume > 10) IMPLIES insuranceRequired",
        message: "Insurance required for shipments over 1000kg or 10m³"
    )
    @invariant(
        condition: "hazardousMaterial IMPLIES insuranceRequired",
        message: "Insurance required for hazardous materials"
    )
}

Generator Note: Invariant implementation varies by target:

  • SQL databases: CHECK constraints where expressible, stored procedures for complex logic
  • JPA: Bean Validation @AssertTrue methods or custom validators
  • SQLAlchemy: @validates decorators or CheckConstraint
  • TypeORM: class-validator decorators
  • Entity Framework: IValidatableObject implementation
  • NoSQL databases: Validation in generated SDK code

Transaction Modeling

The transaction construct defines atomic operations that span multiple entities or involve complex business logic.

Simple Transaction

namespace com.acme.data.banking

/// Transfer funds between accounts atomically.
transaction TransferFunds {
    entities: [Account, TransactionLog],
    isolation: "read_committed",  // or serializable, repeatable_read

    params: {
        fromAccountId: UUID,
        toAccountId: UUID,
        amount: Decimal,
        reference: String
    },

    steps: [
        "debit fromAccount by amount",
        "credit toAccount by amount",
        "create TransactionLog entry"
    ],

    rollbackOn: ["InsufficientFundsException", "AccountFrozenException"]
}

Generated implementations:

Java (Spring):

@Service
public class BankingService {

    @Transactional(
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = {InsufficientFundsException.class, AccountFrozenException.class}
    )
    public TransactionLog transferFunds(
        UUID fromAccountId,
        UUID toAccountId,
        BigDecimal amount,
        String reference
    ) {
        // Generated implementation
        Account fromAccount = accountRepository.findById(fromAccountId)
            .orElseThrow(() -> new AccountNotFoundException(fromAccountId));

        Account toAccount = accountRepository.findById(toAccountId)
            .orElseThrow(() -> new AccountNotFoundException(toAccountId));

        // Business logic
        fromAccount.debit(amount);  // May throw InsufficientFundsException
        toAccount.credit(amount);

        // Save accounts
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);

        // Log transaction
        TransactionLog log = TransactionLog.builder()
            .fromAccountId(fromAccountId)
            .toAccountId(toAccountId)
            .amount(amount)
            .reference(reference)
            .timestamp(Instant.now())
            .build();

        return transactionLogRepository.save(log);
    }
}

Python (SQLAlchemy):

class BankingService:
    def transfer_funds(
        self,
        from_account_id: UUID,
        to_account_id: UUID,
        amount: Decimal,
        reference: str
    ) -> TransactionLog:
        with db.session.begin():  # Automatic rollback on exception
            from_account = Account.query.get_or_404(from_account_id)
            to_account = Account.query.get_or_404(to_account_id)

            from_account.debit(amount)  # May raise InsufficientFundsException
            to_account.credit(amount)

            log = TransactionLog(
                from_account_id=from_account_id,
                to_account_id=to_account_id,
                amount=amount,
                reference=reference,
                timestamp=datetime.utcnow()
            )
            db.session.add(log)

            return log

TypeScript (Prisma):

export class BankingService {
    async transferFunds(
        fromAccountId: string,
        toAccountId: string,
        amount: number,
        reference: string
    ): Promise<TransactionLog> {
        return await prisma.$transaction(async (tx) => {
            const fromAccount = await tx.account.findUniqueOrThrow({
                where: { id: fromAccountId }
            });

            const toAccount = await tx.account.findUniqueOrThrow({
                where: { id: toAccountId }
            });

            // Business logic with validation
            if (fromAccount.balance < amount) {
                throw new InsufficientFundsException();
            }

            await tx.account.update({
                where: { id: fromAccountId },
                data: { balance: fromAccount.balance - amount }
            });

            await tx.account.update({
                where: { id: toAccountId },
                data: { balance: toAccount.balance + amount }
            });

            return await tx.transactionLog.create({
                data: {
                    fromAccountId,
                    toAccountId,
                    amount,
                    reference,
                    timestamp: new Date()
                }
            });
        }, {
            isolationLevel: 'ReadCommitted'
        });
    }
}

Transaction Options

OptionValuesDescription
isolationread_uncommitted, read_committed, repeatable_read, serializableTransaction isolation level
timeoutDuration (e.g., "30s", "5m")Maximum transaction duration
readOnlyBooleanTransaction is read-only (optimization)
rollbackOnList of exception typesExceptions that trigger rollback
noRollbackOnList of exception typesExceptions that don't trigger rollback

Generator Note: Transaction definitions generate framework-specific transactional service methods with proper isolation, rollback rules, and error handling.

Test Fixtures

Fixtures define reusable test data that generators use to create test helpers, seed scripts, and factory functions.

Basic Fixtures

namespace com.acme.data.users

/// Standard test data for user-related tests.
fixture UserFixtures for User {
    activeUser: {
        userId: "550e8400-e29b-41d4-a716-446655440000",
        username: "alice",
        email: "alice@example.com",
        status: UserStatus.ACTIVE,
        createdAt: "2024-01-15T10:00:00Z"
    },

    suspendedUser: {
        userId: "550e8400-e29b-41d4-a716-446655440001",
        username: "bob",
        email: "bob@example.com",
        status: UserStatus.SUSPENDED,
        createdAt: "2024-03-20T14:30:00Z"
    },

    newUser: {
        userId: "550e8400-e29b-41d4-a716-446655440002",
        username: "charlie",
        email: "charlie@example.com",
        status: UserStatus.PENDING,
        createdAt: "2024-06-01T09:00:00Z"
    }
}

Generated test helpers:

Java (JUnit + Spring):

public class UserFixtures {

    public static User activeUser() {
        return User.builder()
            .userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440000"))
            .username("alice")
            .email("alice@example.com")
            .status(UserStatus.ACTIVE)
            .createdAt(Instant.parse("2024-01-15T10:00:00Z"))
            .build();
    }

    public static User suspendedUser() {
        return User.builder()
            .userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440001"))
            .username("bob")
            .email("bob@example.com")
            .status(UserStatus.SUSPENDED)
            .createdAt(Instant.parse("2024-03-20T14:30:00Z"))
            .build();
    }

    public static User newUser() {
        return User.builder()
            .userId(UUID.fromString("550e8400-e29b-41d4-a716-446655440002"))
            .username("charlie")
            .email("charlie@example.com")
            .status(UserStatus.PENDING)
            .createdAt(Instant.parse("2024-06-01T09:00:00Z"))
            .build();
    }
}

// SQL seed script also generated:
// INSERT INTO users (user_id, username, email, status, created_at)
// VALUES ('550e8400-e29b-41d4-a716-446655440000', 'alice', 'alice@example.com', 'ACTIVE', '2024-01-15 10:00:00');

TypeScript (Jest):

export const UserFixtures = {
    activeUser: (): User => ({
        userId: "550e8400-e29b-41d4-a716-446655440000",
        username: "alice",
        email: "alice@example.com",
        status: UserStatus.ACTIVE,
        createdAt: new Date("2024-01-15T10:00:00Z")
    }),

    suspendedUser: (): User => ({
        userId: "550e8400-e29b-41d4-a716-446655440001",
        username: "bob",
        email: "bob@example.com",
        status: UserStatus.SUSPENDED,
        createdAt: new Date("2024-03-20T14:30:00Z")
    }),

    newUser: (): User => ({
        userId: "550e8400-e29b-41d4-a716-446655440002",
        username: "charlie",
        email: "charlie@example.com",
        status: UserStatus.PENDING,
        createdAt: new Date("2024-06-01T09:00:00Z")
    })
};

Python (pytest):

@pytest.fixture
def active_user():
    return User(
        user_id=UUID("550e8400-e29b-41d4-a716-446655440000"),
        username="alice",
        email="alice@example.com",
        status=UserStatus.ACTIVE,
        created_at=datetime(2024, 1, 15, 10, 0, 0, tzinfo=timezone.utc)
    )

@pytest.fixture
def suspended_user():
    return User(
        user_id=UUID("550e8400-e29b-41d4-a716-446655440001"),
        username="bob",
        email="bob@example.com",
        status=UserStatus.SUSPENDED,
        created_at=datetime(2024, 3, 20, 14, 30, 0, tzinfo=timezone.utc)
    )

Parameterized Fixtures

fixture OrderFixtures for Order {
    // Template with parameters
    template orderTemplate(customerId: UUID, total: Decimal): {
        orderId: "uuid()",  // Generate UUID
        customerId: customerId,
        total: total,
        status: OrderStatus.PENDING,
        createdAt: "now()"  // Use current timestamp
    },

    // Predefined instances
    smallOrder: orderTemplate(
        customerId: "550e8400-e29b-41d4-a716-446655440000",
        total: 49.99
    ),

    largeOrder: orderTemplate(
        customerId: "550e8400-e29b-41d4-a716-446655440001",
        total: 999.99
    )
}

Internal Markers

The @internal trait marks fields and entities as internal implementation details that should not be exposed in public APIs.

Internal Fields

entity User {
    @identity
    userId: UUID,

    username: String,
    email: Email,

    @internal
    /// Internal fraud score, never exposed to external consumers
    fraudScore: Double?,

    @internal
    /// Admin-only notes
    internalNotes: String?
}

Effect: Internal fields are:

  • Excluded from public DTOs and output shapes
  • Excluded from generated API documentation
  • Excluded from projections (filtered model views)
  • Marked as private/internal in generated code

Internal Entities

@internal
entity InternalAuditLog {
    @identity
    id: UUID,

    userId: UUID,
    action: String,
    ipAddress: String,
    timestamp: Timestamp,
    metadata: Document
}

service UserDataService {
    version: "1.0.0",
    entities: [User, UserPreferences],
    repositories: [UserRepository]
    // InternalAuditLog is not listed - it's internal only
}

Effect: Internal entities are:

  • Not included in service public API
  • Not exported from generated packages
  • Only accessible within the service implementation
  • Not included in documentation generation

Readonly Trait

The @readonly trait marks fields that cannot be modified after entity creation:

entity User {
    @identity
    @readonly
    userId: UUID,

    username: String,

    @readonly
    createdAt: Timestamp,

    @readonly
    createdBy: UUID?
}

Generated code includes immutability enforcement:

  • JPA: @Column(updatable = false)
  • SQLAlchemy: onupdate=None
  • TypeORM: @Column({ update: false })
  • Prisma: Excluded from update inputs

Best Practices

Invariants:

  • Keep invariant expressions simple and testable
  • Document business rules clearly in the message
  • Prefer database-level CHECK constraints for simple rules
  • Use application-level validation for complex cross-entity logic

Transactions:

  • Define transactions for multi-step operations that must be atomic
  • Use appropriate isolation levels (default is usually sufficient)
  • Keep transactions short-lived to avoid lock contention
  • Handle rollback scenarios explicitly

Fixtures:

  • Create fixtures for common test scenarios
  • Use realistic data that represents actual use cases
  • Include edge cases (empty, maximum, null values)
  • Keep fixture data consistent with constraints

Internal Markers:

  • Mark truly internal fields/entities, not just sensitive data (use @pii for that)
  • Document why something is internal
  • Review internal markers before major version releases

Generator Note: Advanced features demonstrate Capacitor's ability to express complex data semantics that generators implement appropriately for each target platform. These features maintain portability while providing sophisticated data modeling capabilities. See Generator Guide for platform-specific implementations.


Schema Evolution

Capacitor treats time as a first-class variable, allowing for safe migrations without downtime.

Deprecation

entity Profile {
    @identity
    id: UUID,

    @deprecated(message: "Use bio_rich_text instead", removeIn: "3.0")
    bio_text: String?,

    @since("2.0")
    bio_rich_text: Document?
}

Field Renaming

entity User {
    @identity
    userId: UUID,

    @renamed(from: "user_name")
    username: String,

    @renamed(from: "mail")
    email: Email
}

Version Tracking

entity Product {
    @identity
    sku: String,

    name: String,
    price: Decimal,

    @since("1.0")
    inventory: Integer,

    @since("1.5")
    imageUrls: List<URL>?,

    @since("2.0")
    @optional
    variants: List<ProductVariant>?
}

Migration Strategies

@migration(strategy: "dual-write")
entity Order {
    @identity
    orderId: UUID,

    // Old field - read during migration
    @deprecated(removeIn: "2.0")
    status_string: String?,

    // New field - dual write during migration
    @since("1.5")
    status: OrderStatus
}

Migration strategies:

  • dual-write: Write to both old and new fields
  • backfill: Populate new field from old field in batches
  • lazy: Migrate on read/write
  • cutover: Direct switch at specific time

Data Governance and Security

Security policies are defined at the model level to ensure enforcement regardless of access path.

Sensitive Data Markers

entity User {
    @identity
    userId: UUID,

    username: String,

    @pii
    email: Email,

    @pii
    @encrypted
    ssn: String,

    @pii
    phone: Phone?
}

Data Retention

@ttl(duration: "90d", field: "expiresAt")
entity Session {
    @identity
    sessionId: String,

    userId: UUID,
    createdAt: Timestamp,

    @computed(expression: "createdAt + 90d")
    expiresAt: Timestamp
}

Soft deletes:

@softDelete(field: "deletedAt")
entity Document {
    @identity
    docId: UUID,

    title: String,
    content: String,

    deletedAt: Timestamp?  // Null = active
}

Immutability

@immutable
entity AuditLog {
    @identity
    id: UUID,

    @readonly
    userId: UUID,

    @readonly
    action: String,

    @readonly
    timestamp: Timestamp,

    @readonly
    details: Document
}

Audit Trails

@audit(track: ["insert", "update", "delete"])
entity Transaction {
    @identity
    txId: UUID,

    amount: Decimal,

    @pii
    accountNumber: String,

    status: String
}

Multi-tenancy

@multiTenant(strategy: "row-level", key: "tenantId")
entity Account {
    @identity
    accountId: UUID,

    @tenant
    tenantId: UUID,

    name: String,
    data: Document
}

Tenancy strategies:

  • row-level: Shared tables with tenant column
  • schema-level: Separate schema per tenant
  • database-level: Separate database per tenant

Reactivity and Streams

Capacitor captures data changes and can route them to external systems.

Change Data Capture (CDC)

stream UserChanges {
    source: User,
    destination: "kafka/topic/user-events",
    events: ["insert", "update", "delete"],
    format: "json"
}

With filtering:

stream HighValueOrders {
    source: Order,
    destination: "aws/eventbridge",
    filter: "amount > 10000",
    events: ["insert"],
    transform: "order_to_alert"
}

Triggers

@onInsert(fn: "sendWelcomeEmail")
@onUpdate(fn: "auditStatusChange", when: "status != old.status")
entity User {
    @identity
    userId: UUID,

    email: Email,
    status: String
}

Derived Data Sync

stream SyncActiveUsers {
    source: User,
    destination: "materialized_view/active_users",
    filter: "status == 'active'",
    events: ["insert", "update", "delete"],
    mode: "upsert"
}

Generator Note: Streams map to database-native CDC (PostgreSQL logical replication, MySQL binlog, DynamoDB Streams) or require external tools. See Generator Guide.


The Weave

The weave block defines deployment targets and their configurations.

Basic Weave

weave Development {
    target: "postgres/15",
    entities: [User, Order, Product],

    config {
        host: "localhost",
        port: 5432,
        database: "myapp_dev",
        ssl: false
    }
}

Multi-Environment

weave Development {
    target: "postgres/15",
    entities: [User, Order, Product]

    config {
        host: "localhost",
        port: 5432,
        database: "myapp_dev"
    }
}

weave Production {
    target: "postgres/15",
    storage: "aws/rds",
    entities: [User, Order, Product]

    config {
        host: "${env:DB_HOST}",
        port: 5432,
        database: "myapp_prod",
        ssl: true,
        encryption_at_rest: true,
        backup_retention: "30d",
        ha: true,
        replicas: 2
    }
}

Multi-Database

weave UserService {
    target: "postgres/15",
    entities: [User, Session],

    config {
        database: "users"
    }
}

weave Analytics {
    target: "dynamodb",
    storage: "aws/dynamodb",
    entities: [Event, Metric],

    config {
        region: "us-east-1",
        billing_mode: "PAY_PER_REQUEST"
    }
}

CLI and Workflow

Commands

Click any command to expand its full documentation.

capacitor check — Validate model against semantic rules and target compatibility

Validates the model against semantic rules and checks target compatibility.

capacitor check --target postgres

# Output:
# ✓ models/user.capacitor - Valid
# ⚠ models/order-summary.capacitor:5 - Materialized view requires manual refresh
# ✓ 5 entities checked
# ⚠ 1 warning
capacitor plan — Generate a deployment plan showing what will be created or changed

Generates a deployment plan showing what will be created or changed.

capacitor plan --target production

# Output:
# Plan for target: production (PostgreSQL 15)
#
# Changes:
#   + CREATE TABLE users (...)
#   + CREATE INDEX idx_users_email ON users(email)
#   + CREATE MATERIALIZED VIEW order_summary AS ...
#   ~ ALTER TABLE orders ADD COLUMN priority INTEGER
#   - DROP INDEX idx_orders_old
capacitor apply — Apply planned changes to the target environment

Applies the planned changes to the target environment.

capacitor apply --target production

# With confirmation:
capacitor apply --target production --auto-approve=false
capacitor build — Generate and package SDKs defined in the build configuration

Generates all SDKs defined in the build configuration. Also supports packaging and publishing.

# Generate all configured SDKs
capacitor build

# Generate specific SDK by name
capacitor build --generator typescript-sdk

# Generate all SDKs for a target
capacitor build --target development

# Generate and package (creates .jar, .tgz, .whl, etc.)
capacitor build --package

# Generate, package, and publish to configured repositories
capacitor build --publish

# Dry-run publish (shows what would be published)
capacitor build --publish --dry-run

# Generate for a specific projection
capacitor build --generator java-jpa-sdk --projection public-api

# Generate documentation only
capacitor build --docs

Output example for --package:

✓ Generated 47 files in ./generated/java
✓ Packaged com.acme.data:user-service-dal:2.1.0.jar
✓ Generated 32 files in ./generated/typescript
✓ Packaged @acme/user-service-dal-2.1.0.tgz

Output example for --publish:

✓ Published to Maven: com.acme.data:user-service-dal:2.1.0
✓ Published to npm: @acme/user-service-dal@2.1.0
✓ Published to PyPI: acme-user-service-dal==2.1.0
capacitor generate — Generate type-safe SDKs with custom language and framework flags

Generates type-safe SDKs for application code. Can use build configuration or CLI flags.

# Using build config (recommended)
capacitor build --generator typescript-sdk

# Or override with CLI flags
capacitor generate --lang typescript --framework typeorm --output ./src/generated

# Additional examples
capacitor generate --lang java --framework jpa --output ./src/main/java/dal
capacitor generate --lang python --framework sqlalchemy --output ./app/dal
capacitor generate --lang go --framework gorm --output ./internal/dal
capacitor generate --lang csharp --framework entity-framework --output ./Dal
capacitor generate --lang swift --framework core-data --output ./Models
capacitor migrate — Manage schema migrations between versions

Manages schema migrations between versions.

# Generate migration
capacitor migrate create --name add_user_preferences

# Apply migrations
capacitor migrate up --target production

# Rollback
capacitor migrate down --target production --steps 1
capacitor lint — Run linter rules and enforce model conventions

Runs all linter rules defined in capacitor.config.yaml:

# Run all linter rules
capacitor lint

# Run linter in strict mode (warnings become errors)
capacitor lint --strict

# Run specific linter rules
capacitor lint --rules naming-conventions,require-documentation

# Output validation report
capacitor lint --output lint-report.json

# Example output:
# ✓ 5 entities validated
# ⚠ 2 warnings:
#   - User entity missing documentation (line 42)
#   - Product entity has 35 fields (max: 30) (line 108)
# ✓ 0 errors

Available linter rules:

  • require-documentation: Entities and repositories should be documented
  • require-access-patterns: Entities should define access patterns
  • naming-conventions: Enforce naming conventions (PascalCase, camelCase, etc.)
  • max-entity-fields: Limit entity field count
  • shimmed-features: Warn about features requiring shims
  • unsupported-features: Error on unsupported features
  • performance-hints: Performance optimization suggestions
capacitor compatibility — Show compatibility report for generators and target databases

Shows compatibility report for generators and target databases:

# Show compatibility for a specific generator
capacitor compatibility --generator java-jpa-sdk

# Show compatibility for all generators
capacitor compatibility

# Output in JSON format
capacitor compatibility --format json

# Example output:
Compatibility Report for java-jpa-sdk (JPA + PostgreSQL 15):
────────────────────────────────────────────────────────
✅ NATIVE:  entities, shapes, enums, relationships, unique constraints,
            check constraints, sequences, views, materialized views,
            repositories, mixins, invariants

⚡ SHIMMED: soft deletes (application interceptor),
           encryption (@pii - application-level),
           audit trails (JPA event listeners),
           multi-tenancy (Hibernate filter)

⚠️ PARTIAL: full-text search (native tsvector, but limited ranking options)

❌ UNSUPPORTED: none

Generated 47 files in ./src/main/java/com/acme/dal
capacitor validate — Validate cross-namespace dependencies and imports

Validates cross-namespace dependencies and imports:

# Validate all namespaces and imports
capacitor validate

# Validate specific namespace
capacitor validate --namespace com.acme.data.users

# Check for circular dependencies
capacitor validate --check-circular

# Example output:
# ✓ Namespace com.acme.data.users validated
# ✓ All imports resolved
# ✓ No circular dependencies detected
# ✓ 3 external dependencies validated
capacitor docs — Generate documentation in HTML, Markdown, or other formats

Generate documentation in various formats:

# Generate HTML documentation
capacitor docs --format html --output ./docs/api

# Generate Markdown documentation
capacitor docs --format markdown --output ./docs/md

# Generate all configured documentation formats
capacitor docs

# Example output:
# ✓ HTML docs → ./docs/api (12 pages, ER diagram, search index)
# ✓ Markdown docs → ./docs/md (15 files)
# ✓ Generated documentation for 5 entities, 3 repositories

Build Configuration

Capacitor uses a declarative build configuration file (similar to Smithy's smithy-build.json) that defines both database targets and SDK generation.

Important: The build system validates framework/database compatibility. Not all combinations are valid (e.g., JPA requires SQL databases, cannot target MongoDB or DynamoDB). See the Framework Compatibility Matrix below for details.

capacitor.config.yaml (comprehensive example):

version: "1.0"

# ── Project Metadata ──────────────────────────────────────
metadata:
  name: "user-service-dal"
  description: "Data access layer for the User domain"
  organization: "Acme Corp"
  license: "Apache-2.0"
  website: "https://docs.acme.com/data/users"
  contributors:
    - "data-platform-team@acme.com"

# ── Source Configuration ──────────────────────────────────
sources:
  models:
    - "models/"
  config:
    - "config/"

# ── Namespace Configuration ───────────────────────────────
namespace: "com.acme.data.users"

# ── Dependencies (other Capacitor model packages) ─────────
dependencies:
  com.acme.data.common:
    version: "^1.2.0"
    repository: "https://repo.acme.com/capacitor"
  org.capacitor.stdlib.geo:
    version: "~1.0.0"

# ── Plugin Ecosystem ──────────────────────────────────────
plugins:
  repositories:
    - "https://plugins.capacitor-lang.dev"
    - "https://repo.acme.com/capacitor-plugins"
  generators:
    com.acme.capacitor:generator-django: "1.0.0"
  decorators:
    org.capacitor:decorator-lombok: "1.3.0"
    org.capacitor:decorator-mapstruct: "1.1.0"
    org.capacitor:decorator-opentelemetry: "1.2.0"

# ── Database Deployment Targets ───────────────────────────
targets:
  development:
    database: "postgres/15"
    config:
      host: "localhost"
      port: 5432
      database: "myapp_dev"
      ssl: false

  production:
    database: "postgres/15"
    provider: "aws/rds"
    config:
      host: "${env:DB_HOST}"
      port: 5432
      database: "myapp_prod"
      ssl: true
      encryption_at_rest: true
      backup_retention: "30d"
      ha: true
      replicas: 2

  analytics:
    database: "dynamodb"
    provider: "aws/dynamodb"
    config:
      region: "${env:AWS_REGION}"
      billing_mode: "PAY_PER_REQUEST"

# ── Generator Configurations (SDK Generation) ─────────────
generators:
  java-jpa-sdk:
    generator: "capacitor:java-jpa"
    target: "production"
    output: "./generated/java"
    service: "UserDataService"  # Specifies which service to generate
    options:
      package: "com.acme.data.users"
      jakartaEE: true
      includeQueries: true
      includeRepositories: true
      includeDTOs: true
      includeMappers: true
      includeTests: true
      includeFixtures: true
      springBoot:
        enabled: true
        version: "3.2"
        autoConfiguration: true
    decorators:
      - decorator: "org.capacitor:decorator-lombok"
        config:
          builder: true
          data: true
          slf4j: true
      - decorator: "org.capacitor:decorator-mapstruct"
        config:
          componentModel: "spring"
      - decorator: "org.capacitor:decorator-opentelemetry"
        config:
          traceRepositoryMethods: true
          metricsEnabled: true
    artifact:
      groupId: "com.acme.data"
      artifactId: "user-service-dal"
      version: "2.1.0"
      packaging: "jar"
      publish:
        repository: "https://repo.acme.com/maven"
        snapshotRepository: "https://repo.acme.com/maven-snapshots"

  typescript-prisma-sdk:
    generator: "capacitor:typescript-prisma"
    target: "production"
    output: "./generated/typescript"
    service: "UserDataService"
    options:
      includeQueries: true
      includeTypes: true
      strictNullChecks: true
      esm: true
      includeTests: true
    artifact:
      name: "@acme/user-service-dal"
      version: "2.1.0"
      packaging: "npm"
      publish:
        registry: "https://npm.acme.com"

  python-sqlalchemy-sdk:
    generator: "capacitor:python-sqlalchemy"
    target: "production"
    output: "./generated/python"
    service: "UserDataService"
    options:
      package: "acme_data_users"
      asyncSupport: true
      typeHints: true
      pydanticModels: true
      includeQueries: true
      includeTests: true
    artifact:
      name: "acme-user-service-dal"
      version: "2.1.0"
      packaging: "pypi"
      publish:
        repository: "https://pypi.acme.com"

# ── Projections (filtered views of the model) ─────────────
projections:
  public-api:
    description: "Public-facing types only, no internal entities"
    transforms:
      includeEntities:
        - "User"
        - "UserPreferences"
      excludeEntities:
        - "LoginHistory"
        - "InternalAuditLog"
      excludeTraits:
        - "@internal"
      includeShapes:
        - "Address"
        - "ContactInfo"
      excludeFields:
        User:
          - "ssn"
          - "internalNotes"

  analytics-export:
    description: "Read-only types for the analytics team"
    transforms:
      includeEntities:
        - "User"
        - "Session"
        - "LoginHistory"
      readOnly: true
      excludeFields:
        User:
          - "ssn"
          - "phone"

# ── Documentation Generation ──────────────────────────────
documentation:
  html:
    format: "html"
    output: "./docs/api"
    options:
      title: "User Service Data Layer API"
      includeERDiagram: true
      includeAccessPatterns: true
      includeCompatibilityMatrix: true
      includeCodeExamples: true
      exampleLanguages:
        - "java"
        - "typescript"
        - "python"
      theme: "default"
      searchEnabled: true

  markdown:
    format: "markdown"
    output: "./docs/md"
    options:
      singleFile: false
      includeTableOfContents: true

# ── Linter Configuration ──────────────────────────────────
linter:
  strict: false  # Set true to treat warnings as errors

  rules:
    require-documentation:
      level: warn
      scope:
        - "entities"
        - "repositories"

    require-access-patterns:
      level: warn
      message: "Entities should define at least one access pattern"

    shimmed-features:
      level: warn
      message: "Feature requires application-level implementation"

    unsupported-features:
      level: error
      message: "Feature not available for target"

    naming-conventions:
      level: warn
      entities: "PascalCase"
      fields: "camelCase"
      enums: "SCREAMING_SNAKE_CASE"

    max-entity-fields:
      level: info
      max: 30

    performance-hints:
      level: info

  ignore:
    - "deprecated-fields"  # Ignore deprecation warnings during migration

# ── Compatibility Requirements ────────────────────────────
compatibility:
  capacitorVersion: ">=1.0.0 <2.0.0"
  generatorVersions:
    capacitor:java-jpa: ">=1.2.0"
    capacitor:typescript-prisma: ">=1.0.0"

Alternative: capacitor-build.json (for teams preferring JSON):

{
  "version": "1.0",
  "targets": {
    "development": {
      "target": "postgres/15",
      "config": {
        "host": "localhost",
        "port": 5432,
        "database": "myapp_dev"
      }
    },
    "production": {
      "target": "postgres/15",
      "storage": "aws/rds",
      "config": {
        "host": "${env:DB_HOST}",
        "port": 5432,
        "database": "myapp_prod",
        "ssl": true
      }
    }
  },
  "generators": {
    "typescript-sdk": {
      "language": "typescript",
      "framework": "typeorm",
      "output": "./src/generated",
      "target": "development",
      "options": {
        "includeQueries": true,
        "includeTypes": true,
        "strictNullChecks": true
      }
    },
    "java-sdk": {
      "language": "java",
      "framework": "jpa",
      "output": "./src/main/java/com/example/dal",
      "target": "production",
      "options": {
        "package": "com.example.dal",
        "includeQueries": true,
        "jakartaEE": true
      }
    }
  }
}

Build commands:

# Generate all SDKs defined in config
capacitor build

# Generate specific SDK
capacitor build --generator typescript-sdk

# Generate all SDKs for a specific target
capacitor build --target development

# Override config with CLI flags
capacitor generate --lang typescript --framework prisma --output ./custom/path

Framework Compatibility Matrix

Not all framework/database combinations are valid. The Capacitor build system validates configurations and will error on incompatible combinations.

FrameworkPostgreSQLMySQLOracleSQL ServerMongoDBDynamoDBCassandraSQLite
Java/JPA
TypeScript/Prisma
TypeScript/TypeORM
TypeScript/Sequelize
Python/SQLAlchemy
Go/GORM
C#/Entity Framework
Swift/Core Data

Key Notes:

  • SQL-focused frameworks (JPA, Entity Framework, SQLAlchemy, GORM, Sequelize) work with relational databases only
  • TypeORM and Prisma have limited NoSQL support (MongoDB only)
  • NoSQL databases (DynamoDB, Cassandra) require native SDKs rather than traditional ORMs
  • Swift/Core Data primarily works with SQLite for local storage

Validation Errors:

When using incompatible combinations, Capacitor will error during build:

$ capacitor build --generator java-sdk
ERROR: Invalid configuration for generator 'java-sdk'
  Framework 'jpa' is not compatible with target database 'dynamodb'

  Compatible databases for JPA:
    - PostgreSQL
    - MySQL
    - Oracle
    - SQL Server
    - SQLite

  For DynamoDB, consider using:
    - Native AWS SDK generation
    - Language-specific DynamoDB clients

NoSQL SDK Generation:

For NoSQL databases, Capacitor generates native client code instead of ORM abstractions:

generators:
  dynamodb-sdk:
    language: "typescript"
    framework: "aws-sdk"  # Native AWS SDK
    output: "./src/dal"
    target: "production"  # DynamoDB target

  mongodb-sdk:
    language: "typescript"
    framework: "mongoose"  # MongoDB native driver
    output: "./src/dal"
    target: "mongo-target"

Getting Started

1. Installation

The Capacitor CLI is distributed as self-contained native binaries for each platform. Releases are available on the Genairus GitHub releases page.

macOS

Homebrew (Recommended)

brew tap genairus/tap && brew install capacitor-cli

Manual (x86_64 and ARM)

mkdir -p capacitor-install/capacitor && \
curl -L https://github.com/genairus/capacitor/releases/latest/download/capacitor-cli-darwin-$(uname -m).zip \
-o capacitor-install/capacitor-cli.zip && \
unzip -qo capacitor-install/capacitor-cli.zip -d capacitor-install && \
mv capacitor-install/capacitor-cli-darwin-*/* capacitor-install/capacitor
sudo capacitor-install/capacitor/install

Linux

Manual (x86_64 and ARM)

mkdir -p capacitor-install/capacitor && \
curl -L https://github.com/genairus/capacitor/releases/latest/download/capacitor-cli-linux-$(uname -m).zip \
-o capacitor-install/capacitor-cli.zip && \
unzip -qo capacitor-install/capacitor-cli.zip -d capacitor-install && \
mv capacitor-install/capacitor-cli-linux-*/* capacitor-install/capacitor
sudo capacitor-install/capacitor/install

Windows

Scoop (Recommended)

scoop bucket add genairus https://github.com/genairus/scoop-bucket; `
scoop install genairus/capacitor-cli

Manual (x64)

Download capacitor-cli-windows-x86_64.zip from the releases page, extract it, then run install.bat as Administrator.

Verify Installation

capacitor --version

2. Initialize Project

mkdir my-data-project && cd my-data-project
capacitor init

# Output:
# ✓ Created capacitor.config.yaml
# ✓ Created models/ directory
# ✓ Created weaves/ directory

3. Define Your First Model

Create models/user.capacitor:

enum UserStatus {
    ACTIVE,
    INACTIVE,
    SUSPENDED
}

shape ContactInfo {
    @pii
    email: Email,

    @pii
    phone: Phone?
}

entity User {
    @identity
    @default(value: "uuid()")
    userId: UUID,

    @unique
    @access(type: "global", name: "ByUsername")
    username: String,

    @pii
    @access(type: "global", name: "ByEmail")
    contact: ContactInfo,

    status: UserStatus,

    @default(value: "now()")
    createdAt: Timestamp,

    lastLoginAt: Timestamp?
}

4. Configure Build Targets

Create capacitor.config.yaml:

version: "1.0"

targets:
  development:
    target: "postgres/15"
    config:
      host: "localhost"
      port: 5432
      database: "myapp_dev"
      ssl: false

generators:
  typescript-sdk:
    language: "typescript"
    framework: "typeorm"
    output: "./sdk"
    target: "development"
    options:
      includeQueries: true
      includeTypes: true

Or create weaves/development.capacitor for database-only config:

weave Development {
    target: "postgres/15",
    entities: [User],

    config {
        host: "localhost",
        port: 5432,
        database: "myapp_dev",
        ssl: false
    }
}

5. Check and Plan

# Validate model
capacitor check

# Preview changes
capacitor plan --target development

6. Apply Changes

capacitor apply --target development

7. Generate SDK

# Using build config (recommended)
capacitor build

# Or with CLI flags
capacitor generate --lang typescript --framework typeorm --output ./sdk

8. Use in Your Application

import { CapacitorClient } from './sdk';

const db = new CapacitorClient({
    host: 'localhost',
    port: 5432,
    database: 'myapp_dev'
});

// Create user
const user = await db.users.create({
    username: "john_doe",
    contact: {
        email: "john@example.com",
        phone: "+1-555-0123"
    },
    status: UserStatus.ACTIVE
});

// Query by username
const found = await db.users.findByUsername("john_doe");

// Query by email
const byEmail = await db.users.findByEmail("john@example.com");

Generator Architecture

Capacitor uses a generator architecture that separates the universal data model from database-specific implementations.

How Generators Work

  1. Parse Capacitor models into an abstract syntax tree (AST)
  2. Validate semantic rules (e.g., entities must have identity)
  3. Check target compatibility and emit warnings for shimmed features
  4. Generate target-specific code:
    • DDL for databases (SQL, MongoDB schemas, DynamoDB tables)
    • Type-safe SDK code (TypeScript, Go, Python, etc.)
    • Migration scripts
    • Application shims where needed

Support Matrix

Feature support is documented in the Generator Implementation Guide, which provides detailed information on:

  • Native vs. shimmed implementations per database
  • Performance characteristics and trade-offs
  • Code examples for each generator
  • Migration patterns between databases

Creating Custom Generators

Capacitor's generator architecture is pluggable. You can create custom generators for:

  • Custom ORMs (e.g., Django ORM, SQLAlchemy)
  • Proprietary databases
  • GraphQL schemas
  • OpenAPI specifications
  • Infrastructure as Code (Terraform, CloudFormation)

Integration with Other Languages

Capacitor is part of the Genairus Software Factory ecosystem. It integrates seamlessly with all other generative domain languages:

With Chronos (Requirements)

Chronos journeys can reference Capacitor entities:

namespace com.acme.requirements.checkout

use com.acme.data.orders#Order
use com.acme.data.orders#OrderStatus

journey PlaceOrder {
    steps: [
        step CreateOrder {
            expectation: "Order entity created with status PENDING"
        }
    ]
}

Benefit: Requirements directly reference your data model for end-to-end traceability.

Optional trait: Document which journeys create/modify entities:

namespace com.acme.data.orders

use com.acme.requirements.checkout#PlaceOrder

@journey("PlaceOrder")
entity Order {
    @required @id
    id: String

    status: OrderStatus
}

With Smithy (API Contracts)

Smithy operations use Capacitor entities for request/response shapes:

namespace com.acme.api.orders

use com.acme.data.orders#Order

operation GetOrder {
    input: GetOrderInput
    output: GetOrderOutput
}

structure GetOrderOutput {
    @required
    order: Order  // Capacitor entity
}

Benefit: API contracts and data models stay perfectly aligned. Change the entity, and API types update automatically.

With Flux (User Interface)

Flux components use generated SDKs from Capacitor:

namespace com.acme.ui.orders

use com.acme.data.orders#Order

component OrderList {
    props: {
        orders: List<Order>  // Type from Capacitor
    }

    layout: column {
        for order in orders {
            OrderCard(order: order)
        }
    }
}

Benefit: UI components are type-safe against your data model. Generation-time errors if you reference a non-existent field.

With Fusion (Infrastructure)

Fusion deploys Capacitor schemas as databases:

namespace com.acme.infra

service OrderService {
    database: {
        provider: postgres,
        schema: "../data/orders.capacitor",
        migrations: auto
    }
}

Benefit: Database infrastructure defined once, deployed to any cloud.

Standalone Mode

Capacitor works perfectly without other languages:

namespace com.acme.data.orders

entity Order {
    @required @id
    id: String

    items: List<OrderItem>
    total: Money
}

Generate schemas, migrations, and SDKs without any other dependencies.

Configuration for Integration

Enable integrations in capacitor-build.json:

{
  "version": "1.0",
  "sources": ["./data/**/*.capacitor"],
  "generators": {
    "postgres-sdk": {
      "generator": "capacitor:java-jpa",
      "target": "production-sql"
    }
  },
  "integrations": {
    "chronos": {
      "requirements": "./requirements"
    },
    "smithy": {
      "spec": "./api"
    },
    "flux": {
      "components": "./ui"
    },
    "fusion": {
      "infra": "./infrastructure"
    }
  }
}

When integrations are enabled, the validator validates cross-language references and provides type checking.

Example: Complete E-Commerce Stack

Capacitor defines data:

entity Order {
    @required @id
    id: String

    total: Money
    status: OrderStatus
}

Chronos references it:

use com.acme.data.orders#Order

journey PlaceOrder {
    steps: [
        step CreateOrder {
            expectation: "Order entity created"
        }
    ]
}

Smithy uses it:

use com.acme.data.orders#Order

operation GetOrder {
    output: GetOrderOutput
}

structure GetOrderOutput {
    order: Order
}

Flux displays it:

use com.acme.data.orders#Order

component OrderSummary {
    props: {
        order: Order
    }
}

Fusion deploys it:

service OrderService {
    database: {
        schema: "../data/orders.capacitor"
    }
}

For complete integration guide, see Languages Overview.


Appendix

Supported Targets

Databases

  • PostgreSQL 12+
  • MySQL 8.0+
  • Oracle 19c+
  • MS SQL Server 2019+
  • MongoDB 5.0+
  • DynamoDB
  • Cassandra 4.0+
  • SQLite 3.35+
  • CockroachDB 22.0+

ORMs and Frameworks

  • JPA (Hibernate 5.6+, Jakarta EE 9+)
  • Prisma 4.0+
  • TypeORM 0.3+
  • Sequelize 6.0+
  • GORM (Go)
  • SQLAlchemy (Python)

Supported SDK Languages

  • TypeScript/JavaScript
  • Go
  • Python
  • Java
  • Scala
  • Rust
  • C#
  • Swift

Further Reading