My Authors
Read all threads
A friend needed an easy way to accept online payments. The existing tools weren't suitable & her hosting didn't support custom code, so I threw a dozen lines of code onto a Cloudflare Worker, connected to Stripe's beautiful Checkout — job done.

But it got me thinking…
With how easily you can deploy code nowadays (@Cloudflare), accept payments (@stripe), design an app (@tailwindcss)…, how quickly could I spin this into a fully-functional SaaS anyone could use, not just that friend?

If I shared the entire process here would you follow along?
OK, so this is the checkout flow that the SaaS generates links to… as in, this is what the SaaS customer's customer would see when they click their link.
Total build time so far: about 10 minutes. Thanks, Stripe Checkout
…and loading.io/css/, Tailwind CSS, Heroicons.

The very minimal app so far is running on a Cloudflare Worker, using their accurately named "Quick edit" feature. It's probably time to learn how their proper CLI works so I can have more than one Javascript file…
Right now the code just builds up an HTML string and spits it out. That ain't gonna end well, especially with conditionals and so on: Messy HTML
On the other hand, clean and quick is the aim here, so I'm not gonna start finding and learning a whole router/view engine. Let's install `liquidjs` and build a VERY simple view engine ourselves.
There we go.
That's that housekeeping out the way
I feel much happier building out the screens with some kind of templating (Liquid) instead of hacky HTML in JS
OK.

Let's build the part the SaaS' customer logs in to. Since this app's entire purpose is to hook up to your Stripe, and since this walkthrough's entire purpose is to show how minimally we can SaaS… let's use Stripe for login instead of managing our own
"Databases are soo 2010"

I'm not even going to store anything server-side for now. Just Stripe OAuth → store what we need in a JWE (encrypted JSON Web Token) cookie

I know, I know, I'm basically MacGyver
The fact that having an app functioning on the internet was *literally* one click in my Cloudflare dashboard ("Create a Worker") is still blowing my mind, by the way. Even compared to other serverless platforms, it's bloody impressive.

👏 @CloudflareDev
Oh, and the speed. It turns out putting your app in the place where the internet gods usually decide where the place that your app is is[*] makes your app pretty quick. Like, "oh that request must have faile—wooaahhhh" fast.

[*] shh that is literally how DNS works
Anyway. After a quick refresher on how Cookie headers work because I've spent so long using them through libraries and abstractions, we now have registration and login thanks to Stripe OAuth
This is the before… the after says `success: true` plus a bunch of other more secret stuff that gets encrypted and then lives in the cookie
Learning a lot about the more recent web standards like Fetch/Request/Response/URL, which are basically the backbone of Cloudflare Workers. The Javascript world really has come a long way since I started fighting with XMLHttpRequest in the early 2000s.
Some of these APIs are really elegant.

And then arrow functions… and async/await…

10 years ago I was saying how beautiful C# was as a language - it was just let down by the APIs it was generally used with (.NET)

Modern JS might be the whole package (…except typing)
—but I digress. we have a SaaS to build quickly
OK. Once someone's authenticated, we need to check whether they have an active subscription for our app. If they do - show the product (so, let them generate their payment links… and give them a link to the Stripe Billing Portal to manage their app subscription). If they…
…don't, give them a link to subscribe to the app. Which, of course, will be a link that was generated using the app itself #inception
At this point we will regrettably have to introduce some kind of very simple server-side storage so we can keep track of whether someone has an active subscription even at times when they're not logged in.

Obvious option would be Cloudflare Workers KV, but…
it only has eventual consistency (not a dealbreaker but could be annoying), plus at this point I'm kind of enjoying the challenge of building this entire SaaS at no cost. Workers KV would be (gasp) $5.

Going to give Google Cloud Firestore/Datastore a shot instead.
So, if they haven't paid yet, they get this button [1] (/* TODO: design */). It's a link the app generated, for paying for the SaaS itself. Checkout looks like [2]. A webhook handler for Stripe's checkout.session.completed means when they buy we'll get notified…
…When we get notified, we'll store their account ID and the fact they have an active subscription into our Datastore.
At which point, their dashboard will start to look like this - // TODO design this too
And when they click that button they're sent to here. All provided out of the box by Stripe (their "Billing Portal" feature). It's amazing how much stuff that you used to have to build doesn't need building any more.

