AWS Builder Center

Agently (H0: Hack the Zero Stack Hackathon)

Agently is a production legal AI workspace for law firms. This post covers the specific AWS architectural decisions I made and the engineering challenges I solved building on Aurora DSQL, S3, Secrets Manager, ECS, Lambda, and Bedrock.

Building a Multi-Tenant Legal AI Platform on Aurora DSQL: IAM Token Rotation, ECS Browser Automation, and Secrets Manager Integration

This post was written as part of my entry for the H0: Hack the Zero Stack with Vercel v0 and AWS Databases Hackathon. #H0Hackathon
Live app: https://agently-ten.vercel.app

Overview

Agently is a production legal AI workspace for law firms. This post covers the specific AWS architectural decisions I made and the engineering challenges I solved building on Aurora DSQL, S3, Secrets Manager, ECS, Lambda, and Bedrock.

Aurora DSQL as the Primary Database

Aurora DSQL is PostgreSQL-compatible and serverless. For a multi-tenant SaaS product serving law firms — where data isolation is a regulatory requirement, not a feature — it was the right choice.

IAM Token Rotation

DSQL uses short-lived auth tokens generated by DsqlSigner. Tokens expire after 15 minutes. My pool manager refreshes 2 minutes before expiry, deduplicates concurrent rotation requests, and drains old connections gracefully.
The key insight: if you do not deduplicate concurrent refresh requests, multiple in-flight API calls will each try to rotate the token at the same moment, causing connection storms. The solution is a shared promise that all callers await:
If a pool exists and the token is still fresh, return it immediately. If a refresh is already in progress, return the same refresh promise rather than starting a second one. Only when no refresh is in progress do you start a new one, store the promise, and let all waiters resolve together.

Multi-Tenant Isolation Without RLS

DSQL does not support SET LOCAL or session-scoped Postgres configuration, so traditional RLS policies are not available. I implemented firm-level isolation by scoping every query with a firm_id column, validated upstream by Clerk's cryptographically signed org ID from the JWT.
The firm_id is never taken from the request body. It comes only from the verified JWT claim, which Clerk signs cryptographically. A lawyer from Firm A cannot access Firm B's data even if they manipulate the request, because the firm_id in every query is derived from the token, not the payload.
This is a more auditable isolation pattern than RLS for this use case — every query explicitly carries the firm boundary rather than relying on session state.

DDL Migration Runner

DSQL requires each DDL statement in its own transaction. Standard migration libraries send multiple statements in one transaction and fail silently or throw confusing errors. I wrote a custom runner that splits each SQL migration file on semicolons, strips comments, and executes each statement independently. Fifteen migrations handle the full schema including matters, documents, OAuth tokens, workflow runs, client intakes, firm settings, and usage counters.

AWS Secrets Manager for Court Portal Credentials

Each law firm has its own court e-filing portal credentials. These are stored per firm in Secrets Manager, keyed by the Clerk org ID. At filing time, the server fetches credentials and injects them into the browser agent's structured prompt for the login step. The agent receives the username and password as part of a step-by-step instruction set and types them into the portal. The credentials never appear in any response body, are never logged, and never leave the Node server.
If credentials are not configured for a firm, the agent skips the login step and navigates directly to the filing page. The system degrades gracefully.

ECS for the Browser-Use Agent

Python's browser-use library cannot run on AWS Lambda. The deployment package with Playwright binaries exceeds Lambda's 250 MB limit, and the execution timeout for real court filings — which involve document uploads and multi-step wizards — can exceed 15 minutes.
The solution was AWS ECS with a containerised Python service.
Architecture:
  1. Vercel Function receives the filing request
  2. Server creates a Steel.dev browser session and gets a CDP URL
  3. Server fetches documents from S3 and uploads them to the Steel VM via the Steel Files API
  4. Server fires an async HTTP POST to the ECS container with the task description and CDP URL
  5. ECS runs the browser-use Python agent, which connects to Steel Chrome via CDP
  6. Frontend polls the Vercel Function, which proxies to ECS for streaming events
The async fire-and-poll pattern was necessary because Vercel Functions have a response streaming limit. Long-running tasks go to ECS; Vercel just coordinates and proxies events.

S3 Document Pipeline for Secure File Upload

Court e-filing portals require actual file uploads. Chrome's CDP setFileInputFiles command requires the file to exist on the same filesystem as the Chrome process — which in this case is Steel's cloud VM, not the Node server and not Lambda.
The pipeline:
S3 GetObject fetches the file as a Buffer on the Node server. That buffer is then POST'd to the Steel Files API, which places it on the VM's filesystem and returns a path. That VM path is embedded in the agent's prompt. The browser-use agent calls upload_file with that path and Chrome finds the file locally.
The agent never has S3 credentials. The Node server handles all secure file transfer before the agent starts. This was a non-obvious solution that required understanding the full CDP execution path.

AWS Bedrock for AI Inference

All AI inference routes through Bedrock with Claude as the primary model. The AI SDK's streamText streams token-by-token responses via Server-Sent Events directly to the React client. Token usage is tracked per user per day in Aurora DSQL and enforced at the API layer before any Bedrock invocation. Firms that exceed their tier limit get a structured error before any cost is incurred.

Architecture Summary

Users hit the React frontend served from Vercel's CDN. API calls go to an Express 5 app running as a Vercel Serverless Function with Fluid Compute and a 300 second timeout. That function talks to Aurora DSQL for all structured data, S3 for documents, Secrets Manager for credentials, and Bedrock for AI inference. Browser automation tasks are handed off to ECS asynchronously, with Steel.dev providing the remote Chrome session over CDP.
Every data path is scoped by firm_id derived from a Clerk JWT. No cross-firm data access is possible regardless of what a client sends.
Try it: https://agently-ten.vercel.app
Built for the H0: Hack the Zero Stack with Vercel v0 and AWS Databases Hackathon. #H0Hackathon
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article