Curious Koi Daycare
Curious Koi is a daycare center website with a public-facing site for parents to browse programs, view a photo gallery, and request tours, plus a secure admin portal for managing all site content, contact submissions, and user roles.
Curious Koi Daycare
A full-stack content management system and public website for a daycare provider, built with Next.js 16, React 19, and Convex. The site features a playful koi-fish-themed landing page with rich animations alongside a secure admin portal for managing all site content.
Tech Stack
Frontend
| Technology | Why |
|---|---|
| Next.js 16 (App Router) | Server components for fast initial loads, file-based routing for clean URL structure, and built-in image optimization for the gallery. The App Router's layout nesting made it straightforward to share a shell between portal pages without re-rendering the sidebar on every navigation. |
| React 19 | The React compiler eliminates manual memoization across the portal's many list/form views. Server components also let the public pages fetch Convex data at the edge without shipping a client-side query layer to visitors. |
| Tailwind CSS v4 | Inline theme tokens (@theme inline) keep the neo-brutalism design system — custom shadows, brand colors, border styles — co-located with the utility classes that use them, avoiding a separate design-token config file. |
| Motion (Framer Motion) | Powers the landing page's scroll-reveal stagger effects and micro-interactions. Respects prefers-reduced-motion out of the box. |
| @dnd-kit | Accessible drag-and-drop for reordering programs, testimonials, gallery images, and parent forms. Supports pointer, touch, and keyboard sensors with no extra wiring. |
| Radix UI | Unstyled, accessible primitives (dialogs, dropdowns, selects, popovers) that we layer the neo-brutalism styling onto. Handles focus trapping, keyboard navigation, and screen reader announcements. |
| CVA (class-variance-authority) | Keeps the component variant API (button sizes, badge colors, card states) type-safe and composable without runtime CSS-in-JS overhead. |
| Novel / TipTap | Rich-text WYSIWYG editor in the portal that outputs markdown, so page content stays portable and renderable on the server via the remark/rehype pipeline. |
Backend
| Technology | Why |
|---|---|
| Convex | Real-time database with end-to-end type safety from schema to React hooks. Mutations and queries are plain TypeScript functions — no ORM, no REST boilerplate. Reactive subscriptions mean the portal always shows live data without polling. |
| WorkOS | Handles Google OAuth and user management. Offloads session security, token refresh, and provider integration to a managed service rather than hand-rolling it. |
| Cloudflare R2 | S3-compatible object storage for gallery images and downloadable parent forms. Presigned upload URLs let the browser upload directly to R2, keeping file bytes off the Next.js server entirely. Paired with a CDN domain (cdn.curiouskoi.com) for fast delivery. |
Markdown Rendering
| Technology | Why |
|---|---|
| remark / rehype | Converts stored markdown to sanitized HTML on the server. The plugin pipeline adds GFM tables, heading anchors, syntax highlighting, and XSS sanitization in a single pass. |
Architecture
Browser
├── Public site (SSR)
│ └── Convex preloaded queries (programs, testimonials, gallery, pages)
│
├── Admin portal (CSR, auth-gated)
│ ├── Convex reactive queries + mutations
│ ├── Direct R2 uploads via presigned URLs
│ └── WorkOS OAuth (Google)
│
└── API routes (/api/auth/*)
├── OAuth callback → Convex user sync → cookie session
├── Token refresh
└── Sign out
Convex Backend
├── Typed queries & mutations with auth guards
├── Cron: cleanup non-admin users after 24h
└── R2 actions (presigned URL generation, object deletion)
Feature Breakdown
Public Site
- Landing page — Hero with parallax background and animated floating koi fish and bubbles, about section, programs grid (4 max, color-themed cards), testimonial quotes with star ratings, photo gallery with lightbox and keyboard navigation, contact/tour-request form, and wave SVG section dividers.
- Dynamic CMS pages — Slug-based routing renders markdown content authored in the portal. Headings get auto-linked anchors for deep linking.
- Parents page — Downloadable PDF forms and a responsibilities section, both managed from the portal.
- Contact page — Tour request form that writes directly to Convex; admins triage submissions from the portal.
Admin Portal
- Dashboard — Stats cards (program count, testimonials, gallery size), recent inquiries table, and a pending-cleanup user list with countdown timers.
- Content CRUD — Full create/read/update/delete for programs, testimonials, gallery images, parent forms, and CMS pages. All list views support drag-and-drop reordering.
- Gallery management — Drag-and-drop upload via react-dropzone, alt text editing, visibility toggles, sortable grid.
- Contact triage — View submissions, update status (new/contacted/archived), delete.
- User management — View users, assign admin roles. Non-admin accounts auto-expire after 24 hours via a Convex cron job.
- Site settings — Key-value store for hero content, about section, social links, contact info, and availability status.
Security
- All admin mutations and queries require authentication + admin role verification via a shared
requireAdminhelper. - R2 upload/delete actions verify admin identity before generating presigned URLs.
- OAuth callback uses PKCE state validation to prevent CSRF.
- Account linking guards against hijacking — existing WorkOS-linked accounts cannot be overwritten by a different OAuth identity sharing the same email.
- Markdown output is sanitized with
rehype-sanitizeto prevent stored XSS from CMS content. - Non-admin users are automatically purged after 24 hours.
Design System
The UI follows a neo-brutalism aesthetic: bold 2px borders, offset drop shadows, rounded corners, and a warm color palette (cream backgrounds with koi orange, pond blue, lily green, and sunflower yellow accents). Three font families reinforce the playful brand: Baloo 2 for headings, Nunito for body text, and Lobster for the logo.
Custom CSS animations bring the theme to life — swimming koi, rising bubbles, expanding ripples, and gentle floating elements on the landing page. A useScrollReveal hook triggers staggered fade-in animations as sections enter the viewport, with prefers-reduced-motion respected throughout.
Lessons Learned
- Convex's type safety pays off at scale. Having the schema, mutations, and React hooks all share generated types caught a surprising number of bugs at compile time that would have been runtime 500s in a REST setup. The tradeoff is vendor lock-in — the data layer is not portable.
- Presigned uploads simplify file handling dramatically. Generating a presigned URL in a Convex action and letting the browser upload directly to R2 avoids buffering large files through the Next.js server, which matters when a daycare admin is batch-uploading a dozen gallery photos at once.
- Auth on the backend, not just the frontend. The portal's UI is gated behind login, but that's not enough — every Convex mutation and query that touches admin data needs its own auth check. A shared helper (
requireAdmin) made this easy to apply consistently across 8+ files without duplicating the lookup logic. - Sanitize after transform, not before. Placing
rehype-sanitizeat the end of the markdown pipeline (after slug generation and autolink headings) means it needs a custom schema to preserve theidattributes and class names those plugins add. The default GitHub schema strips them. Order matters in plugin pipelines. - Drag-and-drop needs optimistic state. With @dnd-kit, the visual reorder happens instantly in local state, then a batch mutation updates all
sortOrdervalues. If you wait for the server round-trip before reflecting the change, the UI feels laggy and items snap back before settling. - Neo-brutalism is deceptively simple. The style looks straightforward — bold borders, offset shadows — but getting hover/active/focus states to feel right across buttons, cards, inputs, and drag handles required careful layering of shadow sizes, translate transforms, and transition timings.