…or it's amazing that you used to have to build it 🧐
Descriptive git commit messages are vital, he said.
In case it wasn't clear I stopped there for the day, 3 hours in. Will pick up (and ship?) tomorrow if possible, otherwise Monday.
"Don't you always say to research/validate/whatever before jumping into building a SaaS?" is a valid question. However:

1) this one is really quick and cheap and fun to build
2) my primary goal here is to show how true #1 is, not to #bizniss
If someone (me included) learns something, it's been successful.

Sure I could've gone all-out research mode… customer development… pain points…

But today I'd rather hack around for a few hours and share and have fun and see what happens ¯\_(ツ)_/¯
Are you sitting comfortably? 🥬 finish this bad boy. What's left:

- Cancellation (remove access and disable links when customer cancels through the Billing Portal)
- The page where they actually create their product links
- DESIGN
- A domain & a production environment
🥬 is not a suitable replacement for "Let's"
Cancellation is straightforward (yet again, thanks Stripe Billing Portal!) Just need to listen for the webhook Stripe sends, and set isActive = false for that customer in our Datastore.
Hmm, except we're keying our customers based on their Stripe account ID, and the webhook is only going to give us their customer ID.



I think the best way round this is to just store the customer ID as well as the account ID in our Datastore when they first subscribe.
The webhook handler

I was a good boy and abstracted the logic into a separate `storage` module. I'll thank me later if (event.type === 'customer.subscription.deleted') {
That's all the authentication, billing, etc totally done end to end, I think. Let's designnnnn…
What do you think? OK starting point?

Have I mentioned how insanely great @tailwindcss is (and by extension @adamwathan and @steveschoger)? Designed "Manage billing" cardThe HTML and Tailwind CSS that generates the card: <a href=&
And the rest of the admin area:
Given I've added that "Log out" button in the corner, should probably make it do something. In this case just wipe the auth cookie. Again, nice and easy in the Cloudflare Worker -- just return a 303 Response with a Set-Cookie header and a Location header back to /admin
Fun aside: after years of thinking the opposite, it turns out you can absolutely code outside in the sunshine as long as you wear sunglasses and switch your IDE to a light theme
What do we think? I'm pretty happy with this…

I've briefly touched on my feelings toward Tailwind CSS, right?
^ In the interest of minimalist hacky-ness the original plan was to just have the customer paste in their Price ID from their Stripe dashboard. But then I figured since we already have access to their Stripe account why not grab their list of products and prices automatically?
To recap, how this page (and all the others) work:

