Sitemap

Event-Driven GraphQL with Hasura, AWS Lambda, and Amazon RDS (Postgres)

5 min readSep 9, 2025

--

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.

Press enter or click to view image in full size
Architecture: Hasura + RDS + Lambda via Event Triggers and API Gateway

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()
);
Press enter or click to view image in full size
RDS configuration in AWS

2) Connect Hasura to RDS and track the table

  1. In Hasura, add your Postgres database connection string, e.g.
    postgres://appuser:***@<rds-endpoint>:5432/appdb
  2. Track the public.tasks table.

Optional (recommended later): add role-based permissions and row-level rules.

Press enter or click to view image in full size
Hasura console: Data tab with tasks table tracked”

3) Create the Lambda webhook (with SAM)

Project structure:

hasura-rds-lambda/
index.js
template.yaml
README.md

index.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).
Press enter or click to view image in full size
SAM deploy output with API URL

4) Wire Hasura Event Trigger → API Gateway

In Hasura Console:

  1. Events → Triggers → Create
  2. Name: task_insert_trigger
  3. Table: public.tasks • Operation: Insert
  4. Webhook URL: paste the ApiUrl from SAM (ends with /webhook)
  5. Save. New inserts on tasks will now call your Lambda.
Press enter or click to view image in full size
Hasura Event Trigger configuration with webhook URL

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.

Press enter or click to view image in full size
Hasura API tab showing processed=false and slug populated

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 id is BIGSERIAL, Hasura’s GraphQL type is bigint—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.md

Direct 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"}
}'

--

--