Kinde x tRPC x Next.js

By Vlad Ionash — Published

Integrating Kinde auth with tRPC combines Kinde’s streamlined authentication with tRPC’s type-safe APIs, enhancing security and developer efficiency. This synergy simplifies secure user management and boosts productivity by reducing errors in web application development, making it an ideal solution for creating scalable and maintainable applications.

Let’s create one with both Next Pages Router and Next App router!

Prerequisite:

  • This guide assumes that you already have a functional Kinde Next Pages Router or Next App Router Instance (that your authentication credentials are verified and working!)
  • You know your way around the organizational structures and differences between the Next Pages Router and the Next App Router

Next Pages Router Guide

Link to this section

Step 1: Create Kinde Authentication Context

Link to this section

First, you need to create a context that includes Kinde’s authentication information. You will use Kinde’s getKindeServerSession to access the user’s authentication data.

Modify the file: src/server/context.ts:

typescriptCopyimport * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server';

interface KindeAuthContext {
  kindeAuth: ReturnType<typeof getKindeServerSession>;
}

export const createContextInner = async ({ req, res }: trpcNext.CreateNextContextOptions) => {
  const kindeAuth = await getKindeServerSession(req, res);
  return {
    kindeAuth,
  };
};

export const createContext = async (opts: trpcNext.CreateNextContextOptions) => {
  return await createContextInner(opts);
};

export type Context = trpc.inferAsyncReturnType<typeof createContext>;

This gives access and authorization of tRPC with the Kinde client.

Step 2: Create tRPC Context

Link to this section

Next, create the tRPC context to use the Kinde context in your tRPC queries.

Modify the file: src/pages/api/trpc/[trpc].ts

typescriptCopyimport { appRouter } from '@/server/routers';
import * as trpcNext from '@trpc/server/adapters/next';
import { createContext } from 'src/server/context';

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: createContext,
});

Step 3: Access the Context Data in Your Backend

Link to this section

Now, the tRPC context (ctx) will have access to the Kinde authentication context. You can use ctx in your queries to access the context data in any procedure.

Modify the file: src/server/routers/index.ts

typescriptCopyimport { router, publicProcedure } from '../trpc';

export const exampleRouter = router({
  intro: publicProcedure.query(async ({ ctx }) => {
    const user = await ctx.kindeAuth.getUser();
    return {
      greeting: `Welcome! ${user?.id || 'guest'}`
    };
  })
});

Step 4: Create a Protected Procedure

Link to this section

Use tRPC middleware to check if the user is authenticated before allowing access to certain procedures.

Modify the file: src/server/trpc.ts

typescriptCopyimport { initTRPC, TRPCError } from '@trpc/server';
import superjson from 'superjson';
import { type Context } from './context';

const t = initTRPC.context<Context>().create({
  transformer: superjson,
  errorFormatter({ shape }) {
    return shape;
  },
});

// Check if the user is signed in, otherwise throw an UNAUTHORIZED code
const isAuthed = t.middleware(async ({ next, ctx }) => {
  const isAuthenticated = await ctx.kindeAuth.isAuthenticated();
  if (!isAuthenticated) {
    throw new TRPCError({ code: 'UNAUTHORIZED' });
  }
  return next({
    ctx: {
      auth: ctx.auth, // Pass all existing context
    },
  });
});

export const router = t.router;

export const publicProcedure = t.procedure;

// Export this procedure to be used anywhere in your application
export const protectedProcedure = t.procedure.use(isAuthed);

Step 5: Use Your Protected Procedure

Link to this section

Finally, use the protected procedure in any router to ensure that only authenticated users can access certain functionalities.

Modify the file: src/server/routers/index.ts

typescriptCopyimport { router, protectedProcedure } from '../trpc';

export const protectedRouter = router({
  intro: protectedProcedure.query(async ({ ctx }) => {
    const user = await ctx.kindeAuth.getUser();
    return {
      authorized: `${user?.id} is using a protected procedure`
    };
  })
});

And that’s it!

In the above example, we constructed an application that responds to successful authentications with an ‘authorized’ message that includes providing the userID of the active user. Otherwise, the intro procedure will return a tRPC UNAUTHORIZED code response error based on what we configured in Step 4.

Next App Router Guide

Link to this section

Step 1: Modify theroute.ts with the below changes.

Link to this section

We will be creating the Kinde authentication context with the getKindeServerSession.

Take notice of how we are wrapping the createTRPCContext helper and provide the required context for the tRPC API when handling a HTTP request (e.g. when you make requests from Client Components). Here we have a sample route with the endpoint /api/trpc .

// TRPC API endpoint
// src/app/api/trpc/[trpc]/route.ts

import {fetchRequestHandler} from "@trpc/server/adapters/fetch";
import {type NextRequest} from "next/server";

import {env} from "~/env";
import {appRouter} from "~/server/api/root";
import {createTRPCContext} from "~/server/api/trpc";
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";

interface KindeAuthContext {
    kindeAuth: ReturnType<typeof getKindeServerSession>;
}

const createContext = async ({req, res}: trpcNext.CreateNextContextOptions) => {
    const kindeAuth = await getKindeServerSession(req, res);
    return {
        kindeAuth
    };
};

const handler = (req: NextRequest) =>
    fetchRequestHandler({
        endpoint: "/api/trpc",
        req,
        router: appRouter,
        createContext: () => createContext(req),
        onError:
            env.NODE_ENV === "development"
                ? ({path, error}) => {
                      console.error(`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`);
                  }
                : undefined
    });

