Once an internal tool can refund orders or change customer records, someone will ask who did what. Here is how to have an agent add an audit log on AWS by hand, and how to do it with Encore and a Pub/Sub topic.
An agent-built tool usually starts with reads. Then someone asks for a "mark as refunded" button, and the tool starts changing data that matters, at which point you need a record of each change that the tool's own code cannot quietly edit.
This guide covers what to record and two ways to deliver and store the events on AWS.
The OWASP Logging Cheat Sheet lists the fields that make an event useful later: when, where, who and what, plus the result. For an internal tool that means the user ID and email from the login, the action name, the target record, the request ID and whether it succeeded. Leave out tokens and full customer payloads. NIST SP 800-92 adds the operational side: protect the log from modification and decide how long to keep it.
A direct approach is an INSERT INTO audit_log next to each write. It is simple, and it ties the log to the same database credentials as the tool, so the code that can change orders can also change the log.
The sturdier approach publishes an event to SNS, delivers it to an SQS queue, and has a small consumer write it to a separate database. That needs a topic, a queue with a dead-letter queue, a subscription, IAM policies for the publisher and the consumer, and the second database. An agent can write the infrastructure code, but it cannot test the delivery path until someone applies it.
Encore combines an open source SDK for TypeScript and Go backends with a platform that provisions the topics, databases and permissions those services use in your own AWS account. The audit trail becomes a Pub/Sub topic that the tool publishes to and an audit service that subscribes:
// audit/audit.ts
import { Topic, Subscription } from "encore.dev/pubsub";
import { SQLDatabase } from "encore.dev/storage/sqldb";
export interface AuditEvent {
userID: string;
action: string;
target: string;
at: string;
}
export const auditEvents = new Topic<AuditEvent>("audit-events", {
deliveryGuarantee: "at-least-once",
});
const db = new SQLDatabase("audit", { migrations: "./migrations" });
const _ = new Subscription(auditEvents, "store-audit-event", {
handler: async (e) => {
await db.exec`
INSERT INTO audit_log (user_id, action, target, at)
VALUES (${e.userID}, ${e.action}, ${e.target}, ${e.at})`;
},
});
The tool's endpoints publish after each write, using the user from the auth handler:
// orders/refund.ts
import { api } from "encore.dev/api";
import { getAuthData } from "~encore/auth";
import { auditEvents } from "../audit/audit";
export const markRefunded = api(
{ expose: true, auth: true, method: "POST", path: "/orders/:id/refunded" },
async ({ id }: { id: number }): Promise<void> => {
// ...update the order...
await auditEvents.publish({
userID: getAuthData()!.userID,
action: "order.mark_refunded",
target: `order:${id}`,
at: new Date().toISOString(),
});
},
);
The audit database belongs to the audit service, so on AWS only that service's IAM role can reach it. With at-least-once delivery a handler can run twice for one event, so give the table a unique event key if duplicates matter. Publishing after the business write is not atomic, so a crash between the two can leave a refund without an audit event, and writes that must always be audited need an outbox table written in the same transaction. Running encore run starts Postgres locally, and the agent can trigger a refund and see the subscriber's insert in the trace in the local development dashboard. The topic, subscriber and table are added in one change the agent has run locally.
On deploy, Encore provisions SNS, SQS with a dead-letter queue, and the audit database in your AWS account. Cloud access sits with Encore's IAM role, not the agent.
An audit log in Postgres is easy to query, but it is only as tamper-resistant as access to that database. For stricter requirements, have the subscriber also copy each event to an S3 bucket with Object Lock that your security team manages.
Run in your terminal to get started locally.
Ask the agent to publish an event for every write, with the user from the auth context, the action, the target and a timestamp, and to store those events in an append-only table. With Encore, the agent declares a Pub/Sub topic and an audit database in code, tests it with encore run, which starts Postgres locally, and Encore provisions SNS, SQS and RDS in your AWS account on deploy.
At least who acted, what they did, which record it affected, when, and the outcome. The OWASP Logging Cheat Sheet also recommends recording where the request came from and keeping sensitive values such as tokens and passwords out of the log.
A separate database or service keeps the log out of reach of the code that changes business data. In Encore, an audit service with its own database is a few lines of code, and only that service gets access to it.
Deploy to your cloud on AWS and Google Cloud
without managing infrastructure
Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.