惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

罗磊的独立博客
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
MyScale Blog
MyScale Blog
M
MIT News - Artificial intelligence
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
The Blog of Author Tim Ferriss
Martin Fowler
Martin Fowler
博客园 - 【当耐特】
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
Engineering at Meta
Engineering at Meta
WordPress大学
WordPress大学
Google DeepMind News
Google DeepMind News
C
Check Point Blog
Last Week in AI
Last Week in AI
F
Fortinet All Blogs
博客园 - 聂微东
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Turning a wedding gift list into a charity-giving platform
Alan Maizon · 2026-06-06 · via DEV Community

TL;DR — A real wedding asked guests for charity donations instead of gifts. That single-couple prototype became Love That Gives Back: a multi-tenant platform where anyone can spin up a registry for any celebration, guests donate to verified charities through Stripe, leave a moderated public message, and every euro is tracked on an append-only ledger. The original wedding is the live flagship campaign.

Where it started

A year ago, instead of a traditional gift list, a couple — Anna & Alan — asked their wedding guests to donate to three charities that mattered to them: Mary's Meals, Operation Smile, and Xingu Vivo (a grassroots Amazon-basin movement). Each choice had a story: a visit to the Scottish Highlands where Mary's Meals began; a brother born with a cleft palate; a wedding ring "paid for" with a donation instead of cash.

The v0 site did the job, but it was held together with tape:

  • Guests transferred money manually over Revolut/bank, and an admin clicked "confirm" by hand.
  • Bank details were stored in plaintext on a model.
  • The guestbook was a static CSV.
  • Charts were server-rendered matplotlib PNGs dragging in ~8 heavy dependencies.

It worked for one couple. The interesting question was: what if anyone could do this — for a birthday, a memorial, any celebration — without the manual money handling and without a platform ever touching a bank number?

What I built

A modular monolith that treats money with the seriousness it deserves.

v0 prototype v2 platform
Tenancy one hardcoded couple many Campaigns + Charities, row-scoped
Money manual transfer, admin clicks "confirm" Stripe Connect destination charges → verified charity
Bank data plaintext account_number on a model never stored — a Stripe account id + capability flags
Split 50% couple / 50% charity 100% to charity
Source of truth the Donation row append-only LedgerEntry, reconcilable
Guestbook static CSV, unmoderated moderated Message API (approved-only public)
Webhooks signature-verified, idempotent (deduped by event id)
PII donor email in public responses stripped for non-staff; JSON-only API

Stack: Django 5 + DRF (Postgres in prod, SQLite locally), React 19 + Vite on the front, Stripe-hosted Checkout + Connect for payments, all deployable to a deliberately lean AWS footprint.

The invariants I refused to break

Money software is mostly about what you won't let happen. I wrote these down on day one and built guardrails around them:

  1. Money can only ever reach a verified Charity. No payouts to individuals.
  2. Never store raw bank/card data — payout identity is a Stripe account id, full stop.
  3. LedgerEntry is the source of truth for money — append-only, reconciled daily against Stripe. You never mutate money state by editing a Donation.
  4. All public user content is moderatable and consent-gated.
  5. Row-level multitenancy enforced in DRF get_queryset, not just in serializers.
  6. No PII (donor email) in public API responses.
  7. PCI: stay SAQ-A — Stripe-hosted Checkout only; card data never touches the backend.
  8. Idempotency on every payment op; verify and dedupe webhooks by event id.

The one that shaped the most code is #3. A donation isn't "real" because a row says confirmed — it's real because a signed Stripe webhook arrived, was deduped, and wrote a ledger entry inside the same transaction as an OutboxEvent:

# same DB transaction: ledger + outbox, drained later for receipts/emails
with transaction.atomic():
    LedgerEntry.objects.create(donation=donation, amount=amount, ...)
    OutboxEvent.objects.create(kind="donation.confirmed", payload={...})

A worker (manage.py drain_outbox) drains the outbox to send receipts and thank-you emails. If email delivery is down, the money record is still correct and the side effects retry — no lost receipts, no double charges.

The hardest lesson: don't test someone else's UI

My end-to-end test originally drove Stripe's hosted Checkout page in CI with Playwright — typing the 4242… test card into Stripe's iframes. It was chronically flaky, and eventually I understood why: Stripe flags headless/datacenter traffic with an agent-identity challenge and never completes the charge. Automating that page in CI tests Stripe, not my app — and it's PCI SAQ-A territory I don't own.

So I decoupled it:

  • CI test fills my donate form, asserts the backend created a valid checkout.stripe.com session (proving params + idempotency), then confirms the donation through the real webhook code path via a DEBUG-and-E2E_TEST_HOOKS-gated endpoint, and finishes the browser flow: pending guestbook → host approves → public.
  • Live UI walk (the full card entry) stays a local-only @live test.

CI went from a multi-minute flake-fest to a deterministic ~4s run. The principle generalizes: at your trust boundary, assert the contract you control, not the third party's UI.

Shipping it without lighting money on fire

The deploy target is intentionally cheap. Using the AWS pricing tooling against my Terraform, the whole stack lands around $50–60/month:

  • ECS Fargate (0.25 vCPU / 0.5 GB) in public subnets — no NAT Gateway (that alone saves ~$32/mo)
  • RDS db.t4g.micro PostgreSQL, Single-AZ ($0.017/hr)
  • One ALB, S3 + CloudFront for the SPA, SSM Parameter Store for secrets

The budget guardrails are explicit non-goals in the repo: no NAT, no Aurora Serverless v2, no second ALB. I wrapped the whole go-live into one idempotent script (scripts/phase1-golive.sh) with subcommands — infra, acm, ssm, image, migrate, frontend, webhook, verify — that read straight from Terraform outputs.

What it looks like

With the flagship seeded, the "real money plumbing" is visible end to end:

  • Home — hero + a guestbook carousel of 27 real guest messages.
  • Analytics — €3,780 raised, 27 gifts, per-charity bars, goal progress.
  • Admin — Donations, read-only Ledger entries, Webhook events. (These two tables are the whole thesis in one screen.)

What I'd do next

  • Self-serve cover image upload and co-host invites in the registry wizard.
  • Move the platform-admin charity verification queue out of Django admin and into the SPA.
  • Per-campaign analytics for hosts.

Try it / read it

If you take one thing from this: when you build on top of a payment processor, let their hosted surface own the card data and the compliance — and make your own system provably correct with an append-only ledger, idempotent webhooks, and a transactional outbox.