Shipping Tip Jar: coffee, Stripe webhooks, and a stuck TLS cert
How I broke my ‘never shipped solo’ streak by building Tip Jar — an embeddable, Stripe-powered ‘buy me a coffee’ widget — and what actually finishing a side project taught me.

The hardest part was never the code
I can build features all day. What I had somehow never done was own the whole solo lifecycle end to end — deploy, a real domain, payments, and the lonely last 20% of polish. I’d been hiding from that by endlessly hunting for the ‘perfect idea’.
So I picked a deliberately tiny one and forced myself to finish it: Tip Jar — a ‘buy me a coffee’ widget you drop onto any website with a single script tag. Boring and finished beats clever and abandoned.
What it does
One <script> tag renders a self-contained widget inside a Shadow DOM (so the host page’s CSS can’t touch it). A visitor picks a preset or custom amount, gets redirected to Stripe’s hosted Checkout, pays, and lands back on a thank-you page. No card data ever touches my server.
The lesson that stuck: trust the webhook, not the redirect
It’s tempting to record the tip when the browser bounces back to your ‘success’ page. Don’t — that redirect can be faked, dropped, or replayed. The real source of truth is Stripe’s checkout.session.completed webhook, whose signature you verify with your signing secret. Mine records the tip only then, and it’s idempotent (keyed on the session id) so Stripe’s retries can’t create duplicates. That single idea is most of what ‘doing payments properly’ means.
Deploy gremlins (a rite of passage)
The bugs that actually teach you are never the ones in the tutorial. Two favourites: a 502 that turned out to be a port mismatch — the platform was routing to 8000 while the app listened on 8080; and a TLS certificate stuck ‘issuing’ for hours on my custom domain, fixed by removing and re-adding the domain to restart the ACME challenge. Debugging live infra is the muscle you can’t fake.
Then I made it boring-solid
Finishing means the unglamorous 20%:
- A test suite + CI — pytest for the backend and a jsdom suite for the widget, green on every push.
- Rate limiting on the public endpoint, and a signature-verified webhook that also handles delayed (async) payments.
- Sentry with release tracking and strict PII scrubbing, plus a proper social/OG card and favicon.
Then I open-sourced it. The pattern I actually broke wasn’t technical — it was finishing. Turns out the ‘perfect idea’ was just any idea I refused to abandon.
Live demo: tipjar.bilalhasson.com · Source: github.com/bilalhasson/tip-jar