Cloudsteading
Church4Christ administrative dashboard, supplied by the upstream repository

Church4Christ

Run a church website, people directory and volunteer workflows on Cloudflare.

Church4Christ is a self-hosted Planning Center alternative built on Cloudflare (D1, R2, Workers). Paid services required. Inspect the source and license in the linked repository.

Source & license

Upstream license: GPL-3.0-only

License TL;DR

You can run it and change it privately. If you give others copies of the program or a modified version, provide the corresponding source under the GPL. You can charge money. Running it as a web service alone doesn’t trigger that source-sharing requirement.

Explain GPL v3 in plain English →

Summary of the main license. Separate packages and assets can have different terms.

Inspect repository ↗Read this project’s actual license ↗

Repository owner

@leveo

See the upstream repository for the original creator and contributors.

Maintain this project? Maintainer verification →

Cloudflare hosting

Paid services required

Church4Christ has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. Arbitrary-recipient email requires Paid; database, storage and email usage above included allowances add charges.

Hosting requirements
  • Use Workers Paid for arbitrary-recipient email, starting at $5 USD/account/month; 3,000 outbound emails/month are included, then $0.35 per 1,000.
  • Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered.
  • Workers Paid D1 includes 25 billion rows read/month, 50 million written/month and 5 GB storage; database usage above those amounts is metered.
  • Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account.
  • Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations.
Check current pricing ↗
Sources checked 01/10/2026

