Welcome to the Church Ledger developer guide. This document provides a detailed walkthrough of the project setup, development workflow, and coding standards.
Church Ledger is built with Laravel 12 and React 19. We use pnpm for package management and Supabase for our database.
Install the following on your system based on your OS:
npm install -g pnpm.We recommend using Supabase for development. Create a new project on Supabase and grab your connection string.
# 1. Clone & Enter
git clone https://github.com/veikeAgency/church-ledger.git
cd church-ledger
# 2. Setup Environment
cp .env.example .env
php artisan key:generate
# 3. Install Backend & Frontend
composer install
pnpm install
# 4. Migrate Database
php artisan migrateInstead of managing multiple terminals, we use composer run dev which leverages concurrently to start everything:
composer run devThis command starts:
http://localhost:5173.http://localhost:8000.Church Ledger follows a "Modern Monolith" approach using Inertia.js. This removes the need for a client-side API layer and router.
When a user searches for a member, the request flows through a Repository to keep the Controller clean.
// app/Http/Controllers/MemberController.php
public function index(Request $request)
{
$search = $request->input('search');
// Delegate to repository
$members = $this->memberRepository->search($search)
->through(fn ($item) => $item->load('household'));
return Inertia::render('members/index', [
'members' => $members,
'filters' => $request->only(['search']),
]);
}
Always use the BelongsToChurch trait in your models. It automatically applies a global scope to ensure users only see data from their own church.
use App\Traits\BelongsToChurch;
class Pledge extends Model {
use BelongsToChurch; // Automatically filters by church_id
}Define permissions in database/seeders/RolesAndPermissionsSeeder.php. Use the can middleware in routes and the auth.user.can prop in React.
const { auth } = usePage().props
{
auth.user.can["manage offerings"] && (
<Button variant="primary">Add Offering</Button>
)
}Any task that takes longer than 200ms (like PDF generation or bulk emails) should be dispatched to a job.
// Example: PDF Export
public function exportPdf() {
GenerateMemberReport::dispatch(auth()->user());
return back()->with('success', 'Report generation started. Check your email soon.');
}