---
title: 'Build an app: live lists, rows that belong to people, and a chat'
description: 'A site can be an app: pages that stay up to date while they are open,
  visitors who sign in with a name, and rows that belong to the people who wrote them.'
sidebar:
  order: 1
---

A site can be an app: pages that stay up to date while they are open,
visitors who sign in with a name, and rows that belong to the people who
wrote them. This guide is three worked patterns, each smaller than the
next. The language itself is on the card; this is how its pieces fit.

The second and third patterns need visitors who sign in, which a new site
has and an older site may not have yet. On an older site, ask the owner
first; until visitors can sign in there, build the first pattern only.

## 1. A list that stays up to date

Give the part of a page that shows rows an `id` and the word `region`. When
a row it read is added, changed or removed, the region is drawn again and
the open page shows it, with no reload and no script of the site's own.

```html
<script server>
  const going = tables.guests.filter(g => g.coming == true).sortBy(g => g.name)
</script>

<section id="guests" region note="Who is coming. Updates as people answer.">
  <h2>{plural(going.length, { one: "# guest so far", other: "# guests so far" })}</h2>
  <ul>
    <li :for={guest in going} key={guest.id}>{guest.name}</li>
  </ul>
</section>
```

- **A region needs an `id`.** A loop inside it may give each item a `key`;
  without one, each row's number is used, and either way one item changes
  without the others being drawn again.
- **A region reads only what the page may read.** A private table's rows
  never reach a region.
- **A long list is read from its end.** `tail(50)` gives the newest fifty,
  oldest first, in `items`, and the addresses of the windows before and
  after in `earlier` and `later` (null at either end):

  ```js
  const messages = tables.messages.filter(m => m.chat == chat).tail(50)
  ```

- **`region static`** is drawn when the page loads and when a form in it is
  sent, and not after a write. Use it for a form that should answer in
  place, or for a list of everybody, which would otherwise be drawn again
  on every open copy of the page each time anybody joins.

## 2. Rows that belong to people

A visitor joins a site by picking a name. From then on the site knows them
as `visitor`, their own row of the table `people`.

**The table of people** is `tables/people.json`. Its name is fixed, and so
is its one required field, `name`:

```json
{
  "name": "People",
  "access": { "read": "signed-in" },
  "fields": [
    { "name": "name", "type": "text", "label": "Name", "required": true, "filterable": true, "sortable": true }
  ]
}
```

**Signing in** is a form that says `sign-in`. Neither it nor signing out
has an action of its own:

```html
<form sign-in class="flex flex-row gap-sm">
  <input name="name" required maxlength="40" autocomplete="nickname" aria-label="Your name">
  <button class="btn-primary">Join</button>
  <p :if={form.failed} class="text-accent">{form.message}</p>
</form>

<form sign-out forget><button>Leave for good</button></form>
```

While a name is the only way in, signing out leaves a person nobody can be
again, their name still in every list. So sign out with `forget`, which
removes the person too, and say on the button that leaving is for good. The
checker warns of a bare `<form sign-out>` on such a site.

**A page for signed-in visitors only** says so in its server script, and
everybody else is sent to sign in:

```html
<script server>
  export const access = "signed-in"
</script>
```

**Who may do what to a table's rows** is its `access`: `read`, `write`
(add a row), `change` and `remove`. Each is one of these, or a list of them:

- `"owner"`: no visitor. Every verb is this until it says otherwise.
- `"public"`: anybody, signed in or not, on `read` and `write` only.
- `"signed-in"`: anybody who has signed in.
- `"self"`: on `people` only, the person the row is.
- `{ "field": "author" }`: the person that field points at, or anybody in it
  when it is a list of people.
- `{ "through": "chat" }`: whoever may read the row that field points at,
  on every verb. A message is read, and written, by whoever may read its
  chat. A chain of them (a comment through its card, a card through its
  board) may go three steps.

```json
"access": {
  "read":   { "through": "chat" },
  "write":  { "through": "chat" },
  "change": { "field": "author" },
  "remove": { "field": "author" }
}
```

A row a visitor may not read is not found: its page answers 404, and a
list leaves it out. The table's rule is applied to every read and every
write, so write no check of your own. Reading a table with a `through` rule
names the row it goes through, `.filter(m => m.chat == chat)`, and the
checker refuses a read that does not.

**What the platform fills in:**

- `"fill": "visitor"` on a reference to `people` is the visitor who added
  the row. Nothing a form sends can change it.
- `"fill": "now"` on a `datetime` is when the row was added.
- `"includes": "visitor"` on a list of people (`references`) puts the
  visitor in the list of a row they add, so whoever starts a chat is in it.

**A list of people a rule reads** changes only as its own keys say. `"add"`
names who may put somebody in it besides the owner (the same rules as
`access`). `"leave": true` lets a person take themselves out. Nobody else
is added or taken out by a visitor's form.

**The owner approves what widens.** A merge waits for the owner on the
Branches screen when it gives visitors more than they had (a rule that lets
more people at more rows, a list more people may join) or lets visitors
learn more. When `merge_branch` answers that it waits, pass its sentences
on to the owner.

## 3. A chat

A chat is the first two patterns together. The language has no word for
who is online or who is typing, and none that marks messages read when
they are shown, so a chat here has none of them.

**Three tables.** `people` as above. `chats`: a title, its `members` (a list
of people with `"includes": "visitor"` and `"max": 50`), whether it is
between two people, how many messages it has had (`sent`), and its last
message. `messages`: its `chat`, its `author` (`"fill": "visitor"`), its
`text` and when it was `sent` (`"fill": "now"`).

```json
{
  "name": "Chats",
  "access": {
    "read": { "field": "members" },
    "write": "signed-in",
    "change": { "field": "members" }
  },
  "fields": [
    { "name": "title", "type": "text", "label": "Title" },
    { "name": "members", "type": "references", "table": "people", "label": "Members", "required": true,
      "filterable": true, "includes": "visitor", "max": 50 },
    { "name": "direct", "type": "boolean", "label": "Between two people", "filterable": true },
    { "name": "sent", "type": "number", "label": "Messages sent" },
    { "name": "last", "type": "reference", "table": "messages", "label": "Last message" }
  ]
}
```

**One page per chat**, `pages/c/[slug].vzk`, signed-in only. Its regions
are the messages and the box to write in:

```html
<script server>
  export const access = "signed-in"

  const chat = tables.chats.row(params.slug)
  const messages = tables.messages.filter(m => m.chat == chat).tail(50)

  function send(input) {
    const message = tables.messages.insert({ chat, text: input.text })
    tables.chats.update(chat, { sent: (chat.sent ?? 0) + 1, last: message })
  }
</script>

<ol id="messages" region>
  <li :for={message in messages.items} key={message.id}>{message.text}</li>
</ol>

<footer id="write" region static>
  <form id="send" action={send}>
    <textarea name="text" required maxlength="4000" aria-label="Message"></textarea>
    <button class="btn-primary">Send</button>
  </form>
</footer>
```

- **A chat nobody may read is a 404** for whoever is not in it, by the
  table's own rule.

**Prove it before asking to merge.** Write scenarios for what the app
promises: two people sign in, one starts a chat, a message reaches the
other without a reload, changing and removing one's own message, and a 404
for somebody signed in who is not in the chat. Run them with `run_tests` on
the branch. The owner's merge shows that they pass.
