Skip to content

ADR-006: Permission System

Status: Accepted Date: 2026-02-07

Context

The platform has granular visibility requirements: different roles see different content, private relationships are hidden from other players, and storytellers have elevated visibility. Permission checks touch nearly every API read and write operation.

Decision

  • Permission checks live in the Application layer behind an IPermissionService interface
  • Fixed role-based permissions for Phase 1
  • Designed for future swappability to configurable per-project permissions
  • Private content is omitted entirely from API responses (no "hidden but hinted" partial visibility)

Rationale

Application Layer (not database-level RLS)

  • Explicit and testable - permission logic is visible in the codebase
  • Easier to debug than PostgreSQL Row-Level Security policies
  • Business rules like "only Storytellers can change status to Published" are naturally expressed in application code
  • RLS can be added later as defense-in-depth if needed

Fixed Roles

  • The project plan defines 5 roles with specific, well-defined permissions
  • No stated requirement for per-project permission customization
  • Fixed roles are simpler to implement, test, and reason about
  • The IPermissionService interface allows swapping to a configurable implementation later without touching feature code

Full Omission of Private Content

  • Most secure approach - no metadata leaks
  • Simpler to implement than partial visibility
  • A Player's relationship map simply has fewer edges; no greyed-out or placeholder nodes

Permission Matrix

ActionOwnerStorytellerCo-CreatorPlayerViewer
Manage project settingsYes----
Manage membersYes----
Create/edit timelineYesYesYes--
Create NPC charactersYesYesYes--
Create own PC characterYesYesYesYes-
Create relationshipsYesYesYesOwn only-
Moderate contentYesYes---
View contentYesYesYesPublishedPublished

Visibility Filtering Rules

  • Timeline items: Players/Viewers only see items with Published status
  • Relationships: Private relationships omitted for everyone except creator + Storyteller + Owner
  • Characters: Private characters visible only to creator + Storyteller + Owner

Interface Design

csharp
public interface IPermissionService
{
    // Access checks (can this user do this action?)
    Task<bool> CanAccessProject(Guid userId, Guid projectId);
    Task<bool> CanEditTimeline(Guid userId, Guid projectId);
    Task<bool> CanCreateCharacter(Guid userId, Guid projectId);
    Task<bool> CanManageMembers(Guid userId, Guid projectId);
    Task<bool> CanViewRelationship(Guid userId, Relationship relationship);

    // Data filtering (what subset can this user see?)
    Task<IEnumerable<T>> FilterByVisibility<T>(Guid userId, Guid projectId, IEnumerable<T> items);
}

Alternatives Considered

  • PostgreSQL Row-Level Security - Enforces at DB layer, very secure. Rejected as primary mechanism due to difficulty debugging and testing. May add later as defense-in-depth
  • Configurable permissions from day one - More flexible but significantly more complex. Needs a permission matrix UI and per-project configuration storage. Deferred to future phase
  • Partial visibility ("hidden but hinted") - Show existence of private content without details. Rejected for simplicity and security

Consequences

  • Every query/command handler must call IPermissionService before returning data
  • Permission service needs access to project membership data (user's role in the project)
  • Integration tests should verify that private content is not leaked across roles
  • Future migration to configurable permissions requires only a new IPermissionService implementation