How to build an entire GraphQL backend using AWS Lambda and Postgraphile in under two seconds*
*It will actually take around an hour
The best things about writing code is not having to write very much of it. Let’s discuss how to get there using a few magical libraries and a couple of managed services from our favorite cloud provider and supreme leader of the universe, AWS.
This is not meant to be a step-by-step tutorial, but more of a guide for when you get lost in the woods. But first, let’s get on the same page. Our entire application has a few basic requirements:
- The application has users
- Users can create things
- Access to things can be controlled
Here is the basic architecture we are aiming for:
- Users are stored in Cognito, which adds interesting attributes to our request
- A Lambda parses the request and attributes and converts the GraphQL to SQL
- Postgres runs our query and uses Row Level Security to control access to things
So, how are we going to do server-less GraphQL? Hold on, I’ll get there. This story starts at the user service…
1. Users are stored in Cognito
I’m not going to put the word “Cognito” in big letters because, if you used it before, you probably had a bad time. Yes, it’s documentation is notoriously bad and it’s support in CloudFormation basically non-existent at the time of this writing, but it also does a lot for us so let’s use it anyway.
The way you setup Cogntio doesn’t really matter, you just need to have a user pool; all we care about is using the user pool as the authentication provider for our API Gateway endpoint. Obviously, storing users in Cognito is an opinion with massive implications:
- You will end up using things like AWS Amplify to interact with the service on the client-side
- You cannot build foreign key relationships between users and other tables directly in the database
- Access tokens are generated inside Cognito and must play by their rules, but we do get a few triggers with which we can augment them
But with all those considerations, locking down an endpoint is dangerously easy:
If you really don’t want to use Cognito to handle your API Gateway authentication, that is fine. Suffice to say we just want the request to our Lambda function to include some interesting, (and unique), things about our user…which Cognito will do without any special configuration.
If you’re new to Cognito and API Gateway, here are some great places to start:
- Official documentation
- freeCodeCamp’s guide to Securing Lambda services with Cognito
Since there are a lot of great resources on setting up API Gateway I won’t get into more detail here, but let me know in the comments if this is a topic you’d like me to cover in the future.
2. A Lambda parses our request, with magic
Now on to the Lambda function, which is best explained in gist form:
The most important part of this endpoint is postgraphile
, a magical library responsible for reflecting our database as a GraphQL server automatically.
We’re going to closely follow their documented schema-only usage, which explains how to render a schema and run queries against it. On line 11 we are building the actual schema; await
ing the result of that promise will give you a GraphQLSchema instance that you can do whatever you want with. Another trick might be to print the SDL and publish it as a package, which might make you more popular with your friends and front-end developers.
Before actually running our query we do one other interesting thing: extract some variables from the authorizer.claims
. These values were added by Cognito in the last step, remember? Everything in the pgSettings
option on line 39 are set as local variables in the Postgres transaction so we could toss other things in here too, like groups, if we wanted to build policies off of other attributes.
From there, we can follow the reference documentation from Postgraphile to get a result for the query. Returning the results is the same as with any other Lambda invoked via API Gateway, just ensure to set your statusCode
and stringify your body. For this implementation I’m also allowing CORS to make life easier.
3. Postgres runs our query, with magic
The basic premise of how we will use Postgres is simple: it manages our things by both defining their schema and controlling our access to them. The schema part is probably familiar, we have a table for things and a special table that holds their permissions, a.k.a. an access-control-list:
CREATE TABLE IF NOT EXISTS things (
id UUID DEFAULT uuid_generate_v4() NOT NULL PRIMARY KEY,
value TEXT
);CREATE TABLE IF NOT EXISTS permissions (
thing_id UUID REFERENCES things(id),
user_id UUID
);
You’ve probably done that part before, but this next bit is where we’ll get weird.
In this gist we are doing three things:
- Creating a role for our application to use
- Creating a policy on our things which says users can
select
,update
, anddelete
only the items they have permission to access - Enabled Row Level Security on our table
If you’re curious to understand more about the policy, this study explains why I wrote it that way and how efficient it is.
Line 30 is a little confusing so I’ll explain further: a new item
doesn’t have any permissions
yet so the first policy will fail; this second policy allows everyone to insert
new things
without checking permissions
.
The final piece is a trigger which automatically inserts permissions
for each new thing
. This happens after an insert which is why we needed that second policy in the last gist.
But that’s it! That’s our whole database! And our whole application! Now go play in the street, or whatever kids do these days.
For a more complete reference, you can check out this work-in-progress project which makes use of this strategy.
To Recap
We did three interesting things:
- Used Cognito to validate requests to our API since it automatically adds interesting things about our users to the request
- Setup Postgraphile in a Lambda to convert our GraphQL request to SQL, and inject some interesting user attributes into the transaction
- Leveraged Row Level Security to keep our queries simple and our access control comprehensive
And we did it all with not very much code. Nice job.
Though I enjoyed my time with Graphile and Lambda, I’ve since moved on to more managed solutions. Check out my more recent post where I do pretty much the same thing with Supabase, a fully managed, (and real-time), Postgres service.