Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.