Build Your First App
Extend the starter app with your own schema so you can see the schema-to-runtime workflow end to end.
After kizaki init, your app already has a working auth flow and a Todo entity. The best way to learn Kizaki is to change that schema and watch the rest of the stack follow.
This page walks through the core development loop:
- change the schema
- generate a migration
- add a server function
- call it from the app
Add A New Entity
Update schema/main.inspire:
auth {
providers: [email],
sessionDuration: 30d,
principal {
displayName: string @from(Profile.displayName),
}
}
entity Profile {
userId: __User.id @unique,
displayName: string @minLength(1) @maxLength(100),
bio: string?,
@grant read to *
@grant write where resource.userId == principal.id
}
entity Project {
name: string @minLength(1) @maxLength(120),
ownerId: __User.id,
@grant read, write, delete where resource.ownerId == principal.id
}This adds a database entity and a policy model in the same edit.
A schema change in Kizaki is simultaneously a database change, an access control change, and a generated API change. The @grant rules take effect as soon as the migration applies.
The auth {} block here stays intentionally minimal so the first app can focus on the schema-to-runtime workflow. For the complete auth model, including password policy, account linking, and how /__auth/* is exposed, see Inspire Auth and Authentication.
Create The Migration
Schema-affecting changes should become committed migrations:
kizaki migrate plan --name add_projects
kizaki migrate applyIf the planner needs help, it will write [RESOLVE] markers into the generated plan.migration. Edit the plan, then run kizaki migrate apply again.
At this point you have:
- changed the application model
- captured the schema change in committed history
- prepared the generated surfaces to understand the new entity
Add A Server Function
Create an exposed function in src/functions.ts or another file under src/:
import { Project } from "@kizaki/schema";
import { getPrincipal, insert, query } from "@kizaki/sdk";
/** @expose */
export async function createProject(name: string): Promise<Project> {
const me = getPrincipal();
const [project] = await query(
insert(Project)
.values({ name, ownerId: me.id })
.returning(),
);
return project;
}This shows the standard server-side pattern:
- take business input
- read the current principal
- write through the generated schema object
- return a typed result
Call It From The App
import { createProject } from "@kizaki/client";
await createProject("Launch site");The frontend calls a generated function that stays aligned with your server code. No hand-authored network layer sits between them.
What This Teaches
- Inspire defines your model
- committed migrations evolve your schema safely
/** @expose */turns server functions into generated client calls- authorization stays in the schema instead of leaking into UI code
Continue with Add Authentication.