Thnkrr: AWS Deployment Decisions

Decision record and first-deploy plan, 2026-10-06. Written against rehearsal/2026-10-06 (dev/pilot + docs/25 follow-up + D-74 school pages + D-75 safety clocks). Builds on D-70 (AWS from day one, every AI model on Bedrock) and docs/18–19. Written for someone who has never deployed on AWS: every AWS term is explained the first time it appears. Prices are AWS's own published prices for US East, N. Virginia (us-east-1), checked 2026-10-06.

Scope: what we deploy, where, on which AWS services, at what size and cost; what the code must change before it can run on AWS; and the order of clicks for the first deploy. Each decision says what we recommend, why, and what you do. Decisions marked needs your call are open; everything else follows from earlier decisions.


1. The short version

#DecisionRecommendationStatus
1Where everything runsUS East, N. Virginia (us-east-1), certificate includedDecided 2026-10-06: the pilot schools are in the US
2The serverEC2 t4g.medium (2 processors, 4 GB memory), not LightsailDecided 2026-10-06 (replaces D-70's Lightsail shape; §4)
3The databasePostgres on RDS (db.t4g.micro), not DynamoDB, not on the server itselfDecided 2026-10-06 (goes with #2)
4The web appS3 + CloudFront at thnkrr.com, not VercelDecided
5The API's addressapi.thnkrr.com, straight to the server, HTTPS by CaddyDecided
6Photos and videosTwo private S3 buckets; approved media served by CloudFront at media.thnkrr.comDecided
7EmailAmazon SES in US East on our own domain; ask AWS for production access on day oneDecided
8AI writing checkBedrock, gpt-oss-safeguard-20b, in US East. No OpenAI agreement needed (OpenAI isn't a party, Service Terms §50.12.5)Decided: first deploy is a closed staff test with people checking everything; the Bedrock connector ships before the first real school
9AI picture checkOff at first deploy; people check every photo and video (already built)Decided
10App keys and passwordsNo stored AWS keys on the server (EC2 gets a role); app secrets in one locked file + a password managerDecided
11Getting code onto the serverOne deploy script run over Session Manager; no SSH keys, no open port 22Decided
12BackupsRDS daily backups, 7-day restore, plus a manual snapshot before every migrationDecided
13Spending safetyBudgets alerts at $50 / $100 / $200, anomaly alerts, credits checkedDo before anything else

Money: about $46 a month at 20 students, $47 at 100, $63 at 1,000 with the AI writing check on and picture checks off. All of it is paid from AWS credits (§12). The 30 September table said $28 / $32 / $78; §12 says what changed and why.

Before the first deploy, the code needed 11 changes (§13). All 11 are built (6 October, branch feat/deploy-readiness, waiting to be merged), with tests. Four things in them can only be proven on the real AWS account; §13 lists them.


Where we are (6 October), and your steps

Ready for deployment?

Ready?
Setting up the AWS accountYes, start today. Phase A needs no code
Putting the app on AWS for a closed staff testOnce the code changes are merged. They're built (§13); then Phase B
Real studentsNot yet. Phase B, then the real Bedrock check and the founder's names and paperwork. Phase C

Tick each step when it's done. You = Adarsh in the AWS console or the domain's DNS. Team = code we write. Sections in brackets give the detail.

Phase A: the account (you, starting today, about half a day plus waiting on AWS)

Phase B: the closed staff test on AWS (after the team's code changes)

Phase C: before the first real school


2. AWS words used in this doc

WordWhat it means
RegionA group of AWS data centres in one area. We use US East, N. Virginia (us-east-1)
Account / root userThe AWS account, and the email+password that created it. The root user can do anything, including close the account. Use it once to secure the account, then never again
IAMAWS's permission system. An IAM role is a set of permissions a machine wears; an IAM user with access keys is a username+password for a program. Roles are safer because there is no password to leak
IAM Identity CenterWhere you create your own day-to-day login (instead of using root)
EC2A rented virtual computer. t4g.medium is one size: 2 processors, 4 GB memory, Amazon's own ARM chip (cheaper)
Burstable / CPU credits / Unlimited modeCheap EC2 and Lightsail machines run at 20% of their processors on average and save up "credits" to run at full speed for bursts. When credits run out, Lightsail slows to 20%; EC2 in Unlimited mode keeps full speed and charges a little extra
EBSThe server's hard disk
Elastic IPA fixed public internet address for the server, so api.thnkrr.com keeps pointing at it after restarts
Security groupThe server's firewall: which ports are open, and to whom
RDSAWS-run Postgres. AWS patches it, backs it up, and can restore it to any minute in the last 7 days
S3 / bucketFile storage. A bucket is one named storage area
CloudFront / distributionAWS's delivery network: copies files to servers near users so pages and photos load fast. A distribution is one configured website on it
OAC"Origin access control": lets CloudFront read a private bucket so the bucket itself never has to be public
ACMFree HTTPS certificates for CloudFront. Must be requested in us-east-1 — where everything else is too
Route 53AWS's DNS (the internet's address book). Optional: the domain's current DNS provider works too
SES / sandboxAWS's email sender. New accounts start in a "sandbox" that only sends to addresses you've verified, 200 a day, until AWS approves production access
BedrockAWS's service for running AI models we pay per use; the model runs inside AWS, in US East
RekognitionAWS's picture-checking service (nudity, violence, text in images)
Session ManagerA browser terminal into the server through the AWS console, with no SSH keys and no open port
Budgets / Cost Anomaly DetectionEmail alerts when spending passes a line, or jumps unusually

3. Decision 1: US East (N. Virginia) for everything

Decision (2026-10-06): every service in US East (us-east-1): server, database, buckets, email, Bedrock, the CloudFront certificate and (later) Rekognition.

Why US East:

How the regions compare. We compared US East with Mumbai, the other location we'd use once Indian schools join:

US EastMumbai
Picture checks per second (Rekognition default)505
AI writing check (per million tokens read / written)$0.07 / $0.20$0.08 / $0.24
Picture check (per 1,000)$1.00$1.25
File storage (per GB a month)$0.023about $0.025
Server, EC2 t4g.medium (a month)$24.53$16.35
Database, RDS micro (a month)$11.68$15.33
Total a month: 20 / 100 / 1,000 students$46 / $47 / $63$42 / $44 / $61
Everything on, 1,000 students, month 12$110$119

At pilot size the two are within a few dollars, all paid from credits. US East handles pictures ten times faster and gets cheaper as we grow, so it wins.

What changes: nothing in the code. S3_REGION already defaults to us-east-1 (src/config.ts:110).

When Indian schools join: decide then. Either keep one US East setup, with server logs also kept in India for 180 days (CERT-In), or add a Mumbai setup for Indian students.


4. Decision 2: the server, EC2 instead of Lightsail (decided)

D-70 chose Lightsail ($12 server + $15 database) as "the simplest AWS shape". Looking at it in detail for the real deploy, three things make EC2 + RDS the better fit for about $6 a month more:

Lightsail (D-70)EC2 + RDS (recommended)
Size we'd actually need$24 plan: 2 processors, 4 GB. The $12 plan's 2 GB is too tight for the app + video converter + Whispert4g.medium: 2 processors, 4 GB
When video work runs longSlows to 20% of the processors once burst credits are gone, and there is no way to pay for more. The API, video and (if on the box) database all crawl togetherUnlimited mode (on by default): keeps full speed; any sustained extra costs $0.04 per processor-hour
AWS permissionsCannot use IAM roles, so the server must hold long-lived access keys for S3, SES, Bedrock and RekognitionWears an IAM role: no keys stored anywhere
DatabaseLightsail database: AWS doesn't document whether it allows pg_trgm, which search needs (docs/25)RDS: pg_trgm and citext documented as supported
FirewallSimple port listSecurity groups: the database accepts only the server
Ease for a first-timerFewer screensMore screens; §14 gives the exact order
Next step when we outgrow it (D-70)Move to EC2/Fargate + RDS anywayAlready there; Fargate is a smaller step
Monthly price$24 + $15 = $39$24.53 server + $2.40 disk + $3.65 fixed address + $11.68 database + $2.30 database disk ≈ $45

Recommendation: EC2 t4g.medium with a 30 GB disk, an Elastic IP, Ubuntu 24.04 (ARM). D-70's core stands: AWS from day one, Bedrock for every model, no Render. Only Lightsail becomes EC2, which is the move D-70 already named as the next step.

If you'd rather keep Lightsail: take the $24 plan (not $12), test CREATE EXTENSION pg_trgm; on the Lightsail database before anything else, and accept the access-key setup in §11.

Things we're deliberately not using, and why:

ServiceWhy not
Elastic BeanstalkWraps EC2 in extra layers to learn; we have one server
ECS / Fargate (containers)The right step when we need two servers or zero-downtime deploys. Not for 5 schools
Lambda (serverless functions)Our background workers run all the time inside the app (docs/04, D-70)
ElastiCache (Redis)Not in our stack: sessions and jobs live in Postgres (docs/04)
OpenSearch (Elasticsearch)Postgres search measured fine at 1 million profiles (docs/25 §6)
A load balancerOne server doesn't need one; Caddy on the box handles HTTPS for free. Costs about $16+/month

5. Decision 3: the database, Postgres on RDS (decided, goes with Decision 2)

Postgres, not DynamoDB. DynamoDB is AWS's own key-value database. It doesn't fit this app at all:

Switching would mean rewriting most of the API.

RDS, not Postgres on the server itself.

Settings:

SettingValueWhy
EnginePostgreSQL 16Matches dev (docker-compose.dev.yml)
Sizedb.t4g.micro (1 GB), single locationPilot size. One click to resize later; the database is offline for a few minutes when you do
Disk20 GB gp3, auto-grow on, up to 100 GB
Public accessNoOnly the server can reach it
Security groupPort 5432 open only to the server's security group
Backups7 days (free up to the database's size)Restore to any minute
Deletion protectionOnStops a mis-click deleting it
EncryptionOn (default)
Database name / userthnkrr / thnkrr (owner, not postgres)citext and pg_trgm are "trusted" extensions: the owner can create them (drizzle/0020:1-3)

Connections: the app holds about 10 pool connections plus about 20 job workers. That's about 30, well inside the micro's limit (about 80–100).

Encrypted connection: RDS Postgres 16 insists on encrypted connections. Our database client passed no certificate, so this was a code change (§13, item 6; built: set DB_SSL_CA_FILE to AWS's certificate bundle).


6. Decision 4: the web app on S3 + CloudFront, not Vercel

Today: two Vercel projects deploy from GitHub. thnkrr-web builds main and thnkrr-demo builds the mock-backend demo. Neither is production, and D-70 puts everything on AWS.

Recommendation:


7. Decision 5: the API at api.thnkrr.com, and the login-cookie fix

Recommendation:

The cookie problem (blocking).

Proxy trust.

Running it: a systemd service starts the app on boot, restarts it if it crashes, and stops it cleanly on deploys. The app already shuts down tidily on that signal (index.ts:52-60). Logs go to the system journal (journalctl -u thnkrr-api).


8. Decision 6: photos and videos

The code already works the way we need. Browsers upload straight to a private "quarantine" bucket with signed links (storage.ts:110). A person approves the item, then the server copies it to the public bucket.

BucketPublic?Settings
thnkrr-media-quarantinePrivate, Block Public Access onCORS: allow PUT from https://thnkrr.com, expose the ETag header (the video uploader reads it, video-upload.ts:88). Lifecycle rule: abort unfinished multipart uploads after 1 day
thnkrr-media-publicBucket private, Block Public Access on; readable only through CloudFront (OAC)CORS: allow GET from https://thnkrr.com (caption files)

Media is served by a second CloudFront distribution at media.thnkrr.com. That address is S3_PUBLIC_URL.

Three code changes (blocking; all built 6 Oct, §13):

  1. Startup. Today, every startup tries to create the buckets and make the public one readable by everyone (storage.ts:77-101). On real AWS, Block Public Access refuses that. The error is caught, so the app starts but media is silently switched off. In production, skip that setup.
  2. Credentials. The storage code only accepts typed-in keys. It should use the server's IAM role instead (§11).
  3. Removal must actually remove. A takedown deletes the S3 file (storage.ts:151), but CloudFront keeps serving its copy for up to 24 hours. Intimate images must be gone in 2 hours (D-75). Fix: send CloudFront an "invalidation" on every takedown (the first 1,000 a month are free), and set a short cache time on media until that ships.

Storage grows every month. Nothing ever deletes originals, and approved videos are stored twice (quarantine + public). At 1,000 students that is about 45–55 GB more each month: about $1 the first month, about $15 a month by month 12. That's cheap, but not the flat $1.15 the old table showed.

A cleanup rule ("delete originals after 30 days") must respect the evidence hold (365 days, D-75), which lives only in the database. So it's code, not an S3 setting. It can wait.


9. Decision 7: email through SES

Recommendation: SES in US East, sending as no-reply@thnkrr.com, through the SMTP connection the code already uses (src/lib/mailer.ts).

Steps (do the first one on day one, because AWS takes about a day to answer):

  1. Ask for production access. In the SES console, choose Account dashboard → Request production access. Pick "Transactional" and give the website URL. Until AWS approves, SES only sends to addresses you've verified, at most 200 a day.
  2. Verify the domain thnkrr.com with Easy DKIM (2048-bit). AWS gives 3 DNS records to add.
  3. Set a custom MAIL FROM of mail.thnkrr.com, so the SPF check lines up with our address.
  4. Add a DMARC record, starting with p=none. Gmail, Yahoo and Microsoft now refuse bulk senders without one.
  5. Create SMTP credentials in the SES console. This makes a small IAM user that can only send email, and you paste its login into SMTP_URL.
  6. Set the addresses: MAIL_FROM, SUPPORT_EMAIL, and OPS_EMAIL (safety and legal-order alerts go there, D-75).

Who gets email: sign-up, parent consent, password reset, school-email proof (D-74), care referrals, and team safety alerts. Profile views are in-app only. Volume stays tiny: about $0.50 a month at 1,000 students.


10. Decisions 8 and 9: the AI checks

Decision 8: the writing check (decided)

Where it stands: the plan is gpt-oss-safeguard-20b on Bedrock in US East (founder page, 5 Oct; region changed 6 Oct). The Bedrock connector is built (6 Oct) and tested against a stand-in for Amazon; it has not yet talked to real Bedrock (Phase C). The Anthropic connector, for laptop testing only (D-70), is now refused in production.

Two things happen whichever option you choose:

The two ways to launch:

A. Build the Bedrock connector first (recommended)B. Deploy now, people check everything
Extra build timeAbout 2 days, plus a test run on real examplesNone
What students seeText posts go live in secondsEvery post waits for a reviewer
Reviewer load at 1,000 studentsOnly unsure items, photos and videosAbout 100,000 writing items a month
Fine forReal schoolsA closed test with staff only

Decided: B for the first deploy, which is a closed test with our own accounts on the real site. A before the first real school.

No OpenAI agreement is needed. On Bedrock, OpenAI's free models are sold and run by AWS. OpenAI isn't a party to our contract and never receives students' writing. AWS may share only usage information that "does not include Your Content" (Service Terms §50.12.5). Bedrock isn't on the list of AWS services that may use content to improve themselves (§50.3).

Model and price:

Decision 9: the picture check, off at first deploy

Rekognition is not in the code yet. Today a person checks every photo and watches every video. That's already built, and it's what docs/18 planned for the pilot.

When we turn it on:


11. Decision 10: keys and passwords

AWS access: no stored keys.

App secrets: generate once, keep twice.

Production settings checklist (the full list, all 77, is .env.example, with the production value beside each):

SettingProduction value
NODE_ENVproduction. This also forces off the dev moderation stub and the demo consent auto-confirm
DATABASE_URLRDS address, with the certificate setting from §13 item 6
WEB_ORIGINhttps://thnkrr.com
COOKIE_DOMAIN (new).thnkrr.com
S3_REGIONus-east-1 (the default)
S3_BUCKET_QUARANTINE, S3_BUCKET_PUBLICThe two bucket names
S3_PUBLIC_URLhttps://media.thnkrr.com
WEB_INDEX_URLhttps://thnkrr.com/index.html
SMTP_URL, MAIL_FROM, SUPPORT_EMAIL, OPS_EMAILFrom §9
WHISPER_MODEL/opt/whisper/ggml-base.bin
SAFETY_HANDLER_IDSThe named safety handlers. Set before the first real student (docs/22). Empty means any moderator can open child-safety cases
ANTHROPIC_API_KEYUnset, and blocked in production by §13 item 3
DB_SSL_CA_FILE (new)/etc/thnkrr/rds-global-bundle.pem (§5)
EDGE_SHARED_SECRET (new)A random value, also set as the X-Thnkrr-Edge header on CloudFront's API routes (§7)
CLOUDFRONT_MEDIA_DISTRIBUTION_ID (new)The media distribution's id, so takedowns purge its copies (§8)
MODERATION_PROVIDER (new)bedrock (the production default). BEDROCK_MODEL_ID stays openai.gpt-oss-safeguard-20b
BEDROCK_KEEPALIVE_MODEL_IDS (new)qwen.qwen3-32b-v1:0, so the backup stays reachable
MENTOR_MESSAGING_ENABLED (new)false until the grooming check is live

12. What it costs now (replaces the 30 September table)

Same assumptions as before: each student, each month, writes about 100 checked items, uploads about 3 photos, and 1 in 5 posts a 1.5-minute video. US East prices, checked 2026-10-06 (EC2 and RDS from AWS's price list).

How AWS charges us, in plain words

AWS bills two kinds of things.

1. Rent: things switched on all the time. You pay for every hour they exist, whether anyone uses Thnkrr or not. This is most of the bill at pilot size.

ThingWhat it isHow it's chargedA month
Server (EC2)The computer the app runs on$0.0336 an hour × 730 hours$24.53
Server's diskIts hard drive, 30 GB$0.08 per GB$2.40
Fixed addressThe server's permanent internet address$0.005 an hour × 730 hours$3.65
Database (RDS)The computer that holds all our data$0.016 an hour × 730 hours$11.68
Database diskIts hard drive, 20 GB$0.115 per GB$2.30
AlarmsSpending and health alertsRoughly~$1
Rent totalabout $45.50, at any number of students

2. Pay per use: grows with students. Pennies at pilot size.

ThingHow it's chargedHow we got the number at 1,000 students
AI writing check (Bedrock)Per piece of text it reads and writes: $0.07 per million "tokens" read, $0.20 per million written (a token is about ¾ of a word)Each check reads about 1,500 tokens and writes about 250, so about $0.000155 a check. 1,000 students × 100 posts, comments and messages = 100,000 checks = $15.50
Email (SES)$0.10 per 1,000 emailsAbout 5,000 emails = $0.50
File storage (S3)$0.023 per GB stored, per monthAbout 50 GB in month 1 = $1.15, rising as photos and videos pile up
Delivery (CloudFront)First 1 TB a month free$0
Picture check (Rekognition), later$1.00 per 1,000 pictures checkedAbout 6,000 checks = $6
Video frame check, laterSame price, one frame per second of video200 videos × 90 s × 2 checks = 36,000 = about $29 (or $6 using the 30-frame contact sheet)

So: rent ≈ $45 a month no matter what, and usage adds about $0.02 per student a month for the writing check, or about $0.07 with every check on.

What20 students100 students1,000 studentsNotes
Server (EC2 t4g.medium $24.53 + 30 GB disk $2.40 + fixed address $3.65)$30.58$30.58$30.58Lightsail $24 plan instead: $24
Database (RDS micro $11.68 + 20 GB $2.30)$13.98$13.98$13.98Lightsail database: $15
AI writing check (Bedrock gpt-oss-safeguard-20b, ~$0.155 per 1,000)$0.31$1.55$15.50Estimate; measure in week 1
Email (SES)$0.01$0.05$0.50Same everywhere
Storage (S3), month 1 → month 12$0.03 → $0.28$0.12 → $1.38$1.15 → $13.80Grows every month (§8)
Delivery (CloudFront)$0$0$0Free up to 1 TB a month
Alarms and monitoring~$1~$1~$1
Total at first deploy, month 1about $46about $47about $63All AWS credits
Later: photo check (Rekognition, $1.00 per 1,000)+$0.12+$0.60+$6.00When switched on
Later: video check (Rekognition, 1 frame/second)+$0.58+$2.88+$28.80Contact-sheet way: +$0.12 / +$0.60 / +$6.00
Everything on, month 12about $47about $52about $110Contact-sheet video: about $87 at 1,000
Per student, everything onabout $2.35about $0.52about $0.11

What changed since 30 September, and why:

  1. Prices are US East's, where the pilot schools are.
  2. The server costs more than the old $12 plan.
    • The $12 server is too small once Whisper and video run on it.
    • Either way it's about $40–45: Lightsail $24 + $15, or EC2 + RDS $45.
  3. The writing check costs about twice the old estimate.
    • The real rules prompt is longer, and comments and messages carry context.
    • The model bills its reasoning.
    • It's still about 1.5¢ per student a month.
  4. Storage grows every month instead of staying flat, because nothing is cleaned up yet.
  5. Picture checks are off at first deploy, because they aren't built yet. They're priced as a later line.
  6. Reviewer time: the old 60 hours a month assumed the AI check exists.
    • Until the Bedrock connector ships, a person checks everything: about 100,000 writing items a month at 1,000 students.
    • That's why Decision 8 says build it before the first real school.

The credits:


13. Code changes before the first deploy

These are ours to build (they are not founder decisions). In order of importance. Status, 6 October: all built on feat/deploy-readiness (both repos) with tests; merge pending. "Needs real AWS" means it can only be proven on the live account.

#ChangeWhy it blocksStatus
1COOKIE_DOMAIN settingWithout it, every save fails on the real site (§7)Built. Set .thnkrr.com
2Production-safe storage: no bucket creation or public policy at startup; use the server's role, not typed-in keysMedia is silently off otherwise (§8)Built. Startup only checks the buckets exist. Needs real AWS: the role reaching both buckets
3Block the Anthropic connector in productionA stray key would send students' writing outside AWS (D-70)Built. Asking for it in production stops the server starting; a stray key is ignored
4CloudFront purge on takedown, plus a short media cache timeRemoved images stay visible up to 24h; the legal clock is 2h (D-75)Built. Every takedown purges CloudFront; files cache 5 minutes. Needs real AWS: watch a removed photo's link die
5Production guard on db:seedOn an empty production database it creates staff and moderator accounts with a known passwordBuilt. Refuses production, any RDS address, and the RDS certificate setting
6Database certificate: the RDS certificate bundleRDS requires encrypted connectionsBuilt (DB_SSL_CA_FILE). Needs real AWS: the first db:migrate
7Visitor address trusted only from Caddy and CloudFrontAnyone can fake their address and dodge the login rate limitBuilt. CloudFront counts only with its secret header
8Fix the school-page redirect loopIf the page shell can't be fetched, the browser bounces between CloudFront and the API foreverBuilt. Profile links too: a "refresh in a moment" page, never a redirect
9Deploy files in the repo: systemd unit, Caddyfile, scripts/deploy.sh, a complete .env.exampleNothing to deploy with todayBuilt. deploy/README.md is the server runbook; the deploy script rolls back if the new code doesn't answer
10Mentor-messaging off switchdocs/18 says mentor messaging stays off until grooming detection is liveBuilt, off by default. Threads read-only, no request notes; students see a plain notice
11The Bedrock connector (Decision 8, option A)Needed before the first real school, not before the first deployBuilt. Model name stamped on every verdict; daily keep-alive check. Needs real AWS: the first real check (Phase C)
12Checking at Amazon's speed: throttle at 150 a second, 300 at once, messages and possible self-harm first, a per-person posting limit, "checking is busy — about N min"12 at once took 28–83 minutes for 10,000 postsBuilt. Measured with a stand-in AI taking 2–4 s: 10,000 posts cleared in 103 s (500 took 127 s at the old 12)
13Phone numbers written in words ("nine eight seven six…", some Hindi)A known gap in our rulesBuilt
14Four ideas from Discord's Osprey: an editable word list (pnpm moderation:terms), expiring account labels, a plain reason on each rule decision, watch-only rulesRule changes without a release; new rules prove themselves firstBuilt, all in Postgres (migration 0025)
15Complaint links in the footer (/grievance, /remove-intimate-image)India's law wants the complaint route findable; nothing linked to itBuilt (web): every page, signed in or out

Can wait until after the first deploy:


14. The first deploy, in order

The server half of the runbook is deploy/README.md (code item 9). The order matters, so here it is:

Day 0: the account (about 1 hour, you)

  1. Sign in as root. Turn on MFA (an authenticator app).
  2. In IAM Identity Center, create your own admin login. From now on, use only that.
  3. In Billing, check the credits: expiry date and applicable products.
  4. In Budgets, set alerts at $50, $100 and $200 a month. Turn on Cost Anomaly Detection.
  5. Switch the console's region (top right) to US East (N. Virginia).
  6. In SES, request production access (it takes about a day).

Day 1: the domain and certificates (needs access to thnkrr.com's DNS)

  1. In ACM, in us-east-1, request a certificate for thnkrr.com, www.thnkrr.com and media.thnkrr.com. Add the DNS records it shows.
  2. In SES (US East), verify thnkrr.com with DKIM, the custom MAIL FROM, and DMARC. These are DNS records too.

Day 1–2: the machines

  1. Security groups:

    • thnkrr-api: open 80 and 443 to everyone. No port 22.
    • thnkrr-db: open 5432 only to thnkrr-api.
  2. RDS: PostgreSQL 16, db.t4g.micro, with the §5 settings.

  3. IAM role thnkrr-api-server with the §11 permissions.

  4. EC2: t4g.medium, Ubuntu 24.04 ARM, 30 GB disk, the role and security group. Then attach an Elastic IP.

  5. DNS: api.thnkrr.com → the Elastic IP.

  6. Session Manager into the server. Install:

    • Node 22 and pnpm
    • ffmpeg
    • whisper.cpp, plus the ggml-base.bin model
    • Caddy

    Create the thnkrr user and /etc/thnkrr/api.env.

  7. Clone the API (dev/pilot) and run pnpm install, pnpm build and pnpm db:migrate. Never db:seed. Then start the service and check that https://api.thnkrr.com/health answers.

Day 2: the web and media

  1. Buckets: the web bucket and the two media buckets, with the §8 settings.
  2. CloudFront:
    • The media distribution at media.thnkrr.com.
    • The web distribution at thnkrr.com, with the §6 routes and the app-route function.
  3. The web: build with VITE_API_URL, upload dist/ to the web bucket, and invalidate /index.html.
  4. DNS: point thnkrr.com and www at the web distribution, and media.thnkrr.com at the media distribution.

Day 2–3: prove it

  1. Run the smoke test from MODERATION-PLAN §6:
    • Sign up a test student. The consent email should arrive.
    • Post text, a photo and a video.
    • Approve them as a reviewer.
    • Take one down. It should vanish from media.thnkrr.com within minutes.
    • Open a /in/ link while signed out.
  2. Create the first real staff accounts with the ops scripts (pnpm school:page …). Set SAFETY_HANDLER_IDS.

Every later deploy:

  1. Take an RDS snapshot if there's a migration.
  2. Run scripts/deploy.sh: it pulls, installs, builds, migrates and restarts.
  3. For the web: build, upload and invalidate.
  4. A restart can cut off a video being converted; it retries once (video-service.ts:133).

15. Still open

16. When the database grows: 1 million messages and beyond

Status: a plan only. Nothing here is built. It waits for the CTO's go-ahead, and it isn't needed for the first deploy.

How much space messages take

We tested it: 20,000 realistic chat messages, each with its AI check, loaded into a test database and measured.

What gets saved for one chat messageSpace
The check record (the AI's answer, a copy of the text, the reason)681 bytes
A log line ("the automatic check approved this")288 bytes
The message itself281 bytes
The "you have a new message" notification177 bytes
Totalabout 1.4 KB

Two-thirds of that is moderation records, not the message.

Real use takes about 1.5 to 2 times the test figure, because records get updated and leave gaps behind:

MessagesDatabase sizeStorage cost a month
1 million1.5–3 GBabout $0.35
10 million15–30 GBabout $3.50
100 million150–300 GBabout $35, plus a bigger database computer

So space and money aren't the problem. Speed is: once the data no longer fits in the database computer's memory (1 GB on our plan), loading the review list and searching get slower. Deleting millions of old rows one at a time is also slow, and leaves the tables bloated.

How we'd keep it fast

  1. Split big tables by month. Messages, check records, log lines and notifications each become one small table per month, under one name.
    • Recent months stay quick to search.
    • An old month can be removed in one step, instead of deleting millions of rows.
    • Postgres does this itself ("partitioning"), and Amazon's database supports the add-on that runs it automatically (pg_partman).
  2. Stop saving things twice.
    • The check record keeps its own copy of the message text, and the AI's reason is saved twice.
    • A set number of days after a message is approved, the copy can go: the message itself is the original.
    • This roughly halves the moderation records.
  3. Clear out what has no reason to stay. For example, delete notifications after 90 days, as LinkedIn does.
  4. Grow the database computer in steps.
    • Move from today's smallest size to bigger ones. Each move is one click and a few minutes offline.
    • Add a read-only copy for search.
    • Add a connection manager once there's more than one app server.

Moving old records out of the database

Yes, old records can be moved out of the database and kept as files, much more cheaply:

How oldWhere it's keptCost per GB a month
Recent monthsThe database$0.115
OlderCompressed files in Amazon's file storage (S3). They can still be searched when needed, without loading them back, using Amazon Athena (pay per search)$0.023
Rarely neededS3's cheapest long-term tier (Glacier Deep Archive)about $0.001

Each month, a job copies that month out to files, checks the copy, then removes the month from the database. Amazon's database can do this export itself (the aws_s3 add-on), or export a whole backup to S3.

What the law says we must keep:

RecordKept for at least
Anything linked to a safety case1 year, and never deletable by a reviewer (already built)
Content we removed180 days (Indian rule)
Intimate-image removal requests3 years

These go to a locked storage area (S3 "Object Lock") that nobody can edit or delete early.

What the law says we must not keep too long: India's data law expects children's data to be deleted once it has served its purpose, or when consent is withdrawn. Archived files make deleting harder, so every archive gets a fixed end date.

What we need before building it

Sources: AWS pricing pages and AWS's published price lists for EC2, RDS, Lightsail, Bedrock, Rekognition, SES, S3 and CloudFront (fetched 2026-10-06); Lightsail CPU-baseline and IAM docs; Bedrock model cards for gpt-oss-safeguard-20b/120b and Qwen3 32B (in-region only); AWS Price List (RDS, us-east-1 and ap-south-1) and ec2.shop (EC2 t4g.medium, both regions), fetched 2026-10-06; AWS Service Terms §50; DPDP Act §16 / Rules 2025; CERT-In directions 2022; Rekognition quotas page; SES production-access and bulk-sender guidance; AWS promotional credit and Activate terms (2026-01-22); EC2 Unlimited-mode docs. Code references are to rehearsal/2026-10-06.

●  Have your own?

// Publish your own markdown.

Write your own
Thnkrr: AWS Deployment Decisions · DocHouse