Skip to main content

Planning Center Calendar Integration (Pro Website) — Acceptance Spec

Status: APPROVED 2026-05-21 — founder-confirmed. This spec is the source of truth: code that doesn't match it is wrong, and tests assert it.

Scope: v1 — let a Pro Website customer connect their Church Center iCal calendar feed so the site's Events section auto-syncs from Planning Center instead of being typed by hand. Covers the "Connect Planning Center" panel in the Website editor's Events section, its state machine, the hourly sync job, and the public events render.

Architecture source: knowledge/drafts/2026-05-21-pro-website-planning-center-scoping.md (full options analysis + 9-point regression-risk table).


Foundational decisions

  1. Standard, not an add-on. Included in every Pro Website plan (site-only $14.95, bundled $19.95) — no entitlement flag, no separate Stripe price, no gating. (Founder decision 2026-05-21: first considered a +$5/mo modular add-on, then decided same-session to bundle it — v1 costs ~nothing to run, bundling removes the entire billing surface, and the feature's job is to close deals by removing the #1 objection for Planning Center churches, not to be a revenue line.)
  2. iCal feed, not embed or API. v1 uses the Church Center iCal feed URL. It renders through the existing EventsCalendar (on-brand, no foreign iframe), it's a one-paste setup for the pastor, and it needs no PC developer account or OAuth. (Native PC API/OAuth = a possible future v2; an embed/iframe "basic mode" = a possible v1.1 fallback.)
  3. Either/or per church. A church's Events section is sourced from either manual entry or Planning Center — never both merged. Manual is the default. Switching to Planning Center does not delete the church's manual events; switching back restores them.
  4. The public render path is minimally touched. PC events are normalized server-side into the existing ChurchEvent[] shape; EventsCalendar is untouched. UnifiedTemplate gets exactly one additive, conditional change — the "Events synced from Planning Center" attribution line (decision #3), behind a new optional prop, gated so a manual-events church renders byte-identically. A visitor sees the same calendar — only the data origin changes (plus the attribution line when PC-sourced). (Implemented 2026-05-21, PR #538 — a deliberate, documented refinement of the original "untouched" guard, since decision #3 necessarily requires the attribution to render inside the template.)
  5. The page never calls Planning Center. An hourly cron syncs the feed into a cache column; the public /s/[slug] render reads the cache. A PC outage = a stale-but-fine calendar, never a slow or broken page.
  6. Last-good wins on sync failure. If a sync fails, the public site keeps showing the last successfully-cached events; the pastor sees the error in the editor. The public site never goes blank because of a transient PC hiccup.

Tier visibility

TierEvents → "Connect Planning Center" available?
Pro Website site-only ($14.95)YES
Pro Website bundled with Chat ($19.95)YES
Chat / Voice / Suite / Bundle tiers (no Pro Website)N/A — no Website editor
ITW / SermonWise / PewSearch PremiumN/A — different products

Available to every Pro Website plan. The panel lives in the existing Website editor (Events section); no new tier gate.


Per-role behavior

Connecting / changing the Planning Center feed is a Website-content edit — it follows the existing Website-editor role gating (the same roles that can edit events manually today can connect Planning Center). No new role restriction is introduced. (Open question #1 — founder to confirm.)


Data model (additive — premium_churches)

ColumnTypeDraft twin?Purpose
events_sourcetext (manual | planning_center), default manualdraft_events_sourceWhich feed renders. Default preserves today's behavior for every existing church.
pc_calendar_feed_urltext, nullabledraft_pc_calendar_feed_urlThe Church Center iCal feed URL the pastor pastes.
pc_events_cachejsonb, nullablenoLast successfully fetched + normalized ChurchEvent[]. System-managed.
pc_events_synced_attimestamptz, nullablenoLast successful sync time. System-managed.
pc_events_sync_errortext, nullablenoLast sync failure message. System-managed.

The 3 system-managed columns get no draft twin and must not enter DRAFT_TO_CANONICAL (matching the existing ministries_updated_at exclusion). Only draft_events_source + draft_pc_calendar_feed_url join the draft contract.


State machine — events source

State: manual (default — every existing church)

  • Editor: the Events section renders today's EventsEditor (manual card list), unchanged.
  • Public site: the #events section renders premium_churches.events through EventsCalendar, exactly as today.

The source toggle

The Events section gains a segmented control at the top: [ Enter manually ] [ Connect Planning Center ]. It defaults to whatever the church is currently on (manual for every existing church). Selecting "Connect Planning Center" reveals the connect panel and hides the manual editor. Manual event data is retained, not deleted.

State: pc_not_connected (PC selected, no working feed yet)

  • Editor: the connect panel shows — a short explainer; a "Where do I find this?" disclosure (publish your calendar in Church Center → Share / Subscribe → copy the feed link); one text input for the feed URL; a Save button.
  • Public site: still renders the church's manual events (graceful — nothing breaks). PC events take over only once a feed is connected AND synced (see render resolution below).

State: pc_synced (feed connected, last sync OK)

  • Editor: a green confirmation row — "Connected — N upcoming events. Last synced X ago." The feed URL is shown with "change" / "remove" affordances. A "Preview on my site" link opens the ?draft= iframe so the pastor sees real PC events before publishing.
  • Public site: the #events section renders the PC-sourced events through EventsCalendar — identical look (denomination styling, accent color, per-event "Add to calendar" .ics, "next event" hero card).

State: pc_sync_error (feed connected, last sync failed)

  • Editor: an amber error row with plain-English remediation — "We couldn't read that calendar. Make sure it's published in Church Center and that the link is the Subscribe / feed link." The feed URL + change/remove affordances remain.
  • Public site: keeps showing the last-good pc_events_cache. If the cache is empty (sync never once succeeded), falls back to manual events; if that's empty too, the #events section self-hides (same as any church with no events).

Publishing

Connecting or changing the feed is a draft edit (draft_events_source / draft_pc_calendar_feed_url). It goes live on Publish, like every other Website edit — same mental model, same Publish button.


Render resolution (/s/[slug]/page.tsx)

Server-side, after the existing ?draft= overlay, compute a single resolved events array:

  • IF resolved events_source === 'planning_center' AND pc_events_cache is non-empty → pass pc_events_cache.
  • ELSE → pass events (manual).

UnifiedTemplate and EventsCalendar consume premium.events unchanged — they never know the difference.


Sync behavior

  • Hourly cron POST /api/cron/sync-pc-calendars (new; Bearer CRON_SECRET; registered in vercel.json). Iterates churches with events_source='planning_center' and a feed URL: fetches the feed (webcal://https:// scheme swap), parses VEVENT blocks (Planning Center pre-expands recurring events into instances — no RRULE parsing needed), filters to an upcoming window (recommend next ~90 days, cap ~50 events — open question #2), normalizes to ChurchEvent[], and writes pc_events_cache + pc_events_synced_at — or, on failure, pc_events_sync_error (leaving pc_events_cache at last-good).
  • Immediate sync on save — when the pastor saves a new/changed feed URL, kick a sync right away so the editor preview shows real events without waiting for the cron.
  • The parser is wrapped — a malformed feed records pc_events_sync_error and never throws into a render path.

API contracts

RouteMethodAuthBehavior
/api/premium/updatePOSTWebsite-editor rolesThe website section gains events_source + pc_calendar_feed_url, draft-routed via the existing CANONICAL_TO_DRAFT. Must not touch the existing manual events parse block. On a feed-URL change, triggers an immediate sync.
/api/premium/publishPOSTWebsite-editor rolesNo change — it iterates DRAFT_TO_CANONICAL, which now includes the 2 new draft columns. Must also revalidatePath the site.
/api/cron/sync-pc-calendarsPOSTBearer CRON_SECRETNew. Hourly feed sync (above).

No new public API — the public page reads pc_events_cache server-side.


Demo setup

The Grace Community demo church (00000000-0000-4000-a000-000000000001) is wired to the verified demo Planning Center feed when v1 ships, so PC integration is demoable live like the chatbot demo. Demo feed (verified 2026-05-21, 7 events — Sunday Worship + Midweek Bible Study recurring, VBS, Youth Kickoff, New Members' Lunch, Men's Prayer Breakfast, Community Food Drive): https://calendar.planningcenteronline.com/icals/eJxj4ajmsGLLz2SO38RkxZVanF9QUs1uxZGcmOOpxG1obmxkYsxmxeYaYsVWmsms6JFpxV2QWJSYW1zNAACm6Q7h9bb9578a6e39ad2f3f4ce8ac26cc6b10bff69d9a


Non-goals (v1)

  • Embed / iframe "basic mode" (possible v1.1 fallback for feeds the parser can't handle).
  • Native Planning Center API / OAuth (possible future v2 — richer data, multi-product PC connection).
  • Merging manual + PC events into one calendar (strictly either/or per church).
  • Two-way sync — ChurchWiseAI never writes to Planning Center.
  • Editing individual PC events in our editor — PC events are read-only mirrors; the pastor edits them in Planning Center.
  • Planning Center Groups / Registrations / Check-Ins / Services — Calendar only.
  • Feeding PC events to the church's chatbot — an easy follow-up once v1 normalizes the data, but not v1.
  • Per-event moderation of PC text — the events are the church's own published data from their authoritative system (document this; optionally moderate in the sync job).

Regression guards (see scoping doc §9 for the full 9-point table)

  • The public render path (UnifiedTemplate, EventsCalendar) is not modified.
  • Data model is additive — new nullable columns, default manual; every existing church renders unchanged.
  • The 3 system-managed columns are excluded from the draft contract.
  • The existing manual events parse block in /api/premium/update is not touched.
  • The page never calls Planning Center — cron-fed cache only; ISR revalidate=3600 unchanged.
  • pro-website ssr + edit critical-path Playwright suites stay green; new PC-path specs are added, existing assertions unchanged.

Test coverage (to build with the feature)

  • Unit: the iCal parser/normalizer (webcalhttps, VEVENTChurchEvent, upcoming-window filter, malformed-feed handling); the events-source render resolution.
  • E2E: editor source-toggle + connect-panel states (pc_not_connectedpc_syncedpc_sync_error); a manual-events church renders byte-identically (regression); a PC-sourced church renders PC events; save → draft → publish.
  • Critical-path: run cwa-pro-website-ssr + cwa-pro-website-edit before merge; register a new pro-website-planning-center test entry.

Success criteria — the one-paragraph version a pastor can read

In your Website editor, the Events section now lets you switch from typing events by hand to "Connect Planning Center." Paste your Church Center calendar link once, hit Publish, and your site's events stay in sync automatically — nothing more to type, ever. If the link ever stops working, your site keeps showing the last events it pulled, and you'll see a plain-English message in the editor telling you exactly how to fix it.


Resolved decisions (founder-confirmed 2026-05-21)

  1. Per-role: connecting Planning Center follows the existing Website-editor role gating — the same roles that edit events manually today (admin + office_admin). No new restriction.
  2. Upcoming window: the sync keeps the next ~90 days of events, cap ~50 events. Events outside that window are dropped from the cache.
  3. Attribution line: YES — render a discreet "Events synced from Planning Center" line in muted text under the public calendar.
  4. Sync cadence: hourly cron (matches the site's ISR revalidate=3600 cadence).
  5. Embed fallback: SKIP for v1. Build only the iCal-feed path. A Church Center embed/iframe "basic mode" is added later only if a real church's feed proves unparseable.