Skip to main content

Overview

The reminder system helps caregivers stay on schedule with feeds, diaper changes, medications, and custom activities. Reminders can be time-based or event-based, with support for quiet hours and snooze options.

Schema Structure

convex/schema.ts
reminderRules: defineTable({
  babyId: v.id("babyProfiles"),
  category: v.string(),
  title: v.string(),
  triggerType: v.string(),
  triggerConfig: v.optional(v.any()),
  enabled: v.boolean(),
  quietHoursStart: v.optional(v.number()),
  quietHoursEnd: v.optional(v.number()),
  snoozeOptions: v.optional(v.any()),
  createdAt: v.string(),
})
  .index("by_babyId", ["babyId"])
  .index("by_babyId_category", ["babyId", "category"])

Reminder Fields

category
string
required
Category of the reminder: "feed", "diaper", "medicine", "vaccine", or "custom"
title
string
required
Display title for the reminder (e.g., “Feed Baby”, “Check Diaper”, “Give Medicine”)
triggerType
string
required
How the reminder is triggered:
  • "fixedTimes": At specific times of day
  • "afterLastEventType": X hours after last activity
triggerConfig
object
required
Configuration specific to the trigger type (see below)
enabled
boolean
required
Whether the reminder is active. Defaults to true
quietHoursStart
number
Hour (0-23) when quiet hours begin. Reminders won’t fire during quiet hours
quietHoursEnd
number
Hour (0-23) when quiet hours end
snoozeOptions
object
Configuration for snooze behavior (minutes, max snoozes, etc.)

Reminder Categories

src/lib/constants.ts
export const REMINDER_CATEGORIES = {
  FEED: "feed",
  DIAPER: "diaper",
  MEDICINE: "medicine",
  VACCINE: "vaccine",
  CUSTOM: "custom",
} as const;

Trigger Types

Fixed Times

Reminder fires at specific times each day:
type FixedTimesTriggerConfig = {
  times: string[];  // Array of "HH:MM" strings
};

await createReminderRule({
  babyId,
  category: "feed",
  title: "Feed Baby",
  triggerType: "fixedTimes",
  triggerConfig: {
    times: ["08:00", "12:00", "16:00", "20:00"],
  },
  enabled: true,
});

After Last Event

Reminder fires X hours after the last activity of a specific type:
type AfterLastEventTriggerConfig = {
  lastEventType: string;     // e.g., "FEED_BOTTLE", "DIAPER"
  intervalHours: number;     // Hours to wait after last event
};

await createReminderRule({
  babyId,
  category: "feed",
  title: "Time for Next Feed",
  triggerType: "afterLastEventType",
  triggerConfig: {
    lastEventType: "FEED_BOTTLE",
    intervalHours: 3,
  },
  enabled: true,
});

Creating Reminder Rules

convex/events.ts
export const createReminderRule = mutation({
  args: {
    babyId: v.id("babyProfiles"),
    category: v.string(),
    title: v.string(),
    triggerType: v.string(),
    triggerConfig: v.optional(v.any()),
    enabled: v.optional(v.boolean()),
    quietHoursStart: v.optional(v.number()),
    quietHoursEnd: v.optional(v.number()),
    snoozeOptions: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    await requireBabyAccess(ctx, args.babyId, user._id);
    const id = await ctx.db.insert("reminderRules", {
      ...args,
      enabled: args.enabled ?? true,
      createdAt: new Date().toISOString(),
    });
    return id;
  },
});

Example: Feed Reminder

import { useMutation } from "convex/react";
import { api } from "../../convex/_generated/api";

const createReminder = useMutation(api.events.createReminderRule);

await createReminder({
  babyId,
  category: "feed",
  title: "Feed Baby",
  triggerType: "afterLastEventType",
  triggerConfig: {
    lastEventType: "FEED_BOTTLE",
    intervalHours: 3,
  },
  enabled: true,
  quietHoursStart: 22,  // 10 PM
  quietHoursEnd: 7,     // 7 AM
});

