Guides
Guide

How to Build a Paid Membership Site with Stripe and Magic

2021-02-25
Download this example and get started in seconds:
npx make-magic --template magic-stripe

Resources

🍰 Test the live demo here!

  • Test card number: 4242 4242 4242 4242!
  • Choose a valid and random MM/YY (e.g. 09/23), as well as a random CVC (e.g. 123).

🧁 The full code base can be found here.

A Paid Membership Site

If you’re looking for a way to finally charge people for your hard-earned digital work, look no further! In this tutorial, you’ll learn how to create a paid membership app where users can pay for a lifetime access pass to your Premium Content πŸ’Έ.

We'll be using Stripe as our payment processor, Magic as our auth solution, React as our front end framework, Express as our server framework for Node.js, and Heroku to deploy our app!

Prerequisites

  1. If you’re unfamiliar with building a membership app with React, Express, and Magic, take a look at Build Magic auth into your React + Express app. We'll be reusing a lot of the code from this guide 😏.
  2. Also, feel free to check out Stripe’s guide on the Custom payment flow. We followed the steps listed in this guide to help implement our Stripe payment page 🎊.

File Structure

The root directory will contain the server-side files. The client folder will have all of the frontend files.

β”œβ”€β”€ README.md
β”œβ”€β”€ client
β”‚ β”œβ”€β”€ .env
β”‚ β”œβ”€β”€ package.json
β”‚ β”œβ”€β”€ public
β”‚ β”‚ └── (static files, such as images)
β”‚ β”œβ”€β”€ src
β”‚ β”‚ β”œβ”€β”€ App.js
β”‚ β”‚ β”œβ”€β”€ components
β”‚ β”‚ β”‚ β”œβ”€β”€ header.js
β”‚ β”‚ β”‚ β”œβ”€β”€ home.js
β”‚ β”‚ β”‚ β”œβ”€β”€ layout.js
β”‚ β”‚ β”‚ β”œβ”€β”€ loading.js
β”‚ β”‚ β”‚ β”œβ”€β”€ login-form.js
β”‚ β”‚ β”‚ β”œβ”€β”€ login.js
β”‚ β”‚ β”‚ β”œβ”€β”€ payment-form.js
β”‚ β”‚ β”‚ β”œβ”€β”€ payment.js
β”‚ β”‚ β”‚ β”œβ”€β”€ premium-content.js
β”‚ β”‚ β”‚ β”œβ”€β”€ profile.js
β”‚ β”‚ β”‚ β”œβ”€β”€ signup-form.js
β”‚ β”‚ β”‚ β”œβ”€β”€ signup.js
β”‚ β”‚ β”œβ”€β”€ index.js
β”‚ β”‚ └── lib
β”‚ β”‚ β”‚ β”œβ”€β”€ LifetimeAccessRequestStatusContext.js
β”‚ β”‚ β”‚ β”œβ”€β”€ LifetimeContext.js
β”‚ β”‚ β”‚ β”œβ”€β”€ UserContext.js
β”‚ β”‚ β”‚ └── magic.js
β”‚ └── yarn.lock
β”œβ”€β”€ .env
β”œβ”€β”€ package.json
β”œβ”€β”€ server.js
└── yarn.lock

Quick Start Instructions

Magic Setup

Create a Magic account and then grab your REACT_APP_MAGIC_PUBLISHABLE_KEY and MAGIC_SECRET_KEY from your Magic Dashboard.

Stripe Setup

Create a Stripe account and then grab your REACT_APP_STRIPE_PK_KEY and STRIPE_SECRET_KEY from your Stripe Dashboard.

Start your Express Server

  1. git clone https://github.com/magiclabs/magic-stripe.git
  2. cd magic-stripe
  3. mv .env.example .env
  4. Replace MAGIC_SECRET_KEY and STRIPE_SECRET_KEY with the appropriate values you just copied. Your .env file should look something like this: ⁠
    MAGIC_SECRET_KEY=sk_test_XXX
    CLIENT_URL=http://localhost:3000
    STRIPE_SECRET_KEY=sk_test_XXX
  5. yarn
  6. node server.js ⁠

Note: RunningΒ yarn helped us pull the dependencies we need for our server, including the Stripe Node library.

Start your React Client

