Overview
Activity tracking is the core feature of Zen Nurture. Every activity—feeds, diapers, sleep, medicine, growth measurements, and notes—is stored as an event with detailed metadata.
Event Schema
All events share a common structure:
events : defineTable ({
babyId: v . id ( "babyProfiles" ),
type: v . string (),
timestamp: v . string (),
caregiverId: v . optional ( v . id ( "caregivers" )),
payload: v . optional ( v . any ()),
source: v . optional ( v . string ()),
loggedBy: v . optional ( v . string ()),
loggedByName: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
createdAt: v . string (),
updatedAt: v . optional ( v . string ()),
})
. index ( "by_babyId_timestamp" , [ "babyId" , "timestamp" ])
. index ( "by_babyId_type_timestamp" , [ "babyId" , "type" , "timestamp" ])
. index ( "by_type_timestamp" , [ "type" , "timestamp" ])
. index ( "by_babyId_type" , [ "babyId" , "type" ])
Event Types
Zen Nurture supports the following event types:
export const EVENT_TYPES = {
FEED_BOTTLE: "FEED_BOTTLE" ,
FEED_BREAST: "FEED_BREAST" ,
PUMP: "PUMP" ,
DIAPER: "DIAPER" ,
MED_DOSE: "MED_DOSE" ,
SLEEP: "SLEEP" ,
GROWTH: "GROWTH" ,
NOTE: "NOTE" ,
VACCINE_DOSE: "VACCINE_DOSE" ,
} as const ;
Logging Events
Use the QuickLoggerDrawer component for rapid event logging:
src/components/QuickLoggerDrawer.tsx
export type QuickLogPrefill = {
view : "feed" | "diaper" | "sleep" | "meds" ;
feedSubType ?: "bottle" | "breast" ;
volume ?: number ;
duration ?: number ;
diaperKind ?: "wet" | "dirty" | "dry" | "mixed" ;
medName ?: string ;
isSleepingNow ?: boolean ;
};
Create Event Mutation
export const createEvent = mutation ({
args: {
babyId: v . id ( "babyProfiles" ),
type: v . string (),
timestamp: v . string (),
caregiverId: v . optional ( v . id ( "caregivers" )),
payload: v . optional ( v . any ()),
source: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
await requireBabyAccess ( ctx , args . babyId , user . _id );
const id = await ctx . db . insert ( "events" , {
... args ,
source: args . source || "manual" ,
createdAt: new Date (). toISOString (),
loggedBy: user . _id ,
loggedByName: user . name ,
});
return id ;
},
});
Feed Events
Bottle Feeds
type BottleFeedPayload = {
amountMl : number ;
contentType : "formula" | "breast_milk" | "cow_milk" ;
formulaName ?: string ; // e.g., "Enfamil", "Similac"
notes ?: string ;
};
await createEvent ({
babyId ,
type: "FEED_BOTTLE" ,
timestamp: new Date (). toISOString (),
payload: {
amountMl: 120 ,
contentType: "formula" ,
formulaName: "Enfamil" ,
},
});
Breast Feeds
type BreastFeedPayload = {
side : "left" | "right" | "both" ;
durationMin : number ;
notes ?: string ;
};
await createEvent ({
babyId ,
type: "FEED_BREAST" ,
timestamp: new Date (). toISOString (),
payload: {
side: "left" ,
durationMin: 15 ,
},
});
Pumping
type PumpPayload = {
amountMl : number ;
side ?: "left" | "right" | "both" ;
durationMin ?: number ;
notes ?: string ;
};
await createEvent ({
babyId ,
type: "PUMP" ,
timestamp: new Date (). toISOString (),
payload: {
amountMl: 90 ,
side: "both" ,
durationMin: 20 ,
},
});
Diaper Events
type DiaperPayload = {
kind : "wet" | "dirty" | "dry" | "mixed" ;
texture ?: "runny" | "mucousy" | "mushy" | "solid" | "pebbles" ;
color ?: "black" | "green" | "yellow" | "brown" | "red" | "gray" ;
blowout ?: boolean ;
rash ?: boolean ;
notes ?: string ;
};
await createEvent ({
babyId ,
type: "DIAPER" ,
timestamp: new Date (). toISOString (),
payload: {
kind: "dirty" ,
texture: "mushy" ,
color: "yellow" ,
blowout: false ,
rash: false ,
},
});
Diaper Constants
export const DIAPER_KINDS = {
WET: "wet" ,
DIRTY: "dirty" ,
DRY: "dry" ,
MIXED: "mixed" ,
} as const ;
export const DIAPER_TEXTURES = {
RUNNY: "runny" ,
MUCOUSY: "mucousy" ,
MUSHY: "mushy" ,
SOLID: "solid" ,
PEBBLES: "pebbles" ,
} as const ;
export const DIAPER_COLORS = {
BLACK: "black" ,
GREEN: "green" ,
YELLOW: "yellow" ,
BROWN: "brown" ,
RED: "red" ,
GRAY: "gray" ,
} as const ;
Sleep Events
type SleepPayload = {
startTs : string ; // ISO timestamp
endTs ?: string ; // Optional - omit if still sleeping
kind ?: "nap" | "night" ;
notes ?: string ;
};
// Log sleep start
await createEvent ({
babyId ,
type: "SLEEP" ,
timestamp: new Date (). toISOString (),
payload: {
startTs: new Date (). toISOString (),
kind: "nap" ,
},
});
// Update when baby wakes
await updateEvent ({
id: eventId ,
payload: {
startTs: startTime ,
endTs: new Date (). toISOString (),
kind: "nap" ,
},
});
Active sleep sessions (without endTs) are detected and displayed with a pulsing indicator on the dashboard.
Medicine Events
type MedicinePayload = {
medicineName : string ;
dose : number ;
doseUnit : string ; // "ml", "mg", "drops", etc.
outcome : "taken" | "skipped" | "vomited" ;
concentrationText ?: string ; // e.g., "100mg/5ml"
instructions ?: string ;
notes ?: string ;
};
await createEvent ({
babyId ,
type: "MED_DOSE" ,
timestamp: new Date (). toISOString (),
payload: {
medicineName: "Paracetamol (Crocin)" ,
dose: 2.5 ,
doseUnit: "ml" ,
outcome: "taken" ,
concentrationText: "120mg/5ml" ,
},
});
Medicine Database
Commonly used medicines are stored separately:
medicines : defineTable ({
name: v . string (),
defaultDoseUnit: v . optional ( v . string ()),
concentrationText: v . optional ( v . string ()),
instructions: v . optional ( v . string ()),
createdAt: v . string (),
}). index ( "by_name" , [ "name" ])
Growth Events
type GrowthPayload = {
weightKg ?: number ;
lengthCm ?: number ;
headCircumferenceCm ?: number ;
notes ?: string ;
};
await createEvent ({
babyId ,
type: "GROWTH" ,
timestamp: new Date (). toISOString (),
payload: {
weightKg: 5.2 ,
lengthCm: 54.5 ,
headCircumferenceCm: 37.0 ,
},
});
Note Events
type NotePayload = {
notes : string ;
category ?: string ; // Optional categorization
};
await createEvent ({
babyId ,
type: "NOTE" ,
timestamp: new Date (). toISOString (),
payload: {
notes: "First tooth visible!" ,
},
});
Photo Attachments
Any event can include photo attachments:
await createEvent ({
babyId ,
type: "FEED_BOTTLE" ,
timestamp: new Date (). toISOString (),
payload: { amountMl: 120 },
photoIds: [ "file_abc123" , "file_def456" ],
});
Photos are stored in the files table:
files : defineTable ({
babyId: v . id ( "babyProfiles" ),
filename: v . string (),
mimeType: v . string (),
storageId: v . string (),
tags: v . optional ( v . array ( v . string ())),
capturedAt: v . string (),
notes: v . optional ( v . string ()),
createdAt: v . string (),
})
Querying Events
By Date Range
export const listEvents = query ({
args: {
babyId: v . id ( "babyProfiles" ),
from: v . optional ( v . string ()),
to: v . optional ( v . string ()),
type: v . optional ( v . string ()),
limit: v . optional ( v . number ()),
},
handler : async ( ctx , args ) => {
let q = ctx . db
. query ( "events" )
. withIndex ( "by_babyId_timestamp" , ( q ) => q . eq ( "babyId" , args . babyId ));
if ( args . from ) {
q = q . filter (( q ) => q . gte ( q . field ( "timestamp" ), args . from ! ));
}
if ( args . to ) {
q = q . filter (( q ) => q . lte ( q . field ( "timestamp" ), args . to ! ));
}
if ( args . type ) {
q = q . filter (( q ) => q . eq ( q . field ( "type" ), args . type ! ));
}
return await q . order ( "desc" ). take ( args . limit || 100 );
},
});
Get Last Event by Type
const lastFeed = useQuery ( api . events . getLastEventByType , {
babyId ,
eventType: "FEED_BOTTLE" ,
});
Get Multiple Last Events
const lastEvents = useQuery ( api . events . getLastEventsByTypes , {
babyId ,
eventTypes: [ "FEED_BOTTLE" , "FEED_BREAST" , "DIAPER" , "SLEEP" ],
});
// Returns: { "FEED_BOTTLE": {...}, "FEED_BREAST": {...}, ... }
Daily Aggregates
Get computed statistics for a specific day:
const aggregates = useQuery ( api . events . getDailyAggregates , {
babyId ,
date: "2024-03-05" ,
});
// Returns:
// {
// date: "2024-03-05",
// feeds: {
// totalMl: 720,
// totalBreastMin: 45,
// count: 8,
// bottleSizeAvg: 120,
// avgGapMin: 180 // 3 hours
// },
// diapers: {
// wet: 4,
// dirty: 2,
// dry: 0,
// mixed: 1,
// total: 7,
// byTexture: { mushy: 2, solid: 1 },
// byColor: { yellow: 3 },
// blowoutCount: 0,
// rashCount: 0
// },
// sleeps: {
// totalMinutes: 840, // 14 hours
// sessions: 6
// },
// meds: {
// taken: 2,
// skipped: 0,
// adherence: 100
// }
// }
Range Aggregates
Get day-by-day breakdowns for a date range:
const rangeData = useQuery ( api . events . getRangeAggregates , {
babyId ,
from: "2024-03-01T00:00:00.000Z" ,
to: "2024-03-07T23:59:59.999Z" ,
});
// Returns:
// {
// "2024-03-01": { feeds: {...}, diapers: {...}, ... },
// "2024-03-02": { feeds: {...}, diapers: {...}, ... },
// ...
// }
Updating and Deleting Events
Update Event
export const updateEvent = mutation ({
args: {
id: v . id ( "events" ),
timestamp: v . optional ( v . string ()),
caregiverId: v . optional ( v . id ( "caregivers" )),
payload: v . optional ( v . any ()),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const event = await ctx . db . get ( args . id );
if ( ! event ) throw new Error ( "Event not found" );
await requireBabyAccess ( ctx , event . babyId , user . _id );
const { id , ... updates } = args ;
await ctx . db . patch ( id , {
... updates ,
updatedAt: new Date (). toISOString (),
});
return id ;
},
});
Delete Event
const deleteEvent = useMutation ( api . events . deleteEvent );
await deleteEvent ({ id: eventId });
Event Sources
The source field tracks how events were created:
"manual": Logged by user via UI (default)
"quick_action": Via quick action buttons
"suggestion": From AI suggestions
"import": Imported from external source
"api": Created via API
Dashboard View live activity timers and summaries
Reminders Set reminders based on event patterns
Weekly Digests AI-generated summaries of activity data
Baby Profiles All events are linked to a baby profile