Building an app with Stytch and PlanetScale

PlanetScale

PlanetScale is a serverless database platform. PlanetScale was founded in 2018 by the co-creators of Vitess, the same database that has enabled products like Youtube to reach planet-scale. PlanetScale removes the complexities of manually scaling a database by providing a serverless DBaaS. The PlanetScale platform allows developers to branch their database, similar to what you would do with Git. Database branching enables developers to perform non-blocking schema changes, among other things.

Stytch

Building user signup and login can be a frustrating task, especially when you just want to focus on building your core product. Stytch makes it easy to create user authentication; we offer flexible SDKs and APIs so that you can own your UX and let us do the heavy lifting.

What we’ll do in this post

This tutorial will walk you through how to create a lightweight user dashboard that utilizes Stytch and PlanetScale for authentication. All of the code for this example app is hosted and maintained in our public GitHub.

Getting started – Mise en place

Building an app is easier when you’ve got everything you need already laid out and ready, so let’s get all of the requirements out of the way! We’ll walk through the following in this section:

  • Gathering your PlanetScale and Stytch API keys
  • Installing PlanetScale’s CLI

This guide assumes you have Node.js and NPM installed on your system. You can find instructions on how to do so here.

Retrieve API key from the respective dashboards

Stytch API keys

Create PlanetScale service token

Installing PlanetScale’s CLI

Check out the instructions here to install PlanetScale’s CLI, pscale, for your operating system.

The pscale shell is an interactive shell for your database. Think of it as a MySQL shell that lets you directly operate on your PlanetScale database.

Creating and configuring your PlanetScale database

Create your PlanetScale database

$ pscale auth login
$ pscale database create <database-name>

Create your first branch

$ pscale branch create <database-name> <branch-name>

Think of your branch as an isolated copy of your database created with the current production schema. Branches can be used to develop features and test changes without impacting your production database.

Connect to your branch and create your table

$ pscale shell <database-name> <branch-name>
CREATE TABLE users (
  id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name varchar(255),
  email varchar(255) NOT NULL
);

This query will create your users table where you’ll store a record for each user that is created.

Create a deploy request, deploy and merge

$ pscale deploy-request create <database-name> <branch-name>
$ pscale deploy-request deploy <database-name> <deploy-request-number>
$ pscale deploy-request deploy <database-name> <deploy-request-number>

Deploy requests enable teams to collaborate and review changes before deploying them.

Create your backend

If you’re not familiar with Next.js, this is a good companion blog post covering some of its features and benefits. Before getting started, make sure that you’ve set your environment variables inside your .env.local file.

Setup the APIs to connect to PlanetScale

Establish a connection to the PlanetScale main branch using the planetscale-node library, then inside of pages/api/ create the functions to add, remove, and get users from the DB.

const URL = process.env.DATABASE_URL as string;
const sqlConn = mysql.createConnection(URL);
sqlConn.connect();

 
//getUsers retrieve all users
async function getUsers(conn: PSDB, req: NextApiRequest, res: NextApiResponse) {
 try {
   var query = 'select * from users';
 
   const [getRows, _] = await conn.query(query, '');
   res.status(200).json(getRows);
 } catch (error) {
   console.error(error);
   res.status(500).json({ error: 'an error occurred' });
 }
 return;
}
 
//addUser create a new user
async function addUser(conn: PSDB, req: NextApiRequest, res: NextApiResponse) {
 var user = JSON.parse(req.body);
 try {
    var query = 'INSERT INTO users (name, email) VALUES (?,?)';
    var params = [name, email];

    var insertID;
    const result = sqlConn.query(query, params, (err, result) => {
      if (err) {
        throw err;
      }

      insertID = (<OkPacket>result).insertId;
    });

    res.status(201).json({ id: insertID });
 } catch (error) {
   console.error(error);
   res.status(500).json({ error: 'an error occurred' });
 }
 return;
}


 
//deleteUser remove a single user
async function deleteUser(conn: PSDB, req: NextApiRequest, res: NextApiResponse) {
 try {
   var query = 'DELETE from users WHERE id=?';
    var params = [req.query['uid']];

    var status = 200;

    const result = await sqlConn
      .promise()
      .query(query, params)
      .then(([row]) => {
        if ((<OkPacket>row).affectedRows == 0) {
          status = 304;
        }
      });

    res.status(status).json({ message: 'success' });
 } catch (error) {
   console.error(error);
   res.status(500).json({ error: 'an error occurred' });
 }
 return;
}