(in a new terminal session)

  1. cd client
  2. mv .env.example .env
  3. Replace REACT_APP_MAGIC_PUBLISHABLE_KEY and REACT_APP_STRIPE_PK_KEY with the appropriate values you just copied. Your .env file should look something like this: ⁠
    ⁠---
    lang: bash 
    ---
    ⁠
    REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_test_XXX
    REACT_APP_SERVER_URL=http://localhost:8080
    REACT_APP_STRIPE_PK_KEY=pk_test_XXX
  4. yarn
  5. yarn start

Note: RunningΒ yarn helped us pull the dependencies we need for our client, including Stripe.js and the Stripe Elements UI library (both needed to stay PCI compliant; they ensure that card details go directly to Stripe and never reach your server.)

Magic React Storybook

This tutorial was built using Magic React Storybook 🀩. If you wish to swap the Magic UI components out for your own custom CSS, delete @magiclabs/ui and framer-motionfrom your client/package.json dependencies.

Client

Let's dive right in by going over the major steps we need to follow to build the app's Client side:

  1. Set up the user sign up, payment, login, and logout flows.
  2. Build the payment form as well as the Payment page that will house the form.
  3. Make this Payment page accessible to the user by creating a Payment Route.

Standard Auth Setup

Keep Track of the Logged In User

We'll be keeping track of the logged in user's state with React's useContext hook. Inside App.js, wrap the entire app in <UserContext.Provider>. This way, all of the child components will have access to the hook we created (namely, const [user, setUser] = useState();) to help us determine whether or not the user is logged in.

⁠---
lang: javascript 
---

/* File: client/src/App.js */

import React, { useState, useEffect } from 'react';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom';
import { UserContext } from './lib/UserContext';

// Import UI components
import Home from './components/home';
import PremiumContent from './components/premium-content';
import Login from './components/login';
import SignUp from './components/signup';
import Profile from './components/profile';
import Layout from './components/layout';

// Import Magic-related things
import { magic } from './lib/magic';

function App() {
  // Create a hook to help us determine whether or not the  user is logged in
  const [user, setUser] = useState();

  // If isLoggedIn is true, set the UserContext with user data
  // Otherwise, set it to {user: null}
  useEffect(() => {
    setUser({ loading: true });
    magic.user.isLoggedIn().then(isLoggedIn => {
      return isLoggedIn ? magic.user.getMetadata().then(userData => setUser(userData)) : setUser({ user: null });
    });
  }, []);

  return (
    <Router>
      <Switch>
        <UserContext.Provider value={[user, setUser]}>
          <Layout>
            <Route path="/" exact component={Home} />
            <Route path="/premium-content" component={PremiumContent} />
            <Route path="/signup" component={SignUp} />
            <Route path="/login" component={Login} />
            <Route path="/profile" component={Profile} />
          </Layout>
        </UserContext.Provider>
      </Switch>
    </Router>
  );
}

export default App;

Note: Once a user logs in with Magic, unless they log out, they'll remain authenticated for 7 days by default (the session length is customizable by the developer through the Magic dashboard).

Keep Track of the Paid User

We'll also be keeping track of whether or not the user has paid for lifetime access with the useContext hook. Again, inside of App.js, we wrap the entire app with two new contexts: <LifetimeContext>, then <LifetimeAccessRequestStatusContext>.

⁠---
lang: javascript 
---

/* File: client/src/App.js */
import React, { useState, useEffect } from 'react';
import { Switch, Route, BrowserRouter as Router } from 'react-router-dom';
import { UserContext } from './lib/UserContext';
import { LifetimeContext } from './lib/LifetimeContext';
import { LifetimeAccessRequestStatusContext } from './lib/LifetimeAccessRequestStatusContext';

// Import UI components
import Home from './components/home';
import PremiumContent from './components/premium-content';
import Login from './components/login';
import SignUp from './components/signup';
import Profile from './components/profile';
import Layout from './components/layout';

// Import Magic-related things
import { magic } from './lib/magic';

