Crowdfunding Platform Development
Crowdfunding platform is not just "site with payment page". It's campaign management system, funds collection, creator payouts, backer communication and compliance with payment law. Difference between working product and feature list is in implementation details of each part.
Financing Models
Before designing data schema, determine model:
All-or-Nothing (AON) — funds debited only on goal achievement. Kickstarter model. Technically harder: need pre-authorization (hold) or delayed capture.
Keep-it-All (KIA) — funds debited immediately, creator gets everything regardless of goal. Indiegogo model. Simpler implement, but legally requires clear refund terms.
Hybrid — fixed goal, stretch goals open on exceeding. Most complex with campaign state logic.
Data Schema
CREATE TABLE campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
goal_amount NUMERIC(15,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'RUB',
model VARCHAR(20) NOT NULL CHECK (model IN ('aon','kia','hybrid')),
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','active','funded','failed','cancelled')),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE pledges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES campaigns(id),
backer_id UUID NOT NULL REFERENCES users(id),
amount NUMERIC(15,2) NOT NULL,
reward_id UUID REFERENCES rewards(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','authorized','captured','refunded','failed')),
payment_intent VARCHAR(200), -- Stripe PaymentIntent or YooKassa payment_id
captured_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE rewards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES campaigns(id),
title VARCHAR(200) NOT NULL,
description TEXT,
min_pledge NUMERIC(15,2) NOT NULL,
limit_qty INTEGER, -- NULL = unlimited
claimed_qty INTEGER NOT NULL DEFAULT 0,
ships_at DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_pledges_campaign_status
ON pledges(campaign_id, status)
WHERE status IN ('authorized','captured');
Payment Flow for AON Model
AON requires two-stage payment: authorize (hold) first, then capture on campaign success.
Stripe:
import stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
def authorize_pledge(pledge, card_token):
intent = stripe.PaymentIntent.create(
amount=int(pledge.amount * 100),
currency=pledge.campaign.currency.lower(),
payment_method=card_token,
capture_method='manual',
confirm=True,
metadata={
'pledge_id': str(pledge.id),
'campaign_id': str(pledge.campaign_id),
}
)
pledge.payment_intent = intent.id
pledge.status = 'authorized'
pledge.save()
return intent
def capture_pledges_for_campaign(campaign_id):
"""Called on campaign success"""
pledges = Pledge.objects.filter(
campaign_id=campaign_id,
status='authorized'
)
for pledge in pledges:
try:
stripe.PaymentIntent.capture(pledge.payment_intent)
pledge.status = 'captured'
pledge.captured_at = timezone.now()
pledge.save()
except stripe.error.InvalidRequestError as e:
pledge.status = 'failed'
pledge.save()
logger.error(f'Capture failed for pledge {pledge.id}: {e}')
def cancel_pledges_for_campaign(campaign_id):
"""Called on campaign failure"""
pledges = Pledge.objects.filter(
campaign_id=campaign_id,
status='authorized'
)
for pledge in pledges:
stripe.PaymentIntent.cancel(pledge.payment_intent)
pledge.status = 'refunded'
pledge.save()
Important: Stripe holds authorization max 7 days for cards (up to 30 for some types). For campaigns longer than 7 days need different strategy — e.g. save payment method and capture on last day.
Saving Card for Long Campaigns
def save_payment_method(user, card_token):
if not user.stripe_customer_id:
customer = stripe.Customer.create(
email=user.email,
metadata={'user_id': str(user.id)}
)
user.stripe_customer_id = customer.id
user.save()
setup_intent = stripe.SetupIntent.create(
customer=user.stripe_customer_id,
payment_method=card_token,
confirm=True,
usage='off_session',
)
return setup_intent.payment_method
def charge_saved_card(pledge):
intent = stripe.PaymentIntent.create(
amount=int(pledge.amount * 100),
currency='rub',
customer=pledge.backer.stripe_customer_id,
payment_method=pledge.payment_method_id,
confirm=True,
off_session=True,
metadata={'pledge_id': str(pledge.id)},
)
return intent
Celery Tasks for Campaign Completion
from celery import shared_task
from django.utils import timezone
@shared_task
def check_campaign_deadline(campaign_id):
campaign = Campaign.objects.get(id=campaign_id)
if campaign.ends_at > timezone.now():
return # Not finished yet
total = Pledge.objects.filter(
campaign=campaign,
status__in=['authorized', 'captured']
).aggregate(total=Sum('amount'))['total'] or 0
if campaign.model == 'aon' and total < campaign.goal_amount:
campaign.status = 'failed'
campaign.save()
cancel_pledges_for_campaign.delay(campaign_id)
else:
campaign.status = 'funded'
campaign.save()
capture_pledges_for_campaign.delay(campaign_id)
notify_creator_campaign_success.delay(campaign_id)
Payouts to Creators
Stripe Connect is standard for marketplaces. Creators connect bank accounts via onboarding:
def create_connect_account(creator):
account = stripe.Account.create(
type='express',
country='RU',
email=creator.email,
capabilities={
'card_payments': {'requested': True},
'transfers': {'requested': True},
},
)
creator.stripe_account_id = account.id
creator.save()
link = stripe.AccountLink.create(
account=account.id,
refresh_url='https://site.com/dashboard/connect/refresh',
return_url='https://site.com/dashboard/connect/complete',
type='account_onboarding',
)
return link.url
def transfer_to_creator(campaign, net_amount):
"""net_amount = collected - platform commission"""
transfer = stripe.Transfer.create(
amount=int(net_amount * 100),
currency=campaign.currency.lower(),
destination=campaign.creator.stripe_account_id,
metadata={'campaign_id': str(campaign.id)},
)
return transfer
Frontend: Progress Bar and Real-time Updates
// Progress via Server-Sent Events
// GET /api/campaigns/:id/progress
export async function* campaignProgressStream(campaignId: string) {
while (true) {
const stats = await getCampaignStats(campaignId)
yield `data: ${JSON.stringify(stats)}\n\n`
await sleep(10_000) // update every 10 seconds
}
}
function useCampaignProgress(campaignId: string) {
const [progress, setProgress] = useState<CampaignStats | null>(null)
useEffect(() => {
const source = new EventSource(`/api/campaigns/${campaignId}/progress`)
source.onmessage = (e) => setProgress(JSON.parse(e.data))
return () => source.close()
}, [campaignId])
return progress
}
Commission Model
Standard: platform takes 5–8% from collected amount plus Stripe transaction fee (1.4% + 25₽ for European cards, 2.9% + 30¢ for others). Platform commission deducted via application_fee_amount when creating PaymentIntent:
intent = stripe.PaymentIntent.create(
amount=10_000, // 100 RUB
currency='rub',
application_fee_amount=600, // 6% platform commission
transfer_data={'destination': creator.stripe_account_id},
)
Timeframes
MVP crowdfunding platform (KIA model, Stripe, campaigns, rewards, basic dashboard): 6–8 weeks. Full AON platform with Connect, stretch goals, email notifications and analytics: 3–4 months.







