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
- 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.)
- 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.) - 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.
- The public render path is minimally touched. PC events are normalized server-side into the existing
ChurchEvent[]shape;EventsCalendaris untouched.UnifiedTemplategets 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.) - 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. - 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
| Tier | Events → "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 Premium | N/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)
| Column | Type | Draft twin? | Purpose |
|---|---|---|---|
events_source | text (manual | planning_center), default manual | draft_events_source | Which feed renders. Default preserves today's behavior for every existing church. |
pc_calendar_feed_url | text, nullable | draft_pc_calendar_feed_url | The Church Center iCal feed URL the pastor pastes. |
pc_events_cache | jsonb, nullable | no | Last successfully fetched + normalized ChurchEvent[]. System-managed. |
pc_events_synced_at | timestamptz, nullable | no | Last successful sync time. System-managed. |
pc_events_sync_error | text, nullable | no | Last 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
#eventssection renderspremium_churches.eventsthroughEventsCalendar, 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
#eventssection renders the PC-sourced events throughEventsCalendar— 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 manualevents; if that's empty too, the#eventssection 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'ANDpc_events_cacheis non-empty → passpc_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; BearerCRON_SECRET; registered invercel.json). Iterates churches withevents_source='planning_center'and a feed URL: fetches the feed (webcal://→https://scheme swap), parsesVEVENTblocks (Planning Center pre-expands recurring events into instances — noRRULEparsing needed), filters to an upcoming window (recommend next ~90 days, cap ~50 events — open question #2), normalizes toChurchEvent[], and writespc_events_cache+pc_events_synced_at— or, on failure,pc_events_sync_error(leavingpc_events_cacheat 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_errorand never throws into a render path.
API contracts
| Route | Method | Auth | Behavior |
|---|---|---|---|
/api/premium/update | POST | Website-editor roles | The 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/publish | POST | Website-editor roles | No change — it iterates DRAFT_TO_CANONICAL, which now includes the 2 new draft columns. Must also revalidatePath the site. |
/api/cron/sync-pc-calendars | POST | Bearer CRON_SECRET | New. 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
eventsparse block in/api/premium/updateis not touched. - The page never calls Planning Center — cron-fed cache only; ISR
revalidate=3600unchanged. pro-websitessr + 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 (
webcal→https,VEVENT→ChurchEvent, upcoming-window filter, malformed-feed handling); the events-source render resolution. - E2E: editor source-toggle + connect-panel states (
pc_not_connected→pc_synced→pc_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-editbefore merge; register a newpro-website-planning-centertest 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)
- 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.
- Upcoming window: the sync keeps the next ~90 days of events, cap ~50 events. Events outside that window are dropped from the cache.
- Attribution line: YES — render a discreet "Events synced from Planning Center" line in muted text under the public calendar.
- Sync cadence: hourly cron (matches the site's ISR
revalidate=3600cadence). - 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.