EmofyEmofy Developers
Domains

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}/comment

Messaging

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_personal scope is gated by a dedicated PersonalWriteGuard (common/guards/personal-write.guard.ts, PRD-151).
  • Tasks support idempotent creation via the Idempotency-Key header — 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 projection

This loop is the product. Everything an institution-specific EMA does ultimately expresses itself through these channels.

Next

On this page