Repository snapshot: ed4d989. Hosting eligibility reflects the deployment documentation and listed assumptions.

  • planning-center ↗

    **The trade-offs.** Church4Christ is optimized for Cloudflare Workers and its bindings; moving to another hosting stack is possible source work, not a supported one-click

  • workers ↗

    { "$schema": "node_modules/wrangler/config-schema.json", "name": "church4christ", "main": "./src/worker.ts", "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist", "binding": "ASSETS" }, // Keep these cron strings in sync with the switch in src/worker.ts. "triggers": { "crons": ["0 13 * * *", "0 14 * * 4", "0 9 * * *", "0 * * * *", "15,45 * * * *"] }, "send_email":

  • d1 ↗

    the worker // reads/writes Postgres over the HYPERDRIVE binding below; see the commented // hyperdrive block and docs/supabase-setup.md. "DB_BACKEND": "d1" // Optional Google Classroom real-time bindings: uncomment and set all // three together, or leave all three absent for polling-only operation. // "GOOGLE_CLASSROOM_PUBSUB_TOPIC": "projects/PROJECT/topics/TOPIC", // "GOOGLE_PUBSUB_SERVICE_ACCOUNT_EMAIL": "push@PROJECT.iam.gserviceaccount.com", // "GOOGLE_PUBSUB_S

  • r2 ↗

    from `wrangler hyperdrive create`. // "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "YOUR_HYPERDRIVE_ID" }], "r2_buckets": [ { "binding": "MEDIA", "bucket_name": "church4christ-media" } ], "observability": { "enabled": true } }

  • paid ↗

    { "$schema": "node_modules/wrangler/config-schema.json", "name": "church4christ", "main": "./src/worker.ts", "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist", "binding": "ASSETS" }, // Keep these cron strings in sync with the switch in src/worker.ts. "triggers": { "crons": ["0 13 * * *", "0 14 * * 4", "0 9 * * *", "0 * * * *", "15,45 * * * *"] }, "send_email":

  • paid ↗

    the worker // reads/writes Postgres over the HYPERDRIVE binding below; see the commented // hyperdrive block and docs/supabase-setup.md. "DB_BACKEND": "d1" // Optional Google Classroom real-time bindings: uncomment and set all // three together, or leave all three absent for polling-only operation. // "GOOGLE_CLASSROOM_PUBSUB_TOPIC": "projects/PROJECT/topics/TOPIC", // "GOOGLE_PUBSUB_SERVICE_ACCOUNT_EMAIL": "push@PROJECT.iam.gserviceaccount.com", // "GOOGLE_PUBSUB_S

  • paid ↗

    from `wrangler hyperdrive create`. // "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "YOUR_HYPERDRIVE_ID" }], "r2_buckets": [ { "binding": "MEDIA", "bucket_name": "church4christ-media" } ], "observability": { "enabled": true } }

  • paid ↗

    up>1, 2, 3, 4</sup> | Duration | CPU time | | --- | --- | --- | --- | | **Free** | 100,000 per day | No charge for duration | 10 milliseconds of CPU time per invocation | | **Standard** | 10 million included per month <br> +$0.30 per additional million | No charge or limit for duration | 30 million CPU milliseconds included per month<br> +$0.02 per additional million CPU milliseconds<br><br> Max of [5 minutes of CPU time](https://developers.cloudflare.com/workers/platform/limits/#account-plan-limits) per invocation (default: 30 seconds)<br> Max of 15 minutes of CPU time per [Cron Trigger](https://developers.cloudflare.com/workers/configuration/cron-triggers/) or [Queue Consumer](https://developers.cloudflare.co

  • paid ↗

    oudflare.com/workers/platform/pricing/#workers) | | --- | --- | --- | | Rows read | 5 million / day | First 25 billion / month included + $0.001 / million rows | | Rows written | 100,000 / day | First 50 million / month included + $1.00 / million rows | | Storage (per GB stored) | 5 GB (total) | First 5 GB included + $0.75 / GB-mo | Track your D1 usage To accurately track your usage, use the [meta object](https://developers.cloudflare.com/d1/worker-api/return-object/), [GraphQL Analytics API](https://developers.cloudflare.com/d1/observability/metrics-analytics/#query-via-the-graphql-api), or the [Cloudflare dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/workers/d1/). Select your D1 database, then vie

  • paid ↗

    infrequent access storage) for 1.1 GB, you will be billed for 2 GB. ### Free tier You can use the following amount of storage and operations each month for free. | | Free | | --- | --- | | Storage | 10 GB-month / month | | Class A Operations | 1 million requests / month | | Class B Operations | 10 million requests / month | | Egress (data transfer to Internet) | Free <sup>[1](#user-content-fn-1)</sup> | Caution The free tier only applies to Standard storage, and does not apply to Infrequent Access storage. ### Storage usage Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by averaging the *peak* storage per day over a billing period (30 days). For examp

  • paid ↗

    es Functions, Workers KV, Hyperdrive, and Durable Objects usage for a minimum charge of $5 USD per month for an account. The plan includes increased initial usage allotments, with clear charges for usage that exceeds the base plan. There are no additional charges for data transfer (egress) or throughput (bandwidth). All included usage is on a monthly basis. Pages Functions billing All [Pages Functions](https://developers.cloudflare.com/pages/functions/) are billed as Workers. All pricing and inclusions in this document apply to Pages Functions. Refer to [Functions Pricing](https://developers.cloudflare.com/pages/functions/pricing/) for more information on Pages Functions pricing. ## Workers Users on the Wo

  • paid ↗

    Routing is available on both the Workers Free and Workers Paid plans. Sending to arbitrary recipients requires the Workers Paid plan. Sending to [verified destination addresses](https://developers.cloudflare.com/email-service/configuration/email-routing-addresses/#destination-addresses) in your account is free on all plans, including when only Email Routing is configured. | | Workers Free | Workers Paid | | --- | --- | --- | | **Outbound emails (Email Sending)** | Not available | 3,000 included per month, then $0.35 per 1,000 emails | | **Inbound emails (Email Routing)** | Unlimited | Unlimited | The 3,000 included emails apply per account, per month, aligned with your Cloudflare subscription billing cycle. Emails that hard-bounce or are otherwise accepted by Email Service count toward the quota. Emails rejected at the API boundary, including sends blocke

  • GPL-3.0-only ↗

    GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Pub

  • architecture ↗

    { "$schema": "node_modules/wrangler/config-schema.json", "name": "church4christ", "main": "./src/worker.ts", "compatibility_date": "2026-06-01", "compatibility_flags": ["nodejs_compat"], "assets": { "directory": "./dist", "binding": "ASSETS" }, // Keep these cron strings in sync with the switch in src/worker.ts. "triggers": { "crons": ["0 13 * * *", "0 14 * * 4", "0 9 * * *", "0 * * * *", "15,45 * * * *"] }, "send_email":

  • architecture ↗

    the worker // reads/writes Postgres over the HYPERDRIVE binding below; see the commented // hyperdrive block and docs/supabase-setup.md. "DB_BACKEND": "d1" // Optional Google Classroom real-time bindings: uncomment and set all // three together, or leave all three absent for polling-only operation. // "GOOGLE_CLASSROOM_PUBSUB_TOPIC": "projects/PROJECT/topics/TOPIC", // "GOOGLE_PUBSUB_SERVICE_ACCOUNT_EMAIL": "push@PROJECT.iam.gserviceaccount.com", // "GOOGLE_PUBSUB_S

  • architecture ↗

    from `wrangler hyperdrive create`. // "hyperdrive": [{ "binding": "HYPERDRIVE", "id": "YOUR_HYPERDRIVE_ID" }], "r2_buckets": [ { "binding": "MEDIA", "bucket_name": "church4christ-media" } ], "observability": { "enabled": true } }

Upstream screenshot · leveo/church4christ repository contributors ↗. Depicts the upstream project. We have not deployed and tested a fresh installation here.

What it can replace

Compare the workflow you need. These mappings describe overlap; full feature parity requires a separate comparison.

external SaaS target
varies
→ D1 + R2 + Workers

How it works

The shape of Church4Christ on Cloudflare, and how it stacks up against the rented tools it replaces.

Architecture

Diagram of deployment declarations at the reviewed commit. Each app has its own entrypoint; declared resources do not prove runtime calls. Follow file and line sources below.

View upstream source ↗
Public interface
Configured entry points1
church4christ
wrangler.jsonc
↓
App
church4christ
entry
Cloudflare Workers
Entrypoint: ./src/worker.tsConfigured cron (UTC): 0 13 * * *Configured cron (UTC): 0 14 * * 4Configured cron (UTC): 0 9 * * *Configured cron (UTC): 0 * * * *Configured cron (UTC): 15,45 * * * *
↓

Configuration and workflow sources

Reviewed commit ed4d9892a35a. Files were read as data; upstream applications and CI jobs were not executed.

Partial source coverage: 51 files outside collection bounds; 1 collection or parsing issues. Dynamic imports and generated entrypoints may need manual review.

Deployment configuration · 4 files
wrangler.jsonc ↗

Cloudflare Workers · compatibility 2026-06-01

church4christ · default

Entrypoint: ./src/worker.ts

Static assets: ./dist

Cron triggers (UTC): 0 13 * * * · 0 14 * * 4 · 0 9 * * * · 0 * * * * · 15,45 * * * *

  • DB → D1
  • MEDIA → R2
  • EMAIL → Send Email
  • ASSETS → Static assets
test/e2e/wrangler.e2e.jsonc ↗

Cloudflare Workers · example/template, excluded from overview · compatibility 2026-06-01

church4christ-e2e · default

Entrypoint: ../../dist/server/entry.mjs

Static assets: ../../dist/client

  • DB → D1
  • MEDIA → R2
  • ASSETS → Static assets
test/node/setup/fixtures/wrangler-d1-missing-table.json ↗

Cloudflare Workers · example/template, excluded from overview

test/node/setup/fixtures/wrangler-d1-missing-table.json · default

    No resource bindings declared in this scope.

    test/wrangler.test.jsonc ↗

    Cloudflare Workers · example/template, excluded from overview · compatibility 2026-06-01

    church4christ-test · default

    • DB → D1
    • MEDIA → R2

    Named environments are separate deployments. Bindings are shown only where declared. Configured routes are URL patterns, not verified application endpoints.

    Runtime source · handlers, binding usage and workflow steps

    Observed TypeScript/JavaScript declarations from Worker entrypoints and resolved relative imports. Calls and workflow steps may run conditionally; their listed order is not a proven end-to-end request flow. Router declarations may be mounted under a prefix or may not be registered. This shows code wiring, not a successful deployment or runtime test. Dynamic wiring, aliases and generated code may not resolve.

    src/worker.ts ↗
    • L29 · fetch handler exported · calls handle
    • L30 · scheduled handler exported · calls clearModuleCache, openDb, ctx.waitUntil, finally, sendReminders, sendWeeklyDigest, Promise.all, sendAttendanceEmails, runCommunityWorkflowPass, enqueueDuePlanningCenterSyncJobs, runPlanningCenterSyncPass, runIdentityRecoveryNotificationSweep, replace, slice, toISOString, expireIdentityBusinessContinuations, getUTCMinutes, runScheduledLearningSyncPass, runCanvasDisconnectCleanupPass, runGoogleClassroomRegistrationRenewalPass, getBackend, console.log, runBackup, runStripeRecovery, console.warn
    src/lib/workflowReminders.ts ↗
    • L32 · enrollCampusWorkflows calls (conditional paths may differ): getEnabledModules, getBackend, modules.has, all, rawDb.prepare, has, getEffectiveCampusModules, scopeDatabase, buildAutomaticWorkflowStatements, db.batch, console.warn
    • L78 · runWorkflowReminders calls (conditional paths may differ): getEnabledModules, getBackend, modules.has, utcTime, toISOString, run, bind, rawDb.prepare, all, has, getEffectiveCampusModules, scopeDatabase, crypto.randomUUID, Date.parse, db.prepare, first, link.searchParams.set, replace, task.due_at.slice, replaceAll
    • L217 · runCommunityWorkflowPass calls (conditional paths may differ): enrollCampusWorkflows, runWorkflowReminders

    Environment references: env.WORKFLOW_EMAIL_ENABLED · env.APP_ORIGIN

    src/lib/digest.ts ↗
    • L36 · sendWeeklyDigest calls (conditional paths may differ): has, getEnabledModules, getBackend, console.log, isRuleEnabled, todayInTz, addDays, i18nJoin, all, bind, db.prepare, byPerson.get, list.push, byPerson.set, byPerson.values, rows.map, join, langs.map, t, escapeHtml
    • L130 · sendReminders calls (conditional paths may differ): has, getEnabledModules, getBackend, console.log, todayInTz, isRuleEnabled, offsets.push, all, bind, db.prepare, addDays, sendSchedulingRequest

    Environment references: env.APP_ORIGIN

    src/lib/groupAttendance.ts ↗
    • L23 · randomToken calls (conditional paths may differ): crypto.getRandomValues, replaceAll, btoa, String.fromCharCode
    • L37 · createAttendanceToken calls (conditional paths may differ): randomToken, run, bind, db.prepare, sha256Hex
    • L60 · verifyAttendanceToken calls (conditional paths may differ): sha256Hex, first, bind, db.prepare, run
    • L84 · canRecordAttendance calls (conditional paths may differ): hasAreaAccess, first, bind, db.prepare
    • L107 · saveAttendance calls (conditional paths may differ): first, bind, db.prepare, all, db.batch, members.map, present.has
    • L147 · getAttendanceMap calls (conditional paths may differ): all, bind, db.prepare, Object.fromEntries, results.map
    • L159 · claimOccurrenceForEmail calls (conditional paths may differ): first, bind, db.prepare
    • L180 · buildAttendanceEmail calls (conditional paths may differ): join, langs.map, t, intro.join, intro.map, escapeHtml
    • L211 · sendAttendanceEmails calls (conditional paths may differ): has, getEnabledModules, getBackend, console.log, addDays, todayInTz, all, db.prepare, ensureOccurrences, listOccurrencesNeedingAttendance, claimOccurrenceForEmail, bind, createAttendanceToken, buildAttendanceEmail, toLang, sendEmail, console.error

    Environment references: env.APP_ORIGIN

    src/lib/email.ts ↗
    • L38 · escapeHtml calls (conditional paths may differ): value.replace
    • L46 · base64 calls (conditional paths may differ): String.fromCharCode, bytes.subarray, btoa
    • L55 · b64utf8 calls (conditional paths may differ): base64, encode
    • L61 · encodeHeader calls (conditional paths may differ): test, b64utf8
    • L68 · headerSafe calls (conditional paths may differ): value.replace
    • L73 · buildMime calls (conditional paths may differ): replace, crypto.randomUUID, headerSafe, encodeHeader, b64utf8, lines.join
    • L102 · logEmail calls (conditional paths may differ): run, bind, db.prepare, console.error
    • L120 · sendEmail calls (conditional paths may differ): console.log, logEmail, console.warn, env.EMAIL.send, buildMime, console.error

    Environment references: env.DEV · env.EMAIL_DEV_LOG · env.MODE · env.EMAIL_E2E_SINK · env.EMAIL · env.EMAIL_FROM

    src/lib/backup.ts ↗
    • L42 · runD1Backup calls (conditional paths may differ): fetcher, JSON.stringify, slice, kickoff.text, kickoff.json, signedUrlOf, res.text, res.json, dump.arrayBuffer, now.toISOString, env.MEDIA.put
    • L89 · runBackup calls (conditional paths may differ): console.log, runD1Backup, console.error, String

    Environment references: env.CF_ACCOUNT_ID · env.D1_DATABASE_ID · env.D1_EXPORT_TOKEN · env.MEDIA

    src/lib/modules.ts ↗
    • L82 · buildModuleGroups calls (conditional paths may differ): MODULE_GROUP_CONFIG.map, declaredGroups.filter, supported.has, unsupportedDeclared.join, seen.has, seen.add, Object.hasOwn, keys.filter
    • L128 · under calls (conditional paths may differ): path.startsWith
    • L138 · moduleForPath calls (conditional paths may differ): under
    • L151 · paymentOperationsEnabled calls (conditional paths may differ): modules.has
    • L175 · filterByBackend calls (conditional paths may differ): out.add
    • L196 · getEnabledModules calls (conditional paths may differ): Date.now, getSettings, MODULE_KEYS.map, MODULE_KEYS.filter, filterByBackend
    src/lib/dbProvider.ts ↗
    • L26 · getBackend calls (conditional paths may differ): JSON.stringify
    • L38 · openDb calls (conditional paths may differ): getBackend, postgres, catch, sql.end

    Environment references: env.DB_BACKEND · env.HYPERDRIVE · env.DB

    src/lib/stripeRecovery.ts ↗
    • L40 · pruneStripeWebhookRetention calls (conditional paths may differ): pruneStripeWebhookPayloads, opened.end
    • L56 · recoverySecrets calls (conditional paths may differ): filter
    • L62 · runStripeRecovery calls (conditional paths may differ): recoverySecrets, drain, sanitizeStripeDiagnostic, checkoutRecovery, retention

    Environment references: env.STRIPE_SECRET_KEY · env.STRIPE_WEBHOOK_SECRET · env.HYPERDRIVE

    src/lib/learningGoogleRegistrationCron.ts ↗
    • L70 · runGoogleClassroomRegistrationRenewalPass calls (conditional paths may differ): has, getEnabledModules, getBackend, Object.freeze, configured, googleClassroomPushReadiness, dependencies.now, Number.isSafeInteger, dependencies.importKeyRing, setTimeout, controller.abort, dependencies.listCleanupConnectionIds, dependencies.recoverCleanup, dependencies.renew, clearTimeout
    src/lib/learningCanvasCleanupCron.ts ↗
    • L50 · runCanvasDisconnectCleanupPass calls (conditional paths may differ): has, getEnabledModules, getBackend, Object.freeze, configured, readCanvasAllowedOrigins, dependencies.importKeyRing, dependencies.listCleanupConnectionIds, setTimeout, controller.abort, dependencies.recoverCleanup, clearTimeout
    src/lib/learningSyncOrchestration.ts ↗
    • L92 · safeEpoch calls (conditional paths may differ): now, Number.isSafeInteger
    • L101 · defaultSleep calls (conditional paths may differ): Promise.reject, setTimeout, signal.removeEventListener, cleanup, resolve, clearTimeout, reject, signal.addEventListener
    • L105 · cleanup calls (conditional paths may differ): signal.removeEventListener
    • L106 · finish calls (conditional paths may differ): cleanup, resolve
    • L107 · abort calls (conditional paths may differ): clearTimeout, cleanup, reject
    • L116 · defaultLog calls (conditional paths may differ): console.info, JSON.stringify
    • L128 · backoff calls (conditional paths may differ): Math.min, Math.max
    • L135 · linkedSignal calls (conditional paths may differ): combined.abort, parent.addEventListener, deadline.signal.addEventListener, setTimeout, deadline.abort, abort, clearTimeout, parent.removeEventListener, deadline.signal.removeEventListener
    • L158 · listLearningSyncTargets calls (conditional paths may differ): Object.hasOwn, learningValidation.exactRecord, learningValidation.integer, db.prepare, all, statement.bind, Array.isArray, Object.freeze, result.results.map, learningValidation.oneOf, learningValidation.externalId
    • L200 · listLearningAdminSyncCourses calls (conditional paths may differ): learningValidation.integer, all, bind, db.prepare, Array.isArray, Object.freeze, result.results.map, learningValidation.oneOf, learningValidation.externalId, learningValidation.boundedString, learningValidation.timestamp
    • L230 · markLearningConnectionReconnectRequired calls (conditional paths may differ): learningValidation.integer, learningValidation.oneOf, run, bind, db.prepare
    • L248 · markLearningCourseSyncAttempt calls (conditional paths may differ): learningValidation.integer, learningValidation.timestamp, run, bind, db.prepare
    • L268 · runLearningSyncWithRetry calls (conditional paths may differ): learningValidation.oneOf, safeEpoch, linkedSignal, dependencies.reconcile, Math.max, log, Object.freeze, permanentReconnect, dependencies.markReconnectRequired, transient, sleep, backoff, linked.dispose
    • L336 · reconcileLearningSyncTarget calls (conditional paths may differ): reconcileLearningProviderCourse
    • L344 · reconcileLearningProviderCourse calls (conditional paths may differ): configured, importLearningCredentialKeyRing, runLearningSyncWithRetry, markLearningConnectionReconnectRequired, reconcileGoogleClassroomCourse, reconcileCanvasCourse, readCanvasAllowedOrigins
    • L397 · runScheduledLearningSyncPass calls (conditional paths may differ): learningEnabled, Object.freeze, listLearningSyncTargets, reconcileLearningSyncTarget, markLearningCourseSyncAttempt, toISOString, safeEpoch, reconcileTarget, console.warn, JSON.stringify
    src/lib/identityRecoveryOutbox.ts ↗
    • L20 · format calls (conditional paths may differ): replace, slice, date.toISOString
    • L24 · parseInstant calls (conditional paths may differ): value.replace, Number.isFinite, date.getTime, format
    • L39 · prepareIdentityRecoveryNotification calls (conditional paths may differ): Number.isSafeInteger, toLowerCase, input.recipient.trim, first, bind, db.prepare, identityRecoveryKeyMaterial, hmacIdentityRecoveryValue, encryptIdentityRecoveryPayload
    • L59 · enqueueIdentityRecoveryNotification calls (conditional paths may differ): prepareIdentityRecoveryNotification, statement.run
    • L76 · deliverIdentityRecoveryNotifications calls (conditional paths may differ): identityRecoveryKeyMaterial, format, parseInstant, Number.isSafeInteger, Math.min, all, bind, db.prepare, results.some, crypto.randomUUID, hmacIdentityRecoveryValue, nowDate.getTime, run, sendIdentityRecoveryNotice, sendIdentityRecoveryCompletedNotice, decryptIdentityRecoveryPayload, test, sendIdentityRecoveryHoldNotice
    • L139 · runIdentityRecoveryNotificationSweep calls (conditional paths may differ): deliverIdentityRecoveryNotifications
    src/lib/identityBusinessContinuation.ts ↗
    • L70 · validId calls (conditional paths may differ): Number.isSafeInteger
    • L73 · text calls (conditional paths may differ): trim, value.normalize, encoder.encode, test
    • L79 · nowUtc calls (conditional paths may differ): value.replace, Number.isFinite, date.getTime, replace, slice, date.toISOString
    • L84 · expiresAt calls (conditional paths may differ): replace, nowUtc, date.setUTCMinutes, date.getUTCMinutes, slice, date.toISOString
    • L89 · leaseAt calls (conditional paths may differ): replace, nowUtc, date.setUTCMinutes, date.getUTCMinutes, slice, date.toISOString
    • L94 · deliveryRetryAt calls (conditional paths may differ): replace, nowUtc, date.setUTCMinutes, date.getUTCMinutes, slice, date.toISOString
    • L99 · validIntentId calls (conditional paths may differ): UUID.test
    • L100 · databaseId calls (conditional paths may differ): crypto.getRandomValues
    • L102 · normalizeGivingContinuationInput calls (conditional paths may differ): validId, Number.isSafeInteger, toLowerCase, text, test, EMAIL.test, Object.freeze
    • L118 · normalizeRegistrationContinuationInput calls (conditional paths may differ): validId, Number.isSafeInteger, text, toLowerCase, EMAIL.test, test, Array.isArray, sort, input.answers.map, answers.some, Object.freeze
    • L140 · continuationPayloadDigest calls (conditional paths may differ): sha256Utf8, JSON.stringify
    • L144 · canonicalRegistrationAnswers calls (conditional paths may differ): sort, answers.map
    • L149 · revalidateRegistrationAnswers calls (conditional paths may differ): questions.map, questionsById.get, String, JSON.parse, Array.isArray, values.some, canonicalRegistrationAnswers, validateAnswers
    • L167 · registrationQuestionDigest calls (conditional paths may differ): sort, questions.map, sha256Utf8, JSON.stringify
    • L189 · bytesToBase64url calls (conditional paths may differ): String.fromCharCode, replace, replaceAll, btoa
    • L193 · base64urlToBytes calls (conditional paths may differ): replaceAll, value.replaceAll, repeat, Uint8Array.from, atob, char.charCodeAt
    • L199 · deliveryKey calls (conditional paths may differ): crypto.subtle.digest, encoder.encode, crypto.subtle.importKey
    • L205 · sealSignupDelivery calls (conditional paths may differ): crypto.getRandomValues, encoder.encode, crypto.subtle.encrypt, deliveryKey, JSON.stringify, packed.set, bytesToBase64url
    • L214 · openSignupDelivery calls (conditional paths may differ): base64urlToBytes, value.slice, crypto.subtle.decrypt, packed.slice, encoder.encode, deliveryKey, JSON.parse, decode, EMAIL.test, UUID.test, test, Object.freeze
    • L232 · signupOperationPublicId calls (conditional paths may differ): validId, validIntentId, first, bind, db.prepare
    • L240 · loadIntent calls (conditional paths may differ): first, bind, db.prepare
    • L248 · bindSignupOperation calls (conditional paths may differ): first, bind, db.prepare, openSignupDelivery, signupOperationPublicId, nowUtc, run, deliveryRetryAt, consumeIdentityOtpDeliveryRetryLimit, crypto.randomUUID, leaseAt, prepareSignup, sealSignupDelivery, db.batch
    • L333 · createBaseIntent calls (conditional paths may differ): run, bind, db.prepare, first
    • L350 · beginGivingContinuation calls (conditional paths may differ): validId, validIntentId, normalizeGivingContinuationInput, continuationPayloadDigest, registerIdentitySource, createBaseIntent, expiresAt, run, bind, db.prepare, newCheckoutRequestId, first, bindSignupOperation
    • L374 · beginRegistrationContinuation calls (conditional paths may differ): validId, validIntentId, nowUtc, normalizeRegistrationContinuationInput, listQuestions, JSON.stringify, revalidateRegistrationAnswers, registrationQuestionDigest, continuationPayloadDigest, registerIdentitySource, createBaseIntent, expiresAt, run, bind, db.prepare, newCheckoutRequestId, first, bindSignupOperation
    • L411 · authenticateAndAttach calls (conditional paths may differ): completeVerifiedSignup, attachIdentitySourceForSignedInSession, identityGatewaySessionContext
    • L429 · setReady calls (conditional paths may differ): continuationChildTable, db.batch, bind, db.prepare, first, includes
    • L447 · terminalizeContinuation calls (conditional paths may differ): continuationChildTable, nowUtc, db.batch, bind, db.prepare
    • L473 · expireIdentityBusinessContinuations calls (conditional paths may differ): nowUtc, Number.isSafeInteger, all, bind, db.prepare, terminalizeContinuation
    • L502 · claimContinuationChild calls (conditional paths may differ): sha256Utf8, crypto.randomUUID, nowUtc, first, bind, db.prepare, run, leaseAt
    • L516 · markGivingConsumed calls (conditional paths may differ): first, bind, db.prepare, db.batch, crypto.randomUUID
    • L539 · completeGivingContinuation calls (conditional paths may differ): validId, validIntentId, loadIntent, nowUtc, terminalizeContinuation, authenticateAndAttach, first, bind, db.prepare, setReady, claimContinuationChild, run, getFund, createOneTimeCheckout, getStripeCustomer, parseCheckoutRequestId, markGivingConsumed
    • L604 · loadRegistrationContinuation calls (conditional paths may differ): first, bind, db.prepare
    • L633 · continuePaidRegistrationCheckout calls (conditional paths may differ): deps.createCheckout, deps.attachRequest, deps.continueRequest, classifyRegistrationCheckoutFailure, deps.cancelRequest
    • L666 · completeRegistrationContinuation calls (conditional paths may differ): validId, validIntentId, loadRegistrationContinuation, nowUtc, terminalizeContinuation, authenticateAndAttach, setReady, continueRegistrationCheckoutRequest, continuePaidRegistrationCheckout, claimContinuationChild, first, bind, db.prepare, run, JSON.parse, event.currency.toLowerCase, release, listQuestions, registrationQuestionDigest, revalidateRegistrationAnswers
    • L809 · signedInIdentitySourceContext calls (conditional paths may differ): validId, validIntentId, registerIdentitySource, identityGatewaySessionContext, attachIdentitySourceForSignedInSession
    • L822 · signedInExistingIdentitySourceContext calls (conditional paths may differ): validId, validIntentId, getIdentitySourceRecord, identityGatewaySessionContext, attachIdentitySourceForSignedInSession

    Environment references: env.IDENTITY_VERIFICATION_SECRET

    src/lib/planningCenterSync.ts ↗
    • L20 · planningCenterCredentials calls (conditional paths may differ): some, every, test, USER_AGENT_PATTERN.test, Object.freeze
    • L38 · planningCenterDatabaseId calls (conditional paths may differ): crypto.getRandomValues
    • L42 · leaseUntil calls (conditional paths may differ): toISOString, now.getTime
    • L50 · ensurePlanningCenterSyncJob calls (conditional paths may differ): Number.isSafeInteger, run, bind, db.prepare, planningCenterDatabaseId
    • L57 · enqueuePlanningCenterSyncJobs calls (conditional paths may differ): Number.isSafeInteger, all, db.prepare, enqueuePlanningCenterSyncJob
    • L74 · enqueueDuePlanningCenterSyncJobs calls (conditional paths may differ): replace, slice, toISOString, now.getTime, all, bind, db.prepare, rows.filter, row.last_success_at.getTime, Date.parse, row.last_success_at.includes, row.last_success_at.replace, Number.isFinite, enqueuePlanningCenterSyncJob
    • L103 · enqueuePlanningCenterSyncJob calls (conditional paths may differ): Number.isSafeInteger, run, bind, db.prepare, planningCenterDatabaseId
    • L116 · claimPlanningCenterSyncJob calls (conditional paths may differ): ensurePlanningCenterSyncJob, Number.isFinite, crypto.randomUUID, sha256Utf8, leaseUntil, run, bind, db.prepare, now.toISOString, first, Object.freeze
    • L138 · completePlanningCenterSyncJob calls (conditional paths may differ): sha256Utf8, run, bind, db.prepare, toISOString
    • L149 · saveCursor calls (conditional paths may differ): run, bind, db.prepare
    • L155 · failPlanningCenterSyncJob calls (conditional paths may differ): test, sha256Utf8, Number.isFinite, toISOString, Math.max, Date.now, run, bind, db.prepare
    • L165 · attributeString calls (conditional paths may differ): trim
    • L170 · attributeBoolean calls (conditional paths may differ): keys.some
    • L174 · canonicalize calls (conditional paths may differ): Array.isArray, value.map, Object.fromEntries, map, sort, Object.entries, left.localeCompare, canonicalize
    • L180 · relatedIncluded calls (conditional paths may differ): Array.isArray, filter, data.map, sort, page.included.filter, ids.has, attributeBoolean, left.id.localeCompare
    • L193 · selectedIncludedContact calls (conditional paths may differ): filter, map, relatedIncluded, attributeBoolean, attributeString
    • L201 · campusScopedOwner calls (conditional paths may differ): first, bind, db.prepare
    • L208 · syncPerson calls (conditional paths may differ): test, relatedIncluded, selectedIncludedContact, attributeString, join, filter, normalizeEmail, normalizePhone, normalizeName, sha256Utf8, JSON.stringify, canonicalize, first, bind, db.prepare, registerIdentitySource, campusScopedOwner, findVerifiedContactOwner, run
    • L251 · syncPlanningCenterPeoplePage calls (conditional paths may differ): first, bind, db.prepare, test, syncPerson
    • L263 · markPlanningCenterPersonDeleted calls (conditional paths may differ): test, first, bind, db.prepare, run, sha256Utf8
    • L276 · recordPlanningCenterMergerEvidence calls (conditional paths may differ): test, sha256Utf8, JSON.stringify, canonicalize, attributeString, first, bind, db.prepare, Math.min, Math.max, run
    • L311 · runPlanningCenterSyncPass calls (conditional paths may differ): planningCenterCredentials, all, bind, db.prepare, now.toISOString, ensurePlanningCenterSyncJob, claimPlanningCenterSyncJob, first, test, fetchPlanningCenterPage, syncPlanningCenterPeoplePage, recordPlanningCenterMergerEvidence, nextPlanningCenterUrl, completePlanningCenterSyncJob, saveCursor, failPlanningCenterSyncJob, exec, markPlanningCenterPersonDeleted, run

    Environment references: env.PLANNING_CENTER_CLIENT_ID · env.PLANNING_CENTER_SECRET · env.PLANNING_CENTER_WEBHOOK_SECRET · env.PLANNING_CENTER_USER_AGENT

    src/lib/appDb.ts ↗
    • L32 · readSnapshotBatch calls (conditional paths may differ): assertSnapshotBatchSupport, db.snapshotBatch, db.batch
    src/lib/campusScope.ts ↗
    • L148 · escaped calls (conditional paths may differ): value.replace
    • L194 · hasFollowingAlias calls (conditional paths may differ): mask.slice, tail.match, CLAUSE_WORDS.has, toLowerCase
    • L202 · scopeReadTable calls (conditional paths may differ): escaped, maskSql, mask.matchAll, keyword.toUpperCase, test, mask.slice, at, table.split, hasFollowingAlias, replacements.push, replacements.reverse, sql.slice
    • L224 · scopeInsert calls (conditional paths may differ): escaped, maskSql, exec, search, trimEnd, sql.slice, test, mask.indexOf, some, columns.split, toLowerCase, column.trim, toUpperCase, lastIndexOf, mask.slice, insertions.push, insertions.sort
    • L292 · scopeMutationWhere calls (conditional paths may differ): maskSql, mask.slice, exec, trimEnd, sql.slice, trim
    • L307 · scopeUpdate calls (conditional paths may differ): escaped, re.exec, maskSql, at, table.split, scopeMutationWhere
    • L318 · scopeDelete calls (conditional paths may differ): escaped, re.exec, maskSql, at, table.split, scopeMutationWhere
    • L329 · scopePeopleMutations calls (conditional paths may differ): exec, maskSql, scopeMutationWhere
    • L359 · replaceSettingsMutationTarget calls (conditional paths may differ): pattern.exec, maskSql, lastIndexOf, toLowerCase, sql.slice
    • L372 · scopeSettings calls (conditional paths may differ): replaceSettingsMutationTarget, scopeInsert, scopeUpdate, scopeDelete, exec, maskSql, scoped.slice, scopeReadTable
    • L401 · scopeCampusSql calls (conditional paths may differ): Number.isSafeInteger, scopeSettings, scopeInsert, scopeUpdate, scopeDelete, scopeReadTable, scopePeopleMutations
    • L450 · campusIdForDatabase calls (conditional paths may differ): DATABASE_CAMPUS_IDS.get
    src/lib/campusDb.ts ↗
    • L37 · listCampusesForUser calls (conditional paths may differ): all, bind, db.prepare, results.map
    • L56 · resolveCampusContext calls (conditional paths may differ): first, bind, db.prepare, toCampusRow, toMembership
    • L104 · createCampus calls (conditional paths may differ): input.name.trim, toLowerCase, input.slug.trim, test, first, bind, db.prepare, toCampusRow
    • L122 · upsertCampusMembership calls (conditional paths may differ): join, parseAdminAreasForRole, input.adminAreas.join, run, bind, db.prepare
    • L154 · listCampusMemberships calls (conditional paths may differ): all, bind, db.prepare, results.map, toMembership
    • L173 · setCampusModules calls (conditional paths may differ): includes, db.batch, MODULE_KEYS.map, bind, db.prepare, enabled.has
    • L189 · getCampusModules calls (conditional paths may differ): all, bind, db.prepare, filter, results.map, includes
    • L199 · getEffectiveCampusModules calls (conditional paths may differ): all, bind, db.prepare, map, results.filter, globallyEnabled.has, includes
    • L244 · toMembership calls (conditional paths may differ): parseAdminAreasForRole
    src/lib/workflowDb.ts ↗
    • L53 · validateSteps calls (conditional paths may differ): Array.isArray, steps.map, Number.isInteger, cleanText
    • L71 · createWorkflowTemplate calls (conditional paths may differ): requireFellowship, includes, first, bind, db.prepare, requirePerson, cleanText, JSON.stringify, validateSteps
    • L107 · listWorkflowTemplates calls (conditional paths may differ): all, db.prepare
    • L116 · setWorkflowTemplateEnabled calls (conditional paths may differ): requireCampus, run, bind, db.prepare, positiveId
    • L135 · buildRun calls (conditional paths may differ): requireFellowship, requirePerson, utcTime, toISOString, cleanText, crypto.randomUUID, bind, db.prepare, forEach, validateSteps, JSON.parse, getTime, statements.push
    • L186 · startWorkflow calls (conditional paths may differ): requireCampus, first, bind, db.prepare, positiveId, buildRun, db.batch
    • L219 · buildAutomaticWorkflowStatements calls (conditional paths may differ): all, bind, db.prepare, buildRun, statements.push
    • L249 · listWorkflowTasks calls (conditional paths may differ): where.push, bindings.push, all, bind, db.prepare, join, where.map
    • L278 · updateWorkflowTask calls (conditional paths may differ): requireCampus, requirePerson, includes, first, bind, db.prepare, cleanText, utcTime, toISOString, db.batch
    • L366 · cancelWorkflow calls (conditional paths may differ): requireCampus, db.batch, bind, db.prepare, toISOString
    • L384 · retryWorkflowReminder calls (conditional paths may differ): requireCampus, run, bind, db.prepare, toISOString
    src/lib/communityValidation.ts ↗
    • L4 · requireCampus calls (conditional paths may differ): campusIdForDatabase
    • L10 · positiveId calls (conditional paths may differ): test, Number, Number.isSafeInteger
    • L21 · cleanText calls (conditional paths may differ): value.trim, test
    • L32 · requirePerson calls (conditional paths may differ): requireCampus, first, bind, db.prepare, positiveId
    • L48 · requireFellowship calls (conditional paths may differ): requireCampus, first, bind, db.prepare, positiveId
    • L63 · utcTime calls (conditional paths may differ): test, Number.isFinite, time.getTime, slice, time.toISOString, value.slice
    src/lib/dates.ts ↗
    • L13 · isValidDateStr calls (conditional paths may differ): test, map, s.split, Date.UTC, dt.getUTCFullYear, dt.getUTCMonth, dt.getUTCDate
    • L21 · todayInTz calls (conditional paths may differ): format
    • L31 · addDays calls (conditional paths may differ): slice, toISOString, getTime
    • L39 · nextWeekday calls (conditional paths may differ): getUTCDay, addDays
    • L50 · addMonthsSameDom calls (conditional paths may differ): map, dateStr.split, Math.floor, getUTCDate, Date.UTC, padStart, String, pad
    • L63 · addMinutesToUtcSql calls (conditional paths may differ): getTime, utcSql.replace, replace, slice, toISOString
    • L70 · toUtcSql calls (conditional paths may differ): replace, slice, now.toISOString
    • L75 · formatDate calls (conditional paths may differ): map, dateStr.split, format, Date.UTC
    • L94 · formatMonth calls (conditional paths may differ): map, yearMonth.split, format, Date.UTC
    • L106 · wallClock calls (conditional paths may differ): formatToParts, parts.find, get
    • L128 · datetimeLocalToUtc calls (conditional paths may differ): LOCAL_RE.exec, Number, Date.UTC, check.getUTCFullYear, check.getUTCMonth, check.getUTCDate, check.getUTCHours, check.getUTCMinutes, Date.parse, wallClock, replace, slice, toISOString
    • L152 · utcToDatetimeLocal calls (conditional paths may differ): utcSql.replace, Number.isNaN, d.getTime, wallClock
    src/lib/db.ts ↗
    • L21 · assertIdent calls (conditional paths may differ): IDENT.test, JSON.stringify
    • L33 · i18nJoin calls (conditional paths may differ): includes, JSON.stringify, assertIdent, join, cols.map
    • L76 · getPersonByEmail calls (conditional paths may differ): first, bind, db.prepare, toLowerCase, email.trim
    • L84 · getPersonById calls (conditional paths may differ): first, bind, db.prepare
    • L114 · listMinistries calls (conditional paths may differ): i18nJoin, all, db.prepare
    src/lib/emailSettingsDb.ts ↗
    • L31 · listRules calls (conditional paths may differ): all, db.prepare, Object.fromEntries, results.map
    • L36 · isRuleEnabled calls (conditional paths may differ): first, bind, db.prepare
    • L41 · setRule calls (conditional paths may differ): run, bind, db.prepare
    • L52 · getTemplate calls (conditional paths may differ): first, bind, db.prepare
    • L60 · listTemplates calls (conditional paths may differ): all, db.prepare
    • L67 · saveTemplate calls (conditional paths may differ): run, bind, db.prepare
    • L84 · listEmailLog calls (conditional paths may differ): all, bind, db.prepare
    • L93 · logEmail calls (conditional paths may differ): run, bind, db.prepare
    • L104 · fillTemplate calls (conditional paths may differ): text.replace
    src/lib/i18n.ts ↗
    • L12 · escapeHtml calls (conditional paths may differ): replace, value.replace
    • L21 · t calls (conditional paths may differ): template.replace, escapeHtml, String
    Build and deployment pipeline · 1 GitHub Actions workflows

    Repository CI declarations, separate from runtime request processing. Job dependencies and conditions are shown as written; long commands are shortened with an ellipsis; a workflow file does not prove a recent successful run.

    CI · .github/workflows/ci.yml ↗

    Triggers: push, pull_request

    build-test · no job dependencies declared

    1. Checkoutactions/checkout@v7
    2. Setup Nodeactions/setup-node@v7
    3. Install dependenciesnpm ci
    4. Verify generated design artifactstest -f src/styles/tokens.generated.css && test -f src/lib/themeMeta.generated.ts
    5. Generate Cloudflare Worker typesnpx wrangler types
    6. Prepare test reportsnode -e "require('node:fs').mkdirSync('.tmp', { recursive: true })"
    7. Check generated capability documentationnpm run docs:check
    8. Prove setup dry-run and clean D1 installnpx vitest run --project node test/setup/dry-run.test.ts test/setup/clean-room-d1.test.ts
    9. Prove clean Supabase setupnpx vitest run --project node test/setup/clean-room-pg.test.ts --reporter=json --outputFile=.tmp/setup-pg.json
    10. Assert Supabase setup test did not skipnode scripts/ci/assert-vitest-json.mjs .tmp/setup-pg.json 1
    11. Build design tokensnpm run tokens
    12. Check token contrast + no hardcoded valuesnpm run tokens:check
    13. Run tests (node + workers projects)npm test
    14. Type checknpm run check
    15. Buildnpm run build
    16. Apply D1 migrations (local)npm run db:migrate:local
    17. Seed demo data (local)npm run db:seed:local
    18. Seed local media objectsnpm run db:seed-media:local
    19. Smoke testbash scripts/smoke.sh
    20. End-to-end tests (built worker)npm run test:e2e
    21. Apply Supabase migrations (Postgres)npm run db:migrate:supabase
    22. Seed demo data (Postgres)npm run db:seed:supabase
    23. Postgres backend tests (pg project)npx vitest run --project pg --reporter=json --outputFile=.tmp/pg.json
    24. Assert Postgres project did not skipnode scripts/ci/assert-vitest-json.mjs .tmp/pg.json 1
    25. End-to-end tests (Postgres worker)npm run test:e2e:pg
    package.json ↗
    • build: npm run tokens && astro build && node scripts/check-production-bundle.mjs
    • build:e2e: npm run tokens && astro build --mode e2e
    • release:rehearse: node scripts/release/rehearse-upgrade.mjs --d1
    • deploy: npm run build && wrangler deploy

    Full upstream document by @leveo · README.md · snapshot ed4d989

    Church4Christ

    An AI-native, open-source bilingual church website and church-management foundation for customized implementations.

    See the demo: church4christ.yunfei-song.com.

    Watch the Built Around Your Ministry promotional video

    Watch Built Around Your Ministry, the demo site's promotional video (MP4).

    Start here: Setup for people and AI agents. Run npm run onboard to choose your organization's identity, colors, logo, and first features in a local browser page, then pass the saved preferences to the existing installer. Coding agents begin with AGENTS.md; Claude also reads CLAUDE.md.

    Church4Christ combines a bilingual public site with an admin system for content, prayer care, volunteer scheduling, people, and households. Optional modules add a member portal and other church-management workflows. The project aims to lower the startup and ongoing maintenance cost of a customized implementation while keeping the code and deployment configuration available to its operators.

    The current source release is 1.2. Church4Christ is an open-source foundation, not a turnkey managed service. Local evaluation is free, and some deployments can fit within provider free allowances, but production hosting, email, databases, domains, backups, and other services may charge based on configuration and usage. See Deployment profiles and costs before choosing a production setup.

    The English home page The Chinese home page The prayer wall board
    The volunteer scheduling matrix The Midnight theme The Member Portal dashboard
    The English Genesis 1 learner course The Chinese Genesis 1 learner course The Learning provider administration page

    A distinct workspace for every core module. The default Sanctuary design pairs warm ivory surfaces and forest green navigation with layouts suited to each task: publishing editors with local previews, people and household records, care queues, serving matrices, touch-friendly children's check-in, course players, and finance ledgers. Public, member, leader, and administrator menus follow the enabled modules and each person's permissions. Harvest and Midnight remain available, with light and dark modes. See the 21-module design inventory.

    The screenshots use the repository's fictional demo content. First setup lets you include those examples or start without them; both choices keep the same layouts, theme, and bundled default images. English pages use English interface and demo copy; Chinese pages support Chinese and bilingual content. All 29 interface screenshots were recaptured from the running application; see the capture inventory and regeneration command.

    Grouped navigation. A fully enabled site condenses its public destinations into Welcome, Explore, Connect, and Get Involved. Built-in links stay in their audience group, while custom pages and external links appear under More. The order saved in Admin → Navigation is preserved inside each group, and empty groups disappear.

    The Connect menu with its own links and community image

    Two languages are included out of the box (English and Chinese), along with three ready-made looks and a modular starting point for further customization.


    Who is this for?

    Church4Christ is intended for small and mid-size churches, fellowships, and nonprofits — especially bilingual and immigrant congregations — that need a public website and a customizable church-management base. It fits best when a technically comfortable staff member, volunteer, or implementation partner can own deployment and maintenance. A managed platform may be a better fit when the organization prefers vendor-operated setup, support, upgrades, and operational responsibility over source-level customization.

    How does this approach compare?

    Different product categories serve different needs, and provider terms vary. This table compares operating models rather than promising universal prices, portability, or data rights.

    Church4Christ Self-managed plugin CMS Hosted site builder Managed church platform
    Primary fit Customized bilingual website plus modular church workflows Extensible content website assembled from plugins Provider-managed public website Provider-managed church workflows
    Cost model Infrastructure and service usage; some profiles may fit free allowances Hosting, extensions, and maintenance Subscription and add-ons Subscription, often by tier or module
    Customization Source-level changes and optional modules Themes, plugins, and source changes where available Provider-supported templates and extensions Provider-supported configuration and integrations
    Operations Your team or implementation partner deploys, updates, monitors, and backs up Your team or host manages core and plugin upkeep Provider manages most platform operations Provider manages most platform operations
    Portability Code and database access support migration, but migration is manual Depends on hosting, plugins, and formats Depends on provider exports and terms Depends on provider exports, APIs, and terms
    Bilingual starting point English and Chinese included Depends on selected extensions Depends on the service and plan Depends on the service and plan

    The trade-offs. Church4Christ is optimized for Cloudflare Workers and its bindings; moving to another hosting stack is possible source work, not a supported one-click path. There is also no automated D1-to-Supabase content migration. The built-in pages are shaped by themes, while drag-and-drop editing applies only to custom pages. The project is versioned, but adopters should still expect implementation work and carefully reviewed upgrades.

    Operating the project also means maintaining it. Your technical owner remains responsible for dependency updates, security review and configuration, monitoring, backups and restore testing, and deploying fixes. The architecture reduces some infrastructure work, but it does not remove security or operational upkeep. See docs/why-this-stack.md for the design rationale.


    Build it with an AI assistant

    People and agents share the same browser onboarding, installer, feature catalog, and readiness checks. AGENTS.md and CLAUDE.md tell an assistant to inspect the installation first, launch npm run onboard for a fresh setup without preferences, and let you complete the form. Your answers stay in the Git-ignored .church/preferences.json; later sessions read that file to follow your identity, brand, and feature choices. The setup guide includes the commands and recovery steps. Start an assistant with:

    "Read AGENTS.md and docs/setup.md. Start the browser onboarding so I can choose our organization's branding and first features. Use my saved preferences to set up a local preview, verify the site and administrator pages, then report the URL and remaining checks."

    You do not have to make every change by hand. This repository is organized so an AI coding assistant can follow the plain-English guides in docs/features/ and work against extensive automated test coverage. That can lower customization and maintenance effort, but a maintainer must still review the changes, run the relevant tests, and deploy them deliberately.

    The idea: open this project with an AI assistant, describe what you want in normal language, and let it do the editing. Some real examples you could paste in:

    "Read docs/features/public-site-and-themes.md, then change our primary color to royal blue and show me the home page."

    "Add a Spanish (es) locale following docs/i18n.md."

    "Follow docs/setup.md to configure our church with no demo content. Show the setup plan and readiness results, then prepare deployment using docs/deploy.md."

    The same workflow can help with maintenance: describe a change, inspect the proposed diff, test it locally, and deploy only after the result has been reviewed. AI assistance does not replace security decisions, backups, production testing, or operational ownership.


    Our mission

    To lower the cost and effort of starting a customized church-management system and bilingual website. Church4Christ provides a tested, modular foundation that a church or implementation partner can adapt instead of starting from zero. It does not promise a zero-subscription or zero-budget production service: infrastructure, email, database, domain, support, and maintenance choices determine the real operating cost.


    What's inside

    Every feature has its own plain-English guide. Start with any of these:

    The public website, staff admin, and Member Portal connect through a shared Astro Worker, D1 or Supabase database, R2 media, and email platform

    Feature What it does
    Public site & themes Your church's front door — home, sermons, events, staff — in one of three ready-made looks.
    The admin area Passwordless sign-in, roles, and one-click restore for supported versioned editorial content.
    Admin permissions Grant each admin only the areas they need — prayer wall and the member directory come free, the rest by choice.
    Multi-campus Run several campuses on one backend with isolated data, settings, features, and campus-local roles; only the master admin can see all campuses.
    Weekly bulletins Build the Sunday service sheet and schedule it to publish on its own.
    Sermon archive Paste a YouTube link; get a searchable, fast-loading library of past messages.
    Prayer wall Receive prayer requests and work them on a simple board, privately.
    Volunteer scheduling Plan a month of serving at a glance; volunteers confirm by email, no login.
    People & households Profiles and households plus canonical create-only CSV export and reusable source-column mapping for migrations.
    Member identity safety Proof-bound identity across Giving, Registration, Groups, Teams, Newcomer, and imports; ambiguous duplicates go to review instead of name-only merging, with OTP-bound approval and a guarded 24-hour rollback.
    Groups Small groups with a public directory, member checklist, join requests, events, and per-person email-link attendance.
    Children's check-in A touch-friendly kiosk where parents check kids in and out with a pickup code, plus weekly attendance charts.
    Service attendance Record aggregate adult totals, derive optional child totals from check-ins, correct history, and download identity-free CSV reports.
    Newcomer follow-up Receive bilingual consented public cards, triage a scoped staff queue, and hand exact matches into People without leaking notes or answers.
    Launch readiness One bilingual checklist shared by setup, doctor, and every real administrator, with super-admin acknowledgements for manual checks.
    Activity score Combine selected person-linked activities into explainable member scores and a church-wide engagement summary.
    Learning Continue Sunday school and discipleship between meetings with bilingual videos, files, assignments, quizzes, and provider-synchronized status.
    Page builder Drag and drop your own custom pages together — bilingual, always on-theme, and published pages load with zero JavaScript. Optional; switching it off never breaks a page you already built.
    Giving Implemented: record checks and cash in an offline ledger. Preview/test-only: Stripe online checkout.
    Registration Implemented: free event sign-up, custom questions, and roster export. Preview/test-only: paid Stripe checkout.
    Member portal One signed-in hub for current participation and open opportunities, plus household profiles, events, serving, calendar, giving, and scoped prayer.
    Two languages Every page in English and Chinese, with one-click Simplified-to-Traditional.
    Email & automation Sign-in links, reminders, and digests, with local logging and a paid-capable production configuration.
    Modules Switch off the features you don't use; nothing is deleted, flip back anytime.

    Pick your modules. Most optional domain capabilities are modules you can switch off from one panel in Settings — bulletins, sermons, the prayer wall, volunteer scheduling, and more. New installations write every module setting explicitly from the setup selection; the Full Church demo selects all 21. On older installations only, missing module rows retain the legacy default-on behavior. A church that wants only service times and sermons can hide the rest in a click: the module's pages, links, and emails disappear together, and nothing is deleted. See docs/features/modules.md.

    Key English 中文 Required database
    bulletins Bulletins 周报 Either
    sermons Sermons 讲道 Either
    prayer-sheets Prayer Sheets 祷告单 Either
    prayer-wall Prayer Wall 祷告墙 Either
    events Events 活动 Either
    serve Volunteer Scheduling 服事排班 Either
    gifts Spiritual Gifts 恩赐探索 Either
    testimonies Testimonies 见证 Either
    articles Articles 文章 Either
    fellowships Fellowships 团契 Either
    groups Groups 小组 Either
    people People & Households 会友与家庭 Either
    children Children Check-in 儿童报到 Either
    attendance Service Attendance 崇拜出席 Either
    newcomers Newcomer Follow-up 新朋友跟进 Either
    activity-score Activity Score 活跃度评分 Either
    page-builder Page Builder 页面编辑器 Either
    portal Member Portal 会友平台 Supabase
    giving Giving 奉献 Supabase
    registration Registration 活动报名 Supabase
    learning Learning 学习 Either

    Learning beyond Sunday

    Version 1.1 adds the optional Learning module for Sunday school, discipleship, and other ministries that continue between meetings. Learners get a bilingual, privacy-bounded course view for unlisted YouTube videos, files, assignments, and quizzes. Submission stays provider-authoritative: the site links learners to the configured provider instead of collecting homework, answers, comments, grades, or file contents itself.

    Administrators can connect Google Classroom through its official APIs or a separately operated Church4Christ Canvas derivative. Course mapping, encrypted OAuth credentials, signed provider notifications, manual sync, and bounded scheduled reconciliation feed only the activity metadata Church4Christ needs.

    Google Classroom and Church4Christ Canvas feed privacy-bounded course metadata into the bilingual learner experience

    English learner view Chinese learner view Provider administration
    Genesis 1 course in English Genesis 1 course in Chinese Google Classroom and Canvas connection administration

    See the Learning feature guide for provider setup, privacy boundaries, synchronization budgets, the Genesis 1 demo, and the separate Canvas operations and corresponding-source requirements.

    A home for your members

    The optional Member Portal turns the records your church already maintains into a useful signed-in experience. Its opportunity landing page shows every enabled way to learn, belong, and serve in one place: both the groups, classes, and teams already connected to the member and opportunities currently open to join or apply for. Members can also update household details, see their giving, register for events, review serving commitments, subscribe to a personal calendar, and share scoped prayers. It uses the same passwordless sign-in links as the rest of Church4Christ — no new account or password to remember.

    A member's current participation and open opportunities on one page

    Ministry, group, Sunday School, and serving-team leaders get a resource-scoped panel at /<locale>/manage. The church app sign-in protects it, and every operation checks the leader's authority over that specific resource. Cloudflare Zero Trust can therefore stay limited to the full /admin area.

    A leader sees only the ministries, groups, classes, and serving teams assigned to them

    Member opportunity discovery and resource-scoped leader administration workflow

    The portal requires the optional Supabase (Postgres) backend because it adds member relationships, protected group files, and scoped prayer moderation. Churches using the default D1 backend simply do not see the portal controls or routes. Learn more in docs/features/member-portal.md.


    Try it in 5 minutes (on your own computer)

    You can run the site locally, with optional fictional sample content, before choosing a deployment. You will need Node.js 22.22.1 or newer installed. Browser onboarding recommends Website + Community with Cloudflare D1 for the first launch.

    Setup branches from local evaluation or deployment into D1-backed Website and Community presets or the Supabase-backed Full Church preset; production email is optional and Stripe remains preview/test-only

    # 1. Get the code and install it
    git clone https://github.com/leveo/church4christ.git
    cd church4christ
    npm ci
    
    # 2. Open the local browser form, complete it, and save your preferences
    npm run onboard
    
    # 3. Review the plan, then initialize with those same preferences
    node scripts/setup/index.mjs --preferences .church/preferences.json --yes --dry-run --json
    node scripts/setup/index.mjs --preferences .church/preferences.json --yes --json
    
    # 4. For D1, start it (always follow the exact handoff setup prints)
    npm run dev
    

    The form asks whether this is a church, nonprofit, or campus, and collects the name, tagline, address, time zone, primary and secondary colors, optional PNG/JPEG/WebP logo (up to 2 MiB), language, first administrator, and demo-content choice. Check only the features you need. The recommended selection uses D1 as its database; the application still runs on Cloudflare Workers and stores media in R2. Member Portal, Giving, and Registration are advanced options requiring an explicit switch to Supabase-compatible PostgreSQL.

    Saving writes .church/preferences.json and any uploaded logo under .church/, which Git ignores. It does not create databases or deploy a site. Run npm run onboard again to reload and edit the saved preferences. Keep the terminal running while filling out the form; stop it with Ctrl+C when finished. To open the printed URL yourself, use npm run onboard -- --no-open; to choose a port, add --port 4310. The direct entry point is node scripts/onboard/index.mjs. See the setup guide for what the installer applies and how agents use the remaining preferences.

    If you install with npm ci --ignore-scripts, run npm run tokens manually before the installer or npm run dev.

    For local Supabase, the handoff instead exports CLOUDFLARE_HYPERDRIVE_LOCAL_CONNECTION_STRING_HYPERDRIVE in the host shell before npm run dev; that connection URL must not go in .dev.vars.

    During first setup, choose Include demo content or No demo content. Both keep the same bundled design, local decorative images, enabled features, and administrator tools. Demo content adds fictional people, sermons, bulletins, events, ministries, and other examples. Its media step copies the generated image pack from seed/media/ into local R2. No demo content creates the database schema, operational defaults, church settings, module selection, and first administrator without sample business records.

    The preferences file records the form's demo-content choice. For setup with explicit CLI answers, pass --demo-data or --no-demo-data; omitting both in noninteractive setup keeps the existing no-demo default. The flags cannot be combined. Repeating the same setup preserves the recorded content choice and existing records. --no-demo-data does not clear an existing database, and setup refuses to add demo data over existing people. Use a separate fresh workspace/database to try the other starting mode.

    Open the address setup prints (usually http://localhost:4321).

    Signing in to the admin area. There is no password. On the sign-in page, enter the first-admin email from your setup answers, repeated in the setup handoff, and request a link. Because local email is set to print instead of send, the magic-link URL appears right in your terminal. Paste it into the browser and you are in. (For quicker local testing, setup writes that same address as AUTH_DEV_BYPASS_EMAIL in .dev.vars, which signs you in automatically. Remove that line to test the real sign-in flow.)

    In both content modes, setup creates a new administrator and their audited sign-in identity together. Reruns preserve existing contact ownership; they do not verify an existing email or restore revoked access. If setup reports an identity problem, complete the identity review or recovery workflow before rerunning.

    The terminal-only npm run setup remains available. Setup offers Website (8 focused publishing modules), Website + Community (all 18 D1-compatible modules), and Full Church (all 21 modules). Portal, Giving, and Registration select Supabase automatically; D1-compatible selections choose D1 unless you explicitly override the backend. Account requirements depend on Local versus Deploy mode, as detailed below. For automation, pass all answers with --yes; add --json for one machine-readable result. For a human-readable noninteractive run, use the same complete flags with npm run setup -- ... --yes and omit --json. To keep stdout strictly JSON through npm, use the silent form:

    npm run --silent setup -- --mode local --preset website --site-slug my-church \
      --church-name "My Church" --locale en --admin-email admin@example.com \
      --admin-name "First Admin" --app-origin http://localhost:4321 \
      --email-from admin@example.com --demo-data --yes --json
    

    Deployment profiles and costs

    These are planning profiles, not price guarantees. Pricing below is current as of August 2026, is subject to change, and should be confirmed on the linked official pricing pages before deployment.

    Profile Included scope Cost and readiness notes
    Local evaluation Website or community modules with local D1; full modules with compatible local Postgres No hosted-service charge is required for local D1 evaluation. You still provide the computer, development time, and any optional external services.
    D1 website/community Cloudflare Worker, D1, and R2 for up to 18 D1-compatible modules A modest deployment can fit within Cloudflare free allowances. Traffic, storage, operations beyond allowances, a domain, and other services can cost money; check Workers pricing. Production email is separate.
    Production email Transactional sign-in links, reminders, requests, and digests to arbitrary recipients The repository supports a paid-capable Cloudflare email configuration. Arbitrary-recipient sending requires Workers Paid, currently a minimum $5/month including 3,000 emails, then $0.35 per 1,000 emails. These amounts are subject to change; check Cloudflare Email Service pricing.
    Supabase/full modules Cloudflare deployment plus Supabase/Postgres for all 21 modules, including Member Portal, Giving, and Registration Cloudflare costs still apply, and the selected Supabase plan may add subscription or usage charges. As of August 2026, Supabase Free has no automatic daily backups, and low-activity Free projects may be automatically paused based on activity over a seven-day period. For production, take regular off-site database dumps and perform restore drills, or select a paid plan whose backup options meet your continuity requirements. Pricing and service policies are subject to change. Stripe payment paths remain Preview/test-only in this repository.

    Provider free allowances can be useful for evaluation or modest deployments, but they are not a promise that a production service will remain free. Budget for technical ownership, dependency and security maintenance, monitoring, backups, restore testing, and future usage growth as well as the services listed above.


    Putting it online

    New to this? Start with docs/cloudflare-setup.md — a plain-language guide that explains what Cloudflare is, its cost model, and the two ways to get online (including letting an AI assistant do it for you). When you want the exact commands, docs/deploy.md is the full step-by-step walkthrough.

    Start with npm run setup, choose Deploy, and answer the feature and church questions. It creates or imports the required resources, writes the generated configuration, applies migrations, records all 21 module settings, and bootstraps the first admin. It then hands off to npm run deploy. Run npm run doctor for the schema-v2 readiness report, and use the always-on /admin/onboarding checklist for the same stable check identities. Giving, Registration, Groups, Teams, Newcomer, imports, and the optional read-only Planning Center integration share a proof-bound, review-first identity gateway. It never auto-merges people by name or an unverified contact. See Member identity for the workflow, fraud controls, current migration boundary, and provider verification gap; secret setup and rotation rules remain in the deployment runbook. Deployment is intentionally manual: repository automation tests changes but does not publish them or migrate production data for you.

    First deployment and upgrades are different operations. Guided setup provisions or imports resources and bootstraps a reviewed installation. It is not an unattended one-click upgrade for a site that already holds church data. Existing operators should start with the upgrade runbook, back up the database, R2 media, configuration, and secrets inventory, rehearse in staging, and review the Unreleased changelog before applying forward migrations. Maintainers preparing a release should follow the release process.

    Choosing your database. The 18 D1-compatible modules exclude Member Portal, Giving, and Registration, which require Postgres. Account requirements follow the mode: local D1 needs no external account; deployed D1 needs a Cloudflare account; local Supabase needs a Supabase account or compatible local Postgres database; deployed Supabase needs both Cloudflare and Supabase. There is no automated D1↔Supabase content migration yet, so choose the production database before entering real content. See docs/supabase-setup.md.

    Stripe payment paths are Preview/test-only. The Giving offline ledger and free Registration flow are implemented, but online gifts and paid registrations must not be treated as production payment features. Giving and Registration are Supabase-only; D1 does not support those modules. When setup asks for either module, import only an sk_test_… key and whsec_… signing secret with the one-shot CHURCH_SETUP_STRIPE_SECRET_KEY and CHURCH_SETUP_STRIPE_WEBHOOK_SECRET environment variables. Setup stores the runtime secrets automatically and rejects live keys. Signed live events are rejected with 400 live_mode_disabled before storage, while the Supabase-backed Worker runs durable recovery every five minutes. See the Supabase guide for the exact command.


    What's under the hood

    For the curious: Church4Christ is built with Astro rendering pages on the server, running as a single Cloudflare Worker. Application data lives in the selected backend — Cloudflare D1 or Supabase/Postgres — and uploaded media lives in Cloudflare R2 (object storage); email goes out through Cloudflare's email binding. Visitor-facing pages ship no client-side JavaScript framework — they are plain, fast HTML with a sprinkle of vanilla script — which is a big part of why the site loads quickly and costs so little to run. (The one exception lives behind the staff login: the drag-and-drop page builder is a small React editor that only your team ever downloads; the pages it publishes are still plain HTML.)

    The whole look comes from design tokens: a set of color and type values that compile into three ready-made themes (Sanctuary, Harvest, Midnight), each with a light and a dark mode. The project has extensive automated coverage across its core workflows so maintainers can verify changes before deployment.

    Why these choices? The reasons for the Cloudflare-optimized deployment, the Astro + Tailwind + TypeScript stack, and the cases where a managed platform may be the better operating model are laid out in docs/why-this-stack.md. For the technical picture, see docs/architecture.md, docs/design-system.md, and docs/i18n.md.


    License

    Church4Christ is free and open-source software under the GNU General Public License v3 (GPL-3.0). You may use and study it, modify it privately, share copies, and charge for copies, custom development, hosting, support, or other commercial services.

    If you distribute a covered modified version, the GPL's conditions apply: recipients must receive the applicable GPL freedoms, and corresponding source must be made available under the GPL as the license requires. Private modifications do not have to be published. Merely running a modified version as a network service, without distributing a copy, does not by itself trigger an AGPL-style source-sharing obligation under GPLv3.

    LICENSE is authoritative. This summary is provided for orientation and is not legal advice.


    Contributing & roadmap

    Contributions are welcome — bug reports, translations, new features. Start with CONTRIBUTING.md for the dev setup and the project's five rules, and SECURITY.md if you have found a security issue.

    On the horizon (not built yet): a between-churches "swap marketplace" for sharing themes and content is an idea we are considering, not a promise. If it matters to your church, open an issue and let's talk.


    Built with care, and with the help of AI, for churches and nonprofits everywhere.

    Community management and workflows

    Campuses can manage groups and member follow-up directly. Optional fellowships add a community layer with independent membership and child groups. Reusable workflows provide assigned steps, due dates, completion tracking, and Cloudflare email reminders. See the community workflow guide for setup, permissions, and delivery controls.

    Frequently asked about Church4Christ

    What is Church4Christ?+

    Church4Christ is a self-hosted Planning Center alternative built on the Cloudflare developer platform. Run a church website, people directory and volunteer workflows on Cloudflare.

    What does Church4Christ replace?+

    Church4Christ is listed as an alternative to Planning Center. Compare the features and tradeoffs before migrating.

    What Cloudflare primitives does Church4Christ use?+

    Church4Christ is built on D1, R2, Workers.

    How much does Church4Christ cost to run?+

    Church4Christ has a documented low-volume paid Cloudflare path beginning with Workers Paid ($5 USD/account/month), with separately metered usage. Arbitrary-recipient email requires Paid; database, storage and email usage above included allowances add charges. Use Workers Paid for arbitrary-recipient email, starting at $5 USD/account/month; 3,000 outbound emails/month are included, then $0.35 per 1,000. Workers Paid starts at $5 USD/account/month and includes 10 million requests/month plus 30 million CPU milliseconds/month; higher CPU limits do not make execution unmetered. Workers Paid D1 includes 25 billion rows read/month, 50 million written/month and 5 GB storage; database usage above those amounts is metered. Use R2 Standard storage, at most 10 GB-month, 1 million Class A operations and 10 million Class B operations/month; provision an eligible billing-enabled R2 account. Use a small personal or team workload; domain registration and optional third-party providers are separate costs. Provision your own IDs, secrets and migrations. Check current Cloudflare pricing before deploying.

    Is Church4Christ open source?+

    The upstream repository declares the GPL-3.0-only license. Read its terms at https://raw.githubusercontent.com/leveo/church4christ/ed4d9892a35a216b56c69cc80db50864ee37ca5a/LICENSE. Source code and contributor credit are available at https://github.com/leveo/church4christ.

    Discussion · 0

    sign in to comment →
    No comments yet — be the first.