Web App

Calendar & Group Scheduling

Sync

A calendar app that syncs in both directions with Google Calendar. For group scheduling, it pulls real availability from each participant’s connected calendar, finds the overlap across timezones, and lets the group vote on a time.

Next.jsFirebaseGoogle Calendar APIFramer MotionLuxonrrule
Sync login page

Motivation

Scheduling with a group usually means cross-checking calendars by hand.

When2meet works, but every participant has to re-enter their availability manually. Google Calendar’s “Find a Time” works inside one organization, which rarely covers the case of scheduling across companies or personal accounts. The usual workflow is: open your own calendar, scan for gaps, message people, wait for replies, repeat.

Sync connects to the participants’ real calendars, computes the overlap of free times, and lets the group vote on a slot, all in one interface.

Overview

Week view with event blocks and sidebarMonth view with event indicatorsEvent detail popup on week view
Create event dropdown with personal and shared optionsCreate event form with recurrence optionsScheduling session list with filters

What It Does

01

Personal calendar

Events with full support for recurrence (RRULE), all-day and timed, attendees, and timezones. When the user edits a recurring event, Sync asks for the scope of the change: only this occurrence, this and all following, or the entire series. Each scope preserves the dates of existing instances and only rewrites the fields that actually changed.

Event creation form with recurrence settings
Calendar syncing with Google Calendar

02

Two-way sync with Google Calendar

Connect a Google account and events flow in both directions. Creating or editing an event in Sync writes it to Google; editing or deleting an event in Google shows up on the next sync. Events created before a Google account was connected get pushed up automatically the first time sync runs afterward.

03

Scheduling sessions

The organizer creates a session (title, duration, date range), invites participants by email, and shares a link. Each participant either lets Sync read their connected calendar or drags to select availability on a 30-minute grid. The grid is shaded by how many participants are busy at each time; free slots are highlighted, and participants can vote on preferred ones. Once the organizer picks a slot, Sync creates the meeting on everyone’s calendar.

Create scheduling session — wizard entryCreate scheduling session — choose durationCreate scheduling session — select date rangeCreate scheduling session — invite participantsScheduling grid with conflict heatmap, free slots, and participant list
01 / 05

Technical Deep Dive

Architecture

Sync is a single Next.js app with three layers. The calendar layer handles events and recurrence. The sync layer bridges those events to external providers. The scheduling-session layer builds on both to plan meetings with a group. Calendar events live under each user in Cloud Firestore; scheduling sessions live in a top-level collection with participants stored as a subcollection. Google Calendar is the primary integration; Outlook is also wired in through Microsoft Graph.

Three-layer architecture: calendar, sync, scheduling sessions, with Firestore and Google Calendar API as data sources

Calendar sync

The initial sync pulls events from the current week through one year out and stores the nextSyncToken returned by Google. Every sync after that sends the token back; Google responds with only the events that have changed since. The common case is zero events.

The sync token cannot be combined with timeMin, timeMax, orderBy, or singleEvents. If Google invalidates the token (HTTP 410), the sync falls back to a full resync and stores a new token. Access tokens are checked against a 5-minute buffer before every sync and refreshed if close to expiry.

Sync lifecycle: connect, initial sync, incremental sync, error recovery

syncManager.js — incremental sync

async function syncCalendar(user) {
  const { syncToken } = user
    .connectedCalendars.google;
 
  try {
    // Pass syncToken — Google returns
    // ONLY events changed since last sync
    const response = await calendar
      .events.list({
        calendarId: 'primary',
        syncToken,
        // Cannot combine with timeMin,
        // timeMax, orderBy, singleEvents
      });
 
    return {
      events: response.data.items,
      nextSyncToken:
        response.data.nextSyncToken,
    };
  } catch (err) {
    if (err.code === 410) {
      // syncToken expired — full resync
      return fullSync(user);
    }
    throw err;
  }
}

syncManager.js — push local recurring to Google

async function syncLocalRecurringToGoogle(
  user, masterEvent, localInstances
) {
  // 1. Create master in Google with RRULE
  const createdMaster = await createCalendarEvent(
    token, refreshToken, masterEvent
  );
 
  // 2. Fetch Google-expanded instances
  const googleInstances = googleEvents
    .filter(e =>
      e.recurringEventId === createdMaster.id
    );
 
  // 3. Re-apply local modifications
  //    (matched by originalStartTime)
  const modified = localInstances
    .filter(inst => inst.isModified);
  for (const mod of modified) {
    const match = googleInstances.find(gi =>

Recurring events: master and instances

Google stores a recurring event as one master plus an RRULE. Sync stores every occurrence as its own document, so editing or deleting a single instance does not touch the rest. Instances carry a recurringEventId back to the master and an originalStartTime that identifies them across platforms.

The three edit scopes map onto this shape. “Only this event” marks the instance isModified: true. “This and following” rewrites every instance from the current date forward. “All events” updates the master plus every instance. Dates of existing instances are preserved; only changed fields are overwritten.

Deleting one instance sets isDeleted: true rather than removing the document. The deletion survives until the next sync, at which point the matching Google instance (found by originalStartTime) is deleted on Google’s side too.

Finding a common free time

Four-step pipeline:

  1. eventsToBusySlots() — sort each participant’s events by start time and merge overlapping ones into contiguous busy intervals.
  2. findFreeSlots() — walk 30-minute slots from 8am to 11pm on each day, drop any that overlap a busy interval using Luxon’s Interval.overlaps().
  3. intersectFreeSlots() — keep only slots that appear in every participant’s list.
  4. Manual participants — drag-selected “available” cells are inverted into busy intervals and fed through the same pipeline, so manual and calendar participants unify cleanly.

Slot IDs are slot_${utcMillis}. The same UTC instant yields the same ID regardless of the participant’s timezone, so intersection works across zones without any reformatting.

Heatmap opacity per cell: 0.08 + (busy / totalOthers) * 0.35. Lighter cells mean fewer people are busy at that time.

Scheduling session flow: organizer creates, participants respond, availability is computed, organizer schedules

availability.js — computation pipeline

// 1. Events -> merged busy intervals
function eventsToBusySlots(events) {
  const sorted = events
    .sort((a, b) => a.start - b.start);
  const merged = [];
  for (const event of sorted) {
    const last = merged[merged.length - 1];
    if (last && event.start <= last.end) {
      last.end = Math.max(last.end,
        event.end);
    } else {
      merged.push({ ...event });
    }
  }
  return merged;
}
 
// 2. Generate free slots (30-min, 8am–11pm)
// 3. Check each against busy intervals
//    via Luxon Interval.overlaps()
// 4. Intersect all participants' free slots
//    (keep only IDs in ALL lists)
 
// slot ID: slot_${utcMillis}
// heatmap: 0.08 + (busy / totalOthers) * 0.35

Tech Stack

LayerTechnologyRole
FrameworkNext.js (App Router)Full-stack framework
StylingTailwind CSSUtility-first CSS
Framer MotionAnimations & transitions
Auth & DBFirebase AuthAuthentication + Google OAuth
Cloud FirestoreEvents and scheduling sessions
CalendargoogleapisGoogle Calendar API client
Microsoft GraphOutlook Calendar (secondary)
rruleRRULE expansion
LuxonDate/time operations