vela generate form, schema, resource, scaffold and migration all take the same input: a model name followed by fields. Learn it once and every generator reads the same way. This page is the grammar, with real generated output for each rule.
The shape
The name is the model. It is pluralised for you (pet and pets both give a pets collection) and can be nested with a slash: users/pets. Each field is name:type, and modifiers hang off the end.
Types
| Type | Zod | Notes |
|---|---|---|
text | z.string() | plain string; string is an alias |
email | z.email() | validated address |
url | z.url() | validated URL |
number | z.number() | integer, int, float, decimal and double are aliases |
bool | z.boolean() | renders as a checkbox; boolean is an alias |
date | z.string() (ISO) | datetime and timestamp are aliases |
editor | z.string() | long or rich text, renders as a textarea |
password | z.string() | stored hashed |
json | z.any() | a JSON blob |
geoPoint | { lat, lon } | latitude and longitude |
autodate | set on create or update; name it created or updated | |
file / files | one upload, or many when plural | |
select(...) | z.enum([...]) | options in the parentheses, see below |
<model> | z.string() | any existing model name is a relation, see below |
current_user | z.string() | relation to the signed-in user, see below |
Modifiers
! makes a field required, both in the collection and in the Zod schema. Everything else is optional and gets .optional():
import { z } from "zod";
export const addressSchema = z.object({
street: z.string().nonempty(),
city: z.string().nonempty(),
postal: z.string().nonempty(),
note: z.string().optional(),
});
Plural is meaningful. A plural select is a multi-select, a plural files accepts several uploads, and a plural relation is many-to-many.
Selects
Options go inside the parentheses as value:Label pairs, or bare values when the label is the same:
import { z } from "zod";
export const settingSchema = z.object({
theme: z.enum(["light", "dark"]).optional(),
notifications: z.boolean().default(false).optional(),
});
In a scaffold the labels become the options of a select input and the values are what gets stored.
Relations
Use the name of another model as the type. Singular means one, plural means many:
references infers the target from the field name, Rails-style: author:references relates to authors. Relations are stored as record ids, so they are strings in the schema.
Ownership: current_user
With auth enabled, current_user is a relation to users with two extra behaviours: generated forms leave the field out and fill it from the session, and the collection’s access rules restrict rows to their owner.
import { z } from "zod";
import type { Schemas } from "@velastack/pocketbase";
export const articleSchema = z.object({
id: z.string().optional(),
collectionId: z.string().optional(),
title: z.string().nonempty(),
body: z.string().optional(),
author: z.string(),
}) satisfies Schemas["articles"];
The create action a scaffold generates for it reads the user from the session rather than the form:
import { fail, redirect } from "@sveltejs/kit";
import { superValidate } from "sveltekit-superforms";
import { zod4 } from "sveltekit-superforms/adapters";
import { setPocketbaseErrors } from "@velastack/pocketbase/form";
import { petSchema } from "$lib/schemas/pet";
export const load = async ({ locals }) => {
return { form: await superValidate(zod4(petSchema)) };
};
export const actions = {
default: async ({ locals, request }) => {
const form = await superValidate(request, zod4(petSchema));
if (!form.valid) {
return fail(400, { form });
}
let pet;
try {
pet = await locals.pb.collection("pets").create({
...form.data,
owner: locals.pb.authStore.record?.id,
});
} catch (error) {
setPocketbaseErrors(form, error);
return fail(400, { form });
}
return redirect(303, `/pets/${pet.id}`);
},
};
With teams enabled, current_team does the same for the active team.
Nested models
A slash nests one model under another, which nests the routes and scopes the collection:
That generates routes under /users/[user_id]/pets and a pets collection with a user relation. When the parent (or a parent of the parent) has a relation to the signed-in user, access rules follow the chain: only members reach the nested rows.
Placement: --route
Generators put routes under (app) when auth is enabled and (public) when it is not. --route overrides that, dynamic segments included:
Every generated href and redirect interpolates params.team_id. Keep the route you used: vela destroy scaffold projects --route ... needs it to find the files again.
vela generate scaffold pets reads the current fields from the database and generates the pages for them, which is how you add a UI to a model that was created some other way.Going further
- Generate a schema, a form, a model or a full scaffold with this syntax
- Change fields later with migrations, which use the same syntax for
addandreferences - Docs: vela generate is the reference version of this page


