Generation

Schema File Format

Define your collection structure with schema files

Schema files define the structure of your collections, specifying fields, types, validation rules, and metadata. A schema is a JSON object where keys are field names and values are field configurations:

{
  "title": {
    "type": "string",
    "meta": {
      "required": true
    }
  },
  "description": {
    "type": "text"
  },
  "price": {
    "type": "number",
    "meta": {
      "default": 0
    }
  },
  "inStock": {
    "type": "boolean",
    "meta": {
      "default": true
    }
  },
  "publishedAt": {
    "type": "date"
  }
}

Auto-Generated Fields

The generator automatically adds certain fields to every collection based on your configuration. You should NEVER define these fields in your schema JSON files - doing so will cause duplicate key errors.

Always Auto-Generated

These fields are always added to every collection, regardless of configuration:

  • id - Primary key (UUID for PostgreSQL, nanoid for SQLite)

Configuration-Dependent Fields

Additional fields are automatically added based on your crouton.config.js flags:

When useTeamUtility: true

The generator automatically adds:

  • teamId - Team/organization reference for multi-tenancy (required, text field)
  • userId - User who created the record (required, text field)

These fields enable team-based authentication and automatically scope all data to the current team. When this flag is enabled:

  • All API endpoints automatically inject teamId and userId from the authenticated session
  • All database queries are automatically scoped to the user's team
  • Simplified API endpoints with built-in team checking are generated

See Team-Based Authentication for complete details.

When useTeamUtility: false (default): These fields are NOT added, allowing you to implement your own authentication strategy.

When useMetadata: true (default)

The generator automatically adds:

  • createdAt - Timestamp when record was created (auto-populated on insert)
  • updatedAt - Timestamp when record was last modified (auto-updated on change)
  • updatedBy - User ID of who last modified the record (auto-updated on change)

When useMetadata: false: These fields are NOT added, giving you full control over timestamp and audit tracking.

Warning: Duplicate Field ErrorsIf you manually define any auto-generated fields in your schema JSON files, you will get duplicate key errors during build:
[esbuild] (schema.ts:8:2) Duplicate key "userId" in object literal
[esbuild] (schema.ts:9:2) Duplicate key "teamId" in object literal
Solution: Remove these fields from your schema JSON files. The generator adds them automatically based on your configuration flags.Common mistake:
{
  "userId": {           // ❌ Don't define this!
    "type": "string",
    "refTarget": "users"
  },
  "teamId": {           // ❌ Don't define this!
    "type": "string"
  },
  "title": {            // ✅ Only define your custom fields
    "type": "string"
  }
}
See Troubleshooting for more solutions to common errors.
Automatic Audit Trail: When useMetadata: true (default), the system automatically tracks:
  • userId - Who created the record (set once on creation)
  • updatedBy - Who last modified the record (updated automatically on each change)
  • createdAt / updatedAt - When the record was created/modified
This provides a complete audit trail without any manual tracking. On create operations, both userId and updatedBy are set to the creating user. On update operations, updatedBy automatically changes to whoever made the modification.

Supported Field Types

Nuxt Crouton supports the following field types:

  • string - Short text (single line input)
  • text - Long content (textarea)
  • number - Integers
  • decimal - Precise decimal values (e.g., money)
  • boolean - True/false values (checkbox)
  • date - Date/timestamp values
  • json - JSON objects
  • repeater - Arrays of structured data (see Repeater Fields)
  • array - Arrays of strings (stored as JSON text)

Reference Fields

Reference fields create relationships between collections using the refTarget property. The generator automatically creates UI components for selecting and displaying related items.

Basic Reference Field

{
  "authorId": {
    "type": "string",
    "refTarget": "authors",
    "meta": {
      "required": true,
      "label": "Author"
    }
  }
}
Note: Reference fields use type: "string" because they store the referenced item's ID (UUID or nanoid string).

What Gets Generated

