Scope
Per-mentorship message thread. Polling v1 — no websockets (frontend polls with refetchInterval: 5000 while the thread is open).
Data model
public sealed class ThreadMessage
{
public Guid Id { get; set; }
public Guid MentorshipId { get; set; }
public Guid SenderId { get; set; }
public string Body { get; set; }
public DateTimeOffset SentAt { get; set; }
}
// plus LastReadAt per participant (e.g. MentorshipParticipantState) for unread counts
Endpoints (participants only)
| Method |
Path |
Notes |
| GET |
/mentorship/{id}/messages?after= |
incremental fetch for polling; after = cursor (message id or timestamp); capped page size |
| POST |
/mentorship/{id}/messages |
body length-capped (~2000 chars), trimmed, non-empty |
| POST |
/mentorship/{id}/read |
stamp caller's LastReadAt |
Unread counts (per mentorship, for the dashboard) derived from LastReadAt — expose on the GET /mentorship/mine payload from #10 rather than a separate endpoint.
Rules
- Only
Active mentorship participants can post; reading history stays allowed after conclusion.
- Plain text only in v1 — no HTML rendering path at all.
Error codes
mentorship.not_found, mentorship.forbidden, mentorship.invalid_message, mentorship.invalid_state
Dependencies
Blocked by #10.
Acceptance criteria
Conventions (project-wide, non-negotiable)
- .NET 9, records for immutable shapes, file-scoped namespaces, primary constructors where they read well. Minimal-API endpoints grouped per module via
IEndpointModule.MapEndpoints.
Result<T> (SharedKernel) instead of exception-driven control flow. Endpoint results map failures to ProblemDetails with the stable error codes listed above — the frontend keys off them.
- Module owns its EF Core
DbContext mapped to its own Postgres schema. Modules never reference each other's internals — cross-module needs go through a public contract interface or an in-process domain event (IEventPublisher).
- All external calls (GitHub, Stripe, Brevo, Cloudinary, Meilisearch) behind interfaces owned by the consuming module.
- Every list endpoint paginated (offset is fine). xUnit tests in
tests/CommunityPro.Tests/<Module>/ following the existing harness patterns (see Members/MembersTestHarness.cs).
Scope
Per-mentorship message thread. Polling v1 — no websockets (frontend polls with
refetchInterval: 5000while the thread is open).Data model
Endpoints (participants only)
/mentorship/{id}/messages?after=after= cursor (message id or timestamp); capped page size/mentorship/{id}/messages/mentorship/{id}/readLastReadAtUnread counts (per mentorship, for the dashboard) derived from
LastReadAt— expose on theGET /mentorship/minepayload from #10 rather than a separate endpoint.Rules
Activementorship participants can post; reading history stays allowed after conclusion.Error codes
mentorship.not_found,mentorship.forbidden,mentorship.invalid_message,mentorship.invalid_stateDependencies
Blocked by #10.
Acceptance criteria
aftercursor returns strictly newer messages, stable orderingConventions (project-wide, non-negotiable)
IEndpointModule.MapEndpoints.Result<T>(SharedKernel) instead of exception-driven control flow. Endpoint results map failures to ProblemDetails with the stable error codes listed above — the frontend keys off them.DbContextmapped to its own Postgres schema. Modules never reference each other's internals — cross-module needs go through a public contract interface or an in-process domain event (IEventPublisher).tests/CommunityPro.Tests/<Module>/following the existing harness patterns (seeMembers/MembersTestHarness.cs).