function App() {
  // Create a hook to check whether or not user has lifetime access
  const [lifetimeAccess, setLifetimeAccess] = useState(false);
  // Create a hook to prevent infinite loop in useEffect inside of /components/premium-content
  const [lifetimeAccessRequestStatus, setLifetimeAccessRequestStatus] = useState('');
  // Create a hook to help us determine whether or not the  user is logged in
  const [user, setUser] = useState();

  // If isLoggedIn is true, set the UserContext with user data
  // Otherwise, set it to {user: null}
  useEffect(() => {
    setUser({ loading: true });
    magic.user.isLoggedIn().then(isLoggedIn => {
      return isLoggedIn ? magic.user.getMetadata().then(userData => setUser(userData)) : setUser({ user: null });
    });
  }, []);

  return (
    <Router>
      <Switch>
        <UserContext.Provider value={[user, setUser]}>
          <LifetimeContext.Provider value={[lifetimeAccess, setLifetimeAccess]}>
            <LifetimeAccessRequestStatusContext.Provider
              value={[lifetimeAccessRequestStatus, setLifetimeAccessRequestStatus]}
            >
              <Layout>
                <Route path="/" exact component={Home} />
                <Route path="/premium-content" component={PremiumContent} />
                <Route path="/signup" component={SignUp} />
                <Route path="/login" component={Login} />
                <Route path="/profile" component={Profile} />
              </Layout>
            </LifetimeAccessRequestStatusContext.Provider>
          </LifetimeContext.Provider>
        </UserContext.Provider>
      </Switch>
    </Router>
  );
}

export default App;

As you can see, we've added two new hooks. The first hook will help us determine whether or not user has lifetime access:

⁠---
lang: javascript
---

const [lifetimeAccess, setLifetimeAccess] = useState(false);

While the second hook will help us preventΒ an infinite loop of component re-renderings caused by theΒ useEffect inside ofΒ /components/premium-content.

⁠---
lang: javascript
---

const [lifetimeAccessRequestStatus, setLifetimeAccessRequestStatus] = useState('');

Log in with Magic Link Auth

In client/src/components/login.js, magic.auth.loginWithMagicLink() is what triggers the magic link to be emailed to the user. It takes an object with two parameters, email and an optional redirectURI.

Magic allows you to configure the email link to open up a new tab, bringing the user back to your application. Since we won't be using redirect, the user will only get logged in on the original tab.

Once the user clicks the email link, we send the didToken to a server endpoint at /login to validate it. If the token is valid, we update the user's state by setting the UserContext and then redirect them to the profile page.

⁠---
lang: javascript
---

/* File: client/src/components/login.js */

