# The example board

A complete app on rastrillo + idear: a shared message board whose roster is
idear's. It is the worked reference `../SKILL.md` points at, and it is part
of the `amadan.net/rastrillo/idear` module rather than a submodule — a
nested module would need a `replace` directive pointing at its own parent,
which is exactly the thing an app must never write.

## Run it

```sh
BOARD_SEED=1 go run ./example -addr 127.0.0.1:8080 -db /tmp/board.db
```

Then sign in at <http://127.0.0.1:8080/signin> as one of the seeded
accounts, all with the password `demo-password`:

| Address             | Role   | What you can do                                   |
| ------------------- | ------ | ------------------------------------------------- |
| `ada@example.test`  | Owner  | everything, including transferring ownership      |
| `kim@example.test`  | Admin  | invite and manage **Members** only                |
| `sam@example.test`  | Member | read the roster; post; nothing else               |

Three accounts at three roles, because a role gate you cannot click on is a
role gate nobody checks. Sign in as Kim and the role selector offers Member
and nothing else; sign in as Sam and the members page offers no controls at
all.

`BOARD_ORIGIN` sets the external origin (it decides the CSRF check and the
cookie attributes); `BOARD_NAME` sets the instance's display name on the
public invitation page. Both default loudly.

There is no mail server here, so `HandlerConfig.Deliver` is nil and idear
puts the invitation **link itself** in the flash notice shown to the admin
who minted it. Copy it out of the page. A deployed app sets `Deliver` and
keeps the token out of the browser entirely.

## What to read

| File           | What it shows                                                       |
| -------------- | ------------------------------------------------------------------- |
| `app.go`       | the whole wiring, in the order it has to happen, with the reasons    |
| `models.go`    | the app's own `User`, `BootSchema`, and a seed through the real flows |
| `render.go`    | the two idear render callbacks, and `idear.TokenFrom` in `RenderSignup` |
| `handlers.go`  | `idear.From(r)`, and an app route gated on Admin                     |
| `app_test.go`  | the whole flow through real HTTP — the thing to copy                 |

Four things in here are load-bearing rather than stylistic, and each has a
comment at the site saying so:

- **`/` is behind `Require`.** Under the password plugin, deactivation is
  enforced per request by `Require`, **not** at sign-in — `password.Signin`
  runs Lookup → Verify → mint with no idear involvement. An ungated landing
  page is a page a removed member can still read.
- **`POST /signup` is wrapped in `rs.CarryToken`.** Without it the
  invitation token never reaches admission and every invited signup is
  refused.
- **One 404 renderer**, bound once and handed to both `idear.Config.NotFound`
  and chi's own `NotFound`. Two of them is a membership oracle, and it is
  the one misconfiguration idear cannot detect at runtime.
- **`renderSignup` seeds its hidden `invite` field from
  `idear.TokenFrom(r)`.** `password.PageData` has nowhere to carry a token,
  so without this a signup that fails validation re-renders a form with an
  empty field and the invitee's *second* attempt is refused. If you rewrite
  this page — the likeliest thing to do to it — keep that line.