Example: Medicine Reminder

await createReminder({
  babyId,
  category: "medicine",
  title: "Give Vitamin D Drops",
  triggerType: "fixedTimes",
  triggerConfig: {
    times: ["09:00"],  // Once daily at 9 AM
  },
  enabled: true,
});

Example: Diaper Check Reminder

await createReminder({
  babyId,
  category: "diaper",
  title: "Check Diaper",
  triggerType: "afterLastEventType",
  triggerConfig: {
    lastEventType: "DIAPER",
    intervalHours: 2,
  },
  enabled: true,
});

Computing Upcoming Reminders

The system computes when reminders are due:
convex/events.ts
export const computeUpcomingReminders = query({
  args: {
    babyId: v.id("babyProfiles"),
    now: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const currentTime = args.now ? new Date(args.now) : new Date();
    const rules = await ctx.db
      .query("reminderRules")
      .withIndex("by_babyId", (q) => q.eq("babyId", args.babyId))
      .collect();

    const enabledRules = rules.filter((r) => r.enabled);
    const upcoming: Array<{
      rule: any;
      dueTime: string;
      isOverdue: boolean;
    }> = [];

    for (const rule of enabledRules) {
      if (rule.triggerType === "fixedTimes" && rule.triggerConfig?.times) {
        for (const time of rule.triggerConfig.times) {
          const [hours, minutes] = time.split(":").map(Number);
          const dueTime = new Date(currentTime);
          dueTime.setHours(hours, minutes, 0, 0);
          
          if (dueTime < currentTime) {
            dueTime.setDate(dueTime.getDate() + 1);
          }

          upcoming.push({
            rule,
            dueTime: dueTime.toISOString(),
            isOverdue: false,
          });
        }
      } else if (rule.triggerType === "afterLastEventType") {
        const lastEvent = await getLatestEventForType(
          ctx,
          args.babyId,
          rule.triggerConfig?.lastEventType
        );

        if (lastEvent) {
          const lastEventTime = new Date(lastEvent.timestamp);
          const intervalMs = (rule.triggerConfig?.intervalHours || 3) * 60 * 60 * 1000;
          const dueTime = new Date(lastEventTime.getTime() + intervalMs);

          upcoming.push({
            rule,
            dueTime: dueTime.toISOString(),
            isOverdue: dueTime < currentTime,
          });
        }
      }
    }

    return upcoming.sort((a, b) => 
      new Date(a.dueTime).getTime() - new Date(b.dueTime).getTime()
    ).slice(0, 10);
  },
});

Usage on Dashboard

const upcomingReminders = useQuery(
  api.events.computeUpcomingReminders,
  babyId ? { babyId } : "skip"
);

const nextReminder = upcomingReminders?.[0] ?? null;
The next reminder is displayed prominently on the dashboard:
src/app/page.tsx
{nextReminder && (
  <div className={`rounded-[20px] p-5 border mb-8 ${
    nextReminder.isOverdue
      ? "bg-alert-red/5 border-alert-red/20"
      : `${genderTheme.bg} ${genderTheme.border}`
  }`}>
    <h3 className="text-sm font-bold text-muted uppercase tracking-wider mb-2">Next Reminder</h3>
    <div className="flex items-center justify-between">
      <div>
        <div className="font-bold text-espresso">{nextReminder.rule.title}</div>
        <div className={`text-sm ${nextReminder.isOverdue ? "text-alert-red" : "text-muted"}`}>
          {nextReminder.isOverdue
            ? "Overdue"
            : new Date(nextReminder.dueTime).toLocaleTimeString("en-IN", { hour: "2-digit", minute: "2-digit", hour12: true })}
        </div>
      </div>
      <span className={`material-symbols-outlined ${nextReminder.isOverdue ? "text-alert-red" : genderTheme.text}`}>
        notifications
      </span>
    </div>
  </div>
)}

Quiet Hours

