---
title: "RFC: 3-state permission model"
url: https://memory.wiki/El2-Kzy6
updated: 2026-05-14T18:15:49.480Z
hub: https://memory.wiki/hub/demo
concept_count: 11
source: "memory.wiki"
---
# RFC: 3-state permission model

> Status: shipped in v6. Logged here as the historical record.

## Why a 3-state model

For v5 we had two states: "draft" (private) and "public". The model worked for the majority case but produced friction in two scenarios:

1. **"I want to share this with one specific person, not the world."** Users either had to make it public (then send the URL, hoping nobody else found it) or keep it private (then write a separate doc to share). Neither was right.
2. **"Draft" was confusing.** The word implied "unfinished." Users would keep docs in "draft" indefinitely because they didn't think of them as draft work — they thought of them as private work.

## The three states

**Public** — anyone with the URL can view. The default for docs that go in a hub.

**Restricted** — has `allowed_emails` populated. Only users whose Supabase Auth email matches an entry can view. Unauthenticated visitors get a 403 with a "request access" affordance (Pro: actually wires the request; Free: shows the owner's email).

**Private** — `is_draft = true`. Only the owner can view. No request-access affordance.

## Transitions

- Public → Restricted: add an email. The doc is no longer in the public hub view but it's still browsable to the listed users.
- Restricted → Public: clear `allowed_emails`. Doc becomes hub-visible.
- Public/Restricted → Private: set `is_draft = true`. Doc disappears from everywhere except the owner's sidebar.
- Private → Public: clear `is_draft`. Doc lands in the public hub.

## Edge cases

- **Public doc with `allowed_emails` populated.** Treat as restricted. The doc is not in the public listing; only listed emails can view.
- **Private doc with `allowed_emails`.** The emails are ignored — private always wins.
- **Owner email in `allowed_emails`.** Filtered out in the API response so it doesn't show as "shared with myself."

## What we explicitly chose against

- **A four-state model with "team-only" as a separate level.** Team isn't a separate plan yet; folding it in now would lock in a structure that we'd revisit.
- **Per-doc password.** The legacy `password_hash` column still exists but the create API ignores it. Will be dropped in a future migration.

## Migration path

Existing "draft" docs in v5 → "private" in v6. No data change; the column rename happened in the UI only.

## What the rebrand does NOT change

URLs. A doc that was at `mdfy.app/d/abc123` in v5 is still at `mdfy.app/d/abc123` in v6 regardless of which permission state it's in. The URL is the unit; permissions decide who can use it.


---

## Concepts in this document
- **3-state permission model** _(concept)_
  The core design decision replacing a 2-state system with Public, Restricted, and Private states to resolve sharing friction.
- **Public state** _(concept)_
  Anyone with URL can view; default for hub docs; enabled by clearing allowed_emails.
- **Restricted state** _(concept)_
  Selective sharing to specific emails via allowed_emails list; unauthenticated users see request-access affordance.
- **Private state** _(concept)_
  Owner-only visibility via is_draft flag; no request-access affordance; hides doc from hub and shared views.
- **Two-state model pain points** _(concept)_
  Inability to share with individuals and semantic confusion of 'draft' motivated the redesign.
- **is_draft flag** _(entity)_
  Boolean field that marks a doc as Private; replaces v5's draft/public dichotomy.
- **v5 to v6 migration** _(concept)_
  All existing draft docs automatically become Private; UI-only column rename with no data changes.
- **allowed_emails field** _(entity)_
  Database column storing list of email addresses granted access in Restricted state.
- **State transitions** _(concept)_
  Rules for moving between Public, Restricted, and Private states via field mutations.
- **URL stability** _(concept)_
  URLs remain unchanged across permission state transitions; permissions are orthogonal to routing.
- **Supabase Auth integration** _(entity)_
  Authentication system used to validate allowed_emails matches for Restricted doc access.

## Concept relations (within this doc's concepts)
- **3-state permission model** resolves **Two-state model pain points**
- **Restricted state** uses **allowed_emails field**
- **Private state** uses **is_draft flag**
- **Restricted state** validates via **Supabase Auth integration**
- **State transitions** defines **3-state permission model**
- **v5 to v6 migration** maps draft to **Private state**

_Hub canonical:_ https://memory.wiki/hub/demo
_Concept digest:_ https://memory.wiki/raw/hub/demo?digest=1&compact=1
