Build a church contributions app by creating named giving categories (tithe, offering, building fund, missions), collecting each contribution via Paystack with the category in the metadata, storing contributions per member per category, sending a PDF receipt on charge.success, and providing the treasurer with a real-time reporting dashboard showing totals by category, date range, and member.
Tables: giving_categories (name, description, is_recurring_enabled), contributions (member_id, category_id, amount, is_anonymous, paystack_ref, receipt_url, given_at), members (name, phone, email, cell_group).
var GIVING_CATEGORIES = {
tithe: { id: 1, name: 'Tithe', description: '10% of your income' },
offering: { id: 2, name: 'Offering', description: 'General offering' },
building_fund: { id: 3, name: 'Building Fund', description: 'New sanctuary construction' },
missions: { id: 4, name: 'Missions & Outreach', description: 'Support our mission work' },
special: { id: 5, name: 'Special Appeal', description: '' }, // updated per campaign
};
async function give(memberEmail, memberId, categoryId, amount, isAnonymous) {
var category = await db.givingCategories.findById(categoryId);
var reference = 'GIVE-' + categoryId + '-' + Date.now();
var contribution = await db.contributions.create({
member_id: isAnonymous ? null : memberId,
category_id: categoryId,
amount,
is_anonymous: isAnonymous,
paystack_ref: reference,
});
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: memberEmail, amount, currency: 'NGN', reference,
metadata: {
contribution_id: contribution.id,
member_id: isAnonymous ? null : memberId,
category: category.name,
is_anonymous: isAnonymous,
},
}),
});
return (await res.json()).data.authorization_url;
}
// Webhook: record contribution and send receipt
if (event.event === 'charge.success') {
var meta = event.data.metadata;
if (!meta.contribution_id) return res.sendStatus(200);
await db.contributions.update(meta.contribution_id, { given_at: new Date() });
if (!meta.is_anonymous && meta.member_id) {
var receiptUrl = await generateContributionReceipt(meta.contribution_id, event.data);
await db.contributions.update(meta.contribution_id, { receipt_url: receiptUrl });
var member = await db.members.findById(meta.member_id);
await sendReceiptEmail(member.email, member.name, meta.category, event.data.amount / 100, receiptUrl);
}
}
// Set up monthly giving pledge
async function setupRecurringPledge(memberEmail, memberId, categoryId, monthlyAmount) {
var category = await db.givingCategories.findById(categoryId);
if (!category.is_recurring_enabled) throw new Error('Recurring giving not available for this category');
// Use Paystack Plans for recurring giving
var plan = await getOrCreatePlan(category.name + ' - NGN ' + (monthlyAmount / 100) + '/month', monthlyAmount);
var res = await fetch('https://api.paystack.co/transaction/initialize', {
method: 'POST',
headers: { Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
email: memberEmail, amount: monthlyAmount,
plan: plan.plan_code,
metadata: { member_id: memberId, category_id: categoryId, pledge_type: 'recurring' },
}),
});
return (await res.json()).data.authorization_url;
}
// Treasurer: get contribution report
async function getTreasurerReport(startDate, endDate, categoryId) {
var query = { given_at_gte: startDate, given_at_lte: endDate };
if (categoryId) query.category_id = categoryId;
var contributions = await db.contributions.findAll(query);
var totalByCategory = {};
for (var c of contributions) {
var cat = await db.givingCategories.findById(c.category_id);
if (!totalByCategory[cat.name]) totalByCategory[cat.name] = 0;
totalByCategory[cat.name] += c.amount;
}
return {
period: { start: startDate, end: endDate },
total: contributions.reduce(function(sum, c) { return sum + c.amount; }, 0),
count: contributions.length,
by_category: totalByCategory,
average_per_contributor: contributions.length > 0 ?
contributions.reduce(function(sum, c) { return sum + c.amount; }, 0) / contributions.length : 0,
};
}
// Top contributors report (for stewardship recognition, if members consent)
async function getTopContributors(startDate, endDate, limit) {
return await db.contributions.findTopContributors(startDate, endDate, limit, false /* exclude anonymous */);
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
See build a group contribution app for a related group money collection pattern applied to chamas and community savings groups.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course