Skip to content

Content Type System

Hybrid Strategy

Ekklesia uses a hybrid content model:

  • Known church content types (Sermon, Event, Member, etc.) get proper relational columns — typed, indexed, and queryable with standard SQL
  • Administrator-defined custom fields live in a custom_fields JSONB column with a GIN index — flexible, no schema migrations required

This avoids the EAV anti-pattern used by WordPress, which requires multiple JOINs for simple queries and degrades at scale. JSONB gives the same flexibility with far better performance.

Core Content Types

Sermon

ColumnTypeNotes
titlestringRequired
speakerstringPastor name
datedateSermon date
durationintegerSeconds
audio_urlstringNullable
video_urlstringNullable
transcripttextNullable, full-text indexed
series_idforeignIdNullable
tagsarray (JSON)Searchable
custom_fieldsjsonbGIN indexed

Example custom_fields:

json
{
  "scripture_reference": "Jean 3:16",
  "language": "fr",
  "translation_available": true,
  "outline_url": "https://..."
}

Event

ColumnTypeNotes
titlestringRequired
start_atdatetimeRequired
end_atdatetimeNullable
locationstringPhysical or "En ligne"
descriptiontextRich text
imagestringVia Spatie Media Library
registration_urlstringExternal or internal
capacityintegerNullable
custom_fieldsjsonbGIN indexed

Member

ColumnTypeNotes
first_namestringRequired
last_namestringRequired
emailstringUnique per tenant
phonestringMobile money linked
baptism_datedateNullable
cell_group_idforeignIdNullable
statusenumactive, inactive, visitor
custom_fieldsjsonbGIN indexed

Announcement

ColumnTypeNotes
titlestringRequired
bodytextRich text
published_atdatetimeNullable — draft if null
expires_atdatetimeAuto-unpublish
pinnedbooleanShown at top
target_groupstringall, members, leaders
custom_fieldsjsonbGIN indexed

Page IMPLEMENTED

ColumnTypeNotes
titlestringRequired
slugstringUnique per tenant, auto-generated from title
content_blocksjsonbBlock-based content (GIN indexed)
seo_titlestringNullable — meta title for SEO
seo_descriptionstringNullable — meta description for SEO
published_atdatetimeNullable — draft if null, published if past
custom_fieldsjsonbGIN indexed
previous_versionjsonbSoft versioning snapshot

Block types (Filament Builder component):

BlockFieldsUse case
headinglevel (h2/h3/h4), contentSection headings
rich_textbody (Markdown)Main content paragraphs
imageurl, alt, captionPhotos and illustrations
videourl, captionYouTube/Vimeo embeds
call_to_actionlabel, url, style (primary/secondary)Buttons and links
quotetext, attributionBible verses, testimonials

Computed attributes:

  • is_publishedtrue when published_at is set and in the past

API filters:

  • ?published=true — only published pages
  • ?search= — title search (case-insensitive via ilike)

Giving Record IMPLEMENTED

ColumnTypeNotes
member_idforeignIdNullable (anonymous giving)
amountdecimal(12,2)Required
currencystring(3)Default XOF — supports XOF, XAF, EUR, USD, GBP, CAD
datedateRequired
methodstringmobile_money, cash, bank_transfer, card
referencestringNullable — transaction ID / receipt number
campaign_idstringNullable — campaign reference
custom_fieldsjsonbGIN indexed
previous_versionjsonbSoft versioning snapshot

Relationships:

  • belongsTo(Member) — nullable for anonymous giving

Computed attributes:

  • is_anonymoustrue when member_id is null
  • formatted_amount — formatted with currency (e.g. "50 000,00 XOF")

API filters:

  • ?method=mobile_money — filter by payment method
  • ?currency=XOF — filter by currency
  • ?member_id=5 — filter by member
  • ?anonymous=true — only anonymous donations
  • ?campaign_id=easter-2026 — filter by campaign
  • ?from=2026-01-01&to=2026-03-31 — date range filter

Indexes: (tenant_id, date), (tenant_id, method), (tenant_id, currency), (tenant_id, member_id), (tenant_id, campaign_id)


Production Readiness Content Types

The following content types were added during the Production Readiness Sprint to complete the church management feature set.

Service Type & Attendance IMPLEMENTED

