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:
- "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.
- "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_emailspopulated. 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_hashcolumn 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.