---
title: "RFC: 3-state permission model"
url: https://mdfy.app/El2-Kzy6
updated: 2026-05-14T18:15:49.480Z
source: "mdfy.app"
---
# 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.
