Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
- [ ] Tests pass locally
- [ ] Documentation updated (if applicable)
- [ ] No breaking changes, or breaking changes are documented above
- [ ] If you **added or changed an API endpoint**, regenerated the OpenAPI spec and committed the result:
```bash
cd services/api && cargo run --bin generate-openapi > openapi.yaml
git add openapi.yaml && git commit -m "chore: regenerate openapi.yaml"
```
- [ ] If you **changed system architecture** (new service, database, external dependency, or network boundary), updated [`docs/architecture.md`](../docs/architecture.md)

## Related Issues

Expand Down
74 changes: 74 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,79 @@ jobs:
path: frontend/coverage/
retention-days: 14

openapi-drift-check:
name: OpenAPI Spec Drift Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo dependencies
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-api-openapi-${{ hashFiles('services/api/Cargo.lock') }}

- name: Build generate-openapi binary
run: cargo build --bin generate-openapi
working-directory: services/api

- name: Generate OpenAPI spec from code annotations
run: cargo run --bin generate-openapi > /tmp/generated-openapi.yaml
working-directory: services/api

- name: Compare path sets (committed vs generated)
run: |
python3 << 'EOF'
import yaml
import sys

with open('/tmp/generated-openapi.yaml') as f:
generated = yaml.safe_load(f)

with open('services/api/openapi.yaml') as f:
committed = yaml.safe_load(f)

gen_paths = set(generated.get('paths', {}).keys())
com_paths = set(committed.get('paths', {}).keys())

missing_in_committed = gen_paths - com_paths
extra_in_committed = com_paths - gen_paths

failed = False

if missing_in_committed:
print('❌ Paths annotated in code but missing from openapi.yaml:')
for p in sorted(missing_in_committed):
print(f' {p}')
failed = True

if extra_in_committed:
print('⚠️ Paths in openapi.yaml with no matching code annotation (may be stale):')
for p in sorted(extra_in_committed):
print(f' {p}')

if failed:
print()
print('Run the following to regenerate and commit the spec:')
print(' cd services/api && cargo run --bin generate-openapi > openapi.yaml')
sys.exit(1)

print(f'✅ OpenAPI spec is in sync ({len(gen_paths)} paths)')
EOF

- name: Fail if out of sync
if: failure()
run: |
echo "openapi.yaml is out of sync with the code annotations."
echo "See the step above for the list of missing / extra paths."
exit 1

all-tests-passed:
name: All Tests Passed
needs:
Expand All @@ -852,6 +925,7 @@ jobs:
- api-cache-tests
- e2e-market-creation
- documentation-sync
- openapi-drift-check
- validate-migration-rollbacks
- api-criterion-benchmarks
- frontend-unit-coverage
Expand Down
109 changes: 109 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# PredictIQ — System Architecture

## System Context (C4 Level 1)

Shows PredictIQ and the external actors and systems it interacts with.

```mermaid
C4Context
title System Context — PredictIQ

Person(user, "User", "Browses markets, places bets, manages subscriptions via a web browser.")
Person(admin, "Operator", "Monitors system health, manages deployments, reviews audit logs.")

System(predictiq, "PredictIQ", "Prediction-markets platform: creates and resolves markets, tracks bets, sends notifications.")

System_Ext(stellar, "Stellar RPC", "Blockchain network used to record bets and resolve market outcomes on-chain.")
System_Ext(sendgrid, "SendGrid", "Transactional email delivery: subscription confirmations, notifications, GDPR export links.")

Rel(user, predictiq, "Uses", "HTTPS")
Rel(admin, predictiq, "Operates", "HTTPS / AWS Console")
Rel(predictiq, stellar, "Submits & polls transactions", "HTTPS / JSON-RPC")
Rel(predictiq, sendgrid, "Sends emails via", "HTTPS / REST API")
```

---

## Container Diagram (C4 Level 2)

Shows the deployable containers that make up PredictIQ and how they communicate. All containers run inside a single AWS VPC; the ALB and frontend are in public subnets while the API, databases, and TTS service live in private subnets.

```mermaid
C4Container
title Container Diagram — PredictIQ

Person(user, "User")
Person(admin, "Operator")

System_Ext(stellar, "Stellar RPC")
System_Ext(sendgrid, "SendGrid")

Boundary(aws, "AWS (us-east-1)") {

Boundary(pub_subnet, "Public Subnet") {
Container(alb, "Application Load Balancer", "AWS ALB", "Terminates TLS, routes HTTPS traffic to API tasks.")
Container(frontend, "Frontend", "Next.js / Node.js", "Server-side-rendered web UI. Fetches market data from the API.")
}

Boundary(priv_subnet, "Private Subnet") {
Container(api, "API Service", "Rust / Axum on ECS Fargate", "REST API: market CRUD, bet placement, newsletter, email management, audit logging.")
Container(tts, "TTS Service", "Node.js on ECS Fargate", "Text-to-speech micro-service for market narration audio.")
ContainerDb(postgres, "PostgreSQL", "AWS RDS (PostgreSQL)", "Primary persistent store: markets, bets, users, newsletter subscribers, audit events.")
ContainerDb(redis, "Redis", "AWS ElastiCache", "Cache (market data, rate-limit counters, idempotency keys), blockchain circuit-breaker state.")
}
}

Rel(user, alb, "HTTPS requests", "443")
Rel(admin, alb, "HTTPS requests", "443")

Rel(alb, frontend, "HTTP", "3000")
Rel(alb, api, "HTTP", "8080")

Rel(frontend, api, "API calls", "HTTP / JSON")
Rel(frontend, tts, "Audio requests", "HTTP")

Rel(api, postgres, "Reads / writes", "PostgreSQL protocol / TLS")
Rel(api, redis, "Caches / rate-limits", "RESP / TLS")
Rel(api, stellar, "Submits & polls transactions", "HTTPS / JSON-RPC")
Rel(api, sendgrid, "Sends transactional email", "HTTPS / REST API")

Rel(tts, api, "Reads market metadata", "HTTP / JSON")
```

---

## Network Boundaries

| Boundary | Contents | Inbound access |
|---|---|---|
| Public subnets | ALB, (optional) bastion host | Internet (0.0.0.0/0) on port 443 |
| Private subnets | ECS tasks (API, TTS), RDS, ElastiCache | Only from within the VPC |
| AWS Secrets Manager | Credentials (DB password, SendGrid key, etc.) | ECS task IAM role only |

---

## Key Technology Choices

| Component | Technology | Reason |
|---|---|---|
| API runtime | Rust / Axum | Memory-safe, high throughput, low latency |
| TTS service | Node.js | Rapid integration with browser-compatible TTS libraries |
| Frontend | Next.js | SSR for SEO; seamless API integration |
| Primary DB | PostgreSQL (RDS) | ACID guarantees for financial/market data |
| Cache / rate-limit | Redis (ElastiCache) | Sub-millisecond reads, built-in TTL, stream support |
| Blockchain | Stellar (Soroban) | Low-fee, fast-finality smart contracts |
| Email | SendGrid | Reliable delivery, webhook support for tracking |
| Compute | AWS ECS Fargate | Serverless containers; no EC2 management |
| IaC | Terraform | Reproducible, version-controlled infrastructure |

---

## Architecture Review — PR Checklist

When a pull request changes any of the components documented above, update this file as part of the PR. Specifically review if the change affects:

- [ ] A new or removed service / container
- [ ] A new external dependency (third-party API, data store, etc.)
- [ ] A change in network boundary (e.g. moving a service to a public subnet)
- [ ] A change in communication protocol between services
- [ ] A significant change to data-at-rest or data-in-transit trust boundaries
Loading
Loading