Getting kids to do chores has been a challenge since the dawn of parenting. After countless reminders, negotiations, and the occasional standoff over whose turn it is to take out the trash, we decided there had to be a better way. What if chores weren’t just tasks to avoid, but adventures to volunteer for?

That’s how ChoreQuest was born—a gamified chore management system that transforms mundane household tasks into exciting timed challenges. Kids race against the clock, earn virtual coins, and manage their own balance through custom “coin chests.” Parents can generate AI-powered before-and-after images to make tasks more engaging, review completed work through an intuitive dashboard, and push notifications keep everyone in the loop when new opportunities arise.

Each family gets their own unique URL and installable PWA app, complete with 4-digit PIN codes for quick kid access. It’s been running in production since late 2025, serving growing families, and the results have been surprising—turns out kids will fight over who gets to volunteer for the next chore when there’s a timer and coins involved.

Building ChoreQuest: Smart Tech Choices for Real Families

When we set out to build ChoreQuest, a family chore management app, we had one guiding principle: it needs to work for actual families. That meant kids aged 6-12 using mom’s old phone, parents checking in from work, and grandma on her iPad. No app stores, no complex passwords, no “please update to continue.”

Here’s how we made that happen with some clever (and some surprisingly simple) technology choices.

The Problem: Every Family is Different

Traditional multi-tenant apps use subdomains (smith.chorequest.com) or complex user management systems. We needed something simpler. Each family needed their own space, but without the friction of DNS configuration, email verification flows, or managing dozens of user accounts.

Our solution? Path-based multi-tenancy with PIN authentication.

Your family gets a simple URL: chorequest.com/smith. That’s it. Share it with your kids, bookmark it on the iPad, text it to your spouse. It just works.

Two Apps, Two Auth Systems

ChoreQuest is actually two apps in one:

  1. Account management (chorequest.co.za/account) - Email + OTP authentication
  2. Family app (chorequest.co.za/smith) - PIN-only authentication

The Account App: Email + OTP

When you first sign up or manage your subscription, you use the account management interface. This uses email authentication with a 6-digit one-time password:

  1. Enter your email → receive a 6-digit code
  2. Enter the code within 10 minutes → get a long-lived JWT (30 days)
  3. JWT stored in HttpOnly cookie → automatically authenticated
// Generate random 6-digit OTP
const otp = crypto.randomInt(100000, 999999).toString();

// Hash and store with expiry
await db.collection('users').updateOne(
  { email },
  {
    $set: {
      hashedOTP: hashOTP(otp),
      expiresAt: new Date(Date.now() + 10 * 60 * 1000),
    },
  },
  { upsert: true },
);

Why OTP instead of OAuth?

  • No third-party dependencies (Google, Facebook, etc.)
  • Works for any email (no “Sorry, only Gmail” situations)
  • Simpler UX (no popup blockers, no redirect dance)
  • Account creation is seamless (new user? We just created your account)

This is where you manage billing, subscriptions, and account settings. Once you’re done, you rarely come back here—maybe once a month to check on things.

The Family App: PIN-Only

Here’s where the magic happens. At chorequest.co.za/smith, everyone uses a 4-digit PIN. Mom, dad, grandma, the kids—no email addresses, no password resets, just four numbers.

// No email required. No password complexity rules. Just four digits.
const member = await db.collection('members').findOne({
  familyId,
  pinHash: hashPIN(pin),
});

sessionStorage.setItem('currentUser', JSON.stringify(member));

Think about what this means:

  • Mom doesn’t need an email account - She gets a PIN during family setup
  • Dad doesn’t need to remember passwords - Four digits
  • Grandma doesn’t need tech support - Four digits
  • The 9-year-old can log in independently - Four digits

The family app is where all the action happens: creating chores, completing them, reviewing work, managing balances. It’s designed for frequent use by people who just want to get things done.

Separation of Concerns

The two-app architecture creates a clean separation:

  • Account app: Admin stuff (billing, subscriptions, account deletion) - rarely used, secured with email
  • Family app: Daily use (chores, rewards, fun) - constantly used, secured with PINs

You authenticate once per month for account management, but you can jump into the family app dozens of times a day with just four digits. The friction is where it needs to be: low for daily use, appropriately secure for financial stuff.

The Magic of Dynamic Manifests

Here’s where it gets interesting. Progressive Web Apps use a manifest file to control how they behave when “installed” to your home screen. Most apps use a static manifest. We made ours dynamic.

// /api/manifest.json.js
const manifest = {
  name: 'ChoreQuest',
  start_url: familySlug ? `/${familySlug}` : '/',
  display: 'standalone',
  // ... other properties
};

Why does this matter? When the Smith family installs ChoreQuest from chorequest.com/smith, their home screen icon launches directly to their family dashboard. Not a login page. Not a family selector. Their space.

It’s a subtle thing, but it transforms the experience. The app feels like it belongs to them, because technically, it does.

Real-Time Updates: SSE Over WebSockets

When little Timmy completes his chore, mom’s phone should buzz immediately. We need real-time communication. Most developers reach for WebSockets here. We chose Server-Sent Events (SSE).

