Serving
Serving
AWS cloud-base for every Quantapix surface. Four CloudFront-fronted static sites, one t4g.small EC2 fronting two FastAPI apps, GitHub OIDC for CI, KMS-CMK-backed SecureStrings.
Service inventory — what runs in v0.1
Eighteen services live in v0.1; six deferred. Total run rate ~$20–25/mo. Phases 1–5 closed (2026-05-15). Use the chips to scope the table to live or deferred only.
| Service | Scope | State | Cost |
|---|---|---|---|
| S3 | ×4 sites + artifacts | live | ~$0.50/mo |
| CloudFront | ×4 distributions | live | ~$1–2/mo |
| CF Functions | URI rewrite | live | free |
| ACM | us-east-1 certs | live | free |
| Route 53 | 2 zones | live | ~$1/mo |
| IAM | 3 principals + OIDC | live | free |
| EC2 | 1× t4g.small | live | ~$11/mo |
| Elastic IP | static | live | $0/mo |
| Default VPC | no NAT | live | free |
| Security Group | 443 + SSM only | live | free |
| SSM Session Mgr | no SSH | live | free |
| SSM Param Store | KMS-encrypted | live | free |
| KMS | alias/qagents-cmk | live | ~$1/mo |
| CloudWatch Logs | 7-day retention | live | ~$0.50/mo |
| CW Metric Alarms | CPU/disk/spend | defer | free |
| AWS Budgets | $50/mo · 80/100/120 | live | free |
| GuardDuty | account-wide | live | ~$3–5/mo |
| SNS alerts topic | qagents-alerts | live | free |
| Cost Explorer | free | live | free |
| ALB | later | defer | ~$16/mo |
| Auto Scaling Group | later | defer | — |
| AWS Backup | later | defer | — |
| AWS WAF | later | defer | ~$5/mo |
| AWS Config | later | defer | ~$10–15/mo |
| 18 live + 6 deferred. Run rate dominated by EC2 ($7–11/mo) + GuardDuty (~$3–5/mo); everything else under $1/mo. | |||
Static sites — R53 → CloudFront → S3 (OAC)
One stamp per site. Identical in shape; differs only in bucket name, hostname, ACM ARN, Route 53 zone. Four stamps live as of Phase 4 (femfas.net, quantapix.com, qnarre.quantapix.com, qresev.quantapix.com).
Browser
End user. Resolves the apex hostname (e.g. quantapix.com, femfas.net) and pulls HTML/CSS/JS over HTTPS. No cookies, no auth, no JS state — every site under designing/web/+documenting/web/ is fully static.
Route 53
One hosted zone per apex domain. Apex A+AAAA ALIAS records point at the CloudFront distribution; www. redirects to apex via a separate distribution or CNAME. Five zones live: two canonical (quantapix.com, femfas.net) carrying the four site stamps plus the two api.* A-records, three legacy redirect zones (qnarre.com, femfas.com, qaltum.net) intentionally kept.
CloudFront
One distribution per site. PriceClass_100 (US/EU edges only — cheapest). HTTP/2 + HTTP/3, HSTS via security-headers policy, OAC origin-access. Cache: *.html short (300s, must-revalidate); everything else immutable (1y). 403/404 both map to /404.html.
CloudFront Fn
Viewer-request CloudFront Function. Four canonical Functions: qagents-rewrite-index (quantapix.com), qagents-rewrite-index-femfas (femfas.net), qagents-rewrite-index-qnarre, qagents-rewrite-index-qresev (Phase 4 product shells). Rewrites /route → /route/index.html so the S3 REST origin can serve Astro static builds. Required because OAC is the origin-access mode (the static-website endpoint would skip this step but loses bucket-policy scoping). The Phase-3 orphan astro-rewrite-index was deleted 2026-05-11 (INVENTORY § 8 drift #6 closed).
S3 (OAC)
One bucket per site, named for the apex (quantapix.com, femfas.net, …). REST endpoint, BlockPublicAccess on, versioning + 30-day noncurrent lifecycle. Bucket policy grants s3:GetObject only to the CloudFront service principal scoped by AWS:SourceArn — no public reads, no IAM users.
static dist/
Output of pnpm -C <sub>/web build. Astro emits /<route>/index.html for every page + hashed asset bundles under _astro/. scripts/deploy.sh wipes the bucket then runs two-pass aws s3 sync --delete (HTML short-cache, everything else immutable) + a CloudFront /* invalidation.
ACM cert
One ACM cert per distribution, in us-east-1 (CloudFront's only certificate region). DNS-validated via the matching Route 53 zone. Auto-renewed; pinned to the distribution's viewer-protocol-policy (HTTPS-only, modern TLS).
| Site | Bucket | Distribution | R53 zone | State |
|---|---|---|---|---|
| femfas.net | femfas.net | E1HSASY4B6ODER | femfas.net | live |
| quantapix.com | quantapix.com | E27NQG9Y1ZPLGH | quantapix.com | live |
| qnarre.quantapix.com | qnarre.quantapix.com | E1VG1K1746Z49X | quantapix.com | live |
| qresev.quantapix.com | qresev.quantapix.com | E2PFH4Z95BT169 | quantapix.com | live |
| Four stamps live. Phase 4 (2026-05-13) landed the two product shells on the same OAC + REST + URI-rewrite Function pattern as the two canonical sites; Phase 5 (2026-05-15) wired the api.* hostnames straight to the EC2 EIP, bypassing CloudFront for SSE. | ||||
App APIs — R53 → EIP → EC2 (Caddy + dual FastAPI)
Browsers reach api.qnarre.* + api.qresev.* over a direct A→EIP record. Bypasses CloudFront because SSE long-poll connections do not pass cleanly through it.
Browser
End-user browser holds an open text/event-stream connection to the API for the duration of a verifier or evaluator run. CloudFront cannot proxy these reliably (idle-timeout caps + buffering), so the apex API hostname A-records the EIP directly.
Route 53 A
Two A-records, api.qnarre.quantapix.com and api.qresev.quantapix.com, both pointing at the same Elastic IP. The product shells qnarre.quantapix.com+qresev.quantapix.com still go through CloudFront — only the API hostname bypasses it.
Elastic IP
Single account-allocated IPv4 association on the EC2 instance. Stable across stop/start so the DNS A-records do not need to chase a new public IP. ~$0/mo while attached, $3.60/mo if released without re-association.
EC2 t4g.small
Single Graviton t4g.small (2 vCPU, 2 GB RAM, ARM64) + 8 GB gp3 root volume + 4 GB gp3 lake volume. Termination protection on, IMDSv2 required, EBS encryption with the account CMK. Sized for two concurrent users; vertical-scale to t4g.medium is the first scaling lever (PLAN § 11).
Caddy
TLS terminator + reverse proxy. Auto-provisions Let's Encrypt certs for api.qnarre.quantapix.com+api.qresev.quantapix.com via HTTP-01. Two virtual-host blocks (one per product) each forward to the matching uvicorn loopback port. Streaming-friendly (no buffer; flush_interval -1).
uvicorn :8787
FastAPI app from verifying/server/. SSE driver subprocesses proving/ Lean kernels, classifies stdout into typed events ({stage, msg, kind}) per the SSE-adapter pattern, streams to the React island in verifying/web/. Bound to localhost; only Caddy can reach it.
uvicorn :8788
FastAPI app from evaluating/server/. Same SSE shape as Qnarre but sub-processes accounting/ portfolio kernels. Hard-refuses any options leg outside the six-strategy defined-risk allow-list at the API boundary, before reaching the kernel.
lake build
Lean 4 build helper invoked by both FastAPI apps to compile proving/+accounting/ on demand. Bounded execution: capped wall-clock + memory, killed if it overruns. Output cached on the lake volume (/var/lib/qagents/lake/).
qagents-app-role
Instance role attached at launch. Grants the app process narrow read-only access — s3:GetObject on the artifacts bucket, ssm:GetParameter under /qagents/*, kms:Decrypt via the account CMK, logs:* under /aws/qagents/*. No write access to site buckets; no ec2:*; no iam:*.
Identity — three IAM principals
Domain separation by design: deploy user cannot SSH or modify EC2; EC2 role cannot deploy site buckets; OIDC role mirrors deploy with short-lived federated tokens.
ikifor (laptop)
Developer machine. Long-term IAM keys live in the macOS Keychain via aws-vault add qagents; ~/.aws/credentials stays empty. Every command runs aws-vault exec qagents -- ... which mints a 1-hour STS session bound to MFA. No plaintext access keys ever touch disk.
GitHub Actions
CI runs deploys via short-lived federated tokens. The repo trusts the AWS account through GitHub's OIDC provider; the workflow assumes qagents-deploy-ci with a sub-claim scoped to repo:quantapix/qagents:ref:refs/heads/main. No long-lived secrets in repo or org variables.
EC2 instance
The single t4g.small instance from the App APIs diagram. Source of sts:AssumeRole calls against qagents-app-role. Trust policy condition pins the source-account + the EC2 instance metadata service principal.
qagents-deploy
The IAM user assumed from the laptop. Carries permission boundary qagents-deploy-boundary capping every action under it. Inline policy at <sub>/web/scripts/iam-policy-deploy.json per consumer site — minimum-privilege, single bucket + single distribution scope.
qagents-deploy-ci
The CI mirror of qagents-deploy. Same managed policies attached, same permission boundary. Trust policy accepts only GitHub OIDC tokens with the pinned sub-claim. Phase 6 deliverable; not yet wired (status pill = NOT_YET_LIVE).
qagents-app-role
The EC2 instance role. Read-mostly: pulls SecureStrings from SSM, decrypts via the account CMK, writes to its own log group, fetches build artifacts from S3. Cannot list or write any site bucket; cannot touch IAM, EC2, Route 53, or CloudFront.
s3:* on 4 sites
Target: the four site buckets — quantapix.com, femfas.net, qnarre.quantapix.com, qresev.quantapix.com. Reachable from qagents-deploy + qagents-deploy-ci only. The app role explicitly cannot.
s3:* on artifacts
Target: the build-artifacts bucket. Holds CDK assets, Lake build caches, Janet narration intermediates. SSE-KMS encrypted with the account CMK. Both deploy and app principals reach it; deploy writes, app reads.
cloudfront:Invalidate
Target: the four CloudFront distributions matching the four sites. Restricted to cloudfront:CreateInvalidation + cloudfront:GetDistribution — no edits to behaviors, origins, or policies from the deploy principal. Distribution edits go through CDK, not deploy scripts.
ssm:GetParameter
Target: SSM Parameter Store entries under /qagents/* — Anthropic + Perplexity + Alpaca API keys, Caddy admin token. SecureString type, KMS-encrypted at rest with the account CMK. Read by the app role; written manually via SSM CLI.
kms:Decrypt
Target: the account customer-managed key (alias/qagents-cmk). Used to unwrap SSM SecureStrings + read SSE-KMS objects on the artifacts bucket. Key policy grants Decrypt + GenerateDataKey to the app role; the deploy role gets Encrypt+Decrypt+GenerateDataKey for artifact uploads.
logs:* /aws/qagents/*
Target: log groups under /aws/qagents/* — one per app (Qnarre/Qresev), Caddy access logs, lake-build logs. 7-day retention by default; CMK-encrypted. App role writes; alarms (CW Alarms + SNS) read.
Deny ec2:*
Explicit deny on ec2:* applied via the permission boundary. The deploy principal cannot stop, terminate, or reconfigure the EC2 instance even if a future managed policy mistakenly grants it. Hard floor.
Deny iam:* / r53:*
Explicit deny on iam:* + route53:*. Stops the deploy principal from creating new identities or rewriting DNS. Identity surface (IAM, OIDC trust, R53 zones) is CDK-only territory; deploy scripts never reach it.
| Principal | Kind | MFA | Auth | Scope |
|---|---|---|---|---|
qagents-deploy | IAM user | required | aws-vault + Imres-iPhone TOTP | 4 site buckets + artifacts · 4 CF dists · EC2/SSM/EBS-modify · CodeCommit (5 repos) |
qagents-deploy-ci | IAM role | n/a | GitHub OIDC (sub pinned) | CI deploys via OIDC; main-branch only |
AWSReservedSSO_* | IAM Identity Center | required | IIC SSO | admin sessions, not deploy path |
| Boundary policy at qagents-deploy-boundary caps both deploy principals; managed-policy drift cannot escalate. | ||||
| Repo | Workflow | Sub-claim pin | Role assumed | Phase |
|---|---|---|---|---|
quantapix/qagents | .github/workflows/deploy.yml | repo:quantapix/qagents:ref:refs/heads/main | qagents-deploy-ci | phase 6 |
quantapix/qagents | .github/workflows/build-status.yml | repo:quantapix/qagents:ref:refs/heads/main | qagents-deploy-ci | phase 6 |
quantapix/quantapix | .github/workflows/pages.yml | repo:quantapix/quantapix:ref:refs/heads/main | qagents-deploy-ci | phase 6 |
| Trust policy condition pins both audience (sts.amazonaws.com) and sub-claim. No long-lived keys in CI. | ||||
Security baseline — 23 controls
8 enforce, 12 adopt, 3 defer. Modern AWS controls (OAC, OIDC, IMDSv2, GuardDuty, KMS CMK, SSM Session Mgr) — not OAI, not SSH, not plaintext keys.
| Control | Mechanism | State |
|---|---|---|
| No long-lived keys | aws-vault + Keychain | enforce |
| MFA on STS | aws:MFAPresent | enforce |
| Permission boundary | qagents-deploy-boundary | enforce |
| GitHub OIDC | IdentityStack · phase 6 | adopt |
| CloudFront OAC | not OAI | adopt |
| ACM certs | no self-signed | enforce |
| Security headers | shared CDK policy | adopt |
| HSTS preload | max-age=63072000 | adopt |
| CSP | per-site factory | adopt |
| S3 BlockPublicAccess | bucket + account | enforce |
| S3 SSE | SSE-KMS on artifacts | enforce |
| EBS encryption | CMK · account default | enforce |
| No SSH | SSM Session Mgr | adopt |
| KMS CMK | alias/qagents-cmk | adopt |
| CloudTrail org-wide | KMS · validation on | adopt |
| GuardDuty | account-wide | adopt |
| AWS Budgets | $50/mo | adopt |
| Alternate contacts | security + billing | adopt |
| IMDSv2 only | HttpTokens=required | enforce |
| EC2 termination prot | StaticSiteStack | adopt |
| VPC Flow Logs | defer | defer |
| WAF | defer | defer |
| AWS Config | defer | defer |
| Enforce (8) — caps blast radius regardless of policy drift. Adopt (12) — modern best practices wired in. Defer (3) — revisit at scale. | ||
Metrics
- siteCount
- 4
- ec2Count
- 1
- distributionCount
- 4
- runRateUsdPerMo
- 20–25