Skip to content

Core Architectural Decisions

These decisions are firm commitments, not suggestions. They were reached after deliberate research and reasoning. Any future change requires explicit justification and an update to this document.

Decision Log

Multi-Tenancy Strategy DECIDED

Decision: Single database with tenant_id column scoping, using stancl/tenancy v3.

Rationale: For a solo or small team, lower DevOps complexity is more valuable than maximum isolation. A single database with Eloquent global scopes handles tenant data separation cleanly and reliably. PostgreSQL's query optimizer handles tenant-scoped queries efficiently at the scale Ekklesia will operate.

The architecture explicitly supports a hybrid upgrade path — premium tenants can be migrated to dedicated databases without rebuilding the system. This is exposed as an enterprise tier feature.

Why not separate databases per tenant?

Separate-database tenancy offers stronger isolation but requires managing N database connections, N migration runs, and N backup strategies. For a small team building the first version, this overhead is unjustified. The single-database approach is how most successful Laravel SaaS products start.


Database Engine DECIDED

Decision: PostgreSQL 16+.

Rationale: Two reasons made this choice non-negotiable. First, JSONB support — PostgreSQL's JSONB type with GIN indexing is the foundation of the content type flexibility strategy. MySQL's JSON support is inferior for the query patterns Ekklesia needs. Second, PostgreSQL's query planner handles complex tenant-scoped queries with indexed tenant_id columns significantly better than MySQL at scale.


Content Type System DECIDED

Decision: Hybrid model — fixed relational columns for known church content types, JSONB custom_fields column for administrator-defined custom fields.

Rationale: The EAV (Entity-Attribute-Value) pattern, used by WordPress for custom fields, is a known performance anti-pattern. It requires multiple JOINs for simple queries and degrades significantly at scale. JSON columns in PostgreSQL (JSONB) solve the same flexibility problem with a fraction of the query complexity and far better performance — benchmark results show JSONB is 50,000x faster than EAV for full-table scans without indexes, and still faster with indexes.

Core church content (Sermons, Events, Members) gets proper relational columns for type safety and performance. Custom fields defined by church administrators live in a custom_fields JSONB column with a GIN index.


Frontend Delivery DECIDED

Decision: Headless REST API with versioning.

Rationale: Decoupling the admin from the frontend means churches and developers choose their own frontend technology. A provided Blade/Inertia starter theme handles non-technical users who need a website out of the box. Developers building React Native apps, Next.js sites, or custom solutions consume the same REST API. This is the model that makes a CMS genuinely useful as a platform rather than a closed system.


Tenancy Package DECIDED

Decision: stancl/tenancy v3.

Rationale: The most feature-complete Laravel tenancy package. Supports both single-DB and multi-DB modes (enabling the hybrid upgrade path). Has excellent Filament compatibility. Active maintenance and large community.


Deployment Providers DECIDED

Decision: Laravel Cloud for demos, Sevalla for production — both wrapped behind a DeploymentDriver interface.

Rationale: Laravel Cloud offers the best developer experience for Laravel projects and the fastest demo provisioning. Sevalla (Google Cloud) has better infrastructure proximity for African users and is more appropriate for production workloads. The abstraction interface means neither provider is a hard dependency — adding a third provider or swapping one out never touches core business logic.


API Authentication DECIDED

Decision: Laravel Sanctum.

Rationale: Sanctum is first-party, lightweight, and handles both SPA cookie authentication and mobile/API token authentication cleanly. Laravel Passport adds OAuth server complexity that Ekklesia does not need in v1.


Plugin Architecture OPEN

Status: Not yet decided. Must be resolved before v1 alpha.

The contract governing what a community plugin can and cannot do — which data models it can access, which Filament hooks it can use, whether it can extend the API — needs deliberate design to prevent plugins from breaking tenant isolation.


Content Versioning DECIDED

Decision: Soft versioning with a previous_version JSONB column on each content table.

Rationale: Church staff are non-technical users — accidental saves that destroy content would erode trust immediately. Full revision tables (Option 3 from open questions) double write complexity and add a separate _revisions table per content type, which is overkill for v1. Soft versioning adds one nullable JSONB column per content table, handled by a reusable HasSoftVersioning Eloquent trait that automatically snapshots changed fields on the updating event.

Implementation:

  • Each content table includes a previous_version JSONB nullable column
  • The App\Concerns\HasSoftVersioning trait hooks into the Eloquent updating event
  • Before each save, the trait snapshots only the dirty (changed) fields into previous_version
  • A revertToPreviousVersion() method restores the snapshot and clears previous_version
  • Only one level of undo is supported in v1 — this is deliberate to keep the schema simple

Upgrade path: If full revision history is needed later, a _revisions table per content type can be added via migration. The trait can be extended to write to both the inline snapshot and the revision table during a transition period.


AI Context Pipeline DECIDED

Decision: Tenant-scoped context builder with cached aggregate statistics and PII filtering.

Rationale: The AI assistant needs church context to provide relevant responses, but must never expose individual member data (phone numbers, giving amounts) or data from other tenants.

Implementation:

  • TenantContextBuilder assembles system prompts with tenant metadata (name, denomination, settings) and aggregate statistics (member count, giving trends, event counts)
  • Aggregate stats are cached for 15 minutes per tenant to avoid N+1 queries during AI interactions
  • "Safe Totals" provide month-over-month financial trends without individual record exposure
  • All AI requests go through the tenant's configured provider (Claude/OpenAI/Gemini) via the AiManager driver pattern
  • Rate limiting: 10 AI messages per minute per tenant via Laravel RateLimiter
  • Streaming responses via Laravel Reverb on private ai-chat.{userId} channels

Financial Record Immutability DECIDED

Decision: GivingRecord and PaymentTransaction models are immutable — updates and deletes are blocked at the Eloquent event level.

Rationale: Financial records must maintain audit integrity. Once a giving record or payment transaction is created, it cannot be modified or deleted. This is a hard requirement for any church finance system that may face audits.

Implementation:

  • Both models throw ImmutableRecordException on updating or deleting events
  • Corrections are handled via a polymorphic Adjustment model (type: void or correction) with required reason and audit trail
  • HasSoftVersioning was removed from GivingRecord to enforce immutability
  • Filament resources disable Edit and Delete actions for these models
  • API routes exclude PUT/PATCH/DELETE for giving records

Role-Based Access Control DECIDED

Decision: filament/shield v4 with spatie/laravel-permission for tenant-scoped RBAC.

Rationale: Church organizations need granular access control — a Treasurer should access financial records but not member pastoral notes, while a Volunteer should have read-only access to events and announcements.

Implementation:

  • 4 base roles: Super Admin, Pastor, Treasurer, Volunteer
  • Permissions generated per resource via Shield (ViewAny, View, Create, Update, Delete)
  • Tenant-scoped via team_id matching tenant slugs
  • All API controllers enforce model policies via $this->authorize()
  • Gate::before() in TestCase bypasses auth for non-RBAC tests

Released under the MIT License.