Instantiate the Stytch client via our stytch-node client library; you’ll use this client to make any calls to the Stytch API.

import * as stytch from 'stytch';
 
let client: stytch.Client;
const loadStytch = () => {
 if (!client) {
   client = new stytch.Client({
     project_id: process.env.STYTCH_PROJECT_ID || '',
     secret: process.env.STYTCH_SECRET || '',
     env: process.env.STYTCH_PROJECT_ENV === 'live' ? stytch.envs.live : stytch.envs.test,
   });
 }
 
 return client;
};

We would like to be able to invite and add new users to our dashboard, we’ll leverage our stytch-node client library to send Email Magic Links to users to do that.

async function inviteUser(req: NextApiRequest, res: NextApiResponse) {
  const client = loadStytch();

  var email = req.body.email;

  // Params are of type stytch.LoginOrCreateRequest.
  const params = {
    email: email,
    login_magic_link_url: `${BASE_URL}/api/authenticate_magic_link`,
    signup_magic_link_url: `${BASE_URL}/api/authenticate_magic_link`,
  };

  try {
    await client.magicLinks.email.loginOrCreate(params);
    res.status(200).json({"message":"magic link sent"});
  } catch (error) {
    res.status(400).json({ error });
  }
  return;
}

Next you’ll want a way to authenticate the magic links that are being sent by the Stytch SDK.

import type { NextApiRequest, NextApiResponse } from 'next';
import loadStytch from '../../lib/loadStytch';
import { serialize } from 'cookie';
  
  
async function authenticate(req: NextApiRequest, res: NextApiResponse) {
  const client = loadStytch();
  const { token } = req.query;
  
  try {
    // Authenticate request and create 7 day session.
    const resp = await client.magicLinks.authenticate(token as string, { session_duration_minutes: 10080 });
  
    // Send user to profile with cookies in response.
    res.setHeader(
      'Set-Cookie',
      serialize(process.env.COOKIE_NAME as string, resp.session_token as string, { path: '/' }),
    );
    res.redirect('/profile');
    return;
  } catch (error) {
    res.status(400).json({ error });
    return;
  }
}

Setup user sessions

Now that we’ve got authentication in place, what happens when a user leaves and then comes back? We want that returning user to come back to an authenticated experience, so we will use Stytch’s Session Management API to make that happen.

We will start by creating a session helper that will validate the tokens exchanged through the cookies.

export async function validSessionToken(token: string): Promise<boolean> {
 // Authenticate the session.
 try {
   const sessionAuthResp = await client.sessions.authenticate({
     session_token: token
   });
 
   if (sessionAuthResp.status_code != 200) {
     console.error('Failed to validate session');
     return false;
   }
   return true;
 } catch (error) {
   console.error(error);
   return false;
 }
}

Add this session helper to each DB handler so we can prevent unauthenticated API calls.

 // Validate session.
 var isValidSession = await validSessionToken(token);
 if (!isValidSession) {
   res.status(401).json({ error: 'user unauthenticated' });
   return;
 }

Now we’ll want to add logic to clear the client state once a session has expired. Doing so will allow us to gracefully re-prompt the user to log in when their session expires; when we initiated the session, we chose a session_duration_minutes of 10080 minutes, 7 days, so that users won’t be prompted to re-login if they return within that 7 day period.

export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
 if (req.method === 'POST') {
   try {
     //  You can add logic to destroy other sessions or cookies as well.
     res
       .status(200)
       .setHeader('Set-Cookie', [
         serialize(process.env.COOKIE_NAME as string, '', { path: '/', maxAge: -1 }),
         serialize(STYTCH_SESSION_NAME, '', { path: '/', maxAge: -1 }),
       ]).json({"message":"logged out"});
     } catch (error) {
     res.status(400).json({ error });
   }
   return;
 }
}

Backend complete

You just finished all the critical backend components in our example. With the backend complete, your app can now login with Stytch Email Magic Links, manage sessions via Sessions Management, and maintain your user database via PlanetScale.

Give our Stytch + Planetcale example app a try to see what the frontend experience is like as a user!

We can’t wait to see what you build with Stytch! Get in touch with us and tell us what you’re building in our Slack community or via support@stytch.com.