Files
solopm-server/plans/06-projects.md
T

3.7 KiB

06 — Projects, Members, Labels, Dependencies

The project is the permission boundary and the container for everything else. Fields and enums in 02-data-model.md; routes in 04-api.md.

Project lifecycle

  • Create: requires title; creator becomes creator_id, owner_id, and the sole owner-role member; seed the 6 default labels (enhancement, bug, chore, task, feature, documentation) with palette colors
  • Status field: backlog → planned → in_progress → completed | canceled — transitions are not enforced as a state machine (any status settable), but completed_at automation is:
    • status → completed sets completed_at = now()
    • leaving completed clears it
  • Priority: optional, low…frantic; UI color mapping in 14-design-system.md
  • git_repository_url: normalized on save (trim, strip trailing .git and /); used to route incoming webhooks (11-integrations.md)
  • Update/delete emit project.updated / project.status_changed / project.deleted; delete is soft and owner-only
  • Project description is markdown with mention/#ref support (08-collaboration.md)

Ownership & membership

  • Exactly one owner per project — invariant maintained in the service (projects.owner_id ↔ the single role='owner' membership row)
  • Transfer: PUT /projects/{id}/owner (owner only, target must be an existing member); atomically swap roles; emit project.ownership_transferred
  • Add member: owner picks an existing instance user (GET /users) + role; emits project.member_added → triggers added_to_project notification; no email-invitation flow in v1 (small instance, users already exist)
  • Remove member: owner only; removing a member clears their assignee slots in the project? No — assignments persist (history matters); UI shows them as non-members. Their watches are deleted.
  • Owner cannot leave/be removed without transferring first (409)
  • Role change: owner only; only member role assignable via PATCH (ownership moves only through transfer)

Favorites

  • PUT/DELETE /projects/{id}/favorite toggles a project_favorites row
  • GET /projects sorts favorites first (by favorited_at desc), then the rest by latest activity
  • Favorites are personal — no events, no audit entries

Labels

  • Project-scoped; name unique per project (case-sensitive match on citext-free text, uniqueness enforced by partial unique index)
  • CRUD is member-accessible except delete (owner only); color must be a valid #rrggbb
  • Soft delete; a deleted label disappears from issues immediately (join filters deleted_at IS NULL) but rows persist until cull
  • Emit label.created/updated/deleted

Project dependencies

  • Edge project_id depends on depends_on_project_id (i.e. blocked by it)
  • Both endpoints must be projects the caller is a member of
  • Cycle detection before insert: DFS from depends_on_project_id following existing depends_on edges; reaching project_id → 409 cycle_detected
  • Project detail shows both directions: "depends on" and "blocks" (reverse lookup)
  • Emit project.dependency_added/removed

Activity feed

  • GET /projects/{id}/activity: audit_log WHERE project_id = $1 ORDER BY created_at DESC paged; render display text app-side from action_type + previous_values/new_values + actor
  • GET /dashboard/activity: same over project_id IN (my projects), capped at 50 per page
  • Feed entries link to their resource (#N issues resolve via project_issue_id kept in event payloads)
  • Hidden from feeds: pure rank moves (issue.moved with only rank changes) — noise; still audited