Workflows 

Protecting HTTP routes 

Your app's HTTP routes can be protected using the preValidation function you can restrict access to signed-in users only.

api/routes/GET-protected-route.js
JavaScript
import { preValidation, RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ reply }) => { await reply.send("this is a protected route!"); }; route.options = { preValidation, }; export default route;
import { preValidation, RouteHandler } from "gadget-server"; const route: RouteHandler = async ({ reply }) => { await reply.send("this is a protected route!"); }; route.options = { preValidation, }; export default route;

This route will return 403 Forbidden if accessed without signing in, and will run the route handler if accessed by someone who is signed in.

Protecting pages (frontend routes) 

Routes in your app's frontend can be protected using two Gadget helper components, SignedInOrRedirect and SignedOutOrRedirect. These components conditionally render their children based on the user's sign-in status and handle redirection to secure frontend routes. Both components use the window.location.assign method to redirect the browser when necessary.

Let's take a look at an example below using both in tandem:

React
export const SomePage = () => ( <BrowserRouter> <Routes> <Route path="/" element={<Layout />}> {/* This route will be accessible only if the user is signed out */} <Route index element={ <SignedOutOrRedirect> <Home /> </SignedOutOrRedirect> } /> {/* This route will be accessible only if the user is signed in */} <Route path="my-profile" element={ <SignedInOrRedirect> <MyProfile /> </SignedInOrRedirect> } /> </Route> </Routes> </BrowserRouter> );
export const SomePage = () => ( <BrowserRouter> <Routes> <Route path="/" element={<Layout />}> {/* This route will be accessible only if the user is signed out */} <Route index element={ <SignedOutOrRedirect> <Home /> </SignedOutOrRedirect> } /> {/* This route will be accessible only if the user is signed in */} <Route path="my-profile" element={ <SignedInOrRedirect> <MyProfile /> </SignedInOrRedirect> } /> </Route> </Routes> </BrowserRouter> );

Environment-signed JWTs 

You can use a GADGET_ENVIRONMENT_JWT_SIGNING_KEY environment variable to sign JWTs that authenticate requests to your Gadget app.

When a JWT is signed with this key and used as an Authorization: Bearer token, Gadget validates the token and establishes the appropriate session, enabling proper access control and session management.

Setting up the signing key 

First, create an environment variable in your Gadget app with the identifier GADGET_ENVIRONMENT_JWT_SIGNING_KEY. This variable should be marked as a secret and contain a secure random string that will be used to sign your JWTs.

Signing JWTs 

When signing a JWT with GADGET_ENVIRONMENT_JWT_SIGNING_KEY, you must include the following required claims:

  • aud (audience): The primary domain host of your environment. For example: my-app.gadget.app
  • sub (subject): The session record ID that you want to authenticate

You can also include standard JWT claims like iat (issued at) and exp (expiration) to control the token's validity period.

Here's an example of signing a JWT in an action:

