# Permissions and write-gating

Two independent settings decide who can write what in your app: the
glow's **auth mode** (a deploy/console setting — is this glow open to the
world, restricted to an allowlist, or open self-serve?) and each
signed-in user's **permission** (`admin` / `user` / `viewer`). This guide
is about designing a UI around the *combination* of the two — which is
where a static-page mental model tends to go wrong.

The reference documents each layer on its own: the
[auth-mode surface matrix](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows)
says what every surface does for each mode-and-sign-in state, and
[`backlit.auth`](https://sdk.backlit.run/sdk.md#backlitauth) plus
[`auth.me()`](https://sdk.backlit.run/sdk.md#authme-promisebacklituser--null)
carry the `permission` field. This guide links to those rather than
restating them, so it can't drift out of sync with the rules they define.

## Two dials, not one

- **Auth mode** is set on the glow, not in your app code. It decides
  whether a visitor arrives signed in, arrives anonymous, or can't arrive
  at all until they sign in. The three modes and exactly which surfaces
  each mode's visitors can reach are the
  [Public, Private, and Accounts matrix](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows)
  — read that as the authority; this guide won't reproduce its cells.
- **Permission** is a property of a *signed-in user*, surfaced as the
  `permission` field on
  [`auth.me()`](https://sdk.backlit.run/sdk.md#authme-promisebacklituser--null).
  It's what separates an editor from a read-only viewer *after* they're
  signed in.

The trap is treating "signed in" as "can write." It isn't: a signed-in
`viewer` reaches the app but still can't write shared data. Auth mode
gets the user *in*; permission decides what they can *do*.

## The server is the gate — your UI just mirrors it

Every write surface enforces its own rule server-side, and a blocked
write throws a specific `code`. You don't invent the gate; you read it
off the reference and *reflect* it in the UI so the user isn't surprised
by a rejection. The authoritative gate for each store lives with that
store:

- [`backlit.data`](https://sdk.backlit.run/sdk.md#backlitdata--glow-shared-keyvalue)
  — the glow-shared store. Its writes need a signed-in non-`viewer`.
- [`backlit.records`](https://sdk.backlit.run/sdk.md#backlitrecords--public-read-owner-write-store)
  — public-read, owner-write. Any signed-in user (a `viewer` included)
  can create and edit *their own* entries.
- [`backlit.capture`](https://sdk.backlit.run/sdk.md#backlitcapture--write-only-capture-store)
  — write-open, admin-only read. Anyone (even anonymous) can write a
  capture; only an `admin` can read them back.

The two error codes you'll design against are the `forbidden` and
`unauthenticated` rows in the
[error-code table](https://sdk.backlit.run/sdk.md#error-codes) — one
means "signed in, but not allowed," the other "no user session at all."
Consult the table for which call raises which; the point here is only
that a good UI never *lets* the user trip them.

## The recipe: pick the store by who's allowed to write

Instead of memorizing the matrix, run each thing your app writes through
two questions — and a default:

1. **Can a signed-out visitor write it?** If yes, that's a
   [`capture`](https://sdk.backlit.run/sdk.md#backlitcapture--write-only-capture-store)
   — a contact form, a telemetry ping, an anonymous submission the page
   can't read back. It's the only store whose *create* works with no
   session.
2. **Should each signed-in user own their own entries, viewers
   included?** If yes, that's
   [`records`](https://sdk.backlit.run/sdk.md#backlitrecords--public-read-owner-write-store)
   — comments, posts, profiles. A freshly self-served Accounts `viewer`
   can post to `records` but *not* to shared `data`, which makes `records`
   the natural home for user-generated content on an open glow.
3. **Otherwise it's shared state everyone sees but only privileged users
   edit** — that's
   [`data`](https://sdk.backlit.run/sdk.md#backlitdata--glow-shared-keyvalue),
   gated to a signed-in non-`viewer`.

Choosing the right store *is* the permission design. If you find yourself
wishing a `viewer` could write shared `data`, you probably wanted
`records`.

## Gate the control, not just the call

Because `permission` is readable before any write, branch your UI on it
so a user never sees a control they'd be rejected for using. The
canonical move is to disable the write affordance for a `viewer`:

```js
const me = await backlit.auth.me();

// Anonymous visitor: no user session at all.
if (me === null) {
  saveBtn.hidden = true;
  // On an Accounts glow you can offer sign-in here; see below.
} else {
  // Signed in — but a viewer can't write shared data. Reflect that.
  const canWriteShared = me.permission === "user" || me.permission === "admin";
  saveBtn.disabled = !canWriteShared;
  adminPanel.hidden = me.permission !== "admin"; // capture review, user list, …
}
```

This is a *mirror*, not the enforcement — the server still rejects a
`viewer`'s shared-data write no matter what your buttons do (see
[`auth.me()`](https://sdk.backlit.run/sdk.md#authme-promisebacklituser--null)
for why `permission` exists to branch ahead of the write). Reflecting it
in the UI is purely so the user isn't handed a button that will fail.

## Anonymous visitors and `auth.login()`

On a **Private** glow you never handle a signed-out state inside your
page — a visitor without a session is redirected to sign in before your
JavaScript runs. On a **Public** or **Accounts** glow your code *does*
run for anonymous visitors, so `auth.me()` returning `null` is a real
state to design for. What sign-in does from there differs by mode, and
the [auth-mode matrix](https://sdk.backlit.run/sdk.md#public-private-and-accounts-glows)
is the authority on it — in short, an Accounts glow lets any visitor
create an account, while a Public glow only admits allowlisted users
(e.g. an admin unlocking privileged controls).

```js
const me = await backlit.auth.me();
if (me === null) {
  // Anonymous on a Public/Accounts glow. Offer sign-in and route
  // anonymous input through capture in the meantime.
  loginBtn.onclick = () => backlit.auth.login(location.pathname);
}
```

Use [`auth.login()`](https://sdk.backlit.run/sdk.md#backlitauth)
to send the visitor to sign-in; anything they need to submit *before*
signing in belongs in
[`capture`](https://sdk.backlit.run/sdk.md#backlitcapture--write-only-capture-store),
the one store an anonymous session can write.

## A worked shape: an open glow with mixed access

Picture an Accounts glow — a community board. Anyone can read it; a
signed-out visitor can leave a "contact the organizers" note; a signed-in
member posts entries others can see but not edit; an admin curates a
shared banner and reviews the contact notes. That maps cleanly onto the
three stores and the two dials:

- The **contact note** is anonymous-writable → `capture.create`. No
  session required, and only an admin can read the notes back.
- A **member's post** is owned content a `viewer` may create →
  `records.*`. Self-served accounts land as `viewer`, and that's fine
  here because `records` doesn't gate creation on the shared-data
  permission.
- The **shared banner** everyone sees but only staff edit → `data.*`,
  with the edit control gated on `permission` being `user`/`admin`.

Each store's own reference section
([`data`](https://sdk.backlit.run/sdk.md#backlitdata--glow-shared-keyvalue),
[`records`](https://sdk.backlit.run/sdk.md#backlitrecords--public-read-owner-write-store),
[`capture`](https://sdk.backlit.run/sdk.md#backlitcapture--write-only-capture-store))
has the exact method signatures and the precise gate; this guide's job is
only to help you pick the right one and reflect its gate in the UI.
