One row per project with the portal extension enabled (chain V10).
Access mode (V12)
access_mode answers who may resolve the page at all, and it is one enum
rather than a pair of booleans because the quadrants aren't orthogonal —
"secret slug that search engines index" and "human-readable slug where
possession is the authorization" are both nonsense, and booleans let an
admin build them.
link(default, and what every portal was before this existed) — the slug IS the grant: 16 CSPRNG bytes, possession is authorization, rotatable to revoke, never indexed.members— any signed-in site user. The slug keeps its CSPRNG shape because unguessability is free and there is nothing to gain by throwing it away; it is simply no longer the thing being checked.public— anyone, indexable, human-chosen slug.
Participation
submit_access and comment_access are separate because the risks are
different in kind: a submission is invisible until staff act on it, while
a comment is live surface area the moment it lands. Collapsing them into
one control forces a choice between "anonymous comments go live" and
"anonymous submission is as gated as commenting", and both are wrong.
Summary
Functions
The three access modes, most restrictive first.
Changeset — slug/project are server-generated, never user input.
A fresh access slug: 16 CSPRNG bytes, url-base64 (~22 chars) — the entropy floor from the portal security panel (find #9).
The participation values a write policy may take.
Slugs a public board may not take, because they collide with paths a host is likely to mount beside it or with things a reader would misread as system pages.
A starting public slug derived from a project name — a suggestion for the admin to accept or edit, never applied silently.
Types
Functions
@spec access_modes() :: [String.t()]
The three access modes, most restrictive first.
Changeset — slug/project are server-generated, never user input.
@spec generate_slug() :: String.t()
A fresh access slug: 16 CSPRNG bytes, url-base64 (~22 chars) — the entropy floor from the portal security panel (find #9).
@spec participation_levels() :: [String.t()]
The participation values a write policy may take.
@spec reserved_slugs() :: [String.t()]
Slugs a public board may not take, because they collide with paths a host is likely to mount beside it or with things a reader would misread as system pages.
A starting public slug derived from a project name — a suggestion for the admin to accept or edit, never applied silently.