Why SSE?

WebSockets are bi-directional but complex:

  • Requires special server infrastructure
  • Doesn’t play nice with load balancers/proxies
  • Needs heartbeat management
  • More overhead for simple broadcasting

SSE is HTTP-based uni-directional streaming:

  • Works through any reverse proxy
  • Automatic reconnection built into browsers
  • Simpler server implementation
  • Perfect for our use case (server pushes updates to clients)
// Server: Just emit events through Node's EventEmitter
broadcaster.broadcastChoreEvent(familyId, 'chore.completed', chore);

// Client: Native browser EventSource API
eventSource.addEventListener('chore.completed', event => {
  updateUI(JSON.parse(event.data));
});

The beauty of SSE is in what we didn’t have to build: reconnection logic, ping/pong heartbeats, connection state management. The browser handles it all. When the connection drops, it automatically reconnects. When the server needs to send an update, it just emits an event.

We use a singleton EventEmitter pattern on the server—each family subscribes to their channel, and mutations broadcast to all connected clients. Simple, reliable, and scalable.

Web Push Notifications: The Silent Worker

SSE is great when the app is open, but what about when it’s not? Enter web push notifications.

Here’s the thing about web push: it’s surprisingly powerful. No App Store approval, no special permissions beyond the standard notification prompt, and it works across iOS (as of iOS 16.4) and Android.

// Service worker lives in the background
self.addEventListener('push', event => {
  const data = event.data.json();
  self.registration.showNotification(data.title, {
    body: data.body,
    icon: '/icon-192.png',
    vibrate: [200, 100, 200],
    sound: '/coin.wav', // cha-ching!
  });
});

What makes our implementation special:

  1. Automatic subscription when you log in (if you grant permission)
  2. Family-scoped notifications (only send to relevant members)
  3. Smart targeting (notify kids about new chores, parents about completions)
  4. Graceful degradation (app works fine without push, just no proactive notifications)

The VAPID keys system (Voluntary Application Server Identification) means our server authenticates with the browser’s push service. It’s secure, it’s standard, and it Just Works™.

The Service Worker: Your App’s Invisible Assistant

Service workers are the unsung hero of PWAs. They sit between your app and the network, enabling offline functionality and push notifications.

Our service worker is deliberately minimal:

// Install immediately, no waiting
self.addEventListener('install', () => {
  self.skipWaiting();
});

// Take control of all pages ASAP
self.addEventListener('activate', event => {
  event.waitUntil(self.clients.claim());
});

We’re not doing aggressive caching or offline-first patterns (yet). The service worker exists primarily for push notifications. But having it in place means we can progressively enhance the offline experience later without changing the architecture.

The Rewarding Sound of Success

Here’s a delightful detail: when a chore is completed, the app plays a coin sound. But notifications happen in the service worker context (background), which can’t directly play audio.

Solution? The service worker posts a message to any open tabs:

// Service worker
self.clients.matchAll().then(clients => {
  clients.forEach(client => {
    client.postMessage({ type: 'PLAY_SOUND', sound: '/coin.wav' });
  });
});

// Client
navigator.serviceWorker.addEventListener('message', event => {
  if (event.data.type === 'PLAY_SOUND') {
    new Audio(event.data.sound).play();
  }
});

The notification buzz happens in the background, but if the app is open, you also get the satisfying “cha-ching!” It’s a small touch, but these details matter for engagement.

Connection Pooling: The Hidden Performance Win

MongoDB connection management in serverless/SSR environments is tricky. Create a new connection per request? Too slow. Keep connections open? Memory leaks.

We use a global connection pool:

// lib/mongodb.js
if (!globalThis._mongoClientPromise) {
  client = new MongoClient(MONGODB_URI, {
    maxPoolSize: 10,
    minPoolSize: 2,
  });
  globalThis._mongoClientPromise = client.connect();
}

export async function getDb() {
  const client = await globalThis._mongoClientPromise;
  return client.db(DB_NAME);
}

Using globalThis means the connection pool survives hot module reloads during development and persists across requests in production. Combined with MongoDB’s native connection pooling, we get sub-millisecond database access with zero connection overhead.

Lessons Learned

What Worked

  1. PIN authentication: Zero friction for kids, secure enough for the use case
  2. Dynamic manifests: Each family’s app feels personalized
  3. SSE over WebSockets: Simpler, more reliable, easier to deploy
  4. Web push: Native app-like experience without the app stores

What We’d Do Differently

  1. More aggressive caching: The service worker could do more for offline use
  2. Rate limiting from day one: We added it later; should’ve been there from the start

The Bottom Line

Building for real families means thinking about real constraints: kids who can’t remember passwords, parents who don’t want another app to manage, shared devices, spotty internet connections.

The technologies we chose—dynamic manifests, SSE, web push, PIN auth—aren’t the shiniest or newest. But they solve actual problems. They reduce friction. They make the app feel magic even though the implementation is often surprisingly straightforward.

That’s the real lesson: pick boring technology that solves real problems, then use it in clever ways.


Try it yourself: Visit chorequest.co.za and set up your family in under 60 seconds. You only need an email address.