Cosmo: OSS Apollo GraphOS / Federation Alternative
Are you looking for an open source alternative to Apollo GraphOS / Federation? Cosmo is a drop-in replacement. You can 100% self-host Cosmo, e.g. for compliance reasons, or use our managed Cloud.
In this post, I'd like to talk about a new Architecture pattern for building flexible GraphQL APIs. By treating your GraphQL Schema like a Database, you're able to build use-case agnostic and flexible GraphQL APIs.
We're currently in the works of building WunderGraph Cloud, a Serverless GraphQL API Platform with integrated CI/CD, similar to Vercel. Our goal is to offer the best possible Developer Experience for building APIs, with branching and the ability to deploy multiple versions of the same API just by opening a Pull Request. You can sign up for early access if you're interested.
Building WunderGraph Cloud means that we have to build APIs ourselves. We made the decision to use the Open Source WunderGraph Framework to build our own APIs. We believe that the best way to build a great Developer Experience is to use the same tools that we're building for our customers.
Using WunderGraph to build APIs is fundamentally different from the traditional approach. Instead of directly exposing the GraphQL Layer, we're hiding it behind a JSON-HTTP/JSON-RPC API. All the plumbing is handled by our Framework.
This protection against direct exposure of the GraphQL layer has a lot of advantages. But before we get there, let's discuss the traditional approach.
Building GraphQL APIs: The viewer root field
One common pattern you often see in GraphQL APIs is the
viewer root field. It's a field that returns the currently authenticated user. Here's a simple example:
If you send a GraphQL query like below, it will return the currently authenticated user:
The implementation of the
viewer field usually looks like this:
- A middleware extracts the current user from the request, e.g. from a cookie or a JWT token
- The middleware injects the user object into the resolver context
- The resolver of the
viewerfield uses the userID from the context to fetch the user from the database
With this pattern, you can add relationships like groups, friends, etc. to the
User type, allowing each user to access "their" data.
What sounds great at first glance comes with a lot of drawbacks, actually. What if we don't have a user? What if we want to see the data of another user? What if we don't know exactly how authentication will be ultimately implemented?
The limits of building GraphQL APIs with a user context in mind
Sometimes, you don't have a user. If you're building a frontend, this might not be obvious, but there are a lot of use cases where we actually don't have one.
Our API could be used by other microservices. They don't operate in the context of a user, but in the context of a service. They could be authenticated, but they will not have a user ID.
Another example is when we build a CLI tool. The CLI might be used from a CI/CD pipeline. In this case, we might be injecting service credentials into the environment of the CI runner. Again, no user.
How about building an admin dashboard to help your users? WunderGraph Cloud will allow users to deploy "projects". If there's a problem with one of the projects, how could our support team access the data if they need to be authenticated as a user?
As you can see, there are a lot of use cases where we'd have to circumvent the
viewer root field. Possible workarounds might be to create a second GraphQL API only for admin users. Another option would be to create fields prefixed with
admin_. These special fields would grant you more flexible access and require you to be authenticated as an admin user.
Either way, you'd have to maintain another service or another set of resolvers. It's a costly approach that we'd ideally want to avoid.
Building Authentication-agnostic GraphQL APIs around the idea of "actors"
As we've stated above, WunderGraph hides your GraphQL API behind a JSON-RPC layer. This means, you can build your API Authentication-agnostic.
Let's take a look at the first iteration of our API.
viewer root field. Instead, we have a
userByID field that takes a second argument, the
actorID. You might be thinking that this API is insecure, because it allows you to query any user by ID. But that's not the case. Let's see how we can leverage WunderGraph to implement all use cases we've discussed above without compromising security.
Let's build the first Operation for the user to view their own profile
This Operation leverages the
@fromClaim directive . This directives injects the userID into both query variables, userID and actorID are the same. The user must be authenticated to access this field. They can either be authenticated via a cookie or a JWT token.
The logic to implement the underlying resolver could check if the
actorID is the same as the
userID. If that's the case, the user is allowed to access the data.
Next, let's build the Operation for the admin to view any user's profile
In this case, we're only injecting the
actorID from the user context. The
userID variable can be defined by the
viewer of the API.
For this Operation to work, we need to extend the logic of our resolver. E.g. we can check in the database if the
actorID (injected) is an admin user.
Next, let's build an Operation that can be called from another microservice
Actually, we've solved this problem already. A microservice can use the OpenID Connect client credentials flow to authenticate. This means it will acquire a JWT token with a
sub claim. The
sub claim is injected into the
@fromClaim(name: USERID) directive. This means, the Operation above can be called from a microservice.
This can be implemented in the resolver by checking the
actorID for a specific prefix, e.g.
svc_. If the
actorID starts with
svc_, we know that the request is coming from a microservice. We can then check in our database whether the service is allowed to access the data.
Finally, let's build an Operation that can be called from a CI/CD pipeline
Again, this is the same as the previous use case. The only difference might be the issuing of an access token that grants access to all users of an organization. This means the
actorID would be something like
Treating our API like a database makes it flexible and easy to maintain
As you've seen, we've been able to implement all use cases with a single resolver. We didn't need to create a second API or a second set of resolvers. All of this is possible because we're hiding the GraphQL layer behind the WunderGraph API Gateway.
Due to the fact that we know we're going to hide the GraphQL layer, we can design it differently. That's why the title states that we're treating our API like a database. A database usually doesn't care about the user context. This makes it very flexible but also vulnerable. However, we're (hopefully) hiding the database behind an API layer, so it's not an issue. With WunderGraph, we're applying the same principle to our GraphQL API.
The flexibility is great! We're able to write and maintain less code. But there's another benefit in terms of security: We're able to easily audit access to our data.
Building GraphQL APIs with
actors makes data access easy to audit
When every API call needs to have an actor, we know exactly who had access to which data, and when.
If an access token is compromised, We know exactly which Operations were called with that token (actorID) in question The WunderGraph API Gateway can produce an audit log for us.
We've shown a pattern to build flexible and secure GraphQL APIs with the additional benefit of easy auditing. I hope this inspires you to think in new ways about how to build your GraphQL APIs.
If you're interested in learning more about how we're building WunderGraph and how you can use it, please follow us on twitter, linkedin, or join our discord to get updates.
Cloud Early Access
Be amongst the first to try out WunderGraph Cloud for free. It's our take on how Serverless APIs should be built.