Overview
The milestones system helps parents track and celebrate their baby’s developmental achievements. Each milestone can include photos, videos, notes, and an achievement date.
Schema Structure
milestones : defineTable ({
babyId: v . id ( "babyProfiles" ),
key: v . string (),
title: v . string (),
category: v . string (),
achievedAt: v . optional ( v . string ()),
note: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
videoIds: v . optional ( v . array ( v . string ())),
isCustom: v . optional ( v . boolean ()),
createdAt: v . string (),
})
. index ( "by_babyId" , [ "babyId" ])
. index ( "by_babyId_key" , [ "babyId" , "key" ])
Milestone Fields
Unique identifier for the milestone. Predefined milestones use keys like "first_smile", custom milestones use generated keys like "custom_1234567890_abc123"
Display name of the milestone (e.g., “First Smile”, “Rolled Over”, “First Word”)
Category grouping: "motor", "language", "social", "cognitive", or custom categories
ISO timestamp when the milestone was achieved. Omit for unachieved milestones
Optional note about the milestone achievement
Array of file IDs for attached photos
Array of file IDs for attached videos
true for user-created milestones, false or omitted for predefined milestones
Milestone Categories
Milestones are grouped into developmental categories:
Physical development milestones:
Rolling over
Sitting up
Crawling
Standing
First steps
Walking independently
Communication milestones:
First coos
Babbling
First word
Two-word phrases
Simple sentences
Social development milestones:
First smile
Recognizes parents
Stranger anxiety
Waving bye-bye
Playing with others
Mental development milestones:
Tracks objects with eyes
Responds to name
Object permanence
Points to objects
Follows simple instructions
Achieving a Milestone
Mark a predefined milestone as achieved:
export const achieve = mutation ({
args: {
babyId: v . id ( "babyProfiles" ),
key: v . string (),
title: v . string (),
category: v . string (),
achievedAt: v . optional ( v . string ()),
note: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
videoIds: v . optional ( v . array ( v . string ())),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
await requireBabyAccess ( ctx , args . babyId , user . _id );
const existing = await ctx . db
. query ( "milestones" )
. withIndex ( "by_babyId_key" , ( q ) =>
q . eq ( "babyId" , args . babyId ). eq ( "key" , args . key )
)
. first ();
const achievedAt = args . achievedAt ?? new Date (). toISOString ();
if ( existing ) {
await ctx . db . patch ( existing . _id , {
achievedAt ,
note: args . note ,
photoIds: args . photoIds ,
videoIds: args . videoIds ,
});
return existing . _id ;
}
return await ctx . db . insert ( "milestones" , {
babyId: args . babyId ,
key: args . key ,
title: args . title ,
category: args . category ,
achievedAt ,
note: args . note ,
photoIds: args . photoIds ,
videoIds: args . videoIds ,
createdAt: new Date (). toISOString (),
});
},
});
Usage Example
import { useMutation } from "convex/react" ;
import { api } from "../../convex/_generated/api" ;
const achieveMilestone = useMutation ( api . milestones . achieve );
await achieveMilestone ({
babyId ,
key: "first_smile" ,
title: "First Smile" ,
category: "social" ,
achievedAt: "2024-02-14T10:30:00.000Z" ,
note: "Such a beautiful moment!" ,
photoIds: [ "file_abc123" ],
});
Creating Custom Milestones
Parents can add their own milestones:
export const createCustom = mutation ({
args: {
babyId: v . id ( "babyProfiles" ),
title: v . string (),
category: v . string (),
achievedAt: v . optional ( v . string ()),
note: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
videoIds: v . optional ( v . array ( v . string ())),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
await requireBabyAccess ( ctx , args . babyId , user . _id );
const key = `custom_ ${ Date . now () } _ ${ Math . random (). toString ( 36 ). slice ( 2 , 10 ) } ` ;
const achievedAt = args . achievedAt ?? new Date (). toISOString ();
return await ctx . db . insert ( "milestones" , {
babyId: args . babyId ,
key ,
title: args . title . trim (),
category: args . category ,
achievedAt ,
note: args . note ,
photoIds: args . photoIds ,
videoIds: args . videoIds ,
isCustom: true ,
createdAt: new Date (). toISOString (),
});
},
});
Custom Milestone Example
const createCustomMilestone = useMutation ( api . milestones . createCustom );
await createCustomMilestone ({
babyId ,
title: "Tried Solid Food" ,
category: "feeding" ,
achievedAt: "2024-03-05T12:00:00.000Z" ,
note: "First taste of mashed banana - loved it!" ,
photoIds: [ "file_xyz789" ],
});
Custom milestones automatically receive a unique key prefixed with custom_ to distinguish them from predefined milestones.
Listing Milestones
Retrieve all milestones for a baby:
export const list = query ({
args: { babyId: v . id ( "babyProfiles" ) },
handler : async ( ctx , args ) => {
const user = await authComponent . safeGetAuthUser ( ctx );
if ( ! user ) return [];
await requireBabyAccess ( ctx , args . babyId , user . _id );
return await ctx . db
. query ( "milestones" )
. withIndex ( "by_babyId" , ( q ) => q . eq ( "babyId" , args . babyId ))
. collect ();
},
});
Usage
const milestones = useQuery ( api . milestones . list , { babyId });
// Filter achieved vs. unachieved
const achieved = milestones ?. filter ( m => m . achievedAt ) || [];
const upcoming = milestones ?. filter ( m => ! m . achievedAt ) || [];
// Group by category
const byCategory = milestones ?. reduce (( acc , m ) => {
if ( ! acc [ m . category ]) acc [ m . category ] = [];
acc [ m . category ]. push ( m );
return acc ;
}, {} as Record < string , typeof milestones >);
Updating Milestones
Modify notes or attachments:
export const updateMilestone = mutation ({
args: {
id: v . id ( "milestones" ),
note: v . optional ( v . string ()),
photoIds: v . optional ( v . array ( v . string ())),
videoIds: v . optional ( v . array ( v . string ())),
},
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const milestone = await ctx . db . get ( args . id );
if ( ! milestone ) throw new Error ( "Milestone not found" );
await requireBabyAccess ( ctx , milestone . babyId , user . _id );
const updates : Record < string , unknown > = {};
if ( args . note !== undefined ) updates . note = args . note ;
if ( args . photoIds !== undefined ) updates . photoIds = args . photoIds ;
if ( args . videoIds !== undefined ) updates . videoIds = args . videoIds ;
if ( Object . keys ( updates ). length > 0 ) {
await ctx . db . patch ( args . id , updates );
}
return args . id ;
},
});
Unachieving a Milestone
Revert a milestone to unachieved state:
export const unachieve = mutation ({
args: { id: v . id ( "milestones" ) },
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const milestone = await ctx . db . get ( args . id );
if ( ! milestone ) throw new Error ( "Milestone not found" );
await requireBabyAccess ( ctx , milestone . babyId , user . _id );
await ctx . db . patch ( args . id , {
achievedAt: undefined ,
note: undefined ,
photoIds: undefined ,
videoIds: undefined ,
});
},
});
Deleting Milestones
Remove a milestone (typically for custom milestones):
export const remove = mutation ({
args: { id: v . id ( "milestones" ) },
handler : async ( ctx , args ) => {
const user = await requireAuth ( ctx );
const milestone = await ctx . db . get ( args . id );
if ( ! milestone ) throw new Error ( "Milestone not found" );
await requireBabyAccess ( ctx , milestone . babyId , user . _id );
await ctx . db . delete ( args . id );
},
});
Milestones support both photos and videos:
Photo Attachments
// Upload photo first
const photoId = await uploadFile ( file );
// Attach to milestone
await achieveMilestone ({
babyId ,
key: "first_steps" ,
title: "First Steps" ,
category: "motor" ,
photoIds: [ photoId ],
});
Video Attachments
// Upload video
const videoId = await uploadFile ( videoFile );
// Attach to milestone
await updateMilestone ({
id: milestoneId ,
videoIds: [ videoId ],
});
Video files can be large. Ensure proper upload handling and progress indicators for a good user experience.
Age-Appropriate Suggestions
Milestones can be filtered by baby’s age to show relevant achievements:
const MILESTONE_AGE_RANGES = {
"first_smile" : { minWeeks: 4 , maxWeeks: 12 },
"rolled_over" : { minWeeks: 12 , maxWeeks: 24 },
"sat_up" : { minWeeks: 20 , maxWeeks: 32 },
"crawled" : { minWeeks: 24 , maxWeeks: 40 },
"first_steps" : { minWeeks: 36 , maxWeeks: 72 },
};
function getRelevantMilestones ( babyAgeWeeks : number ) {
return Object . entries ( MILESTONE_AGE_RANGES )
. filter (([ key , range ]) =>
babyAgeWeeks >= range . minWeeks &&
babyAgeWeeks <= range . maxWeeks
)
. map (([ key ]) => key );
}
Milestone Timeline
Display milestones in chronological order:
const timeline = milestones
?. filter ( m => m . achievedAt )
. sort (( a , b ) =>
new Date ( a . achievedAt ! ). getTime () -
new Date ( b . achievedAt ! ). getTime ()
);
Export and Sharing
Milestones are included in data exports:
// Export includes milestones
const exportData = await exportBabyData ({ babyId });
// exportData.milestones contains all milestone records
Dashboard Recent milestones shown on dashboard
Baby Profiles Age-appropriate milestone suggestions
Weekly Digests Milestones included in weekly summaries
Activity Tracking Link milestones to specific activities