ModelKey ColumnsNotes
ServiceTypename, description, day_of_week, start_timeDefines recurring service slots
Attendanceservice_type_id, member_id, date, check_in_time, is_first_timePer-member attendance tracking

Features: QR code check-in, first-time visitor flagging, trend dashboards (week-over-week, per campus, per service).


Household IMPLEMENTED

ColumnTypeNotes
namestringFamily/household name
head_of_household_idforeignIdReferences a Member
address, city, phonestringShared contact info
custom_fieldsjsonbGIN indexed

Member additions: household_id, family_role (head, spouse, child, relative), date_of_birth, wedding_anniversary.


Fund & Campaign IMPLEMENTED

ModelKey ColumnsNotes
Fundname, description, type (tithe/offering/building/missions/benevolence), is_activeCategorizes giving
Campaignname, fund_id, goal_amount, currency, start_date, end_date, is_activeTime-bound fundraising

GivingRecord update: fund_id foreign key links giving to specific funds. Real-time progress calculation for campaigns.


Prayer Request & Commitment IMPLEMENTED

ModelKey ColumnsNotes
PrayerRequestmember_id, title, content, type (prayer/praise), visibility (public/group/confidential), status, is_answeredPrayer wall entries
PrayerCommitmentprayer_request_id, member_id"Je prie" commitment pivot

Features: Prayer chain broadcast, answered prayer marking with optional testimony link, commitment counter.


Devotional & Series IMPLEMENTED

ModelKey ColumnsNotes
DevotionalSeriestitle, slug, description, is_activeGroups themed devotionals
Devotionalseries_id, title, verse_reference, content, prayer_point, application, published_atDaily devotional content

Features: Multi-channel delivery (in-app, SMS, email), AI-assisted draft generation, scheduling system.


Testimony IMPLEMENTED

ColumnTypeNotes
member_idforeignIdAuthor (nullable for anonymous)
titlestringTestimony title
contenttextFull testimony text
categorystringhealing, provision, deliverance, conversion, family_restoration
statusstringsubmitted, approved, rejected
is_anonymousbooleanHides member identity
is_featuredbooleanHighlighted testimonies
reactionsjsonb

Features: Moderation workflow, audio recording support, culturally appropriate reactions.


Reading Plan & Progress IMPLEMENTED

ModelKey ColumnsNotes
ReadingPlantitle, slug, description, duration_days, grace_period_days, is_activePlan definition
ReadingPlanDayreading_plan_id, day_number, passage_reference, passage_text, reflectionDaily entries
MemberReadingProgressmember_id, reading_plan_id, current_streak, longest_streak, started_at, completed_atProgress tracking

Features: Streak counter with configurable grace period, cell group plan assignments.


Bulk Message & Template IMPLEMENTED

ModelKey ColumnsNotes
BulkMessagetitle, body, channel (sms/email/whatsapp), target_type, status, scheduled_atBulk delivery
MessageTemplatename, body, channel, placeholdersReusable templates

Features: Scheduled delivery, audience targeting (all, cell_group, campus, status), per-recipient dispatch tracking via NotificationDispatch.


Adjustment IMPLEMENTED

ColumnTypeNotes
adjustable_typestringPolymorphic (GivingRecord, PaymentTransaction)
adjustable_idbigintReference to original record
typestringvoid, correction
reasontextRequired explanation
adjusted_byforeignIdUser who made the adjustment

Purpose: Financial records are immutable — adjustments provide a safe audit trail for voids and corrections without modifying original records.


JSONB Indexing Strategy

sql
-- GIN index on all custom_fields columns
-- Covers general containment queries (@>)
CREATE INDEX sermons_custom_fields_gin
  ON sermons USING GIN (custom_fields);

-- Expression index for a frequently-queried key
-- Add these per-need, not upfront
CREATE INDEX sermons_language
  ON sermons ((custom_fields->>'language'));

Querying Custom Fields

php
// Find all French sermons
Sermon::whereJsonContains('custom_fields->language', 'fr')->get();

// Find sermons with translation available
Sermon::where('custom_fields->translation_available', true)->get();

// All indexes above are used automatically by PostgreSQL

Adding Custom Fields (Church Administrators)

Church administrators define their own custom fields through the Filament admin panel. The field definition is stored in a content_type_schemas table (per tenant), and the Filament form is dynamically rendered from this schema. No migrations are needed — the value is simply added to the custom_fields JSONB object.

Released under the MIT License.