Skip to main content
Our frontend SDK provide automatic session management, prebuilt login or signup, multi-step flow handling, and more. They can also be used alongside our backend API or SDKs.

Quickstart

This quickstart guides you through building a Discovery login flow. Select a framework:
1

Before you start

  • Have your public_token and secret_key from your Stytch project & environment.
  • While the guide uses Express, the Node.js SDK is framework agnostic.
2

Install the Node.js SDK and configure API keys

  1. Install the SDK in your Express environment:
npm
npm install stytch
  1. Then add your Stytch API keys to your project’s .env file:
.env
STYTCH_PROJECT_ID="YOUR_STYTCH_PROJECT_ID"
STYTCH_SECRET="YOUR_STYTCH_PROJECT_SECRET"
# Use your Project's 'live' or any available 'test' credentials
3

Set up your app and login routes

  1. Initialize the Stytch client with your environment variables.
  2. Create a /login route that takes in a user’s email address and initiates the login or signup flow.
POST '/login'
const express = require('express');
const stytch = require('stytch');
const session = require('express-session');
const bodyParser = require('body-parser');

const app = express();
app.use(express.json());

/**
 * Express session management
*/
app.use(session({
  resave: true,
  saveUninitialized: false,
  secret: 'session-signing-secret',
  cookie: {maxAge: 60000}
}));

const stytchClient = new stytch.B2BClient({
  project_id: process.env.STYTCH_PROJECT_ID,
  secret: process.env.STYTCH_SECRET,
});

// Key to retrieve the session token from express-session
// Referenced as req.session[StytchSessionToken]
const StytchSessionToken = 'stytch_session_token'

app.post('/login', (req, res) => {
  const email = req.body.email;
  stytchClient.magicLinks.email.discovery.send({
    email_address: email
  })
  .then(response => {
    res.json(response)
  })
  .catch(err => {
    res.status(500).send(err.toString())
  });
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));
4

Add a route to handle Stytch's redirect callback

  1. Stytch calls the authenticate redirect URL with a token to complete a user’s authentication flow.
  2. Add an /authenticate route to handle the redirect callback in your app.
GET '/authenticate'
app.get('/authenticate', async (req, res) => {
  const token = req.query.token;
  const tokenType = req.query.stytch_token_type;

  // Handle Discovery authentication.
  if (tokenType !== 'discovery') {
    console.error(`Unrecognized token type: '${tokenType}'`);
    res.status(400).send();
    return;
  }

  const authResp = await stytchClient.magicLinks.discovery.authenticate({
    discovery_magic_links_token: token,
  });
  if (authResp.status_code !== 200) {
    console.error('Authentication error')
    res.status(500).send();
    return;
  }
});
5

Log in to an organization

  1. At this point, a user has been authenticated but still needs to specify an Organization to log in to and begin a session.
  2. Stytch provides an intermediate_session_token that you can use to preserve the user’s authentication state while you provide them with options to proceed (e.g. selecting from a list of organizations).
  3. For this example, we’ll add logic to automatically sign in to an existing organization or create one if none already exist:
GET '/authenticate'
app.get('/authenticate', async (req, res) => {
  const token = req.query.token;
  const tokenType = req.query.stytch_token_type;

  // Handle Discovery authentication.
  if (tokenType !== 'discovery') {
    console.error(`Unrecognized token type: '${tokenType}'`);
    res.status(400).send();
    return;
  }

  const authResp = await stytchClient.magicLinks.discovery.authenticate({
    discovery_magic_links_token: token,
  });
  if (authResp.status_code !== 200) {
    console.error('Authentication error')
    res.status(500).send();
    return;
  }

  // Sign into existing Organization if already Member
  const ist = authResp.intermediate_session_token
  if (authResp.discovered_organizations.length > 0 ) {
    const exchangeResp = await stytchClient.discovery.intermediateSessions.exchange({
      intermediate_session_token: ist,
      organization_id: authResp.discovered_organizations[0].organization.organization_id,
    });
    if (exchangeResp.status_code !== 200) {
      console.error(`Error exchanging IST into Organization: ${JSON.stringify(exchangeResp, null, 2)}`);
      res.status(500).send();
      return;
    }
    // Store the returned session and return session member information
    // Using express sessions with the const key of StytchSessionToken
    req.session[StytchSessionToken] = exchangeResp.session_token;
    res.send(`Hello, ${authResp.member.email_address}! You're logged into the ${authResp.organization.organization_name} organization`);
    return;
  }

  // If not eligible to log into an existing org, create new one
  const createResp = await stytchClient.discovery.organizations.create({
    intermediate_session_token: ist,
  });
  if (createResp.status_code !== 200) {
    console.error(`Error creating Organization: '${JSON.stringify(createResp, null, 2)}'`);
    res.status(500).send();
    return;
  }
  // Store the returned session and return session member information
  req.session[StytchSessionToken] = createResp.session_token;
  res.send(`Hello, ${createResp.member.email_address}! You're logged into the ${createResp.organization.organization_name} organization`);
  return;
});
6

Add a session-protected route

  1. Create a helper method that returns the user’s Session information.
  2. Use the method to gate any protected route that should only be accessed by an authenticated user.
