Dynamically creating an account for Gumroad customers

A step-by-step guide to selling a product on Gumroad and then using Nhost to create an account for users who make a purchase.

Dynamically creating an account for Gumroad customers

A step-by-step guide to selling a product on Gumroad and then using Nhost to create an account for users who make a purchase.

This is the second post in a series of posts documenting my journey building Botler - your personal AI butler. In this post, I will kickstart the process of selling access to the bot and creating user accounts in the database whenever a user makes a purchase.

Before we start, let's take a look at what we have so far. In the first post, we scaffolded our NextJS app, connected it to our database, and set up authentication. When we set up authentication, we only did the routes and logic for logging in but not for signing up. That's because we will be selling access to the bot and whenever a user buys a membership, we will dynamically create an account for them.

This process involves several moving parts, so let's take a moment to look at everything we will cover in this post:

  • Selling a membership: We will be using Gumroad for this. Gumroad makes selling digital products super easy, provides sales analytics, has a well-documented API, and comes with a widget that we could simply embed on our website.
  • Creating user accounts: Once a user makes a purchase, we want to create an account for them in our database. To do this, we will have Gumroad send us a webhook and then receive the webhook using the custom API feature provided by Nhost. Upon receiving the webhook, we will create an account for the user.
  • Notifying users on account creation: Once the user account is created, we will send an email to the user asking them to set their password. Upon doing that, they should simply be able to visit the login URL and access their dashboard.

A small note on using the Nhost API over the one provided by NextJS. Both services are very similar in this aspect, what made me go for the Nhost API was the fact that all my environment variables will already be available in the Nhost API and if I ever change them, they'll be updated automatically and that's a good thing to not have to worry about.

Selling a membership

So I gave this a thought and Botler will probably be a subscription-based product. As it will mostly revolve around alerts, notifications, and task automation, there are bound to be ongoing costs, and to cover ongoing costs, I am going to need an ongoing income. But, before we launch, I am going to sell a limited number of lifetime memberships on pre-order at an extremely discounted price of $10.

This serves two purposes. People who've been reading about and believing in the product from the beginning get access for a very low price and I as a creator get validation for my idea and find out whether it is something people even want.

I was thinking 25 sounds like a good number of memberships to sell on pre-order. It's high enough that a decent number of users get access and low enough that I am not losing money from day one. With that out of the way, let's set up that pre-order on Gumroad.

Product creation flow on Gumroad
Product creation flow on Gumroad

In the GIF above, we go through the process of creating and publishing a new pre-order product on Gumroad. Yep, that's how easy it is (well, once you link your account and everything). You can check it out here and maybe even make a purchase? (Seriously though, $10 for a lifetime membership? And you don't even get charged till the product actually gets released!)

Creating user accounts

Now that we've set the product available for sale, we can look at what happens after the user makes a purchase. This section will be broken down into two pieces. First, we'll have Gumroad send us a webhook and then, we will consume that webhook and create an account for the user on our platform.

Adding a webhook on Gumroad

To add a webhook on Gumroad, you simply need to go to settings and then advanced settings. There, you'll see an option to add a Ping. That's where we want to add the URL that we want to send our webhook to.

Add a webhook URL on Gumroad
Add a webhook URL on Gumroad

In the GIF above, you can see that I added a localtunnel URL. That's because we first want to test this locally. Once we're sure that it works, we will replace that with a production URL.

Receiving the webhook and creating a user account

Now that we've added our webhook URL, we can use the events Gumroad sends and create accounts for our users. You can look at the documentation for the webhook Gumroad sends here. We only care about a few of those fields:

  • email: So that we can create an account for the user with that email address.
  • short_product_id: A unique identifier for our product. If we're selling multiple products, we don't want to be creating accounts on Botler for all of them but just for the ones specifically paying for Botler.
  • test: If it is a test purchase, we don't want to be creating production accounts. Maybe we can have this work based on the environment we're in where we create accounts from test purchases in development but not in production.

