Using GraphQL Envelop+Helix in Redwood v0.35+

Currently, Redwood uses Apollo Server (via apollo-server-lambda) as its GraphQL server. With v0.35, a new @redwoodjs/graphql-server package built upon GraphQL Helix and the Envelop plugin system is available. (Read more about Envelop here.)

This is a preview feature requiring opt-in. We need your help with testing and feedback!

Contents

  1. Why are we excited?
    • Collaboration with the Guild
    • More Choice
    • Better Plugins
    • Security and Best Practices
    • Future Roadmap
    • Benchmarks
  2. How to Use Redwood with GraphQL Helix and Envelop
  3. Let Us Know: Testing and Feedback

Why are we excited?

Collaboration with the Guild

This project has been done with close collaboration between The Guild and the Redwood Core Team. From opening the original PR to creating new plugins to advising on GraphQL best practices and security, members of The Guild like @dotansimha (github) and @n1ru4l (github) have been along for the ride every step of the way. And they are committed to the future success of this integration.

More Choice

One of the tenets of the Redwood philosophy is “Redwood believes that, as much as possible, you should be able to operate in a serverless mindset and deploy to a generic computational grid.”

To be, able to deploy to a “generic computation grid” means that as a developer you should be able to deploy using the provider or technology of your choosing. You should be able to deploy to Netlify, Vercel, Render, AWS Serverless, or elsewhere with easy and no vendor or platform lock in. You should be in control of the framework, what the response looks like and how your client consume it.

The same should be true of your GraphQL Server. GraphQL Helix aims fix that:

Existing libraries like Apollo Server provide you with either a complete HTTP server or else a middleware function that you can plug into your framework of choice. GraphQL Helix takes a different approach — it just provides a handful of functions that you can use to turn an HTTP request into a GraphQL execution result. In other words, GraphQL Helix leaves it up to you to decide how to send back the response.

Instead of closing tying a GraphQL server to a client (as is the case with Apollo), GraphQL Helix offers a solution using GraphQL over HTTP, adhering to best practices and standards that offer more options to developers.

For example, Redwood’s graphql-server simply uses the APIGatewayProxyResult from aws-lambda to shape the response. And that means Redwood can soon offer different ways to handle the response to support Azure, Google Cloud, Cloudfront and more.

All this gets us closer to Redwood’s goal of being able to deploy to a “generic computation grid”. And that’s exciting!

Better Plugins

Envelop describes itself as the “missing GraphQL plugin system”.

envelop is a lightweight JavaScript (/TypeScript) library for customizing the GraphQL execution layer and flow, allowing developers to build, share and collaborate on GraphQL-related plugins while filling the missing pieces in GraphQL implementations.

envelop aims to extend the GraphQL execution flow by adding plugins that enrich the feature set of your application

If you haven’t used GraohQL plugins before, you should check out envelop’s Plugin Hub.

These plugins can do some amazing things like validation, logging, sending analytics or errors to Sentry, and importantly making your endpoint more secure.

Because envelop provides “a standardized interface” and “hook(s) into specific phases within the GraphQL execution pipeline” you can alter each phase based on your needs. You have complete control over what your GraphQL Server can do. And it’s easy to write your own.

Plugins help keep your GraphQL api secure and follow best practices, too.

Security and Best Practices

We’ll have more to share around securing your GraphQL API in addition to securing your services in upcoming posts.

With v0.35, the GraphQL server improves security by:

  • Enforcing depth limits: Attackers often submit expensive, nested queries to abuse query depth that could overload your database or expend costly resources. The GraphQL Server is now setup by default to limit depth and can be configured to your custom depth.
  • Error masking: Exceptions and stack traces (such as Prisma query errors) if leaked to the client can expose sensitive information about your api, schema, and data. The GraphQL Server now masks any errors that are not specifically GraphQL related and won’t reveal the inner workings that could be exploited by attackers.
  • Logging: Logging is essential in production apps to be alerted about critical errors and to be able to respond effectively to support issues. In staging and development environments, logging helps you debug queries, resolvers and cell requests. Logging also has the added benefit from being able to redact sensitive information, can keep your service concise and free of logger-bloat, and can send to transports, like DataDog…

Future Roadmap

What’s most exciting about GraphQL Helix and Envelop being part of the RedwoodJS Framework is what the future holds and the opportunities that are possible.

  • First, by moving off apollo-server-lambda, RedwoodJS can provide a more plugin-based, configurable approach to offer different ways of shaping the GraphQL to support Azure, Google Cloud, Cloudflare, and more.
  • Second, one of the most asked about topics is how to implement GraphQL subscriptions in a serverless infrastructure. Now, instead of having a web-sockets solution, RedwoodJS might be able to take advantage of new ways of performing live queries with the @live directive.
  • Third, by being able to better control the shape of the GraphQL response and also adhering to more standards, supporting more sides (mobile, CLI, etc.) will be easier since they won’t have to adhere to the Apollo Server response or use the Apollo client.
  • Last, RedwoodJS can have much more control over ways to filter the schema — one can remove fields from types to keep the data much more secure (ie, never send a private field with sensitive data like emails, credit card numbers, etc.) or even generate a filtered schema dedicated to a side. That means from a single SDL and schema one could define that only certain queries and mutations are available to the CLI side while others open to a public web side and even still just certain only available to authenticated users.