GET '/dashboard'
app.get('/dashboard', async (req, res) => {
  const sessionInfo = await getAuthenticatedMemberAndOrg(req);
  if (sessionInfo && sessionInfo.member && sessionInfo.organization) {
    res.send(`Hello, ${sessionInfo.member.email_address}! You're logged into the ${sessionInfo.organization.organization_name} organization`);
    return;
  }
  res.send("Log in to view this page");
});

async function getAuthenticatedMemberAndOrg(req) {
  const sessionToken = req.session[StytchSessionToken];
  if (!sessionToken) {
    return null;
  }

  const resp = await stytchClient.sessions.authenticate({session_token: sessionToken});
  if (resp.status_code !== 200) {
    console.error('Invalid session found');
    req.session[StytchSessionToken] = undefined;
    return null;
  }

  req.session[StytchSessionToken] = resp.session_token;
  return {
    member: resp.member,
    organization: resp.organization,
  };
}
7

Full completed example:

const express = require('express');
const stytch = require('stytch');
const session = require('express-session');
const bodyParser = require('body-parser');

const app = express();
app.use(express.json());

/**
* Express session management
*/
app.use(session({
  resave: true,
  saveUninitialized: false,
  secret: 'session-signing-secret',
  cookie: {maxAge: 60000}
}));

const stytchClient = new stytch.B2BClient({
  project_id: process.env.STYTCH_PROJECT_ID,
  secret: process.env.STYTCH_SECRET,
});

// Key to retrieve the session token from express-session
// Referenced as req.session[StytchSessionToken]
const StytchSessionToken = 'stytch_session_token'

/**
* Retrieves the authenticated member and organization from the session, if present.
* @param req Express request.
* @returns {Promise<{organization: Organization, member: Member}|null>}
*/
async function getAuthenticatedMemberAndOrg(req) {
  const sessionToken = req.session[StytchSessionToken];
  if (!sessionToken) {
      return null;
  }

  const resp = await stytchClient.sessions.authenticate({session_token: sessionToken});
  if (resp.status_code !== 200) {
      console.error('Invalid session found');
      req.session[StytchSessionToken] = undefined;
      return null;
  }

  req.session[StytchSessionToken] = resp.session_token;
  return {
      member: resp.member,
      organization: resp.organization,
  };
}

/**
* Routes
*/

app.post('/login', (req, res) => {
  const email = req.body.email;
  stytchClient.magicLinks.email.discovery.send({
    email_address: email
  })
  .then(response => {
    res.json(response)
  })
  .catch(err => {
    res.status(500).send(err.toString())
  });
});

app.get('/authenticate', async (req, res) => {
  const token = req.query.token;
  const tokenType = req.query.stytch_token_type;

// Handle Discovery authentication.
  if (tokenType !== 'discovery') {
    console.error(`Unrecognized token type: '${tokenType}'`);
    res.status(400).send();
    return;
}

  const authResp = await stytchClient.magicLinks.discovery.authenticate({
    discovery_magic_links_token: token,
  });
  if (authResp.status_code !== 200) {
    console.error('Authentication error')
    res.status(500).send();
    return;
  }

  // Sign into existing Organization if already Member
  const ist = authResp.intermediate_session_token
  if (authResp.discovered_organizations.length > 0 ) {
    const exchangeResp = await stytchClient.discovery.intermediateSessions.exchange({
      intermediate_session_token: ist,
      organization_id: authResp.discovered_organizations[0].organization.organization_id,
    });
    if (exchangeResp.status_code !== 200) {
      console.error(`Error exchanging IST into Organization: ${JSON.stringify(exchangeResp, null, 2)}`);
      res.status(500).send();
      return;
    }
    // Store the returned session and return session member information
    req.session[StytchSessionToken] = exchangeResp.session_token;
    res.redirect('/dashboard');
    return;
  }

  // If not eligible to log into an existing org, create new one
  const createResp = await stytchClient.discovery.organizations.create({
    intermediate_session_token: ist,
  });
  if (createResp.status_code !== 200) {
    console.error(`Error creating Organization: '${JSON.stringify(createResp, null, 2)}'`);
    res.status(500).send();
    return;
  }
  // Store the returned session and return session member information
  req.session[StytchSessionToken] = createResp.session_token;
  res.redirect('/dashboard');
  return;
});

app.get('/logout', (req, res) => {
  req.session[StytchSessionToken] = undefined;
  res.redirect('/dashboard');
});

app.get('/dashboard', async (req, res) => {
  const sessionInfo = await getAuthenticatedMemberAndOrg(req);
  if (sessionInfo && sessionInfo.member && sessionInfo.organization) {
    res.send(`Hello, ${sessionInfo.member.email_address}! You're logged into the ${sessionInfo.organization.organization_name} organization`);
    return;
  }
  res.send("Log in to view this page");
});

app.listen(3000, () => console.log('Server running on http://localhost:3000'));

Test your application

  • Run your application and send a POST request to the /login endpoint with your email address.
  • You should receive an email magic link in your inbox.
  • Click on the magic link you recieved to finish the login or signup flow.

Next steps

  • Handle a session