- Cloudflare Worker listener is called
- Check the URL path. In this case it's /admin/products
- Check they're logged in and subscribed
- We hit Stripe's API to grab all their products and prices and massage into:
products = [
{ id, prices: [
{ id, name, price },

],

]

- We pass that nice data block to our view. Liquid templating like {% for price in prices %} handles rendering the data
Wow, I think that might be the core of the app complete 🎉

You can login (using Stripe). You can subscribe. You then get shown a list of all your Stripe products, with a checkout link for each. You can copy those links and send them to people. Those people get taken to a…
…beautiful checkout. You can also update your own billing details, cancel your subscription, view your invoices.
I can say with high confidence that not-so-many years ago it wouldn't have been possible to create and deploy all of that within 5 hours.
There's still a bunch more we can do to tidy the app up, but I want to change gears for a minute and look at deploying to production
Which means we get to delve into my favourite hobby… domain collecting 🥳

Spent some time the other day trying to come up with a suitable name. And I'm now proud to present…

shutupandtakemymoney.app
Staying absolutely silent on how many domains I had to buy before settling on that one.
"Shai, why don't you choose the name and then purchase it?" would be a reasonable question to ask to a reasonable person
Spinning up a production environment for the app on a Cloudflare Worker was depressingly easy.

Depressing because the mind starts counting how many hours you (and the world) have ever spent fighting AWS (cc @QuinnyPig @mike_julian)
CF Workers support (encrypted) env variables out of the box. So spinning up a new env involved:

- Add the domain to CF
- Add [env.production] to your wrangler.toml, along with the domain and Zone ID from CF
- wrangler publish --env production
- Set the production env vars
I'd recommend setting a webpack.production.js too, so the app builds in production mode… but it's not strictly necessary and wouldn't have fit in the 1 tweet making the point about how easy spinning up a new environment is
With all the time I've saved I've even written a 50 line README.md detailing every environment variable, how to set up Stripe/Google Datastore/everything from scratch, in case I need to spin up another new env in future

What have I become
Thing I didn't know until this week: Stripe are totally OK with you creating multiple Stripe accounts for multiple projects, even if the legal entity is the same
First draft of landing page 👉
Now that there's a landing page I'm thinking more about the first-run experience for a customer. They press the Get started button, auth with Stripe, and then get hit with a button to start paying.

Friction is high.

Are there easy ways to lower it? A couple of things jump out…
—First, what do I mean by friction?

The aim is to SHOW THEM AS MUCH AS POSSIBLE while they PROVIDE AS LITTLE AS POSSIBLE.

So, what might that look like for us?

(This might be the first time in the thread I've put my product hat on in place of my hacker hat. Feels good)
1) We could link to an example checkout experience before they have to do anything at all

2) We could show screenshots of what they'd get within the app if they signed up

Added example checkout demo as secondary call-to-action. Skipping screenshots for now.

3) Currently once they connect Stripe, next step is to start paying. No other option. Why don't we already show them their product list??? We have access to their Stripe, after all
So, instead of

[if subscribed]
show product list
[else]
show SUBSCRIBE button

we can do

[everyone]
show product list; with big message slapped across is if not subscribed
At which point I'm also thinking - this app only has two screens right now - Products & Manage Billing. Products screen is very much the main part of the app. Why don't we skip the 'dashboard' page altogether (which just links to those two) and make the whole app be:
^ So that's what you'll get once you have an active subscription. If you don't (so, you've just come in for the first time- or you've cancelled), instead of showing a paywall, we can still show your price list pulled in from Stripe just with this caveat banner. Much better!
Ha, actually, better yet………… Products screen, with a banner warning of "Activate you
There's actually a third possible state. Aside from `activated` and `needs_activating` there's a middle state where they've been redirected back to the app after purchasing but we haven't received the webhook from Stripe to confirm yet. Let's add that so they don't get scared:
I also want to make it really clear to the customer that unless they activate their account the link will not work for people they send it to. So let's add a confirmation if they click a checkout link that only works because they are the owner of it:
Tweaked the copy on the landing page a bit too Shut Up And Take My Money  Shut Up And Take My Money is the
…At what length does twitter stop being an appropriate medium? Asking for a friend
By the way this is now a fully functional SaaS, at shutupandtakemymoney.app. Gonna leave it there for today. I'm certain there'd be some edge cases to fix, improvements to make, ancillaries like /terms to add. But if you're feeling brave go take a look 😀
Missing some Tweet in this thread? You can try to force a refresh.

Keep Current with Shai Schechter

Profile picture

Stay in touch and get notified when new unrolls are available from this author!

Read all threads

This Thread may be Removed Anytime!

Twitter may remove this content at anytime, convert it as a PDF, save and print for later use!

Try unrolling a thread yourself!

how to unroll video

1) Follow Thread Reader App on Twitter so you can easily mention us!

2) Go to a Twitter thread (series of Tweets by the same owner) and mention us with a keyword "unroll" @threadreaderapp unroll

You can practice here first or read more on our help page!

Follow Us on Twitter!

Did Thread Reader help you today?

Support us! We are indie developers!


This site is made by just two indie developers on a laptop doing marketing, support and development! Read more about the story.

Become a Premium Member ($3.00/month or $30.00/year) and get exclusive features!

Become Premium

Too expensive? Make a small donation by buying us coffee ($5) or help with server cost ($10)

Donate via Paypal Become our Patreon

Thank you for your support!