Core Channels
Feed, messaging, tasks, timeline, and notifications — the institution-agnostic interaction channels at the heart of the platform.
The core domain holds the interaction channels that every institution shares, regardless of type. By ADR-003 these are the only things that live in core; everything domain-specific is an EMA. Crucially, EMAs can write back to the platform only through these channels — feed, messages, and tasks — which is what makes them the load-bearing surface of the product.
Feed
The social activity stream of an organization. A teacher's daily report, a photo, an announcement card — all land in the feed.
- Posts carry content plus typed attachments; attachment type and size limits are derived per category (PRD-228).
- Visibility is enforced fail-closed: a private post is never org-wide by default (PRD-190).
- EMAs contribute to the feed by posting feed cards.
Representative routes (internal surface):
GET /api/feed # list posts in the active org
POST /api/feed # create a post (requires feed:create)
POST /api/feed/{id}/commentMessaging
One-to-one and group conversations. Messaging is Convex-first: Convex is the live datastore for real-time delivery and presence, and Postgres serves as the compliance archive (ADR-025).
- Real-time updates flow over Convex; the backend owns mutation authorization and the archival mirror.
- The mirror is monitored — a silent mirror outage is a known failure mode that is explicitly guarded.
EMAs send messages through a scoped channel rather than touching the message tables directly.
Tasks
Actionable items, either org tasks or personal tasks.
- Personal-task writes are a high-risk capability: the
tasks:write_personalscope is gated by a dedicatedPersonalWriteGuard(common/guards/personal-write.guard.ts, PRD-151). - Tasks support idempotent creation via the
Idempotency-Keyheader — see API conventions.
GET /api/v1/tasks?limit=5 # developer (v1) surface
POST /api/v1/tasks # requires tasks:create(This is the same endpoint used in the Developer Getting Started walkthrough.)
Why personal-task writes are guarded separately
A personal task is one with no org (orgId IS NULL), owned by a single
user. The PersonalWriteGuard ensures a caller with tasks:write_personal can
only write to their own personal task space — it can't be used to create or
mutate tasks belonging to other users, which a generic tasks:write check
wouldn't catch.
Timeline
A per-entity activity log — the chronological record of what happened to a task, a child, or a conversation. The timeline is how the product turns discrete events into a narrative the user can scroll.
Notifications
The delivery domain. When something happens in a core channel, notifications fan out to the right users across channels (in-app, push, email digest). Delivery is durable: notifications are dispatched via the transactional outbox, so they fire if and only if the originating change committed, and never twice.
Notification permissions (notifications:*) are user-scoped — they bypass
the org requirement in the permission guard,
because a user manages their own notification state.
How the channels combine: the daily loop
teacher posts daily report ─▶ FEED
│
└─▶ outbox event ─▶ NOTIFICATION ─▶ parent
│
parent replies ─▶ MESSAGING
│
activity ────────▶ TIMELINE + passport projectionThis loop is the product. Everything an institution-specific EMA does ultimately expresses itself through these channels.