A small note here. These are the only fields I am looking at for now but that might change later if I add tiers and want to create different sorts of accounts for different users. I also know that some people store all the information payment gateways send in their own database but we won't be covering that in this post.

Okay, so the webhook is ready and we've decided what fields we want to read the data from. Now, we're going to make use of the Custom API feature that Nhost has so we can receive the event and register the user.

At the root of our project, we will create a folder called api and within that folder, we will create another folder called webhooks which contains a single file called gumroad-ping.ts. This will create /webhooks/gumroad-ping/ as an API route for us.

Within that file, we will write all the logic for creating the user account in our database.

export default async (req: Request, res: Response) => {
  const { email, short_product_id: shortProductId, test } = req.body
  
  if (test || shortProductId !== BOTLER_SHORT_PRODUCT_ID) {
    res.sendStatus(200)
    return
  }
  
  try {
    await registerUser(email)
    await requestNewPassword(email)
    res.sendStatus(200)
  } catch (error) {
    if (error.response?.data) {
      const { message } = error.response.data
      console.error(message)
    } else {
      console.error(error)
    }
    res.status(500).json({ message: 'Internal Server Error' })
  }
}
/api/webhooks/gumroad-ping/

As mentioned above, we first make sure that this isn't a test purchase and verify that the purchase is indeed for the right product. Once that's done, we create the user and trigger a reset password request for them.

const registerUser = (email: string) => {
  return axios.post(`${process.env.NHOST_HBP_URL}/auth/register/`, {
    email,
    password: generateSecurePassword(),
  })
}

const requestNewPassword = (email: string) => {
  return axios.post(
    `${process.env.NHOST_HBP_URL}/auth/change-password/request`,
    { email }
  )
}
Helper functions for creating a new user and requesting a password

Another note here, we're sort of handling errors but not really? That's because we will eventually (before launching) set up Sentry for both the bot and the dashboard and that's where all this semi-handling will come in handy as all the console.error calls will be replaced by a function call to send the errors to Sentry.

Notifying the user on account creation

Remember the reset password request we just triggered? That's actually also going to work as the method to set a password the first time around. Nhost doesn't have an invite flow built yet but I think they're working on it. So for now, we'll just use this workaround.

We can adjust the copy of the email so that it works in both situations and build the reset password page so that the users can actually submit a new password.

Here's what the email will look like:

Set new password email
Set new password email

Allowing the user to set a password

We've created the user's account and we've sent them an email asking them to set their password. So let's create a page where they can do so.

import Link from 'next/link'
import { useRouter } from 'next/router'
import { Formik, Form, Field, ErrorMessage } from 'formik'
import * as Yup from 'yup'
import ErrorPage from 'next/error'

import { auth } from '@lib/nhost'

import { Spinner } from '@components/common/spinner'
import { showErrorAlert } from '@components/common/alert'

const ResetPasswordSchema = Yup.object().shape({
  password: Yup.string()
    .min(8, 'Your passsword must be at least 8 characters long')
    .required('Required'),
})

