Your first application
A minimal application, built up one file at a time. Nothing here is scaffolded — every file is one you write.
The smallest thing that runs
helpdesk/
app.yaml
modules/support/module.yaml
# app.yaml
schemaVersion: 1
name: helpdesk
version: 0.1.0
modules: [support]
userRoles:
- name: Agent
grants: [support.Agent]
# modules/support/module.yaml
schemaVersion: 1
name: support
runtime model validate helpdesk
It validates, and serves nothing at all. Modules are listed rather than discovered, because a module on disk that nobody listed is almost always a half-finished rename — and failing loudly beats silently serving half a model.
Something to store
# modules/support/domain.yaml
schemaVersion: 1
module: support
entities:
- name: Ticket
description: One customer problem.
attributes:
- { name: subject, type: string, maxLength: 200, required: true }
- {
name: status,
type: enum,
values: [Open, Waiting, Closed],
default: Open,
}
- { name: reporterEmail, type: string, maxLength: 254 }
- { name: urgent, type: boolean, default: false }
indexes:
- { attributes: [status] }
runtime db plan helpdesk # what it would do
runtime db apply helpdesk # do it
db plan is safe against production and worth running there — it names every
change that would take a lock or lose data.
Somebody to see it
An entity nobody can reach is invisible. Access is denied by default, which is the only safe direction for a rule somebody may forget to write:
# modules/support/security.yaml
schemaVersion: 1
module: support
roles:
- name: Agent
access:
- entity: support.Ticket
allow: [read, create, write]
constraint: reporterEmail = $currentUser.email
That constraint is compiled into the WHERE clause of every query an Agent
issues. Not a filter applied afterwards — there is no code path that can forget
it.
runtime serve helpdesk
Logic the model cannot express
“A ticket cannot be closed while it is waiting on the customer” is not something a type system says. It is a flow:
# modules/support/flows.yaml
schemaVersion: 1
module: support
flows:
- name: CloseTicket
entity: support.Ticket
allow: [Agent]
script: flows/close-ticket.lua
parameters:
- { name: resolution, type: string, maxLength: 500, required: true }
-- modules/support/flows/close-ticket.lua
if row.status == "Waiting" then
runtime.fail("this ticket is waiting on the customer; chase it or reopen it first")
end
local closed = runtime.update("support.Ticket", row.id, { status = "Closed" })
runtime.log("closed " .. row.id .. ": " .. params.resolution)
return { status = closed.status }
curl -X POST localhost:8080/api/v1/support/Ticket/$ID/CloseTicket \
-d '{"resolution":"restarted it"}'
runtime.fail is the script refusing — a 422 carrying your message, because
you wrote that message for the caller. An error() would be a 500 carrying
nothing.
Something to count
# modules/support/metrics.yaml
schemaVersion: 1
module: support
metrics:
- name: support.tickets.written
description: Tickets created or changed, by status.
entity: support.Ticket
on: [create, write]
labels: [status, urgent]
No code. The runtime maintains it as rows commit, and it arrives wherever your platform team sends OpenTelemetry.
Try labelling by subject instead and the model stops loading — labels are
restricted to enum and boolean, the only types whose value set your model
declares. Metrics explains why, and what to do when the
restriction is in your way.
What you have
Six files. A database schema, a REST API with pagination and conditional writes, row-level access control, a business rule in Lua, and a metric — with no generated code to maintain and nothing to regenerate when you change your mind.