DEV Community

Cover image for Build a Paid Membership Site with Magic and Stripe: Pt. 3 - Node Server
Maricris Bonzo for Magic Labs

Posted on

Build a Paid Membership Site with Magic and Stripe: Pt. 3 - Node Server

This is the third part of the Build a Paid Membership Site with Magic and Stripe series. Make sure to read the following before proceeding:

  1. Quickstart to set up Stripe and Magic.
  2. How the React client side is built.


Now that we understand how the Client side is built and how it works, let's learn about our Server side code (located in server.js).

Here are the major functions our server needs to work seamlessly with the Client:

  1. Validate the Auth Token (didToken) returned by Magic's loginWithMagicLink().
  2. Create a Stripe PaymentIntent to keep track of the Customer's payment lifecycle.
  3. Create a Stripe Customer so that we can tie the Customer to a matching PaymentIntent and keep track of whether or not the Customer has successfully paid.
  4. Validate that the user is indeed a Customer who has lifetime access to your Premium Content.

Validate the Auth Token (didToken)

As mentioned, when the user clicks the email link to log in, we send the didToken to a server endpoint called /login in order to validate it. It's best practice to validate the DID Token before continuing to avoid invalid or malformed tokens.

We verify the didToken with Magic's validate method. If the didToken is indeed valid, we then send a 200 status code back to the client.

/* File: server.js */

// Import, then initiate Magic instance for server-side methods
const { Magic } = require("@magic-sdk/admin");
const magic = new Magic(process.env.MAGIC_SECRET_KEY);

// Route to validate the user's DID token"/login", async (req, res) => {
  try {
    const didToken = req.headers.authorization.substr(7);
    await magic.token.validate(didToken);
    res.status(200).json({ authenticated: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
Enter fullscreen mode Exit fullscreen mode

Create a Stripe Payment Intent and Stripe Customer

Once a user decides to buy a lifetime access pass to our Premium Content, we consider them a customer. In order to keep track of the customer's payment cycle, we'll need to add a new server endpoint called /create-payment-intent in server.js that:

  1. Creates a customer with their email address.
  2. And then creates a PaymentIntent that is linked to this customer.

The PaymentIntent will keep track of any failed payment attempts and ensures that the customer is only charged once.

/* File: server.js */

// Import & initiate Stripe instance
const  stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);

// Add the user to your list of customers
// Then create a PaymentIntent to track the customer's payment cycle"/create-payment-intent", async (req, res) => {
  const { email } = req.body;

  const paymentIntent = await stripe.customers
    .then((customer) =>
          amount: 50000, // Replace this constant with the price of your service
          currency: "usd",
        .catch((error) => console.log("error: ", error))

    clientSecret: paymentIntent.client_secret,
    customer: paymentIntent.customer,
Enter fullscreen mode Exit fullscreen mode

As you can see, we'll be charging our customers $500 for a lifetime access pass to our awesome Premium Content. 😎

Update the Stripe Customer's Info

As soon as the payment goes through, the Client side will send a request to /update-customer to update the Stripe Customer's information with a metadata of { lifetimeAccess: true }. Since we have a special use case; charging customers a one time fee, setting this metadata will help us validate whether or not the customer has paid.

/* File: server.js */

// Update the customer's info to reflect that they've
// paid for lifetime access to your Premium Content"/update-customer", async (req, res) => {
  const { customerID } = req.body;

  const customer = await stripe.customers.update(customerID, {
    metadata: { lifetimeAccess: true },

Enter fullscreen mode Exit fullscreen mode

Validate a Paid Customer

Now that the user has successfully paid, they should be able to access the Premium Content page. To check whether or not the user is authorized, we'll be using the /validate-customer endpoint. It expects the user's email address, and returns a list of customers who has that email.

Ideally, your customer should know to only buy their lifetime access once. This way, the list that stripe.customers.list() returns will always have the single customer who paid.

However, accidents do happen. 🤷🏻‍♀️

To prevent users from purchasing a lifetime access twice, I suggest adding some logic to your SignUp component that checks whether or not the user who's trying to sign up is already a Stripe Customer with lifetime access. If they are, send them to the Premium Content page. Otherwise, they can continue to the Payment page.

/* File: server.js */

// Collect the customer's information to help validate
// that they've paid for lifetime access"/validate-customer", async (req, res) => {
  const { email } = req.body;

  const customer = await stripe.customers.list({
    limit: 1,

Enter fullscreen mode Exit fullscreen mode

Test the integration

Alright, now that we know how the paid membership app works on both the Client and Server side, let's give it a test run! Here are a few UX flows I suggest testing out:

  1. Head to the Premium Content page to sign up and pay for lifetime access.
  2. Log in as a paid Customer and try to access the Premium Content page.
  3. Using a different email, log in as an unpaid Customer and try to access the Premium Content page.

Btw, you can make your payments with this test card number: 4242 4242 4242 4242

What's next

YAY! You now have a paid membership site. Read our final article of this series to learn how to deploy your app to Heroku.

Top comments (0)