Oct 23, 20265 min read

How to Deploy an AI-Built Internal Tool to AWS with SSO and a Private Database

An internal tool needs two things a prototype skips: login through your company's identity provider and a database nobody can reach from the internet. Here is how to set up both on AWS by hand, and how it looks with Encore.

A coding agent will happily ship an internal tool with a hard-coded password or no login at all, and with a database that has a public endpoint so it can connect from the laptop. Both are fine for a demo and a problem as soon as the tool touches customer data.

This guide covers the two pieces that make an internal tool safe to share: single sign-on through your identity provider and a database inside a private network.

SSO and a private database at a glance

StepWith EncoreHand-built on AWS
Verify the loginAuth handler in code, run by the API gatewayMiddleware you add to each route or router
Protect endpointsauth: true on the endpointRemember to apply the middleware
Database networkStorage subnet with no internet access, by defaultPrivate subnets, subnet group and security group you create
Local databaseencore run starts Postgres locallyLocal Postgres you set up, or a tunnel to RDS
Who changes AWSEncore's IAM role in your accountWhoever runs the infrastructure apply

Step 1: register the tool with your identity provider

Create an OIDC web application in Okta, Entra ID or Google Workspace with a redirect URI for each environment. OpenID Connect gives the frontend an ID token signed by the provider. The frontend sends it as a bearer token, and the backend checks the signature, issuer, audience and expiry on each request. Restrict the application to the groups that should use the tool, so access is removed when someone leaves.

Step 2: verify tokens in the backend

On a hand-built Express backend, an agent usually writes middleware with jose:

import type { NextFunction, Request, Response } from "express"; import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://acme.okta.com/oauth2/default"; const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/v1/keys`)); export async function requireUser(req: Request, res: Response, next: NextFunction) { try { const token = (req.headers.authorization ?? "").replace("Bearer ", ""); const { payload } = await jwtVerify(token, JWKS, { issuer: ISSUER, audience: "api://default" }); res.locals.user = { id: payload.sub, email: String(payload.email ?? "") }; next(); } catch { res.status(401).end(); } }

The weak point is coverage, since every router has to use the middleware and a new route an agent adds later is public unless it remembers.

Step 3: put the database in private subnets

For RDS, the AWS guide to DB instances in a VPC covers the parts: a DB subnet group across private subnets, public accessibility set to no, and a security group that allows port 5432 only from the application's security group. The application then needs the connection details as a secret and a task role that can read it. Agents tend to fail here by opening the database to 0.0.0.0/0 so a local migration can run, which is the opposite of what you want.

The same tool with Encore

Encore

Encore combines an open source SDK for TypeScript and Go backends with a platform that provisions the infrastructure the code uses in your own AWS account, including the network around it. The token check moves into an auth handler that the API gateway runs before any endpoint that needs it:

// auth/auth.ts import { APIError, Gateway, Header } from "encore.dev/api"; import { authHandler } from "encore.dev/auth"; import { createRemoteJWKSet, jwtVerify } from "jose"; const ISSUER = "https://acme.okta.com/oauth2/default"; const JWKS = createRemoteJWKSet(new URL(`${ISSUER}/v1/keys`)); interface AuthParams { authorization: Header<"Authorization">; } interface AuthData { userID: string; email: string; } export const auth = authHandler<AuthParams, AuthData>(async (params) => { const token = params.authorization.replace("Bearer ", ""); try { const { payload } = await jwtVerify(token, JWKS, { issuer: ISSUER, audience: "api://default" }); return { userID: payload.sub!, email: String(payload.email ?? "") }; } catch { throw APIError.unauthenticated("invalid token"); } }); export const gateway = new Gateway({ authHandler: auth });

Any endpoint declared with auth: true is rejected unless this handler accepts the request, so a route the agent adds next month is protected by writing auth: true in the declaration. The database is declared in the service that uses it, and encore run starts Postgres locally so the agent can test migrations without a tunnel to RDS.

On deploy, Encore creates a VPC in your AWS account and places RDS in a storage subnet with no inbound or outbound internet access, reachable only from the compute subnet. The service gets an IAM role scoped to its own resources. For an agent, this removes a step that is easy to get wrong, because the network is not something it writes. Cloud access sits with Encore's IAM role, not the agent.

To review a change before it reaches production, open a pull request and test it in a preview environment with its own database, available on the Encore Pro plan.

Good to know

Encore does not ship an identity provider. You keep Okta, Entra ID or Google Workspace and verify their tokens in the handler. When a person needs to query production directly, encore db shell <database-name> --env=prod opens a psql shell that is read-only by default, and encore db proxy forwards local connections, so the database never needs a public endpoint.

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

How to choose

  • Encore when you want login enforced at the gateway and a private database by default, without the agent writing networking.
  • Hand-built on AWS when the tool must join an existing VPC and identity setup that your platform team manages in its own infrastructure code.

Frequently asked questions

How do I add SSO to an internal tool built by an AI agent?

Register the tool as an OIDC application in your identity provider, have the frontend sign users in, and verify the ID token on every backend request. With Encore, the verification goes in an auth handler, and endpoints marked auth: true are rejected by the API gateway unless the handler accepts the token.

How do I keep the internal tool's database private on AWS?

Put RDS in private subnets with public accessibility turned off and a security group that only allows the application. Encore does this by default: it places databases in a storage subnet with no inbound or outbound internet access, reachable only from the compute subnet.

Can the agent test SSO locally?

Yes, if your identity provider allows a localhost redirect URI for a development client. The agent runs the app with encore run, which starts Postgres locally, and signs in against the development client.

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.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders