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
| # | Decision | Recommendation | Status |
|---|---|---|---|
| 1 | Where everything runs | US East, N. Virginia (us-east-1), certificate included | Decided 2026-10-06: the pilot schools are in the US |
| 2 | The server | EC2 t4g.medium (2 processors, 4 GB memory), not Lightsail | Decided 2026-10-06 (replaces D-70's Lightsail shape; §4) |
| 3 | The database | Postgres on RDS (db.t4g.micro), not DynamoDB, not on the server itself | Decided 2026-10-06 (goes with #2) |
| 4 | The web app | S3 + CloudFront at thnkrr.com, not Vercel | Decided |
| 5 | The API's address | api.thnkrr.com, straight to the server, HTTPS by Caddy | Decided |
| 6 | Photos and videos | Two private S3 buckets; approved media served by CloudFront at media.thnkrr.com | Decided |
| 7 | Amazon SES in US East on our own domain; ask AWS for production access on day one | Decided | |
| 8 | AI writing check | Bedrock, 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 |
| 9 | AI picture check | Off at first deploy; people check every photo and video (already built) | Decided |
| 10 | App keys and passwords | No stored AWS keys on the server (EC2 gets a role); app secrets in one locked file + a password manager | Decided |
| 11 | Getting code onto the server | One deploy script run over Session Manager; no SSH keys, no open port 22 | Decided |
| 12 | Backups | RDS daily backups, 7-day restore, plus a manual snapshot before every migration | Decided |
| 13 | Spending safety | Budgets alerts at $50 / $100 / $200, anomaly alerts, credits checked | Do 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 account | Yes, start today. Phase A needs no code |
| Putting the app on AWS for a closed staff test | Once the code changes are merged. They're built (§13); then Phase B |
| Real students | Not 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)
- A1. Sign in as root. Turn on MFA (an authenticator app). (§14 step 1)
- A2. In IAM Identity Center, create your own admin login. Never use root again. (§14 step 2)
- A3. Billing → Credits: write down the expiry date and the applicable products. Check that EC2, RDS, S3, CloudFront, SES, Bedrock and Rekognition are covered. (§12)
- A4. Budgets: alerts at $50 / $100 / $200 a month. Turn on Cost Anomaly Detection. (§14 step 4)
- A5. Set the console region (top right) to US East (N. Virginia).
- A6. Account → Alternate contacts: add billing, operations and security contacts, so Amazon's model-retirement notices reach a person.
- A7. SES → Request production access ("Transactional", website URL). AWS takes about a day. (§9)
- A8. Bedrock data retention → "none" for us-east-1, so no request or response data is kept. There's no console button for this yet, so it's one command in CloudShell (the terminal built into the console):
curl -X PUT https://bedrock.us-east-1.amazonaws.com/data-retention -H "Authorization: Bearer $AWS_BEARER_TOKEN_BEDROCK" -H "Content-Type: application/json" -d '{"mode":"none"}'Make a Bedrock API key for it under Bedrock → API keys, then delete the key afterwards. Leave model invocation logging off (the default). (§10) - A9. AWS Organizations (free; turn it on if it's off) → Policies → AI services opt-out policy for all services. This stops Amazon using our pictures to improve its own AI, and must be on before any picture is ever checked. (§10)
- A10. Service Quotas → write down our real limits for
openai.gpt-oss-safeguard-20b(requests and tokens per minute) and for Rekognition DetectModerationLabels. - A11. Confirm the domain: is it
thnkrr.com, and where is its DNS managed? Phase B needs DNS access.
Phase B: the closed staff test on AWS (after the team's code changes)
- Team: the 10 code changes in §13 (items 1–10). Built and tested 6 Oct; merge pending (PRs on
feat/deploy-readiness). - B1. ACM certificate in us-east-1 for
thnkrr.com,wwwandmedia; add its DNS records. (§14 step 7) - B2. SES: verify
thnkrr.com(DKIM), custom MAIL FROM, DMARC. (§9) - B3. Security groups, then RDS, the IAM role and the EC2 server, in that order. (§14 steps 9–13)
- B4. On the server: Node 22, ffmpeg, whisper.cpp with its model, and Caddy. Then deploy, migrate and check
/health. Never rundb:seedthere. (§14 steps 14–15) - B5. Buckets, both CloudFront distributions, the web build and DNS. (§14 steps 16–19)
- B6. Smoke test (§14 step 20, and MODERATION-PLAN §6):
- sign up a test student and see the consent email arrive;
- post text, a photo and a video;
- approve them as a reviewer;
- take a photo down and see its link die within minutes;
- open an
/in/link while signed out; - file a test intimate-image request and check the alert email arrives.
- B7. Two staff on two laptops review the same item at once: one decision, and a clear "already done by" message.
Phase C: before the first real school
- Team: the Bedrock connector (§13 item 11). Built 6 Oct, tested only against a stand-in for Amazon. Then, on the real account: the first real check, a week of speed measurements and the test-set comparison (MODERATION-PLAN §6).
- Team: raise parallel checks to the measured safe level, ramp-tested at 50 → 150 → 300.
- You: set
SAFETY_HANDLER_IDSto Vidhi and the second handler, andOPS_EMAILto the team inbox. (§11) - Founder: the names, NCMEC registration, the child-abuse list applications, and the Community Guidelines text (MODERATION-PLAN §5).
- You: the 10,000-post load test. (MODERATION-PLAN §6)
2. AWS words used in this doc
| Word | What it means |
|---|---|
| Region | A group of AWS data centres in one area. We use US East, N. Virginia (us-east-1) |
| Account / root user | The 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 |
| IAM | AWS'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 Center | Where you create your own day-to-day login (instead of using root) |
| EC2 | A rented virtual computer. t4g.medium is one size: 2 processors, 4 GB memory, Amazon's own ARM chip (cheaper) |
| Burstable / CPU credits / Unlimited mode | Cheap 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 |
| EBS | The server's hard disk |
| Elastic IP | A fixed public internet address for the server, so api.thnkrr.com keeps pointing at it after restarts |
| Security group | The server's firewall: which ports are open, and to whom |
| RDS | AWS-run Postgres. AWS patches it, backs it up, and can restore it to any minute in the last 7 days |
| S3 / bucket | File storage. A bucket is one named storage area |
| CloudFront / distribution | AWS'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 |
| ACM | Free HTTPS certificates for CloudFront. Must be requested in us-east-1 — where everything else is too |
| Route 53 | AWS's DNS (the internet's address book). Optional: the domain's current DNS provider works too |
| SES / sandbox | AWS'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 |
| Bedrock | AWS's service for running AI models we pay per use; the model runs inside AWS, in US East |
| Rekognition | AWS's picture-checking service (nudity, violence, text in images) |
| Session Manager | A browser terminal into the server through the AWS console, with no SSH keys and no open port |
| Budgets / Cost Anomaly Detection | Email 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:
- The pilot schools are in the US. US East is the AWS location closest to them.
- No law requires a particular country. US student-privacy law has no location rule (we'll still check each school's agreement). India's data law allows storing abroad; its cross-border rules start 13 May 2027.
- The AI still runs in-region. gpt-oss-safeguard-20b, its 120b sibling and Qwen3 32B all run inside US East, so a check never leaves it.
- The retirement plan has the most room here. US East has the most Bedrock models, and it's where Bedrock lets us import our own copy of gpt-oss, the last-resort backup.
How the regions compare. We compared US East with Mumbai, the other location we'd use once Indian schools join:
| US East | Mumbai | |
|---|---|---|
| Picture checks per second (Rekognition default) | 50 | 5 |
| 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.023 | about $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 + Whisper | t4g.medium: 2 processors, 4 GB |
| When video work runs long | Slows 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 together | Unlimited mode (on by default): keeps full speed; any sustained extra costs $0.04 per processor-hour |
| AWS permissions | Cannot use IAM roles, so the server must hold long-lived access keys for S3, SES, Bedrock and Rekognition | Wears an IAM role: no keys stored anywhere |
| Database | Lightsail database: AWS doesn't document whether it allows pg_trgm, which search needs (docs/25) | RDS: pg_trgm and citext documented as supported |
| Firewall | Simple port list | Security groups: the database accepts only the server |
| Ease for a first-timer | Fewer screens | More screens; §14 gives the exact order |
| Next step when we outgrow it (D-70) | Move to EC2/Fargate + RDS anyway | Already 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:
| Service | Why not |
|---|---|
| Elastic Beanstalk | Wraps 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 balancer | One 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:
- It has no joins. Feeds, follows, schools, consent and moderation all link tables together.
- It has no text search. Search uses Postgres's trigram matching (docs/25).
- Our job queue (pg-boss) is built on Postgres. So are the per-item reviewer locks (D-75).
- It has no migrations, and it locks us into AWS. The 10 Sep rule bans DynamoDB for that reason.
Switching would mean rewriting most of the API.
RDS, not Postgres on the server itself.
- Running Postgres on the server saves about $18 a month. But then we own backups and upgrades.
- The video converter and Whisper would also compete with the database for the same 2 processors.
- For minors' data, RDS's automatic backups and 7-day restore-to-any-minute are worth $18.
Settings:
| Setting | Value | Why |
|---|---|---|
| Engine | PostgreSQL 16 | Matches dev (docker-compose.dev.yml) |
| Size | db.t4g.micro (1 GB), single location | Pilot size. One click to resize later; the database is offline for a few minutes when you do |
| Disk | 20 GB gp3, auto-grow on, up to 100 GB | |
| Public access | No | Only the server can reach it |
| Security group | Port 5432 open only to the server's security group | |
| Backups | 7 days (free up to the database's size) | Restore to any minute |
| Deletion protection | On | Stops a mis-click deleting it |
| Encryption | On (default) | |
| Database name / user | thnkrr / 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:
-
Hosting: the built web app (
vite build→dist/) goes in a private S3 bucket, served by one CloudFront distribution atthnkrr.com(andwww). OAC lets CloudFront read the bucket. -
CloudFront routes:
Path Goes to Caching /in/*(public profile links)The API at api.thnkrr.comOff (docs/25 §link previews) /school/*(school pages)The API at api.thnkrr.comOff (docs/26) Everything else The web bucket. A small CloudFront Function sends app routes like /feedtoindex.htmlNormal; index.htmlitself shortDon't use CloudFront's "custom error pages" for the app routes. They would also rewrite the API's own 404s on
/in/*. -
Build:
VITE_API_URL=https://api.thnkrr.com pnpm build. The build fails without it (src/lib/api.ts:7-10). -
Vercel: archive or disconnect the
thnkrr-webproject so nobody mistakes it for the live site. Keepthnkrr-demoif the founder still uses the demo.
7. Decision 5: the API at api.thnkrr.com, and the login-cookie fix
Recommendation:
- Address:
api.thnkrr.compoints at the server's Elastic IP. - On the server: Caddy gets and renews the HTTPS certificate on its own, and passes traffic to the API on
127.0.0.1:3001. - Open ports: only 80 and 443. Port 3001 is never open to the internet.
The cookie problem (blocking).
- The setup: the web page lives at
thnkrr.comand the API atapi.thnkrr.com. - What breaks: the API sets its login and anti-forgery cookies without a domain (
auth-plugin.ts:18-27), so they belong toapi.thnkrr.comonly. The web app has to read the anti-forgery cookie and send it back (src/lib/api.ts:28), and it can't see a cookie that belongs to another address. Every post, comment, save or setting change would fail with "CSRF check failed". - Why dev never showed it: locally both run on
localhost, and cookies ignore the port. - Fix: a
COOKIE_DOMAIN=.thnkrr.comsetting, which docs/04 always intended (docs/04:33, :375).
Proxy trust.
- The problem:
trustProxy: true(server.ts:33) believes any visitor's claimed address. Rate limits and the hashed addresses on consent records depend on that address. - Fix: trust only Caddy (
127.0.0.1), and CloudFront for the/in/*and/school/*paths (it adds a secret header we check).
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.
| Bucket | Public? | Settings |
|---|---|---|
thnkrr-media-quarantine | Private, Block Public Access on | CORS: 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-public | Bucket 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):
- 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. - Credentials. The storage code only accepts typed-in keys. It should use the server's IAM role instead (§11).
- 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):
- 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.
- Verify the domain
thnkrr.comwith Easy DKIM (2048-bit). AWS gives 3 DNS records to add. - Set a custom MAIL FROM of
mail.thnkrr.com, so the SPF check lines up with our address. - Add a DMARC record, starting with
p=none. Gmail, Yahoo and Microsoft now refuse bulk senders without one. - 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. - Set the addresses:
MAIL_FROM,SUPPORT_EMAIL, andOPS_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:
- Block the Anthropic connector in production (blocking, §13 item 3).
- The safety net already works. If no AI answers, every item waits for a person. Nothing goes live unchecked.
The two ways to launch:
| A. Build the Bedrock connector first (recommended) | B. Deploy now, people check everything | |
|---|---|---|
| Extra build time | About 2 days, plus a test run on real examples | None |
| What students see | Text posts go live in seconds | Every post waits for a reviewer |
| Reviewer load at 1,000 students | Only unsure items, photos and videos | About 100,000 writing items a month |
| Fine for | Real schools | A 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:
- Model:
openai.gpt-oss-safeguard-20binus-east-1. Backups: Qwen3 32B (qwen.qwen3-32b-v1:0), then the 120b. - Price: $0.07 per million tokens read and $0.20 per million tokens written in US East (a token is about three-quarters of a word). Our rules prompt is about 900 tokens, and comments and messages carry recent context. gpt-oss also bills its thinking as output. So about 1,500 tokens read and 250 written per check comes to about $0.155 per 1,000 checks, not the $0.07 in the old table. It's still small: about $15.50 a month at 1,000 students. Measure it in the first week.
- Speed limit: 10,000 checks a minute on this model, the same in every region (MODERATION-PLAN §2).
- No access request needed: since October 2025 Bedrock switches non-Anthropic models on automatically.
- Leave Bedrock's "model invocation logging" off. That's the default, and it keeps the "keep nothing" promise on the founder page.
- Retirement: AWS says the model can't be retired before 29 Oct 2026. Its notices go to the account email, so the daily check on the founder page still applies.
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:
- Price: $1.00 per 1,000 pictures, with 2 checks per photo: one for nudity and violence, one for text in the image.
- Video: run the check on the 30-frame contact sheet the code already makes, about 3¢ a video. Checking one frame every second is about 14¢ for a 1.5-minute video, and the cost grows with the length (the limit is still 15 minutes).
- Speed: 50 checks a second by default. That's 3,000 a minute; ask for more only if photo volume needs it.
11. Decision 10: keys and passwords
AWS access: no stored keys.
- The server wears an IAM role named
thnkrr-api-server. It is allowed only:- read/write on the 2 media buckets
bedrock:InvokeModelon the chosen model IDscloudfront:CreateInvalidationon the media distributionrekognition:Detect*, added later- Session Manager (
AmazonSSMManagedInstanceCore)
- Code change (built): the storage code no longer needs
S3_ACCESS_KEY/S3_SECRET_KEY; AWS's SDK picks up the role on its own (§13 item 2). - Exception: SES SMTP needs its own small login (§9 step 5). It can only send email.
App secrets: generate once, keep twice.
COOKIE_SECRET,TOKEN_SECRET: random, 32+ characters each.DATA_ENCRYPTION_KEY: 32 random bytes in base64. If this is lost, the encrypted fields in the database can never be read again. Save it in a password manager the day it's made.- They live in
/etc/thnkrr/api.envon the server, readable only by the app's own user (chmod 600), never in git.
Production settings checklist (the full list, all 77, is .env.example, with the
production value beside each):
| Setting | Production value |
|---|---|
NODE_ENV | production. This also forces off the dev moderation stub and the demo consent auto-confirm |
DATABASE_URL | RDS address, with the certificate setting from §13 item 6 |
WEB_ORIGIN | https://thnkrr.com |
COOKIE_DOMAIN (new) | .thnkrr.com |
S3_REGION | us-east-1 (the default) |
S3_BUCKET_QUARANTINE, S3_BUCKET_PUBLIC | The two bucket names |
S3_PUBLIC_URL | https://media.thnkrr.com |
WEB_INDEX_URL | https://thnkrr.com/index.html |
SMTP_URL, MAIL_FROM, SUPPORT_EMAIL, OPS_EMAIL | From §9 |
WHISPER_MODEL | /opt/whisper/ggml-base.bin |
SAFETY_HANDLER_IDS | The named safety handlers. Set before the first real student (docs/22). Empty means any moderator can open child-safety cases |
ANTHROPIC_API_KEY | Unset, 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.
| Thing | What it is | How it's charged | A month |
|---|---|---|---|
| Server (EC2) | The computer the app runs on | $0.0336 an hour × 730 hours | $24.53 |
| Server's disk | Its hard drive, 30 GB | $0.08 per GB | $2.40 |
| Fixed address | The 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 disk | Its hard drive, 20 GB | $0.115 per GB | $2.30 |
| Alarms | Spending and health alerts | Roughly | ~$1 |
| Rent total | about $45.50, at any number of students |
2. Pay per use: grows with students. Pennies at pilot size.
| Thing | How it's charged | How 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 emails | About 5,000 emails = $0.50 |
| File storage (S3) | $0.023 per GB stored, per month | About 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 checked | About 6,000 checks = $6 |
| Video frame check, later | Same price, one frame per second of video | 200 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.
| What | 20 students | 100 students | 1,000 students | Notes |
|---|---|---|---|---|
| Server (EC2 t4g.medium $24.53 + 30 GB disk $2.40 + fixed address $3.65) | $30.58 | $30.58 | $30.58 | Lightsail $24 plan instead: $24 |
| Database (RDS micro $11.68 + 20 GB $2.30) | $13.98 | $13.98 | $13.98 | Lightsail database: $15 |
| AI writing check (Bedrock gpt-oss-safeguard-20b, ~$0.155 per 1,000) | $0.31 | $1.55 | $15.50 | Estimate; measure in week 1 |
| Email (SES) | $0.01 | $0.05 | $0.50 | Same everywhere |
| Storage (S3), month 1 → month 12 | $0.03 → $0.28 | $0.12 → $1.38 | $1.15 → $13.80 | Grows every month (§8) |
| Delivery (CloudFront) | $0 | $0 | $0 | Free up to 1 TB a month |
| Alarms and monitoring | ~$1 | ~$1 | ~$1 | |
| Total at first deploy, month 1 | about $46 | about $47 | about $63 | All AWS credits |
| Later: photo check (Rekognition, $1.00 per 1,000) | +$0.12 | +$0.60 | +$6.00 | When switched on |
| Later: video check (Rekognition, 1 frame/second) | +$0.58 | +$2.88 | +$28.80 | Contact-sheet way: +$0.12 / +$0.60 / +$6.00 |
| Everything on, month 12 | about $47 | about $52 | about $110 | Contact-sheet video: about $87 at 1,000 |
| Per student, everything on | about $2.35 | about $0.52 | about $0.11 |
What changed since 30 September, and why:
- Prices are US East's, where the pilot schools are.
- 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.
- 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.
- Storage grows every month instead of staying flat, because nothing is cleaned up yet.
- Picture checks are off at first deploy, because they aren't built yet. They're priced as a later line.
- 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:
- What's covered: AWS's credit terms exclude Marketplace purchases, domain registration and support plans. EC2, RDS, S3, CloudFront, SES and Rekognition are not excluded.
- Bedrock: gpt-oss and Qwen are billed by AWS directly, according to AWS's own price list. The 2026 Activate terms also cover third-party Bedrock models. Confirm on the first bill.
- Check before deploying: open Billing → Credits and read the credits' expiry date and "applicable products". D-70 left this open.
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.
| # | Change | Why it blocks | Status |
|---|---|---|---|
| 1 | COOKIE_DOMAIN setting | Without it, every save fails on the real site (§7) | Built. Set .thnkrr.com |
| 2 | Production-safe storage: no bucket creation or public policy at startup; use the server's role, not typed-in keys | Media is silently off otherwise (§8) | Built. Startup only checks the buckets exist. Needs real AWS: the role reaching both buckets |
| 3 | Block the Anthropic connector in production | A 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 |
| 4 | CloudFront purge on takedown, plus a short media cache time | Removed 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 |
| 5 | Production guard on db:seed | On an empty production database it creates staff and moderator accounts with a known password | Built. Refuses production, any RDS address, and the RDS certificate setting |
| 6 | Database certificate: the RDS certificate bundle | RDS requires encrypted connections | Built (DB_SSL_CA_FILE). Needs real AWS: the first db:migrate |
| 7 | Visitor address trusted only from Caddy and CloudFront | Anyone can fake their address and dodge the login rate limit | Built. CloudFront counts only with its secret header |
| 8 | Fix the school-page redirect loop | If the page shell can't be fetched, the browser bounces between CloudFront and the API forever | Built. Profile links too: a "refresh in a moment" page, never a redirect |
| 9 | Deploy files in the repo: systemd unit, Caddyfile, scripts/deploy.sh, a complete .env.example | Nothing to deploy with today | Built. deploy/README.md is the server runbook; the deploy script rolls back if the new code doesn't answer |
| 10 | Mentor-messaging off switch | docs/18 says mentor messaging stays off until grooming detection is live | Built, off by default. Threads read-only, no request notes; students see a plain notice |
| 11 | The Bedrock connector (Decision 8, option A) | Needed before the first real school, not before the first deploy | Built. Model name stamped on every verdict; daily keep-alive check. Needs real AWS: the first real check (Phase C) |
| 12 | Checking 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 posts | Built. 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) |
| 13 | Phone numbers written in words ("nine eight seven six…", some Hindi) | A known gap in our rules | Built |
| 14 | Four ideas from Discord's Osprey: an editable word list (pnpm moderation:terms), expiring account labels, a plain reason on each rule decision, watch-only rules | Rule changes without a release; new rules prove themselves first | Built, all in Postgres (migration 0025) |
| 15 | Complaint links in the footer (/grievance, /remove-intimate-image) | India's law wants the complaint route findable; nothing linked to it | Built (web): every page, signed in or out |
Can wait until after the first deploy:
- Security headers (CSP, HSTS)
- A nightly
pg_dumpto S3 (RDS backups cover us at first) - Shipping logs to CloudWatch
- Storage cleanup rules
- Rekognition
- Child-abuse image matching (needs the founder's applications, docs/22)
- Per-account login lockout
- A separate video server
- A search read-replica
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)
- Sign in as root. Turn on MFA (an authenticator app).
- In IAM Identity Center, create your own admin login. From now on, use only that.
- In Billing, check the credits: expiry date and applicable products.
- In Budgets, set alerts at $50, $100 and $200 a month. Turn on Cost Anomaly Detection.
- Switch the console's region (top right) to US East (N. Virginia).
- In SES, request production access (it takes about a day).
Day 1: the domain and certificates (needs access to thnkrr.com's DNS)
- In ACM, in us-east-1, request a certificate for
thnkrr.com,www.thnkrr.comandmedia.thnkrr.com. Add the DNS records it shows. - In SES (US East), verify
thnkrr.comwith DKIM, the custom MAIL FROM, and DMARC. These are DNS records too.
Day 1–2: the machines
-
Security groups:
thnkrr-api: open 80 and 443 to everyone. No port 22.thnkrr-db: open 5432 only tothnkrr-api.
-
RDS: PostgreSQL 16, db.t4g.micro, with the §5 settings.
-
IAM role
thnkrr-api-serverwith the §11 permissions. -
EC2: t4g.medium, Ubuntu 24.04 ARM, 30 GB disk, the role and security group. Then attach an Elastic IP.
-
DNS:
api.thnkrr.com→ the Elastic IP. -
Session Manager into the server. Install:
- Node 22 and pnpm
- ffmpeg
- whisper.cpp, plus the
ggml-base.binmodel - Caddy
Create the
thnkrruser and/etc/thnkrr/api.env. -
Clone the API (dev/pilot) and run
pnpm install,pnpm buildandpnpm db:migrate. Neverdb:seed. Then start the service and check thathttps://api.thnkrr.com/healthanswers.
Day 2: the web and media
- Buckets: the web bucket and the two media buckets, with the §8 settings.
- CloudFront:
- The media distribution at
media.thnkrr.com. - The web distribution at
thnkrr.com, with the §6 routes and the app-route function.
- The media distribution at
- The web: build with
VITE_API_URL, uploaddist/to the web bucket, and invalidate/index.html. - DNS: point
thnkrr.comandwwwat the web distribution, andmedia.thnkrr.comat the media distribution.
Day 2–3: prove it
- 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.comwithin minutes. - Open a
/in/link while signed out.
- Create the first real staff accounts with the ops scripts (
pnpm school:page…). SetSAFETY_HANDLER_IDS.
Every later deploy:
- Take an RDS snapshot if there's a migration.
- Run
scripts/deploy.sh: it pulls, installs, builds, migrates and restarts. - For the web: build, upload and invalidate.
- A restart can cut off a video being converted; it retries once (
video-service.ts:133).
15. Still open
- The domain: is it
thnkrr.com, and who controls its DNS? (Phase A11) - The credits: expiry date and applicable products. (Phase A3)
- Mentors in the pilot? The mentor-messaging off switch (code item 10) is built and off by default: mentorships work, but student–mentor messages and request notes stay off until the grooming check is live.
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 message | Space |
|---|---|
| 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 itself | 281 bytes |
| The "you have a new message" notification | 177 bytes |
| Total | about 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:
| Messages | Database size | Storage cost a month |
|---|---|---|
| 1 million | 1.5–3 GB | about $0.35 |
| 10 million | 15–30 GB | about $3.50 |
| 100 million | 150–300 GB | about $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
- 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).
- 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.
- Clear out what has no reason to stay. For example, delete notifications after 90 days, as LinkedIn does.
- 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 old | Where it's kept | Cost per GB a month |
|---|---|---|
| Recent months | The database | $0.115 |
| Older | Compressed 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 needed | S3'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:
| Record | Kept for at least |
|---|---|
| Anything linked to a safety case | 1 year, and never deletable by a reviewer (already built) |
| Content we removed | 180 days (Indian rule) |
| Intimate-image removal requests | 3 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
- The CTO's go-ahead on this plan.
- How long we keep messages. The messaging plan (docs/10) says messages are kept, but never says for how long. That number decides the archive rules. It's a founder and lawyer decision.
- When to build it: before we pass about 1 million messages. It's about 2–3 days of work.
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.