async function handleLoginWithEmail(email) {
  try {
    setDisabled(true); // Disable login button to prevent multiple emails from being triggered

    // Trigger Magic link to be sent to user
    let didToken = await magic.auth.loginWithMagicLink({
      email,
    });

    // Validate didToken with server
    const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/login`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: 'Bearer ' + didToken,
      },
    });

    if (res.status === 200) {
      // Get info for the logged in user
      let userMetadata = await magic.user.getMetadata();
      // Set the UserContext to the now logged in user
      await setUser(userMetadata);
      history.push('/profile');
    }
  } catch (error) {
    setDisabled(false); // Re-enable login button - user may have requested to edit their email
    console.log(error);
  }
}

Sign up with Magic Link Auth

We'll be applying practically the same code as in the Login component to our SignUp component (located in client/src/components/signup.js). The only difference is the user experience.

When a user first lands on our page, they'll have access to our Free Content.

We can also show them a sneak peek of our awesomeness in the Premium Content page.

Once they realize how awesome we are, and have decided to pay $500 for a lifetime access pass, they can clickΒ Count Me In.

Since the user is not logged in, our app will ask them to first sign up for a new account. Once they've been authenticated by Magic, they'll be redirected to the Payment page where they can seal the deal to a lifetime access of awesomeness!

Log out with Magic

To allow users to log out, we'll add a logout function in our Header component. logout() ends the user's session with Magic, clears the user's information from the UserContext, resets both the user's lifetime access as well as the lifetime access request status, and redirects the user back to the login page.

⁠---
lang: javascript
---

⁠/* File: client/src/components/header.js */

const logout = () => {
  magic.user.logout().then(() => {
    setUser({ user: null }); // Clear user's info
    setLifetimeAccess(false); // Reset user's lifetime access state
    setLifetimeAccessRequestStatus(''); // Reset status of lifetime access request
    history.push('/login');
  });
};

Build the Payment Form

This is where we'll build out the PaymentForm component that's located in client/src/components/payment-form.js.

Set up the State

To create the PaymentForm component, we'll first need to initialize some state to keep track of the payment, show errors, and manage the user interface.

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState('');
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState('');

There are two more states we need. One to keep track of the customer we create:

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

const [customerID, setCustomerID] = useState('');

And the other to set the lifetime access state to true if the user's payment was successful:

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

const [, setLifetimeAccess] = useContext(LifetimeContext);

Store a Reference to Stripe

Since we're using Stripe to process the Customer's payment, we'll need to access the Stripe library. We do this by calling Stripe's useStripe() and useElements() hooks.

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

const stripe = useStripe();
const elements = useElements();

Fetch a PaymentIntent

As soon as the PaymentForm loads, we'll be making a request to the /create-payment-intent endpoint in our server.js file. Calling this route will create a Stripe Customer as well as a Stripe PaymentIntent. PaymentIntent will help us keep track of the Customer's payment cycle.

The data that the Client gets back includes the clientSecret returned by PaymentIntent. We'll be using this to complete the payment, so we've saved it using setClientSecret(). The data also includes the ID of the Customer that the PaymentIntent belongs to. We'll need this ID when we update the Customer's information, so we’ll also be saving it with setCustomerID().

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

useEffect(() => {
  // Create PaymentIntent as soon as the page loads
  fetch(`${process.env.REACT_APP_SERVER_URL}/create-payment-intent`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ email }),
  })
    .then(res => {
      return res.json();
    })
    .then(data => {
      setClientSecret(data.clientSecret);
      setCustomerID(data.customer);
    });
}, [email]);

Update the Stripe Customer

If the Stripe payment transaction was successful, we'll send the Customer's ID to our server's /update-customer endpoint to update the Stripe Customer's information so that it includes a metadata which will help us determine whether or not the user has lifetime access.

Once this request has completed, we can finally redirect the Customer to the Premium Content page and let them bask in the awesomeness of our content.

⁠---
lang: javascript
---

/* File: client/src/components/payment-form.js */

const handleSubmit = async ev => {
  ev.preventDefault();
  setProcessing(true);
  const payload = await stripe.confirmCardPayment(clientSecret, {
    payment_method: {
      card: elements.getElement(CardElement),
    },
  });

  if (payload.error) {
    setError(`Payment failed ${payload.error.message}`);
    setProcessing(false);
  } else {
    setError(null);
    setProcessing(false);
    setSucceeded(true);
    setLifetimeAccess(true);
    // Update Stripe customer info to include metadata
    // which will help us determine whether or not they
    // are a Lifetime Access member.
    fetch(`${process.env.REACT_APP_SERVER_URL}/update-customer`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ customerID }),
    })
      .then(res => {
        return res.json();
      })
      .then(data => {
        console.log('Updated Stripe customer object: ', data);
        history.push('/premium-content');
      });
  }
};

Add a CardElement

One last task to complete the PaymentForm component is to add the CardElement component provided by Stripe. The CardElement embeds an iframe with the necessary input fields to collect the card data. This creates a single input that collects the card number, expiry date, CVC, and postal code.

⁠---
lang: javascript
---
⁠
/* File: client/src/components/payment-form.js */

<CardElement id="card-element" options={cardStyle} onChange={handleChange} />

Build the Payment Page

Now that our PaymentForm component is ready, it's time to build the Payment component which will house it! This component is located in client/src/components/payment.js.

The two most important points to note about the Payment component are:

  1. We'll be using the user state that we set in UserContext to check whether or not the user is logged in.
  2. The component will take in Elements, PaymentForm, and promise as props to help us properly render the Stripe payment form.
⁠---
lang: javascript
---
⁠
/* File: client/src/components/payment.js */

import { useContext, useEffect } from 'react';
import { useHistory } from 'react-router';
import { UserContext } from '../lib/UserContext';
import Loading from './loading';

export default function Payment({ Elements, PaymentForm, promise }) {
  const [user] = useContext(UserContext);
  const history = useHistory();

  // If not loading and no user found, redirect to /login
  useEffect(() => {
    user && !user.loading && !user.issuer && history.push('/login');
  }, [user, history]);

  return (
    <>
      <h3 className="h3-header">Purchase Lifetime Access Pass to Awesomeness 🀩</h3>
      <p>
        Hi again {user?.loading ? <Loading /> : user?.email}! You successfully signed up with your email. Please enter
        your card information below to purchase your Lifetime Access Pass securely via Stripe:
      </p>
      {user?.loading ? (
        <Loading />
      ) : (
        <Elements stripe={promise}>
          <PaymentForm email={user.email} />
        </Elements>
      )}
      <style>{`
        p {
          margin-bottom: 15px;
        }
        .h3-header {
          font-size: 22px;
          margin: 25px 0;
        }
      `}</style>
    </>
  );
}

Add the Payment Route to App.js

Alright, with the PaymentForm and Payment components complete, we can finally route the user to the /payment page by updating client/src/App.js!

First, we import Stripe.js and the Stripe Elements UI library into our App.js file:

⁠⁠---
lang: javascript
---

⁠/* File: client/src/App.js */

import { loadStripe } from '@stripe/stripe-js';
import { Elements } from '@stripe/react-stripe-js';

Then we'll loadΒ Stripe.js outside of theΒ App.js's render to avoid recreating the Stripe object on every render:

⁠⁠---
lang: javascript
---

/* File: client/src/App.js */

const promise = loadStripe(process.env.REACT_APP_STRIPE_PK_KEY);

Note: As we saw in Build the Payment Page, promise is a prop that is passed into the Payment component and is used by the Elements provider to give it's child element, PaymentForm, access to the Stripe service.

Next, let's add a new route called /payment which returns the Payment component we created earlier with the props required to properly render the Stripe payment form.

⁠⁠---
lang: javascript
---

/* File: client/src/App.js */

function  App() {

...
                <Route
                  path="/payment"
                  render={(props) => {
                    return (
                      <Payment
                        Elements={Elements}
                        PaymentForm={PaymentForm}
                        promise={promise}
                      />
                    );
                  }}
                />
 ...
}
export  default  App;

Server

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.

⁠⁠---
lang: javascript
---

/* 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
app.post('/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 });
  }
});

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.

⁠⁠---
lang: javascript
---

/* 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
app.post('/create-payment-intent', async (req, res) => {
  const { email } = req.body;

  const paymentIntent = await stripe.customers
    .create({
      email,
    })
    .then(customer =>
      stripe.paymentIntents
        .create({
          amount: 50000, // Replace this constant with the price of your service
          currency: 'usd',
          customer: customer.id,
        })
        .catch(error => console.log('error: ', error)),
    );

  res.send({
    clientSecret: paymentIntent.client_secret,
    customer: paymentIntent.customer,
  });
});

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.

⁠⁠---
lang: javascript
---

/* File: server.js */

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

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

  res.send({
    customer,
  });
});

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.

⁠⁠---
lang: javascript
---

/* File: server.js */

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

  const customer = await stripe.customers.list({
    limit: 1,
    email,
  });

  res.send({
    customer: customer.data,
  });
});

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

Deploying to Heroku

Create a Project

πŸš€ Want to deploy your app on Heroku? First, install the Heroku CLI. Then run heroku create to generate a new Heroku project. It will return your Heroku app URL, similar to what is shown below.

⁠⁠---
lang: bash
---

$ heroku create

Creating app... done, β¬’ cryptic-waters-25194
https://cryptic-waters-25194.herokuapp.com/ | https://git.heroku.com/cryptic-waters-25194.git

Set Config Vars (.env)

Now let's set the production .env variables for our app. Locate your new project on Heroku and go to Settings. In Heroku, we set up .env variables under Config Vars. Click Reveal Config Vars and enter both of your client and server side environment variables.

Update Server.js

Add the following into your server.js file so that Heroku knows how to build your app.

⁠⁠⁠---
lang: javascript
---

⁠/* File: server.js */

// For heroku deployment
if (process.env.NODE_ENV === 'production') {
  app.use(express.static('client/build'));
  app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
  });
}

Update Package.json

In package.json, add this to the scripts object:

⁠⁠---
lang: bash
---
⁠
/* File: The server's package.json */

"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"

Now you can run the following commands to deploy your application:

⁠⁠---
lang: bash
---
⁠
$ git add .
$ git commit -m 'your message'
$ git push heroku master

Heroku should have given you a link to your live app. Congrats! πŸŽ‰

Outro

That's it for today! If you'd like more tutorials on Stripe x Magic, (e.g. how to create a subscription membership website) please let us know in the Discussion box below. Until next time πŸ™‹πŸ»β€β™€οΈ β™‘.

Let's make some magic!