When you define a reference field, the generator creates:

  1. Form component - A ReferenceSelect dropdown with:
    • Searchable list of all items from the referenced collection
    • "+" button to create new related items
    • Auto-selection of newly created items
  2. List component - A CardMini cell showing:
    • The referenced item's title
    • Quick-edit button on hover

Example Schema with References

{
  "title": {
    "type": "string",
    "meta": {
      "required": true,
      "label": "Article Title"
    }
  },
  "authorId": {
    "type": "string",
    "refTarget": "authors",
    "meta": {
      "required": true,
      "label": "Author"
    }
  },
  "categoryId": {
    "type": "string",
    "refTarget": "categories",
    "meta": {
      "label": "Category"
    }
  }
}

Referencing External Collections

By default, refTarget values are prefixed with the current layer name. For example, in the shop layer, "refTarget": "categories" becomes shopCategories.

To reference a collection outside your layer or an existing table (like a shared users table), prefix the collection name with a colon (:):

{
  "authorId": {
    "type": "string",
    "refTarget": ":users",
    "meta": {
      "label": "Author"
    }
  }
}

This generates collection="users" instead of collection="shopUsers", allowing you to reference global collections or existing database tables.

Use cases:

  • Referencing a shared users table across multiple layers
  • Linking to system-wide collections like teams or organizations
  • Connecting to external collections not managed by this layer

User Experience

When working with reference fields:

  1. Creating items: Select from dropdown OR click "+" to create new
  2. Viewing lists: See referenced item titles with quick-edit buttons
  3. Editing items: Change relationships via searchable dropdown

For more details, see Working with Relations.

Repeater Fields

Repeater fields allow you to store and manage arrays of structured data without creating separate database tables. They're perfect for time slots, contact information, price tiers, or any scenario where you need multiple items of the same type.

Basic Repeater Field

{
  "slots": {
    "type": "repeater",
    "meta": {
      "label": "Available Time Slots",
      "repeaterComponent": "Slot",
      "addLabel": "Add Time Slot",
      "sortable": true,
      "area": "main"
    }
  }
}

Repeater Meta Properties

The meta object for repeater fields supports these properties:

  • repeaterComponent (required) - Name of the component that renders each item
  • addLabel (optional) - Button text for adding items (default: "Add Item")
  • sortable (optional) - Enable drag-to-reorder functionality (default: true)
  • label (optional) - Form field label
  • area (optional) - Form area placement

What Gets Generated

When you define a repeater field, the generator automatically creates:

  1. Database column - JSON/JSONB column to store the array
  2. Form component - CroutonRepeater with add/remove/reorder functionality
  3. Zod validation - z.array(z.any()).optional() for type safety
  4. Default value - Empty array []
  5. Placeholder item component - A working Vue component with TODO comments for customization
NEW: Auto-generated Placeholder ComponentsThe generator now automatically creates a working placeholder component for each repeater field. This placeholder includes:
  • Proper Vue component structure (props, emits, v-model)
  • Default values and TypeScript interfaces
  • TODO comments guiding you to customize
  • Debug section showing raw data
  • Visual styling indicating it's a placeholder
You can immediately test your repeater functionality, then customize the component with your specific fields.

Customizing Item Components

The auto-generated placeholder component is fully functional but basic. You'll want to customize it to match your data structure.

Location convention:

layers/[layer]/collections/[collection]/app/components/[ComponentName].vue

Example: Slot.vue

<script setup lang="ts">
import { nanoid } from 'nanoid'

interface TimeSlot {
  id: string
  label: string
  startTime: string
  endTime: string
}

const props = defineProps<{
  modelValue: TimeSlot
}>()

const emit = defineEmits<{
  'update:modelValue': [value: TimeSlot]
}>()

// Initialize with defaults if empty
const localValue = computed({
  get: () => props.modelValue || {
    id: nanoid(),
    label: '',
    startTime: '09:00',
    endTime: '17:00'
  },
  set: (val) => emit('update:modelValue', val)
})
</script>

