Skip to content

14. Announcements & Notifications ​

In this chapter, we will build section communication channels using CommunicationService, enforce strict audience separation between students and parents, and route SDK domain events into an email/push notification dispatcher.


1. Implementing Communication Routes (src/routes/communication.ts) ​

Create src/routes/communication.ts:

ts
import { Hono } from "hono";
import { commsService } from "../lib/services";
import { requireAuth } from "../lib/middleware";
import { handleDomainError } from "../lib/errors";

export const commsRoutes = new Hono();

// 1. Post a Section Announcement
commsRoutes.post(
  "/sections/:sectionId/announcements",
  requireAuth,
  async (c) => {
    const actorId = c.get("actorId");
    const sectionId = c.req.param("sectionId");
    const { title, body, audience } = await c.req.json();

    try {
      const announcement = await commsService.postAnnouncement(
        {
          sectionId,
          title,
          body,
          audience: (audience as "students" | "guardians") || "students",
        },
        { actorId }, // Requires communication.postAnnouncement or postGuardianAnnouncement
      );

      return c.json({ ok: true, announcement }, 201);
    } catch (err) {
      return handleDomainError(c, err);
    }
  },
);

// 2. View Announcements (Audience Segregated)
commsRoutes.get(
  "/sections/:sectionId/announcements",
  requireAuth,
  async (c) => {
    const actorId = c.get("actorId");
    const sectionId = c.req.param("sectionId");

    try {
      const list = await commsService.listAnnouncements(sectionId, { actorId });
      return c.json({ ok: true, announcements: list });
    } catch (err) {
      return handleDomainError(c, err);
    }
  },
);

// 3. Create Discussion Thread
commsRoutes.post("/sections/:sectionId/threads", requireAuth, async (c) => {
  const actorId = c.get("actorId");
  const sectionId = c.req.param("sectionId");
  const { title, contentId, firstPost } = await c.req.json();

  try {
    const thread = await commsService.createThread(
      {
        sectionId,
        title,
        contentId: contentId ?? undefined,
        firstPost: { authorId: actorId, body: firstPost },
      },
      { actorId },
    );

    return c.json({ ok: true, thread }, 201);
  } catch (err) {
    return handleDomainError(c, err);
  }
});

// 4. Reply to Discussion Thread
commsRoutes.post("/threads/:threadId/reply", requireAuth, async (c) => {
  const actorId = c.get("actorId");
  const threadId = c.req.param("threadId");
  const { body } = await c.req.json();

  try {
    const post = await commsService.reply(
      threadId,
      { authorId: actorId, body },
      { actorId },
    );

    return c.json({ ok: true, post }, 201);
  } catch (err) {
    return handleDomainError(c, err);
  }
});

2. Bridging Domain Events to Email / Push Notifications ​

The SDK includes bridgeEventBusToNotificationSink to automatically turn SDK events into notification dispatches:

Configure the bridge in src/lib/services.ts:

ts
import { bridgeEventBusToNotificationSink } from "discendo-sdk/communication";
import type { NotificationSink } from "discendo-sdk/communication";

// 1. Implement your notification delivery sink
const emailNotificationSink: NotificationSink = {
  dispatch: async (event) => {
    if (event.type === "gradePosted") {
      const user = await repos.users.findById(event.userId);
      console.log(
        `[Email Mock] Sending grade update to ${user?.id}: Score ${event.score}`,
      );
    } else if (event.type === "announcementCreated") {
      console.log(
        `[Push Mock] Broadcasting announcement "${event.title}" to section ${event.sectionId} (audience: ${event.audience})`,
      );
    }
  },
};

// 2. Connect the bridge
bridgeEventBusToNotificationSink(bus, emailNotificationSink, {
  resolveContentId: async (submissionId) => {
    // Resolve contentId from submission if needed
    return null;
  },
});

Because events are fire-and-forget, notification delivery never delays the HTTP response of an instructor posting a grade or announcement.


Next Steps ​

Next, we will track student attendance roll calls and export reports in Chapter 15: Attendance & Reports.

Released under the GPL-3.0 License.