Schema File Format
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
teamIdanduserIdfrom 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.
[esbuild] (schema.ts:8:2) Duplicate key "userId" in object literal
[esbuild] (schema.ts:9:2) Duplicate key "teamId" in object literal
{
"userId": { // ❌ Don't define this!
"type": "string",
"refTarget": "users"
},
"teamId": { // ❌ Don't define this!
"type": "string"
},
"title": { // ✅ Only define your custom fields
"type": "string"
}
}
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
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- Integersdecimal- Precise decimal values (e.g., money)boolean- True/false values (checkbox)date- Date/timestamp valuesjson- JSON objectsrepeater- 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"
}
}
}
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:
- Form component - A
ReferenceSelectdropdown with:- Searchable list of all items from the referenced collection
- "+" button to create new related items
- Auto-selection of newly created items
- List component - A
CardMinicell 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
userstable across multiple layers - Linking to system-wide collections like
teamsororganizations - Connecting to external collections not managed by this layer
User Experience
When working with reference fields:
- Creating items: Select from dropdown OR click "+" to create new
- Viewing lists: See referenced item titles with quick-edit buttons
- 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 itemaddLabel(optional) - Button text for adding items (default: "Add Item")sortable(optional) - Enable drag-to-reorder functionality (default: true)label(optional) - Form field labelarea(optional) - Form area placement
What Gets Generated
When you define a repeater field, the generator automatically creates:
- Database column - JSON/JSONB column to store the array
- Form component -
CroutonRepeaterwith add/remove/reorder functionality - Zod validation -
z.array(z.any()).optional()for type safety - Default value - Empty array
[] - Placeholder item component - A working Vue component with TODO comments for customization
- 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
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:
- Accept a
modelValueprop with the item data - Emit
update:modelValuewhen data changes - Provide default values in the computed getter
- Use two-way binding with
v-modelon 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"
}
]
}
nanoid(), providing collision-resistant identifiers that remain stable through reordering and editing. These IDs are more reliable than timestamps for rapid user interactions.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 changesdependsOnField(required) - Field name in the referenced record to loaddependsOnCollection(required) - Collection to fetch data fromdisplayAs(required) - Display mode for the field (currently supports:slotButtonGroup)
What Gets Generated
When you define a dependent field, the generator automatically creates:
- Data Fetching -
useFetchwith watch on the dependent field - Reactive Updates - Automatic refetch when dependency changes
- Reset Logic - Clears field value when dependency changes
- 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:
- User selects a location from dropdown
- Form automatically fetches location data
- Available time slots appear as visual buttons
- User clicks desired slot
- 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"
}
}
}
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.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
- Learn about Generator Commands to create collections
- Explore Multi-Collection Configuration for complex projects
- See Customizing Generated Code to modify the generated forms