Back to case files
> case_file

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.

Type

// dev

Client

Curious Koi Daycare

Date

February 2026

Stack
Next.jsTailwindcssConvexReact
Details

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 requireAdmin helper.
  • 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-sanitize to 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-sanitize at the end of the markdown pipeline (after slug generation and autolink headings) means it needs a custom schema to preserve the id attributes 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 sortOrder values. 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.