<template>
  <div class="grid grid-cols-4 gap-4">
    <UFormField label="ID" name="id">
      <UInput
        v-model="localValue.id"
        disabled
        class="bg-gray-50"
      />
    </UFormField>

    <UFormField label="Label" name="label">
      <UInput v-model="localValue.label" />
    </UFormField>

    <UFormField label="Start Time" name="startTime">
      <UInput
        v-model="localValue.startTime"
        type="time"
      />
    </UFormField>

    <UFormField label="End Time" name="endTime">
      <UInput
        v-model="localValue.endTime"
        type="time"
      />
    </UFormField>
  </div>
</template>

Component Requirements

Your item component must:

  1. Accept a modelValue prop with the item data
  2. Emit update:modelValue when data changes
  3. Provide default values in the computed getter
  4. Use two-way binding with v-model on form fields

User Experience

The repeater field provides:

  • Add items - Click the button to create new items with auto-generated IDs
  • Remove items - Click × to delete an item (no confirmation)
  • Reorder items - Drag the handle (⋮⋮) to reorder (if sortable: true)
  • Empty state - Shows helpful message when no items exist

Common Use Cases

Time Slots (Bookings)

{
  "slots": {
    "type": "repeater",
    "meta": {
      "label": "Available Time Slots",
      "repeaterComponent": "TimeSlot",
      "addLabel": "Add Slot"
    }
  }
}

Contact Persons

{
  "contacts": {
    "type": "repeater",
    "meta": {
      "label": "Contact Persons",
      "repeaterComponent": "ContactPerson",
      "addLabel": "Add Contact"
    }
  }
}

Price Tiers

{
  "priceTiers": {
    "type": "repeater",
    "meta": {
      "label": "Pricing Tiers",
      "repeaterComponent": "PriceTier",
      "addLabel": "Add Tier",
      "sortable": false
    }
  }
}

Social Media Links

{
  "socialLinks": {
    "type": "repeater",
    "meta": {
      "label": "Social Media",
      "repeaterComponent": "SocialLink",
      "addLabel": "Add Link"
    }
  }
}

Data Storage

Repeater data is stored as JSON in the database:

{
  "slots": [
    {
      "id": 1697123456789,
      "label": "Morning",
      "startTime": "09:00",
      "endTime": "12:00"
    },
    {
      "id": 1697123456790,
      "label": "Afternoon",
      "startTime": "13:00",
      "endTime": "17:00"
    }
  ]
}
Auto-generated IDs: Each item gets a unique ID using nanoid(), providing collision-resistant identifiers that remain stable through reordering and editing. These IDs are more reliable than timestamps for rapid user interactions.
When to use repeater vs relations: Use repeater fields when items are tightly coupled to their parent and don't need to be queried independently. Use relation fields when items need their own table, complex relationships, or querying/filtering capabilities.

Dependent Fields

Dependent fields create dynamic form inputs that load data from a referenced collection and display it conditionally. This is perfect for scenarios like selecting time slots from a location, choosing sizes from a product, or picking rooms from a building.

Basic Dependent Field

{
  "location": {
    "type": "string",
    "refTarget": "locations",
    "meta": {
      "required": true,
      "label": "Location",
      "area": "main"
    }
  },
  "slot": {
    "type": "number",
    "meta": {
      "required": true,
      "label": "Time Slot",
      "area": "main",
      "dependsOn": "location",
      "dependsOnField": "slots",
      "dependsOnCollection": "locations",
      "displayAs": "slotButtonGroup"
    }
  }
}

Dependent Field Meta Properties

The meta object for dependent fields supports these properties:

  • dependsOn (required) - Name of the field to watch for changes
  • dependsOnField (required) - Field name in the referenced record to load
  • dependsOnCollection (required) - Collection to fetch data from
  • displayAs (required) - Display mode for the field (currently supports: slotButtonGroup)

What Gets Generated

When you define a dependent field, the generator automatically creates:

  1. Data Fetching - useFetch with watch on the dependent field
  2. Reactive Updates - Automatic refetch when dependency changes
  3. Reset Logic - Clears field value when dependency changes
  4. Conditional UI - Shows different states:
    • "Please select dependency first" when no selection
    • Dynamic options when data is loaded
    • "No items configured" when empty

