Skip to content

Your first app

A packing list for one trip, built from HTML and SQL with no JavaScript of its own. By the end you will have a .dai.html file you can open, use, and send. It is examples/packing-list in the repository, so every file below is the real one.

1. Decide the shape

One family's list, kept on one phone. Nobody else adds to it and no second copy's changes need keeping, so it is solo: ordinary tables, no sharing. The decision goes at the top of the schema, where the next person to change the application will read it.

SHAPE-FIRST

Decide the shape before writing a table

Before writing schema.sql, decide which of the four shapes the application is — solo, passable, session or broadcast — using the two questions above, and state the decision in a comment at the top of schema.sql. The shape decides which tables are replicated, whether there is a session profile, and every rule below that applies only to shared tables.

Why. The shape is the decision every table depends on, and it cannot be fixed afterward by editing a table. The first chess application written against these instructions was never asked it, and it stored the board — derived state that is wrong the moment two copies merge.

Applies to every shape · not checked by anything · SHAPE-FIRST in Constraints

2. Declare the tables

Every table is in schema.sql, and nowhere else.

sql
-- Beach trip · shape: solo
--
-- The decision: one family's packing list, kept on one phone. Nobody else
-- adds to it and no second copy's changes need keeping, so nothing is shared:
-- ordinary tables, written with the kit, keys and all.

CREATE TABLE IF NOT EXISTS items (
      id INTEGER PRIMARY KEY,
      kind TEXT NOT NULL,
      what TEXT NOT NULL,
      packed INTEGER NOT NULL DEFAULT 0
    );

CREATE TABLE IF NOT EXISTS trip (
      id INTEGER PRIMARY KEY,
      dates TEXT NOT NULL
    );

SCHEMA-FILE

Every table is in schema.sql

Declare every table in one file named schema.sql, each with CREATE TABLE IF NOT EXISTS. Create no table anywhere else — not in index.html, not in JavaScript.

Why. schema.sql runs first on every open and its shape is sealed with the file; that record is what protects a person's data when a later version changes the application.

Applies to every shape · not checked by anything · SCHEMA-FILE in Constraints

3. Give it something to show

A first open should not be an empty shell. The seed rows are in a <script type="application/sql"> block in index.html, written so a second open adds nothing — here, INSERT OR IGNORE with fixed ids.

SEED-IDEMPOTENT

Seed rows are idempotent, and local

Put a few example rows in a <script type="application/sql"> block in index.html so the application is not an empty shell on first open, written so a second open adds nothing (INSERT … WHERE NOT EXISTS, or INSERT OR IGNORE with a fixed id). Never seed from schema.sql. Seed only local tables this way; see SHARED-SEED-THROUGH-SURFACE for shared ones.

Why. The block runs on every open, so a seed that is not idempotent duplicates itself each time the document is opened.

Applies to every shape · not checked by anything · SEED-IDEMPOTENT in Constraints

4. Draw it with the kit

<dai-rows> runs a query and repeats its template for each row; <dai-value> shows one number; <dai-form> turns a form's fields into a statement's parameters; a data-run attribute on a control runs a statement when it is used. Every one of them redraws the page after a write.

The whole index.html
html
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="description" content="Everything for the beach, ticked off as it goes in the bag" />
  <meta name="dai:does" content="Keeps everything for the trip in one list you can add to" />
  <meta name="theme-color" content="#f2f8fb" />
  <meta name="dai:does" content="Tick each thing off as it goes in the bag" />
  <meta name="dai:does" content="Shows what is still missing before you leave" />
  <title>Beach trip</title>
  <link rel="stylesheet" href="./app.css" />
