Building Splashnest: A Daycare Management System
Why This Exists
Ever walked into a daycare center and seen staff drowning in paperwork? Attendance sheets scattered everywhere, parents frantically texting about schedule changes, and someone trying to remember if little Timmy is allergic to peanuts by checking a three-month-old email? Yeah, that was the reality.
So I built Splashnest. Not because I thought "hey, the world needs another CRUD app," but because daycare centers were legitimately struggling with spreadsheets, paper forms, and way too many WhatsApp messages. Real people, real problems, real solution.
Check it out live:dms.splashnest.com
The Tech Stack (and the reasoning behind it)
Let's break down what powers this thing and, more importantly, why I chose each piece:
Frontend Arsenal
- Next.js 15 with React 19 - Server components are genuinely great now, and the App Router finally feels mature. Plus, having everything in one codebase beats managing separate frontend/backend repos
- TypeScript - Because catching bugs at compile time is way better than debugging at 2 AM when production breaks
- Tailwind CSS - Utility-first styling that actually makes sense. No more naming classes or context-switching to CSS files
- Radix UI - Accessible components out of the box. Keyboard navigation, ARIA labels, screen reader support - all handled
- shadcn/ui - Beautiful pre-built components that you actually own. Copy, paste, customize. No black-box npm packages
Backend Power
- Next.js API Routes - Keep it simple. Everything in one repo, shared types, no CORS headaches
- Prisma ORM - Database queries that actually read like English. Plus that auto-generated TypeScript client? Chef's kiss
- PostgreSQL via Supabase - Rock-solid relational database with real-time superpowers and a clean dashboard
- Supabase Auth - JWT tokens, session management, password resets - all handled. No need to roll our own auth
The Supporting Cast
- PWA support (@ducanh2912/next-pwa) - Works offline, installs like a native app, feels professional
- date-fns - Date manipulation without wanting to throw your laptop out the window
- ExcelJS - Export attendance to Excel because accountants still live in Excel world
- Zod - Runtime type validation. TypeScript guards compile time, Zod guards runtime
- React Hook Form - Forms that don't make you want to cry
Architecture: Keeping It Real
I went with a straightforward architecture. No buzzwords, no over-engineering:
Frontend (Next.js) → API Routes → Prisma → Supabase PostgreSQLThat's it. No microservices. No GraphQL layer "just in case we need it later." No Kafka message queues. Just a solid monolith that does its job really well.
Why Supabase?
Could've gone with a raw PostgreSQL instance somewhere. But Supabase gives me so much more:
- PostgreSQL database with an actually good dashboard (no more raw SQL for simple queries)
- Built-in authentication system that just works
- Row-level security policies (super important for multi-tenant apps)
- Real-time subscriptions for live updates (parents see schedule changes instantly)
- Edge functions if we ever need serverless compute
- Automatic backups (sleep better at night)
Plus, the developer experience is fantastic. Setting up a new table? Write a Prisma schema, run migration, boom, done. The Supabase dashboard updates automatically.
The Fun Problems I Actually Solved
Problem 1: Timezone Hell (The Big One)
Oh man, this one almost broke us. When you're dealing with schedules, dates, and attendance across different timezones, JavaScript's Date object becomes your worst enemy.
What Was Happening
Picture this nightmare scenario:
- Parent in PST creates a schedule for "September 7th, 2025"
- Frontend sends the date string "2025-09-07" to the API
- Backend does
new Date("2025-09-07")which creates UTC midnight - Database stores this as a UTC timestamp
- Admin in EST queries for "today's schedules"
- Gets the wrong day's data because of timezone conversion
Parents were seeing schedules appear and disappear. Admins couldn't find records. It was chaos.
The Solution: Centralized Date Utilities
I built a fortress of date utilities in utils/date-utils.ts that handles ALL date operations:
// Get today's date in local timezone as string
getTodayString() // Returns: "2025-10-08"
// Create proper date range for database queries
// This ensures we capture the ENTIRE day in UTC
getUTCDateRange("2025-09-07")
// Returns: {
// start: Date (2025-09-07 00:00:00 in local, converted to UTC),
// end: Date (2025-09-07 23:59:59 in local, converted to UTC)
// }
// Create a local date for storage (uses noon to avoid DST issues)
createLocalDate("2025-09-07")
// Returns: Date object representing noon on that day in local timezone
// Format dates consistently for display
formatDisplayDate(date) // "Monday, Sep 7, 2025"
formatTimeDisplay(date) // "3:30 PM"Now every single date operation in the app goes through these utilities. API routes use them. Components use them. Cron jobs use them. No exceptions.
I even wrote a comprehensive guide (TIMEZONE-HANDLING-GUIDE.md) explaining the gotchas and how to avoid them. Because I know future devs (or future me) will need it.
Problem 2: Making It a Progressive Web App (The Smart Financial Move)
Daycare staff work in environments where WiFi can be spotty. Kids running around, busy mornings, sometimes the internet just drops. I needed the app to work offline. But more importantly, I needed to make a smart business decision.
Why PWA Instead of Native Apps?
Here's the thing - I could've built separate iOS and Android apps. But let's break down why that would've been ridiculous:
Cost Savings (The Big One)
- Apple Developer Fee: $99/year - Why pay Apple $99 annually just to distribute an app? That's money that could go toward actual features or infrastructure
- Google Play: $25 one-time - Not terrible, but still an unnecessary expense
- No app store review delays - Deploy instantly, no waiting 2-5 days for Apple's review team
- No 30% commission - If we ever add paid features, no app store cut
- Single codebase = lower maintenance costs - One app to maintain, not three (web, iOS, Android)
Cross-Platform Perfection
PWAs work everywhere. Literally everywhere:
- iOS Safari - Add to Home Screen, full screen experience, no browser chrome
- Android Chrome - Native install prompt, appears in app drawer, indistinguishable from native
- Desktop - Works on Windows, Mac, Linux. Any browser. Same code
- Tablets - Responsive design adapts perfectly
- Future platforms - Whatever comes next will probably support PWAs
Lightweight & Fast
Native apps are bloated. PWAs are lean:
- ~2MB total download - vs 50-100MB for typical native apps
- Instant updates - Users always get the latest version, no app store update prompts
- Progressive loading - App shell loads first, content follows. Feels instant
- Cached assets - After first visit, loads from cache. Sub-second load times
- No bloat - Just the web code, no native SDK overhead
PWA Features Implemented
- Service Workers - Cache app shell, API responses, and critical assets
- Offline Fallback - Graceful degradation when network is unavailable
- Install Prompts - Custom UI encouraging users to "Add to Home Screen"
- App-like Experience - Full screen, no browser UI, native-feeling transitions
- Push Notification Ready - Infrastructure in place for future implementation
- Background Sync - Queues actions when offline, syncs when connection returns
The Technical Setup
The setup was surprisingly straightforward thanks to @ducanh2912/next-pwa. Just wrap the Next.js config:
import withPWA from '@ducanh2912/next-pwa';
const nextConfig = {
// ... your config
};
export default withPWA({
dest: 'public',
register: true,
skipWaiting: true,
disable: process.env.NODE_ENV === 'development',
})(nextConfig);Add a manifest.json file:
{
"name": "Splashnest Daycare",
"short_name": "Splashnest",
"description": "Daycare Management System",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}Boom. PWA. Works offline. Installs like a native app. Zero recurring fees. Cross-platform by default. Parents love it because it feels professional and responsive. I love it because it's financially smart and technically elegant.
The Real-World Impact
- Parents don't care it's a PWA - To them, it's just "the app." Looks native, feels native, works native
- Staff use it offline - Mark attendance even when WiFi drops. Syncs when connection returns
- Zero distribution friction - Share a link. That's it. No "download from App Store" barrier
- Instant bug fixes - Deploy to web, users get it immediately. No waiting for app store approval
- One codebase - Fix once, works everywhere. Saved countless development hours
This is the kind of decision that makes you realize: sometimes the "simpler" option is actually the smarter option. No Apple tax, no fragmentation, no complexity. Just a web app that works really, really well.
Problem 3: Role-Based Access Control
Two completely different user types need completely different experiences. This isn't just hiding a few buttons - it's fundamentally different apps living in the same codebase.
What Parents See
- Only their own children (never someone else's kid)
- Create and edit schedules for their kids
- Add special instructions (allergies, pickup notes, etc.)
- View daily menus
- Check attendance history
What Admins See
- All children across all families
- All parent accounts
- User management (create, edit, delete accounts)
- Menu management (create daily menus)
- Global instructions and policies
- Class scheduling and templates
- Analytics and reports
How I Handle It
// Prisma schema defines roles
enum UserRole {
PARENT
ADMIN
}
model User {
id String @id
email String @unique
role UserRole @default(PARENT)
children Child[]
}
// API route protection
const token = await getToken(req)
const user = await verifyToken(token)
if (user.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
// Frontend conditional rendering
{user.role === 'admin' ? (
<AdminDashboard />
) : (
<ParentDashboard />
)}Every API route checks authentication AND authorization. Every component conditionally renders based on role. No shortcuts, no "I'll add that later."
Problem 4: Global Data Management
I needed a way to avoid every component making separate API calls. Twenty components all fetching the same children data? Inefficient and janky.
Enter DataContext
I built a global data context provider that:
- Fetches all necessary data on mount (children, schedules, menus)
- Caches it in React context
- Provides refresh functions for each data type
- Handles loading states globally
- Manages error states
// One fetch, shared everywhere
const {
children,
schedules,
isLoading,
refreshChildren
} = useData();
// When a child is added
await createChild(childData);
await refreshChildren(); // Update contextThis pattern keeps the API calls manageable and the UI snappy. Plus, it's way easier to debug when data lives in one place.
Problem 5: Recurring Classes & Templates
Parents wanted to set up recurring classes (like "Ballet every Tuesday at 3 PM") without manually creating each instance. Makes sense, but implementing it is trickier than it sounds.
The Database Design
model ClassTemplate {
id String @id @default(uuid())
name String // "Ballet Class"
startTime String // "15:00"
endTime String // "16:00"
isRecurring Boolean
dayOfWeek String? // "tuesday"
startDate DateTime?
endDate DateTime?
// Generated class instances
instances ClassSchedule[]
childId String
child Child @relation(fields: [childId])
}
model ClassSchedule {
id String @id @default(uuid())
date DateTime // Specific date: 2025-09-09
completed Boolean?
droppedOff Boolean?
pickedUp Boolean?
templateId String?
template ClassTemplate? @relation(fields: [templateId])
}The Cron Magic
A daily cron job runs and:
- Finds all active recurring templates
- Checks if instances exist for the next 7 days
- Generates missing instances based on dayOfWeek and date range
- Links them back to the template
Parents set it up once, and the system handles the rest. Check out utils/cron-class-scheduler.ts for the implementation.
The Database Schema (it's actually well-thought-out)
We spent real time designing this. Eight main models, all properly related:
Core Models
- User - Parents and admin accounts with authentication
- Child - The kids, linked to parent users
- Schedule - Daily drop-off and pickup times
- ClassTemplate - Recurring class definitions
- ClassSchedule - Actual class instances with attendance tracking
- DailyMenu - What's for breakfast, lunch, and snack each day
- SpecialInstructions - Allergies, medical notes, pickup authorization
- DaycareSettings - Operating hours, capacity limits, fee structure
Smart Relationships
- Everything uses UUIDs (no sequential IDs that leak information)
- Proper foreign key constraints
- Cascade deletes configured (delete parent → removes their children from DB)
- Indexes on frequently queried fields (date ranges, user IDs)
- Enum types for roles and other fixed options
UI/UX: Making It Actually Usable
Mobile-First Philosophy
I designed for mobile first because that's what parents and daycare staff actually use. The desktop view is just a wider version with better spacing.
Bottom Navigation Bar
Always visible, always accessible:
- Dashboard - Home screen with today's info
- Schedule - Create and view schedules
- History - Past attendance and activity
- Settings - Profile, preferences, logout
Clean, simple, thumb-friendly. No hunting for navigation.
Component Library: shadcn/ui
I used shadcn/ui components throughout, customized to match the design:
- Cards - Content grouping with subtle shadows
- Dialogs - Forms and confirmations
- Toasts - Success and error notifications
- Accordions - Collapsible sections for long content
- Select & Combobox - Searchable dropdowns
- Date Pickers - Calendar selection with proper timezone handling
The Admin Dashboard
Check out components/dashboard/admin/admin-dashboard.tsx. I've got:
- Compact Stat Cards - Total children, active schedules, pending tasks
- Quick Actions - Jump directly to menus, instructions, user management
- Today's Activity - Count of scheduled classes and attendance status
- Age Demographics - Visual breakdown (toddlers vs preschool vs school age)
It's colorful without being overwhelming. Gradient backgrounds, subtle shadows, good spacing. Everything you need at a glance.
Automation: Cron Jobs That Actually Run
I set up several cron jobs to handle recurring tasks:
1. Attendance Tracking
Runs every morning at 6 AM:
- Finds all schedules for today
- Checks which kids haven't been marked as dropped off
- Auto-marks them as absent
- Notifies admins of absences
2. Class Schedule Generation
Runs daily at midnight:
- Finds all recurring class templates
- Generates instances for the next 7 days
- Skips dates that already have instances
- Respects start and end dates of templates
3. Data Cleanup
Runs weekly:
- Archives records older than 1 year
- Cleans up orphaned data
- Optimizes database indexes
GitHub Actions for Automation
I use GitHub Actions to run these cron jobs instead of Vercel's cron feature. More control, easier debugging, and works with any hosting platform.
Configured through .github/workflows/cron.yml:
name: Cron Jobs
on:
schedule:
# Attendance tracking - 6 AM daily
- cron: '0 6 * * *'
# Class scheduler - midnight daily
- cron: '0 0 * * *'
workflow_dispatch: # Manual trigger for testing
jobs:
run-crons:
runs-on: ubuntu-latest
steps:
- name: Trigger Attendance Cron
if: github.event.schedule == '0 6 * * *'
run: |
curl -X POST https://dms.splashnest.com/api/cron/attendance \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}"
- name: Trigger Class Scheduler
if: github.event.schedule == '0 0 * * *'
run: |
curl -X POST https://dms.splashnest.com/api/cron/class-scheduler \
-H "Authorization: Bearer ${{ secrets.CRON_SECRET }}"GitHub Actions hits these endpoints on schedule. Free, reliable, and I can see the execution logs right in GitHub. Plus, I can manually trigger them for testing.
Things We'd Do Differently (Hindsight is 20/20)
1. More Comprehensive Testing
Yeah, we should have more tests. We know. The project works great in production, but automated tests would make refactoring way less scary. Unit tests for utilities, integration tests for API routes, E2E tests for critical flows.
2. State Management Evolution
DataContext works well for this app size, but if we scaled up significantly, we'd probably reach for Zustand or break things into more granular contexts. The current approach starts feeling heavy around 10+ data types.
3. Real-time Updates
While Supabase supports real-time subscriptions, I'm not using them yet. Implementing websocket connections for live updates when admins change schedules or menus would be a great addition.
The Deployment Story
Hosting: Vercel (Obviously)
It's a Next.js app, and Vercel just works:
- Push to main branch → automatic deployment
- Preview deployments for every PR
- Environment variables managed through dashboard
- Edge functions available if needed
- Analytics built-in
- Zero configuration needed
Automation: GitHub Actions
- Cron jobs run on schedule via GitHub Actions workflows
- Manual triggers available for testing
- Execution logs visible in GitHub
- Secrets managed securely
- Free for public repositories
- Works with any hosting platform
Database: Supabase
- Connection pooling handled automatically
- Migrations run through Prisma CLI
- Automatic daily backups
- Point-in-time recovery available
- Row-level security for multi-tenant data
CI/CD Pipeline
- Push code to GitHub
- Vercel detects the push
- Runs build process (TypeScript compile, Next.js build)
- Runs database migrations if schema changed
- Deploys to edge network
- GitHub Actions trigger cron jobs on schedule
Takes about 2 minutes from push to live. Cron jobs run automatically via GitHub Actions. Clean separation of concerns.
What I Actually Learned
1. Date Handling Is Genuinely Hard
Seriously, timezones are a trap. JavaScript's Date object will betray you. Build utilities early, document them thoroughly, and stick to them religiously. Future me will send thanks.
2. PWAs Are Underrated
For many use cases, a well-built PWA is better than native apps. Cross-platform, instant updates, no app store hassles. Parents and staff don't care if it's "native" as long as it works well.
3. Type Safety Is Worth Every Keystroke
TypeScript + Prisma + Zod = catch bugs before users do. The initial setup time pays dividends immediately. Refactoring with confidence is a superpower.
4. Mobile Design Can't Be an Afterthought
Design for the smallest screen first. Most users will be on mobile. A mobile-optimized experience that scales up to desktop is better than desktop-first that gets squeezed down.
5. Documentation Actually Saves Lives
I wrote guides for timezone handling, cron setup, and user workflows. When I came back to the code after two weeks, I was thankful. Good docs aren't optional.
6. Real Problems Need Real Solutions
Building something people actually use is different from building a portfolio piece. Edge cases matter. Error handling matters. Performance matters. Reliability matters.
The Numbers (Because People Ask)
- Active Users: 30+ families using it daily
- Children Managed: 40+ kids tracked
- Uptime: 99.8% (that 0.2% was Vercel maintenance, not my fault)
- Average Load Time: Under 1.5 seconds
- API Response Time: 200-300ms average
- Bugs Reported Post-Launch: Surprisingly few (timezone utilities FTW)
Wrapping Up
Building Splashnest was a genuine learning experience. Not because I used cutting-edge tech or invented something new, but because I solved real problems for real people.
The code is clean. The architecture makes sense. The documentation exists (and is actually helpful). It works reliably in production. Parents love the convenience. Daycare staff appreciate the organization.
Would I build it again? Absolutely. Maybe with more tests next time. Definitely with the same attention to timezones.
The app is live at dms.splashnest.com. The codebase is well-structured, properly commented, and actually maintainable. That's what matters.
Status: Live in Production at dms.splashnest.com
Users: 50+ families