Display Modes

Slot Button Group (displayAs: "slotButtonGroup")

Displays options as a visual button group, perfect for time slots or preset choices:

<UButtonGroup class="w-full flex-wrap">
  <UButton
    v-for="option in locationData.slots"
    :key="option.id"
    :variant="state.slot === option.id ? 'solid' : 'outline'"
    @click="state.slot = option.id"
    class="flex-1 min-w-[200px]"
  >
    <div class="flex flex-col items-center py-2">
      <span class="font-medium">{{ option.label }}</span>
      <span class="text-xs opacity-75">{{ option.startTime }} - {{ option.endTime }}</span>
    </div>
  </UButton>
</UButtonGroup>

Complete Example: Booking System

Location Schema (location-schema.json):

{
  "title": {
    "type": "string",
    "meta": {
      "required": true,
      "label": "Location Name"
    }
  },
  "slots": {
    "type": "repeater",
    "meta": {
      "label": "Available Time Slots",
      "repeaterComponent": "Slot",
      "addLabel": "Add Time Slot"
    }
  }
}

Booking Schema (booking-schema.json):

{
  "location": {
    "type": "string",
    "refTarget": "locations",
    "meta": {
      "required": true,
      "label": "Location"
    }
  },
  "date": {
    "type": "date",
    "meta": {
      "required": true,
      "label": "Booking Date"
    }
  },
  "slot": {
    "type": "number",
    "meta": {
      "required": true,
      "label": "Time Slot",
      "dependsOn": "location",
      "dependsOnField": "slots",
      "dependsOnCollection": "locations",
      "displayAs": "slotButtonGroup"
    }
  }
}

Generated Form Behavior:

  1. User selects a location from dropdown
  2. Form automatically fetches location data
  3. Available time slots appear as visual buttons
  4. User clicks desired slot
  5. If location changes, slot resets to empty

Generated Code Example

The generator creates this form code automatically:

<script setup lang="ts">
// ... existing setup code ...

const state = ref<BookingFormData>(initialValues)

// Auto-generated dependent field logic
const route = useRoute()
const teamId = computed(() => route.params.teamId)

// Fetch location data when location is selected
const { data: locationData } = await useFetch(() =>
  state.value.location
    ? `/api/teams/${teamId}/bookingsLocations/${state.value.location}`
    : null
, {
  watch: [() => state.value.location],
  immediate: false
})

// Reset slot when location changes
watch(() => state.value.location, () => {
  state.value.slot = 0
})
</script>

<template>
  <UFormField label="Time Slot" name="slot">
    <div v-if="!state.location" class="text-gray-400 text-sm">
      Please select location first
    </div>
    <UButtonGroup v-else-if="locationData && locationData.slots && locationData.slots.length > 0" class="w-full flex-wrap">
      <UButton
        v-for="option in locationData.slots"
        :key="option.id"
        :variant="state.slot === option.id ? 'solid' : 'outline'"
        @click="state.slot = option.id"
        class="flex-1 min-w-[200px]"
      >
        <div class="flex flex-col items-center py-2">
          <span class="font-medium">{{ option.label }}</span>
          <span class="text-xs opacity-75">{{ option.startTime }} - {{ option.endTime }}</span>
        </div>
      </UButton>
    </UButtonGroup>
    <div v-else class="text-gray-400 text-sm">
      No slots configured for this location
    </div>
  </UFormField>
</template>

Use Cases

Time Slot Selection

{
  "slot": {
    "type": "number",
    "meta": {
      "dependsOn": "location",
      "dependsOnField": "slots",
      "dependsOnCollection": "locations",
      "displayAs": "slotButtonGroup"
    }
  }
}

Product Sizes

{
  "size": {
    "type": "string",
    "meta": {
      "dependsOn": "product",
      "dependsOnField": "availableSizes",
      "dependsOnCollection": "products",
      "displayAs": "slotButtonGroup"
    }
  }
}