Prevent reminders during sleep hours:
await createReminder({
  babyId,
  category: "feed",
  title: "Feed Baby",
  triggerType: "afterLastEventType",
  triggerConfig: {
    lastEventType: "FEED_BOTTLE",
    intervalHours: 3,
  },
  enabled: true,
  quietHoursStart: 22,  // 10 PM
  quietHoursEnd: 7,     // 7 AM
});
Quiet hours are specified in 24-hour format (0-23). Reminders scheduled during quiet hours will be suppressed or delayed until quiet hours end.

Snooze Options

type SnoozeOptions = {
  defaultMinutes: number;     // Default snooze duration
  allowCustom: boolean;       // Allow custom snooze times
  maxSnoozes: number;         // Maximum times to snooze
  intervals: number[];        // Quick snooze intervals (minutes)
};

await createReminder({
  babyId,
  category: "medicine",
  title: "Give Medicine",
  triggerType: "fixedTimes",
  triggerConfig: { times: ["09:00"] },
  snoozeOptions: {
    defaultMinutes: 10,
    allowCustom: true,
    maxSnoozes: 3,
    intervals: [5, 10, 15, 30],
  },
});

Listing Reminder Rules

const reminders = useQuery(api.events.listReminderRules, { babyId });

// Filter by category
const feedReminders = reminders?.filter(r => r.category === "feed");
const medicineReminders = reminders?.filter(r => r.category === "medicine");

// Filter by enabled status
const active = reminders?.filter(r => r.enabled);
const disabled = reminders?.filter(r => !r.enabled);

Updating Reminder Rules

convex/events.ts
export const updateReminderRule = mutation({
  args: {
    id: v.id("reminderRules"),
    title: v.optional(v.string()),
    triggerType: v.optional(v.string()),
    triggerConfig: v.optional(v.any()),
    enabled: v.optional(v.boolean()),
    quietHoursStart: v.optional(v.number()),
    quietHoursEnd: v.optional(v.number()),
    snoozeOptions: v.optional(v.any()),
  },
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    const rule = await ctx.db.get(args.id);
    if (!rule) throw new Error("Reminder rule not found");
    await requireBabyAccess(ctx, rule.babyId, user._id);
    const { id, ...updates } = args;
    await ctx.db.patch(id, updates);
    return id;
  },
});

Enabling/Disabling Reminders

const updateReminder = useMutation(api.events.updateReminderRule);

// Disable reminder
await updateReminder({ id: reminderId, enabled: false });

// Re-enable reminder
await updateReminder({ id: reminderId, enabled: true });

Deleting Reminder Rules

convex/events.ts
export const deleteReminderRule = mutation({
  args: { id: v.id("reminderRules") },
  handler: async (ctx, args) => {
    const user = await requireAuth(ctx);
    const rule = await ctx.db.get(args.id);
    if (!rule) throw new Error("Reminder rule not found");
    await requireBabyAccess(ctx, rule.babyId, user._id);
    await ctx.db.delete(args.id);
  },
});

Push Notifications

Reminders can trigger push notifications (requires push subscription setup):
convex/schema.ts
pushSubscriptions: defineTable({
  userId: v.string(),
  endpoint: v.string(),
  keys: v.object({
    p256dh: v.string(),
    auth: v.string(),
  }),
  createdAt: v.string(),
})
  .index("by_userId", ["userId"])
  .index("by_endpoint", ["endpoint"])
Push notification delivery is handled by background jobs that check for due reminders and send notifications to subscribed devices.

Common Reminder Patterns

// Every 3 hours, 24/7
await createReminder({
  babyId,
  category: "feed",
  title: "Feed Time",
  triggerType: "afterLastEventType",
  triggerConfig: {
    lastEventType: "FEED_BOTTLE",
    intervalHours: 3,
  },
  enabled: true,
});

Dashboard

Next reminder displayed on dashboard

Activity Tracking

Reminders trigger based on logged activities

Baby Profiles

Each profile has its own reminder rules

Weekly Digests

Reminder adherence tracked in digests

Build docs developers (and LLMs) love