And of course, having The Guild onboard helps give RedwoodJS GraphQL API a bright future.

Benchmarks

Both GraphQL Helix and Envelop run performance tests and share their benchmark results.

You can see what they do to and how they measure here:

If you want to run your own benchmarks, you can enable tracing in your logger.

If you want to run your own benchmarks, you can enable tracing in your logger.

GraphQL Helix aims to be lightweight and the RedwoodJS released packages show that the graphql-server is smaller by ~100kb, while still being backwards compatible with the api package. These savings add up when deployed in a serverless environment and can show improved build and deploy times and also faster cold starts.

API Package: 330kb

GraphQL-Server Package: 225kb

:information_source: Helix and envelop are now the default in Redwood as of v0.37+ the guide below is no longer required.

Old opt-in instructions. **No longer requried** # How to Use Redwood with GraphQL Helix and Envelop

This is a preview feature and should be tested in staging or non-critical contexts prior to production use.

In order to use GraphQL Helix and Envelop in a new or existing app, you will have to make the following changes.

  1. In your redwood.toml config, add useEnvelop=true to the [experimental] section in order to inform the dev-server how to handle the response. For example:
[web]
  port = 8910
  apiProxyPath = "/.redwood/functions"
[api]
  port = 8911
[browser]
  open = false
[experimental]
  esbuild = false
  useEnvelop = true
[generate]
  nestScaffoldByModel = false
  1. Add @redwoodjs/graphql-server to dependencies in api/package.json
yarn workspace api add @redwoodjs/graphql-server
  1. Be sure to have a logger defined in /api/src/lib/logger.js|ts and update the import to use the new graphql-server package
import { createLogger } from '@redwoodjs/graphql-server/logger'
// ...
export const logger = createLogger({
  options: { level: 'info', prettyPrint: true },
})

Note: options here are an example. If you are sending logs to DataDog or some other log transport, then you likely want prettyPrint to be false (the default in non dev environments).

  1. Update your graphql.js|ts function to use the new graphql-serverpackage and add yourlogger:
import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices,
} from '@redwoodjs/graphql-server'

import schemas from 'src/graphql/**/*.{js,ts}'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'
import services from 'src/services/**/*.{js,ts}'

export const handler = createGraphQLHandler({
  loggerConfig: {
    logger,
    options: {}
  },
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})
  1. Change any '@redwoodjs/api' imports to @redwoodjs/graphql-server

  2. Make Custom ContextFunction Changes if needed

If you use a custom ContextFunction to modify the context in the createGraphQL handler, now the function is provided only the context and not also the event. However, the event information is available as an attribute of the context as context.event.

Therefore, your custom ContextFunction setIpAddress() accessed the event as in:

const ipAddress = ({ event }) => {
  return (
    event?.headers?.['client-ip'] ||
    event?.requestContext?.identity?.sourceIp ||
    'localhost'
  )
}

const setIpAddress = async ({ event, context }) => {
  context.ipAddress = ipAddress({ event })
}

with envelop, you would use simple get the event from the context:


const setIpAddress = async ({ context }) => {
  context.ipAddress = ipAddress({ event: context.event })
}

What should I look for?

  • The graphql-server should work equivalently and be backward compatible with the classic api package’s GraphQL Server. That is, “things should work just as they did before.”
  • Please configure logging. Do you like it? Try setting up data, operation name and query logging to get insight into your services without having do add logging in each service.
  • Let us know if you use any existing plugins or validation rules and we’ll help find equivalents.
  • Any issues with custom context
  • Forms and form errors are one area where the client still retains some Apollo Server-like backward compatibility. If you use Forms and display errors in any custom way or access error messages, please try that out and report back any issues you may see.

Let Us Know: Testing and Feedback

We’re looking for you, as part of the RedwoodJS community, to help validate and improve the new feature.

  • Please enable this GraphQL Server in your new apps and test out in developer.
  • If you upgrade your old apps, please test in a Staging environment before promoting to Production once you feel confident that you are seeing your API side work as expected.

How to report and give feedback?

You can reply to this post, of course, but also you can create a New Issue and reference “graphql-server” or “helix” or “envelop” in the title.

7 Likes

Awesome! Really appreciate!

And here is my first feedback after applying it.

