what

Authorization

Control who can create, read, update, and delete each collection — with record ownership, tenant scoping, and field-level rules — from a few lines of what.toml. No policy code, no middleware.

The default: owner-protected

Every collection you never configure gets this policy automatically:

create = "all"      # anyone may create
update = "owner"    # only the record's owner may update
delete = "owner"    # only the record's owner may delete
read   = "all"      # everyone may read

When a record is created, the framework stamps an _owner onto it — user:<id> if the visitor is authenticated, otherwise session:<id>. Update and delete then verify ownership server-side, so a visitor cannot edit or delete someone else's record even by guessing its id. This is real enforcement, not a display filter.

Upgrading from an earlier version? Records created before this feature existed have no _owner and stay editable by anyone (a dev-mode warning flags them). Apps where different visitors intentionally edit shared records should opt out — see shared collections.

Declaring a policy

Add a [collections.<name>] section to what.toml:

[collections.notes]
create = "all"
update = "owner"
delete = "owner, admin"     # the owner OR anyone with the admin role
read   = "owner"            # each visitor sees only their own notes

Each rule is a comma-separated list of terms, matched as OR:

TermMatches
allAnyone, including anonymous visitors
userAny authenticated user
ownerThe record's owner (not valid for create)
noneNo one
<role>A user holding that JWT role, e.g. admin

Reserved words (all, user, owner, none) can't be combined with other terms or used as role names, and mistakes fail loud when the server starts.

Read scoping

When read is anything other than all, the framework forces a WHERE clause onto every fetch of that collection — you don't filter manually. read = "owner" restricts results to the current visitor's records:

<what>
fetch.notes = "local:notes"
</what>

<loop data="#notes#" as="n">#n.title#</loop>   <!-- only YOUR notes -->

This works across SQLite, Cloudflare D1, and Supabase, and it also removes the collection from the whole-store context so it can't leak through an un-directed loop. On a static build (no server, no session) a scoped collection renders empty.

Ownership in templates

#user.owner# holds the current visitor's owner key, so you can show controls only to the owner:

<if note._owner == user.owner>
  <form method="post" action="/w-action/notes/#note.id#?w-action=delete">
    <button>Delete</button>
  </form>
</if>

Multi-tenant scoping

A filter scopes a collection by any field, resolved per request from #user.*# or #session.*#. It's AND-ed into every read and checked on every update/delete by id, so one tenant can neither see nor modify another's rows:

[collections.orders]
filter = "org_id=#user.org_id#"

If the variable can't be resolved (no logged-in user), the collection fails closed — it returns nothing rather than everything.

Field rules

[collections.products]
fields.readonly = ["price"]     # ignored if sent by the client
fields.private  = ["cost"]      # never rendered into templates

readonly fields are stripped from create/update input (the server owns their value); private fields are stripped from records before they reach any template.

They're independent. readonly means unwritable; private means unreadable. A private field can still be set by a client form — it's only hidden from output. A field that must be both server-controlled and hidden (e.g. role, credits) must be listed in both readonly and private.

Shared collections

For a collaborative board, a wiki, or a guestbook where anyone edits anyone's entries, opt out of ownership:

[collections.wiki]
update = "all"
delete = "all"
# or turn ownership off entirely:
# owner = "none"

State mutations

Shared state written with w-set can be role-gated too. A [role] bracket on a data.wired or data.application declaration restricts who may write the key (not just who receives updates):

<!-- application.what -->
data.wired = ["revenue [admin]"]

<!-- only an admin's w-set succeeds; others get 403 -->
<button w-set="wired.revenue += 100">Book revenue</button>