Appearance
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
IPermissionServiceinterface - 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
IPermissionServiceinterface 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
| Action | Owner | Storyteller | Co-Creator | Player | Viewer |
|---|---|---|---|---|---|
| Manage project settings | Yes | - | - | - | - |
| Manage members | Yes | - | - | - | - |
| Create/edit timeline | Yes | Yes | Yes | - | - |
| Create NPC characters | Yes | Yes | Yes | - | - |
| Create own PC character | Yes | Yes | Yes | Yes | - |
| Create relationships | Yes | Yes | Yes | Own only | - |
| Moderate content | Yes | Yes | - | - | - |
| View content | Yes | Yes | Yes | Published | Published |
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
IPermissionServicebefore 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
IPermissionServiceimplementation