import { ResolverArgs } from '@redwoodjs/api';

I think ResolverArgs should be added to @redwoodjs/graphql-server. :slight_smile:

Hi @sangheestyle and thanks much for the feedback!

Could you explain why and where you think this import should be changed?

I know that

import { ResolverArgs } from '@redwoodjs/api';

is used when generating services, but one thing I did not do yet is if you start using the graphql-server, and you generate scaffolds or services, it will still use api imports.

In other words, we didn’t update the templates to be smart enough to know to change the imports.

Might that be what you are encountering? If so, that’s a good point and we should let people know that in the release notes info because I can see it being confusing

1 Like

@dthyresson Ah my bad! Understood.

It makes sense because @redwoodjs/graphql-server is experimental. Thank you!

Hi all.

Added a section about custom context and a change needed if you happen to use them when creating the GraphQL handler-- which is not often.

In short - the custom function no longer is passed both the event and context – just the context. But, don’t fret, the event information is available as context.event.

Would you be able to expand the usage of ContextFunction and how we can add it to the handler?

Can we use it to read cookies from the client? such as the current workspace or current team selected on the UI.

Sure you could and then set the cookie the cookie value on an easier to access attribute to be used in services.

You should be able fetch it from the event in event.headers['cookie'] or in more specifically in the Helix/Envelop case context.event.headers['cookie'].

With H/E thought, if you import

import { context } from '@redwoodjs/api

Then I think you can access the cookies from headers: context.event.headers['cookie']

But you’ve always had access to those in the getCurrentUser function since it gets passed

decoded, { type, schema, token }, { event, context }

and thus you can pluck the headers off the event and thus get the cookie there – so no need to use the context function if you need the cookie value simply while authenticating.

Oh that’s great! I actually had no idea I could access cookies and request event details there. Totally makes sense now that I think about the request/response cycle.

I think a cookie makes the most sense for what I want to do - for support multiple teams and having a team switcher. It’s the closest thing the web/api layer has to some sort of session.

The Guild make it official with Envelop v1.0!

the most significant open-source project @TheGuildDev has ever launched

https://twitter.com/dotansimha/status/1418204383813144584?s=20

1 Like

Read all about envelop in the Guild’s new blog post:

1 Like

Hey DT - I am having some trouble upgrading and getting this running as expected. It seems to be permissions and auth context related.

Here is the graphq.js file I am starting from

import { createGraphQLHandler, makeMergedSchema, makeServices } from '@redwoodjs/api'

import { getAuthenticationContext } from '@redwoodjs/api/dist/auth'

import schemas from 'src/graphql/**/*.{js,ts}'

import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'

import { db } from 'src/lib/db'

import { reportErrorFromContext } from 'src/lib/sentry'

import { logger } from 'src/lib/logger'

const SentryExceptionPlugin = {

  requestDidStart(_) {

    return {

      didEncounterErrors(requestContext) {

        reportErrorFromContext(requestContext)

      },

    }

  },

}

export const handler = createGraphQLHandler({

  context: async ({ event, context }) => {

    const authContext = await getAuthenticationContext({ event, context })

    return authContext

  },

  loggerConfig: { logger, options: { operationName: true } },

  getCurrentUser,

  plugins: [SentryExceptionPlugin],

  schema: makeMergedSchema({

    schemas,

    services: makeServices({ services }),

  }),

  onException: () => {

    // Disconnect from your database with an unhandled exception.

    db.$disconnect()

  },

  cors: {

    origin: true,

    credentials: true,

  },

})

From the existing code, I am converting all the existing imports to the new graphql-server. For now I will also remove the sentry plugin - to later use the envelop plugin.

import { createGraphQLHandler, makeMergedSchema, makeServices } from '@redwoodjs/graphql-server'

import { getAuthenticationContext } from '@redwoodjs/graphql-server'

import schemas from 'src/graphql/**/*.{js,ts}'

import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'

import { db } from 'src/lib/db'

import { logger } from 'src/lib/logger'

import { reportErrorFromContext } from 'src/lib/sentry'

export const handler = createGraphQLHandler({

  loggerConfig: { logger, options: { operationName: true } },

  getCurrentUser,

  schema: makeMergedSchema({

    schemas,

    services: makeServices({ services }),

  }),

  onException: () => {

    // Disconnect from your database with an unhandled exception.

    db.$disconnect()

  },

  cors: {

    origin: true,

    credentials: true,

  },

})

At this point, if I try to use my app, all my contexts are empty. So all my APIs using requireAuth throw a permission error. Same happens for any APIs relying on auth context.

Ive also added a context import like below - but it did not make a difference.

import { context } from '@redwoodjs/api'

Ive also removed the getCurrentUser passed to the createGraphQLHandler as I do see that is being done automatically in the source code.