const ResetPassword = () => {
  const router = useRouter()
  const { ticket } = router.query

  if (!ticket) return <ErrorPage statusCode={401} />

  return (
    <section className="bg-indigo-900 h-screen flex flex-col items-center justify-center space-y-5">
      <div className="px-5 py-8 bg-white rounded-none shadow-xl sm:rounded-lg sm:w-10/12 md:w-8/12 lg:w-6/12 xl:w-4/12">
        <h1 className="mb-5 text-lg font-semibold text-left text-gray-900">
          Reset your password
        </h1>
        <Formik
          initialValues={{ password: '' }}
          validationSchema={ResetPasswordSchema}
          onSubmit={async (values, { setSubmitting }) => {
            const { password } = values

            try {
              await auth.confirmPasswordChange(password, ticket.toString())
              router.push('/login')
              setSubmitting(false)
            } catch (error) {
              if (error.response?.data) {
                const { message } = error.response.data
                showErrorAlert({ text: message })
              } else {
                showErrorAlert({
                  text: 'Something went wrong. Please try again later.',
                })
              }
            }
          }}
        >
          {({ isSubmitting }) => (
            <Form>
              <label className="mt-5 block">
                <span className="block mb-1 text-sm font-medium text-gray-700">
                  New password
                </span>
                <Field
                  type="password"
                  name="password"
                  placeholder="••••••••"
                  className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
                />
              </label>
              <ErrorMessage
                name="password"
                component="span"
                className="text-red-500 text-sm"
              />
              <button
                type="submit"
                disabled={isSubmitting}
                className="mt-5 group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-indigo-800 hover:bg-indigo-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
              >
                <span className="absolute left-0 inset-y-0 flex items-center pl-3">
                  <svg
                    xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 20 20"
                    fill="currentColor"
                    className="h-5 w-5 text-indigo-500 group-hover:text-indigo-400"
                  >
                    <path
                      fillRule="evenodd"
                      d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
                      clipRule="evenodd"
                    />
                  </svg>
                </span>
                {isSubmitting ? (
                  <Spinner classes="text-white h-5 w-5" />
                ) : (
                  <>Reset password</>
                )}
              </button>
            </Form>
          )}
        </Formik>
      </div>
      <div className="text-sm text-center text-gray-400">
        <Link href="/login">
          <a className="text-indigo-200 underline hover:text-white">Login</a>
        </Link>
      </div>
    </section>
  )
}

export default ResetPassword
pages/reset-password.tsx

Again, a big snippet because I didn't want to share an incomplete example but we'll break it down and discuss the logic. Also, I moved to Formik because I realized we'll be using a lot of forms, and having something that helps with error handling, validations, and just cleans up the code seemed like a good step.

So the way Nhost works when you request a password reset for a user is that it sends them an email with the password reset URL and a ticket. So the first thing we do in this component is, we check if the ticket is present in the URL and if it isn't, we simply show a 401 because the user isn't supposed to be here.

const router = useRouter()
const { ticket } = router.query

if (!ticket) return <ErrorPage statusCode={401} />
Check for password reset ticket

If the URL has a ticket, we show the user our password reset form and ask them to enter their new password.

onSubmit={async (values, { setSubmitting }) => {
  const { password } = values

  try {
    await auth.confirmPasswordChange(password, ticket.toString())
    router.push('/login')
    setSubmitting(false)
  } catch (error) {
    if (error.response?.data) {
      const { message } = error.response.data
      showErrorAlert({ text: message })
    } else {
      showErrorAlert({
        text: 'Something went wrong. Please try again later.',
      })
    }
  }
}}
On submitting a new password

Once the user submits a new password, we make the API request to update their password along with the ticket. If the ticket is invalid, the API throws an error and we show it to the user. If everything works fine, we simply redirect the user to the login screen. Now that I am writing this, it seems like showing a toast here would be a good idea so I am just going to go ahead and do that. My go-to library for showing toasts is CogoToast.

Reset password flow
Reset password flow

The GIF above shows the UI along with the toast and redirection. And with that, we're done with part 2 of our journey building Botler!

Closing

Another pretty long post but again, we did so much. We created and published a product for sale on Gumroad, then we set up a webhook that gets triggered whenever a user makes a purchase, and then we created an account for that user in our database along with sending them an email to set their password!

Let me know what you think about this article and if you have any feedback at all here. Also, feel free to tweet at me if you're following along or building something similar yourself and get stuck somewhere. I'll try and respond as soon as possible.

In part 3, we create our Telegram bot from scratch using NodeJS and TypeScript, and then deploy it to production on Railway.

Faraz Patankar

Probably obsessing over fantasy football, figuring out what to eat for my next meal or working on my next great (soon to be abandoned) side project but hello to you too! 🤗

More posts from this author