Skip to content

Build your first app

This page builds a small members application from nothing and serves it. Finish Run the demo first, so that you have a working checkout.

An application is a directory. The compiler reads the whole directory as one application, so the file names are for you rather than for the compiler.

my-club/
app.mtd
schema.mtd
pages/
members.mtd
member.mtd

app.mtd holds the identity, the roles, the login method, and the menu.

app.mtd
app my_club "My Club" {
locale: en
mount: /
timezone: Europe/Berlin
roles {
board "Board"
member "Member"
}
auth {
methods: [password]
}
nav main "Club" {
members
}
}

Every page names the roles that can open it. Anything a page does not name is denied.

timezone: is the time zone this application is written in. A form reads a time in it, and a page shows a time in it. The default is UTC, so declare the zone you mean. See Set the time zone.

mount: is the first path segment the application is served under. mount: / puts this application at the root of its organization, so route: /members is served at /members. An application that does not declare a mount is served under its own name, at /my_club/members. An organization can hold several applications, and at most one of them can be the root. See App mount.

The menu lists page names. An entry that the reader cannot open is left out automatically, so you never write a second rule to hide it.

schema.mtd holds the tables. The column type decides storage, the form control, and the checks that run before a write.

schema.mtd
table members "Members" {
display: last_name, first_name
first_name text required "First name"
last_name text required "Last name"
email email unique "Email address"
joined_on date required "Joined on" { default: today }
active bool "Active" { default: true }
}

display: names the columns that identify a row. Any other table that references this one shows those columns in its select and in its reports.

The engine adds the primary key and the four audit columns created_at, created_by, updated_at, and updated_by. A request can never write one of them.

pages/members.mtd
page members "Members" {
route: /members
access: board
report of members "All members" {
columns: last_name, first_name, email, joined_on
search: last_name, first_name, email
sort: last_name
page_size: 25
empty: "No members yet."
row -> member
}
}

row -> member makes each row open the member page, which you write next.

pages/member.mtd
page member "Member" {
route: /members/{id}
new_route: /members/new
access: board
form of members {
fields: first_name, last_name, email, joined_on, active
actions: save, delete
success: "Member saved."
}
}

One page serves two addresses. /members/new renders an empty form that inserts. /members/42 renders a filled form that updates.

Terminal window
go run ./engine/cmd/matterdata install \
-dir .local/tenants \
-tenant my-club \
-app path/to/my-club \
-email admin@example.org \
-password 'choose-a-development-password' \
-role board

The command compiles the model, creates the tenant database, applies the schema, and creates the first account.

If the model has an error, the command prints the file, the line, the column, and a repair, and writes nothing.

Start the engine:

Terminal window
make engine

Then open http://my-club.localhost:8080/login and sign in with the address and password you passed to install.

Sign-in returns you to /, and this application declares no page there, so open http://my-club.localhost:8080/members. Give a page route: / when you want a home page.

The subdomain is the tenant name. One engine serves every tenant, and each one has its own database file. Because this application declares mount: /, its pages keep the addresses you wrote. Remove the mount: line and the same page moves to /my_club/members.