Event-Driven GraphQL with Hasura, AWS Lambda, and Amazon RDS (Postgres)
TL;DR: Use Hasura Event Triggers to call a tiny AWS Lambda via API Gateway whenever data changes in Postgres on RDS. The Lambda computes a slug and marks the row as processed — simple, fast, and production-friendly with the right hardening.
Click here to checkout the Repo
Stack: Hasura Cloud • Amazon RDS (Postgres) • AWS Lambda • API Gateway • AWS SAM
Why this pattern?
- Zero boilerplate GraphQL: Hasura auto-generates queries/mutations over Postgres.
- Serverless glue: Lambda handles side-effects (slugs, webhooks, notifications) without managing servers.
- Clear responsibilities: Postgres stores data, Hasura exposes it, Lambda reacts to changes.
Architecture
When a new task is inserted, Hasura emits an Event Trigger → API Gateway → Lambda. Lambda computes a slug and sets processed = true via a GraphQL mutation back to Hasura.
Prerequisites
- AWS account with permissions for RDS, Lambda, API Gateway, CloudWatch
- Hasura Cloud project (or self-hosted Hasura)
- Node.js 18+ and AWS SAM CLI installed
1) Create the Postgres schema on RDS
Spin up an RDS Postgres (dev size is fine). Allow your IP to connect (tighten later). Create a database (e.g., appdb) and run:
CREATE TABLE public.tasks (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
slug TEXT,
processed BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);2) Connect Hasura to RDS and track the table
- In Hasura, add your Postgres database connection string, e.g.
postgres://appuser:***@<rds-endpoint>:5432/appdb - Track the
public.taskstable.
Optional (recommended later): add role-based permissions and row-level rules.
3) Create the Lambda webhook (with SAM)
Project structure:
hasura-rds-lambda/
index.js
template.yaml
README.mdindex.js — a minimal handler that reads the Hasura event payload, computes a slug, and updates the row:
export const handler = async (event) => {
try {
const body = typeof event.body === 'string' ? JSON.parse(event.body) : event.body;
const newRow = body?.event?.data?.new;
if (!newRow || !newRow.title || !newRow.id) {
return { statusCode: 400, body: JSON.stringify({ error: 'Bad payload', got: body }) };
}
const slug = newRow.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
const mutation = `
mutation Update($id: bigint!, $slug: String!) {
update_tasks_by_pk(pk_columns: {id: $id}, _set: {processed: true, slug: $slug}) {
id slug processed
}
}
`;
const resp = await fetch(process.env.HASURA_GRAPHQL_ENDPOINT, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-hasura-admin-secret': process.env.HASURA_ADMIN_SECRET
},
body: JSON.stringify({ query: mutation, variables: { id: newRow.id, slug } })
});
const json = await resp.json();
if (json.errors) {
console.error('Hasura errors', json.errors);
return { statusCode: 500, body: JSON.stringify({ ok: false, errors: json.errors }) };
}
return { statusCode: 200, body: JSON.stringify({ ok: true, slug }) };
} catch (e) {
console.error(e);
return { statusCode: 500, body: JSON.stringify({ error: e.message }) };
}
};template.yaml — exposes the Lambda behind API Gateway and passes Hasura env vars:
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Hasura → API Gateway → Lambda webhook
Parameters:
HasuraEndpoint:
Type: String
Description: Hasura GraphQL endpoint (e.g., https://<project>.hasura.app/v1/graphql)
HasuraAdminSecret:
Type: String
NoEcho: true
Description: Hasura admin secret
Resources:
WebhookFn:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs18.x
Handler: index.handler
CodeUri: .
Timeout: 15
MemorySize: 256
Policies:
- AWSLambdaBasicExecutionRole
Environment:
Variables:
HASURA_GRAPHQL_ENDPOINT: !Ref HasuraEndpoint
HASURA_ADMIN_SECRET: !Ref HasuraAdminSecret
Events:
Api:
Type: Api
Properties:
Path: /webhook
Method: POST
Outputs:
ApiUrl:
Description: Invoke URL
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/webhook"Deploy with SAM:
sam build
sam deploy --guided
# Provide parameters for HasuraEndpoint and HasuraAdminSecret when prompted.
# Copy the output ApiUrl (ends with /webhook).4) Wire Hasura Event Trigger → API Gateway
In Hasura Console:
- Events → Triggers → Create
- Name:
task_insert_trigger - Table:
public.tasks• Operation: Insert - Webhook URL: paste the ApiUrl from SAM (ends with
/webhook) - Save. New inserts on
taskswill now call your Lambda.
5) Test the flow end-to-end
Insert a task:
mutation Add {
insert_tasks_one(object: { title: "Write tiny demo" }) {
id
title
processed
slug
}
}You’ll see processed: false, slug: null immediately (that’s expected—event runs after insert).
Get Indranilchakrabortysc’s stories in your inbox
Join Medium for free to get updates from this writer.
Verify Lambda’s update:
query Check {
tasks(order_by: {id: desc}, limit: 1) {
id
title
processed
slug
created_at
}
}The newest row should now show processed: false and a non-empty slug.
If something fails, open Events → Triggers → task_insert_trigger to see invocation logs, or check CloudWatch Logs for the Lambda.
Production hardening checklist
- Auth: Replace admin secret in Lambda with a Hasura Action/role or a restricted admin key scoped to needed mutations. Consider a webhook secret header validated in Lambda.
- Networking: Keep RDS non-public, connect via VPC. Use Secrets Manager for DB and Hasura credentials.
- Retries & idempotency: Event Triggers retry on failure; make your Lambda idempotent (e.g., only update if
processed=false). - Observability: Emit structured logs; add CloudWatch alarms on 5xx and latency.
- Costs: RDS free tier or Amazon Aurora Serverless v2 for bursty traffic; Lambda is pay-per-use.
- Schema types: If
idisBIGSERIAL, Hasura’s GraphQL type isbigint—use$id: bigint!in your mutation (as shown). - IaC: Keep SAM/Terraform templates in version control and parameterize everything.
What you get
- A clean event-driven GraphQL backend with a tiny Lambda for side-effects
- Clear separation of concerns and minimal code surface
- A foundation you can extend with more triggers, actions, and permissions
Appendix: Useful snippets
Repo skeleton
hasura-rds-lambda/
index.js
template.yaml
README.mdDirect webhook test (optional)
curl -X POST "<API_URL>/webhook" -H 'Content-Type: application/json' --data '{
"event":{"op":"INSERT","data":{"new":{"id": 1, "title":"Write tiny demo"}}},
"table":{"schema":"public","name":"tasks"},
"trigger":{"name":"task_insert_trigger"}
}'




