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).

Privateis_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.