Skip to main content

Setting up Speakeasy managed API keys without an API gateway

When setting up Speakeasy managed API keys in production we recommend using an API gateway to provide request termination before requests reach your application. Here is the complete documentation for integrating with an API gateway.

However, it is possible to use Speakeasy managed API keys without an API gateway. This is not recommended for production use, but can be useful for testing and development. Here is an example of using Speakeasy middleware with an API using Express.js and tRPC for routing.

Define a generic middleware

This middleware can be used to ensure users are logged in before handling requests. In the following snippet SPEAKEASY_API_KEY is an API key created in your Speakeasy workspace and SPEAKEASY_API_ID is the ID of the API which can be found on your API Dashboard.

* This is where the trpc api is initialized, connecting the context and
* transformer
import { initTRPC, TRPCError } from "@trpc/server";
import { OpenApiMeta } from "trpc-openapi";
import superjson from "superjson";
import { env } from "./env.mjs";
import { validateEmailWithACL } from "./utils";
import jwt from "jsonwebtoken";
import jwksClient from "jwks-rsa";

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

* This is how you create new routers and subrouters in your tRPC API
* @see
export const createTRPCRouter = t.router;

* Public (unauthed) 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;

const speakeasyMiddleware = t.middleware(({ ctx, next }) => {
const handler = speakeasy.expressMiddleware();
handler(ctx.req as any, ctx.res as any, next);
} else {
"Speakeasy config not ready, controller is not set up"
return next();

* Reusable middleware that enforces users are logged in before running any
* procedures
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
// if we have no auth set, reject
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
// infers auth as non-nullable
auth: ctx.auth,


Speakeasy middleware allows you to set custom claims on the JWT token. You can use these claims to check if the user is authorized to access a specific API. JWKS Client in the example below is validating the JWT, but if you're using a Gateway the Gateway will validate it for you, and you can usually trust the traffic without needing to re-validate it.

const speakeasyClaimsByApiKey = async (apiKey: string) => {
logger.error("SPEAKEASY_WORKSPACE_ID is not set and cannot accept api key");
return null;

// process jwt token
const decodedToken = jwt.decode(apiKey, { complete: true });
if (!decodedToken) {
logger.error({ apiKey }, "Failed to decode jwt");
return null;
const kid = decodedToken.header.kid;
logger.debug({ jwtHeader: decodedToken.header }, "Decoded jwt headers");

const issuer = `${env.SPEAKEASY_WORKSPACE_ID}`;
const audience = env.SPEAKEASY_WORKSPACE_ID;

const client = jwksClient({
jwksUri: `${env.SPEAKEASY_WORKSPACE_ID}/.well-known/jwks.json`,
requestHeaders: {}, // Optional
cache: true,
cacheMaxAge: 300000, // cache for 5 min
timeout: 30000, // Defaults to 30s

const signingKey = await client.getSigningKey(kid);
const publicKey = signingKey.getPublicKey();

const decoded = jwt.verify(apiKey, publicKey, {

if (typeof decoded === "string") {
logger.error({ decoded }, "We failed to properly decode the claim");
return null;

{ productId: decoded.productId, userId: decoded.userId },
"We decoded the api key successfully"

return {
productId: decoded.productId as string,
userId: decoded.userId as string,

Integrate with a developer portal to expose API keys to your customer

Use the speakeasy middleware to create a portal login token that is used to authenticate the user in the developer portal.

  // method based on
speakeasyPortalLoginToken: protectedProcedure
.input(z.object({ productId: z.string() }))
.query(async ({ ctx, input }) => {
if (!input.productId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid product id",
// String customerId="some-customer-id";
const customerId = `${input.productId}${}`;
"Creating speakeasy portal login token"
const req = new EmbedAccessTokenRequest();

if (!ctx.req.controller) {
return "";
// Restrict data by time (last 24 hours)
// Instant*60*24);
// filterBuilder.withTimeFilter(startTime,SpeakeasyAccessTokenFilterOperator.GreaterThan);
const timeFilter = new EmbedAccessTokenRequest.Filter();
add(, {
days: -1,


// Populate with any custom claims you want added to your created API keys
// Map<String, String> jwtCustomClaims = new HashMap<>();
const jwtClaimsMap = req.getJwtCustomClaimsMap();
jwtClaimsMap.set("productId", input.productId);


// Populate with any permissions you want enabled/disabled for the user
const permissionsMap = req.getPermissionsMap();
permissionsMap.set("end_user:api_keys:write", true);
permissionsMap.set("end_user:api_keys:read", true);

return ctx.req.controller.getSDKInstance().getEmbedAccessToken(req);