api/actions/signJwt.js
JavaScript
import { ActionRun, Config } from "gadget-server"; import jwt from "jsonwebtoken"; export const run: ActionRun = async ({ api, logger, params }) => { // Get the signing key from environment variables const signingKey = process.env["GADGET_ENVIRONMENT_JWT_SIGNING_KEY"]; if (!signingKey) { throw new Error("GADGET_ENVIRONMENT_JWT_SIGNING_KEY not configured"); } // Find or create a "shopper" user based on your authentication logic // The upsert method will find an existing shopper by email, or create one if it doesn't exist const shopper = await api.shopper.upsert( { email: params.email, // ... other user fields // Match on the email field to determine if the record exists on: ["email"], }, { select: { id: true, session: { id: true, }, }, } ); // Get or create a session for the user let sessionRecord = shopper.session; if (!shopper.session || !shopper.session.id) { // Create a new session linked to the user sessionRecord = await api.internal.session.create({ shopper: { _link: shopper.id }, // Optionally set roles for the session roles: ["authenticated"], }); } // Sign the JWT with the session ID const jwtToken = jwt.sign( { aud: Config.primaryDomain, sub: sessionRecord.id, // ... other claims like iat, exp, etc. }, signingKey ); return { token: jwtToken }; };
import { ActionRun, Config } from "gadget-server"; import jwt from "jsonwebtoken"; export const run: ActionRun = async ({ api, logger, params }) => { // Get the signing key from environment variables const signingKey = process.env["GADGET_ENVIRONMENT_JWT_SIGNING_KEY"]; if (!signingKey) { throw new Error("GADGET_ENVIRONMENT_JWT_SIGNING_KEY not configured"); } // Find or create a "shopper" user based on your authentication logic // The upsert method will find an existing shopper by email, or create one if it doesn't exist const shopper = await api.shopper.upsert( { email: params.email, // ... other user fields // Match on the email field to determine if the record exists on: ["email"], }, { select: { id: true, session: { id: true, }, }, } ); // Get or create a session for the user let sessionRecord = shopper.session; if (!shopper.session || !shopper.session.id) { // Create a new session linked to the user sessionRecord = await api.internal.session.create({ shopper: { _link: shopper.id }, // Optionally set roles for the session roles: ["authenticated"], }); } // Sign the JWT with the session ID const jwtToken = jwt.sign( { aud: Config.primaryDomain, sub: sessionRecord.id, // ... other claims like iat, exp, etc. }, signingKey ); return { token: jwtToken }; };

Configuring the Gadget API client 

Instead of manually adding the Authorization header to each request, you can configure your Gadget API client to automatically include the JWT token in all requests. Use the custom authentication mode:

Configuring an API client
JavaScript
import { ExampleAppClient } from "@gadget-client/example-app"; // fetch the jwt token from the server first const jwtToken = "your-signed-jwt-token"; const api = new ExampleAppClient({ authenticationMode: { custom: { processFetch: async (_input, init) => { init.headers ??= {}; ( init.headers as Record<string, string> ).authorization = `Bearer ${jwtToken}`; }, processTransactionConnectionParams: async (params) => { // For websocket connections used in transactions params.auth = { type: "custom", token: jwtToken }; }, }, }, }); // Now all API calls will automatically include the JWT token const session = await api.currentSession.get();
import { ExampleAppClient } from "@gadget-client/example-app"; // fetch the jwt token from the server first const jwtToken = "your-signed-jwt-token"; const api = new ExampleAppClient({ authenticationMode: { custom: { processFetch: async (_input, init) => { init.headers ??= {}; ( init.headers as Record<string, string> ).authorization = `Bearer ${jwtToken}`; }, processTransactionConnectionParams: async (params) => { // For websocket connections used in transactions params.auth = { type: "custom", token: jwtToken }; }, }, }, }); // Now all API calls will automatically include the JWT token const session = await api.currentSession.get();

When Gadget receives a request with a Bearer token signed with GADGET_ENVIRONMENT_JWT_SIGNING_KEY, it:

  1. Verifies the JWT signature using the signing key
  2. Validates that the aud claim matches the environment's primary domain
  3. Looks up the session using the sub claim (session ID)
  4. Establishes the session on the request, enabling access control and session-based features

This allows you to create custom authentication flows, like in Shopify Shop Mini apps, while leveraging Gadget's built-in session management and access control system.

Using the JWT to make manual requests 

You can also use the JWT to authenticate manual calls to your Gadget API by including it as a Bearer token in the Authorization header:

use the jwt in the Authorization header
JavaScript
const response = await fetch("https://my-app.gadget.app/api/graphql", { method: "POST", headers: { Authorization: `Bearer ${jwtToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: "{ currentSession { id } }", }), });
const response = await fetch("https://my-app.gadget.app/api/graphql", { method: "POST", headers: { Authorization: `Bearer ${jwtToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ query: "{ currentSession { id } }", }), });

Access control is not applied to your HTTP routes when you are using the JWT to authenticate requests. You need to manually verify the JWT or use the preValidation route option.

Was this page helpful?