Skip to content

Models & Validation ​

In modern software, data shapes are often duplicated across multiple layers: you write a SQL schema, a backend TypeScript interface, a Zod validation schema, and a frontend client type. When one changes, the others break.

In Carotene, your .carrot contract is the single source of truth for your data. The compiler automatically generates the database schemas, the cross-language types, and the runtime validation middleware.

Carotene accomplishes this with a single, unified primitive: the model.

The Contextual Model (Scoping) ​

Carotene is radically minimalist. You do not need different keywords for database tables, network DTOs, or frontend memory slices. Instead, the compiler relies entirely on Scope (where the model is placed) to determine what infrastructure to build.

1. Global Scope (Shared Data in Transit) ​

If you define a model globally (outside of a backend or frontend), it represents Data in Transit. It has no physical lifecycle, no database table, and no memory store. It is an abstract shape used for Network Payloads or Data Transfer Objects (DTOs) shared between your frontend and backend.

dart
domain Commerce {
  // GLOBAL: A shared shape. Provisions ZERO infrastructure.
  model ProfileSummary {
    id: UUID
    username: String
  }
}

2. Private Scope (Encapsulated Data in Transit) ​

If you define a model directly inside a backend or frontend (but not inside a store or state), it is still just Data in Transit, but it is Encapsulated. The compiler will only generate the type definitions for that specific block, keeping your other environments clean. This is perfect for internal worker payloads or UI-only prop shapes.

dart
backend {
  // PRIVATE: Only generates types for the backend code. 
  // The frontend SDK never sees this.
  model StripeWebhookPayload {
    eventId: String
    amount: Float
  }
}

3. Storage Scope (Data at Rest / RAM) ​

If you define a model inside an infrastructure block (store for Disk, state for RAM), it represents a concrete entity with a lifecycle. The compiler generates the physical Postgres tables or reactive state slices to hold this data.

dart
backend {
  store {
    // STORAGE: A lifecycled entity. Generates a Postgres table and ORM schema.
    model User {
      id: UUID { primary, generated: true }
      username: String
    }
  }
}

Composition: The includes Keyword ​

To keep your models DRY (Don't Repeat Yourself), Carotene allows you to compose shapes together using the includeskeyword.

Unlike traditional Object-Oriented inheritance (extends), which creates fragile runtime prototype chains, includesperforms Compile-Time Flattening. The compiler simply copies the fields from the target model and pastes them into your new model.

dart
// 1. Define a global base shape
model BaseEntity {
  id: UUID { primary, generated: true }
  createdAt: DateTime { default: NOW }
}

backend {
  store {
    // 2. Compose the global shape into a concrete database table
    model Article includes BaseEntity {
      title: String
      content: String
    }
  }
}

You can compose multiple models at once (includes A, B). If there is a field name collision between them, the compiler strictly fails the build, preventing silent overwrites and hidden bugs.

Shared Logic: Defaults, Formulas & Validation ​

Because model is a unified keyword, Carotene allows you to bake incredibly powerful logic directly into the properties using Configuration Brackets { }. The compiler seamlessly translates this logic to the target environment—whether that means writing a SQL constraint or a reactive frontend getter.

  • Defaults: startedAt: DateTime { default: NOW }
  • Computed Data: total: Float { computed: "subtotal * (1 + taxRate)" }

Field-Level Validation ​

AI coding agents are notoriously bad at writing exhaustive input validation. You strip this burden from the AI entirely by declaring strict validation constraints in the brackets.

dart
model CreateUserPayload {
  email: String { format: "email", unique: true }
  password: String { minLength: 8, secret: true }
  age: Int? { min: 18, max: 120 }
}

The Full-Stack Validation Guarantee ​

The most powerful feature of Carotene's validation system is how it handles the ingress of data. Because the .carrotcontract generates code for both the frontend and the backend, validation rules are replicated across the entire stack automatically.

  1. The Frontend Net: When the AI builds a React form for CreateUserPayload, Carotene injects the validation rules directly into the client SDK. If a user types a 4-character password, the frontend UI instantly rejects it. No network request is even made.
  2. The Backend Shield: If a malicious user bypasses the frontend and sends an API request directly, Carotene's generated backend middleware catches the invalid password and rejects it with a 400 Bad Request before it ever reaches the application logic.

Because of this, when the AI agent writes your backend functions, it does not need to write a single manual if (!payload.email.includes('@')) check. It can confidently assume the data is 100% clean.

Reusing Models: Cross-Boundary Type Referencing ​

Often, data defined in your backend database (store) needs to be held in your frontend's volatile memory (state).

Carotene strictly prevents "magic synchronization"—it will never automatically copy your database schema into frontend memory, as this causes massive performance leaks. Instead, Carotene uses Cross-Boundary Type Referencing.

You define the blueprint in the store, and you build specific UI models in the state that use that blueprint strictly as their property Types:

dart
frontend {
  state {
    // A clean memory slice containing arrays of the backend store model
    model ArticleList {
      trending: backend.store.Article[]
      savedForLater: backend.store.Article[]
      isLoading: Boolean { default: false }
    }
    
    // Composing a UI-specific model that flattens the backend shape into itself
    model ArticleCard includes backend.store.Article {
      isExpanded: Boolean { default: false }
    }
  }
}

By referencing the store model, you guarantee that if a database column changes in the backend, the frontend state manager will automatically inherit the new shape and validation rules.