A practical AWS setup for a small TypeScript project: a separate account, a budget alert, and the app itself with an API, a bucket and a nightly job, compared across Encore, Lightsail and Lambda.
AWS is a reasonable home for a side project that might grow, but the first hour decides whether it stays cheap and tidy. The usual problems are a personal account mixed with work, no budget alert, long-lived access keys on a laptop and a pile of console-created resources nobody remembers.
This guide covers the account setup first and then deploys a small TypeScript link shortener with an API, an S3 bucket and a nightly export job, using Encore, Lightsail and Lambda.
What you need to decide:
Create the project's account inside AWS Organizations, so its bill and resources stay separate and closing it later removes everything. Turn on MFA for the root user, then sign in through IAM Identity Center rather than creating IAM users with access keys.
Add a monthly cost budget with email alerts before deploying anything:
aws budgets create-budget --account-id "$ACCOUNT_ID" \
--budget '{"BudgetName":"side-project","BudgetLimit":{"Amount":"20","Unit":"USD"},"TimeUnit":"MONTHLY","BudgetType":"COST"}' \
--notifications-with-subscribers '[{"Notification":{"NotificationType":"ACTUAL","ComparisonOperator":"GREATER_THAN","Threshold":50},"Subscribers":[{"SubscriptionType":"EMAIL","Address":"you@example.com"}]}]'
AWS Budgets alerts you but does not stop resources, so the alert is a prompt to look, not a cap. Check which services fall under the AWS Free Tier for new accounts before choosing instance sizes.
Encore combines the API, bucket and cron job declared in your TypeScript code with a local environment and deploys them to your own AWS account.
A cron job calls a nightly export that writes the day's CSV to a bucket, so a second run overwrites the same file:
import { api } from "encore.dev/api";
import { CronJob } from "encore.dev/cron";
import { Bucket } from "encore.dev/storage/objects";
const links = new Map<string, string>(); // Stand-in for a table.
const exports_ = new Bucket("exports", { versioned: false });
export const exportLinks = api({ expose: false }, async () => {
const key = `links-${new Date().toISOString().slice(0, 10)}.csv`;
const csv = [...links].map(([s, u]) => `${s},${u}`).join("\n");
await exports_.upload(key, Buffer.from(csv), { contentType: "text/csv" });
});
const _ = new CronJob("nightly-export", { schedule: "0 3 * * *", endpoint: exportLinks });
A database can replace the Map, and encore run starts Postgres locally. Cron jobs do not fire locally. Once the AWS account is connected, pushes through the GitHub integration deploy to AWS, and Encore provisions an IAM role for the service.
An agent can test a new bucket locally before the deploy creates it.
Lightsail gives you a virtual server with a fixed monthly price. A Node.js app runs under a process manager:
aws lightsail create-instances --instance-names links \
--availability-zone eu-central-1a --blueprint-id ubuntu_24_04 --bundle-id nano_3_0
# On the server:
sudo apt-get install -y nodejs npm && sudo npm install -g pm2
pm2 start dist/main.js --name links && pm2 save
You patch the OS, set up TLS, deploys and backups, and add a Lightsail bucket or S3 separately. A crontab on the server runs the nightly export. The price is easy to predict.
Lambda charges per request, which suits a project with little traffic. The handler is a plain function:
import type { APIGatewayProxyHandlerV2 } from "aws-lambda";
export const get: APIGatewayProxyHandlerV2 = async (event) => {
const slug = event.pathParameters?.slug;
return slug
? { statusCode: 302, headers: { location: `https://example.com/${slug}` } }
: { statusCode: 404 };
};
The API, the bucket and the nightly schedule are declared separately, for example in a SAM template:
Transform: AWS::Serverless-2016-10-31
Resources:
Exports:
Type: AWS::S3::Bucket
GetLink:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Handler: get.get
Events:
Api:
Type: HttpApi
Properties: { Path: /links/{slug}, Method: GET }
ExportLinks:
Type: AWS::Serverless::Function
Properties:
Runtime: nodejs22.x
Handler: export.handler
Policies:
- S3WritePolicy: { BucketName: !Ref Exports }
Events:
Nightly:
Type: ScheduleV2
Properties: { ScheduleExpression: "cron(0 3 * * ? *)" }
sam deploy --guided creates the stack and the functions' roles. Locally, sam local start-api runs the functions in Docker, while the bucket and schedule only exist once deployed. A database adds cost, since an always-on RDS instance costs more than the functions. Many short-lived Lambda connections can exhaust a Postgres instance, so RDS Proxy is the usual fix, and the RDS Data API, which runs queries over HTTPS, is only available for Aurora.
Create a separate account for the project under AWS Organizations, add a budget alert, lock away the root user and sign in through IAM Identity Center. Then deploy the app. With Encore, write the API in TypeScript, declare new Bucket("exports") and new CronJob("nightly-export", { schedule: "0 3 * * *", endpoint: exportLinks }), and deploy to your AWS account, where Encore provisions the compute and S3 bucket.
Create a monthly cost budget with email alerts at, for example, 50 and 100 percent of a small amount. Budgets alert but do not stop resources, so also delete what you do not use and avoid always-on resources you do not need.
Yes. A separate account in an AWS Organization keeps its bill, permissions and resources apart from other work, and closing it later removes everything at once.
Lightsail bundles a virtual server, storage and transfer for a fixed monthly price, which is simple to reason about. You manage the server, deploys and backups yourself, and it does not connect as naturally to other AWS services.
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.