</head>
<body>
  <!--
    A packing list for one trip. Made for the website's front page: somebody
    should look at this and think of the trip they have coming up.

    No JavaScript of its own; the kit does the wiring, and the schema and
    starting data are SQL in the document.
  -->
  <script type="application/sql">
      -- seed rows; the tables are declared in schema.sql
    INSERT OR IGNORE INTO items (id, kind, what, packed) VALUES
      (1, 'Beach', 'Sunscreen', 1),
      (2, 'Beach', 'Towels ×4', 1),
      (3, 'Beach', 'Bucket and spade', 0),
      (4, 'Beach', 'Umbrella', 0),
      (5, 'Clothes', 'Swimsuits', 1),
      (6, 'Clothes', 'Hats', 0),
      (7, 'Clothes', 'Sandals', 0),
      (8, 'Kids', 'Floaties', 1),
      (9, 'Kids', 'Snacks for the car', 0),
      (10, 'Kids', 'Bear', 0);

    INSERT OR IGNORE INTO trip (id, dates) VALUES (1, 'Sat 14 - Sun 22');
  
    </script>

  <main>
    <header>
      <!--
        The dates, which are the one thing on this page that is about a
        particular trip rather than about packing. Typed over, not decoration:
        a list somebody cannot date is a list about somebody else's holiday.
      -->
      <dai-rows query="SELECT id, dates FROM trip WHERE id = 1">
        <template>
          <input
            class="eyebrow"
            aria-label="When the trip is"
            data-text="dates"
            data-run="UPDATE trip SET dates = :typed WHERE id = 1"
          />
        </template>
      </dai-rows>
      <h1>Beach trip</h1>
      <p class="progress">
        <dai-value query="SELECT count(*) FROM items WHERE packed = 1"></dai-value>
        of
        <dai-value query="SELECT count(*) FROM items"></dai-value>
        packed
      </p>
    </header>

    <section>
      <h2>Beach</h2>
      <dai-rows query="SELECT id, what, packed FROM items WHERE kind = 'Beach' ORDER BY packed, id">
        <template>
          <label class="item">
            <input type="checkbox" data-run="UPDATE items SET packed = 1 - packed WHERE id = :id" />
            <span class="box" aria-hidden="true"></span>
            <span class="what" data-text="what"></span>
            <span class="in" data-when="packed">packed</span>
          </label>
        </template>
      </dai-rows>
    </section>

    <section>
      <h2>Clothes</h2>
      <dai-rows query="SELECT id, what, packed FROM items WHERE kind = 'Clothes' ORDER BY packed, id">
        <template>
          <label class="item">
            <input type="checkbox" data-run="UPDATE items SET packed = 1 - packed WHERE id = :id" />
            <span class="box" aria-hidden="true"></span>
            <span class="what" data-text="what"></span>
            <span class="in" data-when="packed">packed</span>
          </label>
        </template>
      </dai-rows>
    </section>

    <section>
      <h2>Kids</h2>
      <dai-rows query="SELECT id, what, packed FROM items WHERE kind = 'Kids' ORDER BY packed, id">
        <template>
          <label class="item">
            <input type="checkbox" data-run="UPDATE items SET packed = 1 - packed WHERE id = :id" />
            <span class="box" aria-hidden="true"></span>
            <span class="what" data-text="what"></span>
            <span class="in" data-when="packed">packed</span>
          </label>
        </template>
      </dai-rows>
    </section>

    <dai-form run="INSERT INTO items (kind, what) VALUES (:kind, :what)">
      <select name="kind" aria-label="Group">
        <option>Beach</option>
        <option>Clothes</option>
        <option>Kids</option>
      </select>
      <input name="what" placeholder="Don't forget…" required maxlength="80" />
      <button>Add</button>
    </dai-form>

    <footer>
      <dai-save>Save</dai-save>
    </footer>
  </main>

  <script type="module" src="./dai-kit.js"></script>
</body>
</html>

KIT-FIRST

Prefer the kit for local tables

Use dai-kit for local tables: <dai-rows>, <dai-value>, <dai-form>, <dai-attach> and <dai-save>, with <script type="module" src="./dai-kit.js"></script> at the end of the body. Reach for JavaScript only for what the kit cannot express. Do not write dai-kit.js yourself or put it in the bundle: the compiler adds it to every container.

Why. The kit removes the dangerous sinks by construction — no statement built from a value, text-only rendering — and does the querying, rendering and redrawing a hand-written application gets wrong.

Applies to every shape · not checked by anything · KIT-FIRST in Constraints

NO-SAVE-BUTTON

Saving is automatic

Build no Save button, no "saved" indicator and no dirty flag. Under a host every write is saved as it happens (window.dai.autosaves is true). A page that uses the kit includes <dai-save> once, at the bottom: it appears only when the file was opened straight in a browser with no host, where saving takes a tap, and hides itself everywhere else. A page without the kit that must save with no host calls window.dai.saveDatabase(db) from a control shown only when window.dai.autosaves is false. A shared document needs no such control: with no host it cannot write its shared tables at all (SHARED-NEEDS-HOST).

Why. A save control under a host is a control that does nothing, and a person who presses it learns to distrust the rest.

Applies to every shape · not checked by anything · NO-SAVE-BUTTON in Constraints

5. Say what it is

The description and the three dai:does lines in the <head> are what a person sees on the card before they open the file. icon.svg is its icon on a home screen.

DESCRIBE-ON-CARD

One line, and three things it does

In the <head> of index.html: <meta name="description" content="…"> — what it is for, under 60 characters, the way a store page puts a line under an app's name; and exactly three <meta name="dai:does" content="…"> lines, each under 90 characters, starting with a verb, saying what somebody would tell a friend it does. Three, or none. Beside them, <meta name="theme-color" content="…"> with the application's own background color.

Why. These are the whole of what a person sees on the card before they decide to open the document; two lines is a card with a gap in it.

Applies to every shape · not checked by anything · DESCRIBE-ON-CARD in Constraints

6. Check it, build it, open it

bash
npx dai check ./examples/packing-list
npx dai build ./examples/packing-list -n "Beach trip"

check runs the same checks the MCP server and the website's paste page run — the network, browser storage, and, for a shared application, the shared-table constraints. Open the file it builds in the DAI opener, or double-click it.

Next

A list two people add to is a different shape. Choose a shape says which, and Your first two-player app builds a session.

Released under the MIT License. Dynamic Application Interface standard.