Building Rooms

{
  "room": {
    "type": "number",
    "meta": {
      "dependsOn": "building",
      "dependsOnField": "rooms",
      "dependsOnCollection": "buildings",
      "displayAs": "slotButtonGroup"
    }
  }
}
Regeneration-Safe: Unlike manual form customizations, dependent field logic is defined in your schema and regenerates automatically. Your dynamic form behavior persists through schema changes and regeneration.
Data Structure: The referenced field (dependsOnField) should contain an array of objects with at least an id property. For slotButtonGroup display, objects should also have label, startTime, and endTime properties.
When to use dependent fields vs conditional fields: Use dependent fields when you need to fetch and display data from a related collection. Use conditional fields (manual v-if) when you only need to show/hide fields based on local state without data fetching.

Field Metadata

Fields can include a meta object with additional configuration:

{
  "title": {
    "type": "string",
    "meta": {
      "required": true,
      "label": "Article Title",
      "maxLength": 200,
      "component": "CustomInput",
      "area": "main"
    }
  }
}

Metadata Properties

You can add metadata properties like required for validation, label for human-readable form labels, maxLength for string length limits, component to specify a custom input component, readOnly to display reference fields as non-editable cards (useful for audit fields), and area to logically group fields for form layout.

Form Areas

The area property lets you organize fields into logical sections. You might use main for primary content, sidebar for secondary metadata like status and categories, meta for SEO and publishing info, or advanced for advanced options. Currently all fields render in a single list regardless of area, but this property sets up the infrastructure for future layout features where fields can be organized into columns, tabs, or sections.

{
  "title": {
    "type": "string",
    "meta": {
      "area": "main"  // Primary content area
    }
  },
  "status": {
    "type": "string",
    "meta": {
      "area": "sidebar"  // Sidebar metadata
    }
  },
  "publishedAt": {
    "type": "date",
    "meta": {
      "area": "meta"  // SEO/publishing info
    }
  }
}

Complete Example

Here's a complete product schema with various field types and metadata:

{
  "name": {
    "type": "string",
    "meta": {
      "required": true,
      "label": "Product Name",
      "maxLength": 200,
      "area": "main"
    }
  },
  "description": {
    "type": "text",
    "meta": {
      "label": "Description",
      "area": "main"
    }
  },
  "price": {
    "type": "decimal",
    "meta": {
      "required": true,
      "default": 0,
      "label": "Price",
      "area": "sidebar"
    }
  },
  "priceTiers": {
    "type": "repeater",
    "meta": {
      "label": "Volume Pricing Tiers",
      "repeaterComponent": "PriceTier",
      "addLabel": "Add Tier",
      "area": "main"
    }
  },
  "inStock": {
    "type": "boolean",
    "meta": {
      "default": true,
      "label": "In Stock",
      "area": "sidebar"
    }
  },
  "sku": {
    "type": "string",
    "meta": {
      "required": true,
      "label": "SKU",
      "maxLength": 50,
      "area": "meta"
    }
  },
  "publishedAt": {
    "type": "date",
    "meta": {
      "label": "Publish Date",
      "area": "meta"
    }
  }
}

Best Practices

Use descriptive names like publishedAt instead of abbreviations like pub. Set sensible defaults for fields when appropriate. Mark fields as required when they're necessary for your data model. Choose the right types—use decimal for money, text for long descriptions, and string for short labels. Remember that properties like required, default, and translatable go inside the meta object.

// Good practices
{
  "publishedAt": {
    "type": "date",
    "meta": { "label": "Published At" }
  },
  "status": {
    "type": "string",
    "meta": { "default": "draft" }
  },
  "email": {
    "type": "string",
    "meta": { "required": true }
  },
  "price": {
    "type": "decimal",
    "meta": { "label": "Price" }
  },
  "description": {
    "type": "text",
    "meta": { "label": "Description" }
  }
}

Next Steps