I am sure it’s something simple as I am sure this has already been tested with authentication. Just wanted to provide enough info so you can see the steps I took.

This is my graphql.ts that I use in an app that uses Netlify Identity to authenticate:

import {
  createGraphQLHandler,
  makeMergedSchema,
  makeServices,
} from '@redwoodjs/graphql-server'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'

import { getCurrentUser } from 'src/lib/auth'
import { db } from 'src/lib/db'
import { logger } from 'src/lib/logger'

export const handler = createGraphQLHandler({
  loggerConfig: {
    logger,
    options: { tracing: true, operationName: true },
  },
  getCurrentUser,
  schema: makeMergedSchema({
    schemas,
    services: makeServices({ services }),
  }),
  db,
})

That’s all I need.

If you need the Sentry plugin, then I suggest using useSentry – GraphQL Envelop and then setting that in the extraPlugins: [useSentry({...yourOptions}] attribute.

Not sure why you’d need to have

  context: async ({ event, context }) => {

    const authContext = await getAuthenticationContext({ event, context })

    return authContext

  },

as that is done here:

Question:

  • What/who’s your auth provider type?
  • You are running on Render?
  • You are running via api serve? Ie, not serverless?
  • What is in requireAuth() that is checking and saying Forbidden? Is current user present?
  • If you log while in getCurrentUser() do you see the user info?
  • Do you see same behavior locally running via yarn rw serve?
  • Firebase is auth provider.
  • Not deployed to Render yet - was just testing locally so far. But yes I do use render.
  • Yep that’s right. When deployed it’s ran with yarn api-server --functions ./dist/functions --port 80.
  • For local dev - Right now I use yarn rw dev --fwd=\"--disable-host-check=true\" & CONTEXT=PORTAL_APP yarn rw dev web --fwd=\"--disable-host-check=true --port=7910\" --generate=false for dev.

This is my requireAuth

export const requireAuth = ({ role } = {}) => {

  if (!context.currentUser) {

    throw new AuthenticationError("You don't have permission to do that.")

  }

  if (typeof role !== 'undefined' && typeof role === 'string' && !context.currentUser.roles?.includes(role)) {

    throw new ForbiddenError("You don't have access to do that.")

  }

  if (typeof role !== 'undefined' && Array.isArray(role) && !context.currentUser.roles?.some((r) => role.includes(r))) {

    throw new ForbiddenError("You don't have access to do that.")

  }

}
  • If you log while in getCurrentUser() do you see the user info?

Do you mean the token and details like that?

Update:

If I log user info in getCurrentUser I do see a token, and firebase user does get resolved.

export const getCurrentUser = async (_decoded, { token, _type }) => {

  const { _email, uid, ...userProps } = await firebaseApp.auth().verifyIdToken(token)

  console.log(token, uid, userProps)

All of that prints out valid data.

Ok, so the handler does call getCurrentUser and you say the user is decoded and the requireAuth() function is called but context.currrentUser is null or undefined thus raising a Forbidden error? And if you log or inspect the context in requireAuth() it isn’t there?

In local dev do you have the useEnvelop experimental flag set to true in redwood.toml?

Yep useEnvelop is set to true in redwood.toml. I can see api esbuild with envelop in the logs

context.currentUser is actually an empty object, when I print it out in the requireAuth.

This is a stack trace for one of the queries.

api esbuild with envelop | INFO [2021-07-23 05:00:04.942 +0000] (graphql-server): GraphQL execution started
api esbuild with envelop |     operationName: "PublicPortalSidebarQuery"
api esbuild with envelop | {}
api esbuild with envelop | ERROR [2021-07-23 05:00:04.955 +0000] (graphql-server): GraphQL execution completed with errors:
api esbuild with envelop |     errors: [
api esbuild with envelop |       {
api esbuild with envelop |         "message": "You don't have permission to do that.",
api esbuild with envelop |         "locations": [
api esbuild with envelop |           {
api esbuild with envelop |             "line": 2,
api esbuild with envelop |             "column": 3
api esbuild with envelop |           }
api esbuild with envelop |         ],
api esbuild with envelop |         "path": [
api esbuild with envelop |           "myPortalToken"
api esbuild with envelop |         ],
api esbuild with envelop |         "extensions": {
api esbuild with envelop |           "code": "UNAUTHENTICATED"
api esbuild with envelop |         }
api esbuild with envelop |       }
api esbuild with envelop |     ]
api esbuild with envelop |     operationName: "PublicPortalSidebarQuery"
api esbuild with envelop | POST /graphql 200 157.606 ms - 195

Hey DT - were you able to reproduce it on your end? or have any suggestions based on the changes I made to the code?

I was not.

But do you still have

  context: async ({ event, context }) => {

    const authContext = await getAuthenticationContext({ event, context })

    return authContext

  },

in your handler?