export {handler as GET, handler as POST};

Step 2: Modify theserver.ts with the following changes.

Link to this section

Notice the similar wrapping as we did in the routes file above.

We want to also provide the custom RSC link that lets us invoke procedures without using http requests. Since server components always run on the server, we can just call the procedure as a function. The rest should be the same as the sample tRPC code.

// TRPC Proxy for server side usage
// src/trpc/server.ts

import "server-only";

import {createTRPCProxyClient, loggerLink, TRPCClientError} from "@trpc/client";
import {callProcedure} from "@trpc/server";
import {observable} from "@trpc/server/observable";
import {type TRPCErrorResponse} from "@trpc/server/rpc";
import {cookies, headers} from "next/headers";
import {NextRequest} from "next/server";
import {cache} from "react";
import {appRouter, type AppRouter} from "~/server/api/root";
import {createTRPCContext} from "~/server/api/trpc";
import {transformer} from "./shared";
import {getKindeServerSession} from "@kinde-oss/kinde-auth-nextjs/server";

interface KindeAuthContext {
    kindeAuth: ReturnType<typeof getKindeServerSession>;
}

const createContext = cache(() => {
    return createTRPCContext({
        headers: new Headers({
            cookie: cookies().toString(),
            "x-trpc-source": "rsc"
        }),
        auth: kindeAuth(new NextRequest("https://notused.com", {headers: headers()}))
    });
});

export const api =
    createTRPCProxyClient <
    AppRouter >
    {
        transformer,
        links: [
            loggerLink({
                enabled: (op) =>
                    process.env.NODE_ENV === "development" ||
                    (op.direction === "down" && op.result instanceof Error)
            }),
            /**
             * Custom RSC link
             */
            () =>
                ({op}) =>
                    observable((observer) => {
                        createContext()
                            .then((ctx) => {
                                return callProcedure({
                                    procedures: appRouter._def.procedures,
                                    path: op.path,
                                    rawInput: op.input,
                                    ctx,
                                    type: op.type
                                });
                            })
                            .then((data) => {
                                observer.next({result: {data}});
                                observer.complete();
                            })
                            .catch((cause: TRPCErrorResponse) => {
                                observer.error(TRPCClientError.from(cause));
                            });
                    })
        ]
    };

Step 3: Modify thetrpc.ts with the following changes.

Link to this section

This part should be straightforward, the authentication is coming from the createTRPCContext and you will use the file the same as the example that is provided in the demo. A full completed file would look something like the following:

// TRPC server response
// src/server/api/trpc.ts

/**
 * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
 * 1. You want to modify request context (see Part 1).
 * 2. You want to create a new middleware or type of procedure (see Part 3).
 *
 * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
 * need to use are documented accordingly near the end.
 */
import { initTRPC, TRPCError } from "@trpc/server";
import superjson from "superjson";
import { ZodError } from "zod";

import { db } from "~/server/db";
import { getKindeServerSession } from '@kinde-oss/kinde-auth-nextjs/server';

type kindeAuth = ReturnType<typeof getKindeServerSession>>;

/**
 * 1. CONTEXT
 *
 * This section defines the "contexts" that are available in the backend API.
 *
 * These allow you to access things when processing a request, like the database, the session, etc.
 *
 * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
 * wrap this and provides the required context.
 *
 * @see https://trpc.io/docs/server/context
 */

export const createTRPCContext = async (opts: {
  headers: Headers;
  auth: kindeAuth;
}) => {
  return {
    db,
    userId: opts.auth.userId,
    ...opts,
  };
};

/**
 * 2. INITIALIZATION
 *
 * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
 * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
 * errors on the backend.
 */

const t = initTRPC.context<typeof createTRPCContext>().create({
  transformer: superjson,
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

/**
 * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
 *
 * These are the pieces you use to build your tRPC API. You should import these a lot in the
 * "/src/server/api/routers" directory.
 */

/**
 * This is how you create new routers and sub-routers in your tRPC API.
 *
 * @see https://trpc.io/docs/router
 */
export const createTRPCRouter = t.router;

/**
 * Public (unauthenticated) procedure
 *
 * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
 * guarantee that a user querying is authorized, but you can still access user session data if they
 * are logged in.
 */
export const publicProcedure = t.procedure;

/** Reusable middleware that enforces users are logged in before running the procedure. */

const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  // Make ctx.userId non-nullable in protected procedures
  return next({ ctx: { userId: ctx.userId } });
});

/**
 * Protected (authenticated) procedure
 *
 * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
 * the session is valid and guarantees `ctx.session.user` is not null.
 *
 * @see https://trpc.io/docs/procedures
 */
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);

From here you should be able to run the project and have the familiar flow from the Pages Router implementation!

We will get the tRPC UNAUTHORIZED code response error if the auth logs out or expires, thus securing the route.

Congratulations 🎉

Link to this section

We successfully secured our routes, implementing tRPC with both Next.js Pages Router and Next.js App Router configurations with Kinde Authentication! This integration not only streamlines the development process but also ensures a robust authentication mechanism, providing a seamless and secure experience for our users. By prioritizing these advancements, we are setting a new standard for application performance and user trust, demonstrating our commitment to delivering top-tier, secure solutions.

If you need any assistance with getting Kinde connected reach out to support@kinde.com.

You can also join the Kinde community on Slack or the Kinde community on Discord for support and advice from the team and others working with Kinde.