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
16 changes: 8 additions & 8 deletions docs/database-schema/database-schema-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -1797,7 +1797,7 @@ asset's inception date, `GET /assets/{asset_identifier}/ohlcv` includes a
| Layer | Strategy |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API Gateway caching** | Built-in response cache (0.5 GB). Per-endpoint TTLs: `/assets` list 60s, `/ohlcv` 60s, `/price` 15s, `/backfill/status` 30s. Cache key includes query params. `POST /prices/batch` uncached |
| **API Gateway throttling** | Request throttling (100/s per API key, 1000/s global burst) |
| **API Gateway throttling** | Request throttling (1 req/s sustained, burst 5, 100 000 req/month per self-service key — task 0157; 200 req/s per method stage-wide) |
| **Lambda** | Rust binary with `lambda_runtime`. Sub-millisecond cold starts. Stateless, auto-scales to concurrency limit. No VPC, so no ENI provisioning latency on cold start |
| **ClickHouse client (`clickhouse` crate)** | Warm connection pool reused across Lambda invocations to amortise mTLS handshake (~80-130 ms cross-cloud RTT to Caddy). Per-request payloads batched per-ledger so a typical invocation issues 1–2 INSERTs, not one per trade |
| **Sort key + partitioning** | Per-granularity tables sorted by `(asset_id, quote_asset_id, source, timestamp)`; monthly partitions on `timestamp`. Partition pruning + sort-key skip eliminate irrelevant months and assets on hot reads |
Expand All @@ -1811,13 +1811,13 @@ instance on a single Hetzner box behind Caddy:443). Prices-api joins as a
second tenant via its own `prices` database, isolated by ClickHouse's native
multi-tenant primitives (database, user, quota, profile).

| Metric | Value | Source |
| ---------------------------- | --------------------------------------------------------------------------- | -------------------------- |
| Prices-api storage footprint | **~3.5-6 GB/year** (realistic, retention-amortised) | Tasks 0060 + 0063 measured |
| Average per-ledger storage | **~1.9-3.7 KB/ledger** (activity-dependent, ~2× spread) | Tasks 0060 + 0063 measured |
| Strongest size lever | Retention-cap `_1h`/`_4h` → bounds DB at ~9 GB @ 10yr (vs ~43 GB unbounded) | Task 0060 measured |
| Write rate | ~1 INSERT per ledger (~12k/day per env at mainnet cadence) | §6.1 |
| Read rate | API-Gateway-throttled ≤100 req/s per key, cached at gateway | §8.2 |
| Metric | Value | Source |
| ---------------------------- | --------------------------------------------------------------------------------- | -------------------------- |
| Prices-api storage footprint | **~3.5-6 GB/year** (realistic, retention-amortised) | Tasks 0060 + 0063 measured |
| Average per-ledger storage | **~1.9-3.7 KB/ledger** (activity-dependent, ~2× spread) | Tasks 0060 + 0063 measured |
| Strongest size lever | Retention-cap `_1h`/`_4h` → bounds DB at ~9 GB @ 10yr (vs ~43 GB unbounded) | Task 0060 measured |
| Write rate | ~1 INSERT per ledger (~12k/day per env at mainnet cadence) | §6.1 |
| Read rate | ≤1 req/s per key (task 0157); ≤200 req/s per method stage-wide, cached at gateway | §8.2 |

> **Sizing superseded (2026-06-19).** The original ~74 B/ledger / ~0.45 GB/yr
> figure was the task-0046 _per-event estimate_. Three ground-truth backfill
Expand Down
56 changes: 48 additions & 8 deletions docs/epics/self-service-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,52 @@ appears as an acceptance criterion in every iteration of our design response

- **Login:** Discord OAuth. A user must have a real Discord account to request a key — no
email/captcha flow needed on top of this.
- Residual risk: someone can still spin up throwaway Discord accounts. **Unverified
assumption (confirm before build):** our understanding is Stellar's own Discord requires
- Residual risk: someone can still spin up throwaway Discord accounts. ~~**Unverified
assumption (confirm before build):**~~ our understanding is Stellar's own Discord requires
some form of verification for new accounts/members, which makes churning through
disposable accounts non-trivial — this is part of why we're not building anything extra
(captcha, email, etc.) on top of Discord login. If that verification turns out not to be
there, this residual risk is bigger than assumed and worth revisiting.
- **RESOLVED 2026-08-10 — see ADR 0010 (task 0156). The assumption was half right, and
the half that was wrong mattered more.** Stellar's Discord _does_ have Membership
Screening enabled, so the gate exists. But joining is a public one-click invite and the
server's verification level is "registered on Discord for longer than 5 minutes" — and,
decisively, **the flow described above would never have observed any of it**: Discord
OAuth under `identify` authenticates a Discord _account_, and exposes no email-verified
flag (that needs the `email` scope), no phone field at all, and no server membership.
SDF's own SCF Dashboard does not treat a Discord account as sufficient either.
**Scope therefore grows by two gates:** the flow now requests
`identify` + `guilds.members.read` and requires **membership of the Stellar Discord**,
plus a **minimum Discord account age of 5 minutes** derived from the user's snowflake
(free — no extra scope), matching Stellar's own server setting. Both are checked
**once, at issuance** — nothing re-checks them, which is the consistent extension of
the non-goal directly below. Captcha, email confirmation and manual approval were
costed and declined; a fully-drained key is worth ~$0.38/month, so no paid mitigation
pays for itself. Downstream effects are in tasks 0159, 0162, 0163 and 0164.
**Read the residual risk above as unchanged in size, not eliminated:** at a 5-minute
threshold the age check is a speed-bump, so the barrier is effectively "joined a public
Discord server and accepted its rules". That is proportionate to the exposure, but it
rests on SDF keeping Membership Screening enabled — tracked as task 0170.
- **Account leaving the Discord server after key issuance:** not actively handled — a key,
once issued, keeps working on its own schedule regardless of the user's later Discord
membership status. This is a conscious "not solving this now" rather than an oversight;
revisit only if it turns out to be exploited in practice.
- **Sharpened 2026-08-10 (ADR 0010 §8):** the key never expires and is never
deactivated, and the dashboard keeps working for a departed member — reveal and usage
require no Discord check at all. What such a user loses is the **right to rework**,
which re-checks membership at the moment it is requested. Eligibility is proved per
action by re-authentication rather than carried in the session, because sign-in and
issuance are separate requests and a session cookie proves only that a Discord account
exists.
- **Account model:** the Discord identity _is_ the account. A signed-in user lands on a
dashboard tied to their Discord ID. **Recommendation: one active key per Discord account**
(keeps the abuse story simple — confirm before build).
dashboard tied to their Discord ID. ~~**Recommendation:**~~ **Confirmed 2026-08-10 (ADR
0010): one active key per Discord account.** This also resolves the contradiction between
this line and "Out of scope" below, which already stated it as settled — the "Out of
scope" reading was correct. The confirmation is stronger than "keeps the abuse story
simple": AWS charges quota per `(usage plan, API key)` and has **no principal that
aggregates keys**, so a multi-key model would force us to fan out `GetUsage` per key and
sum it ourselves — precisely the work the rotation cap below exists to avoid. Note AWS
will not enforce one-key-per-account for us; the registry owns that invariant.
- **Key delivery:** shown on-screen immediately after the Discord sign-in completes the
request, **and viewable again later on the dashboard** — not a one-time reveal. This is
simpler than the "shown once" pattern common elsewhere, and it's workable here because AWS
Expand Down Expand Up @@ -76,10 +109,17 @@ appears as an acceptance criterion in every iteration of our design response
monthly quota is enough on its own once rotation can't happen more often than the quota
resets anyway.
- **When the next rework becomes available (settled 2026-08-07):** the boundary is the
**first day of the month following the last rework, 00:00 UTC** — the same instant the
AWS quota period rolls over. Worked example: a key reworked on **3 August** cannot be
reworked again until **1 September**. One date to render, not two: on 1 September the
user regains both a clean quota counter and the right to rework.
**first day of the month following the last rework, 00:00 UTC**. Worked example: a key
reworked on **3 August** cannot be reworked again until **1 September**.
- **Correction 2026-08-10 (task 0156):** "the same instant the AWS quota period rolls
over" was asserted here and inherited by tasks 0157/0158/0160 — **AWS does not
document it.** Its only statement anywhere is an example caption, "creates a usage
plan that resets at the beginning of the month", with no timezone and no instant; and
`offset` is a _request count_, not a way to shift the reset day. The boundary above
stands as **our own product rule** — it is sound and gives one date to render — but
the claim that our date and AWS's coincide is unverified until measured (task 0171).
If they turn out to differ, we render our date and the quota counter does its own
thing; that is a UX wrinkle, not a correctness bug, because the cap is ours to define.
- **Rework is a swap, not a delete-and-wait (settled 2026-08-07):** the old key is
deleted and a new one issued in the same operation, so a user is never left without
a working key. The cap blocks the _next_ rework, not the replacement. Eligibility is
Expand Down
11 changes: 8 additions & 3 deletions docs/prices-api-general-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ to their own infrastructure at any time if needed.
| **Lambda — Asset Discovery** | Asset registry | EventBridge rate(1 hour). Detects new SEP-41 contract deployments and classic asset issuances; UPSERTs into `prices.assets` |
| **Lambda — Cleanup Worker** | Data retention | EventBridge cron(02:00 UTC daily). `ALTER TABLE … DROP PARTITION` on old monthly partitions of each per-granularity OHLCV table |
| **Lambda — API handlers** | Public API | Individual functions per route group. Rust / axum via `lambda_runtime`, 256–512 MB, 15s timeout. No VPC; outbound HTTPS-mTLS to Caddy:443 |
| **API Gateway** | Public API entry point | REST API, usage plans, API key auth, rate limiting (100 req/s per key), request validation. Built-in response cache (0.5 GB) with per-endpoint TTLs |
| **API Gateway** | Public API entry point | REST API, usage plans, API key auth, rate limiting (1 req/s sustained, burst 5, 100 000 req/month per self-service key — task 0157), request validation. Built-in response cache (0.5 GB) with per-endpoint TTLs |
| **EventBridge Scheduler** | Scheduled triggers | Cron/rate rules for all periodic Lambda workers |
| **Secrets Manager** | Credentials & mTLS material | Per-env client `{cert,key,ca}` for Caddy:443 mTLS (single JSON bundle secret per identity, named by `MTLS_SECRET_NAME`); Soroswap/Aquarius API keys; oracle contract address |
| **CloudWatch + X-Ray** | Observability | API latency, error rates, ingestion lag, Lambda duration/concurrency, backfill progress; mTLS cert NotAfter alarm |
Expand Down Expand Up @@ -1093,7 +1093,7 @@ CLI progress via direct SQL on the local workstation ClickHouse.
| Layer | Strategy |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **API Gateway caching** | Built-in response cache (0.5 GB). Per-endpoint TTLs: `/assets` list 60s, `/ohlcv` 60s, `/price` 15s, `/backfill/status` 30s. Cache key includes query params. POST `/prices/batch` uncached |
| **API Gateway throttling** | Request throttling (100/s per API key, 1000/s global burst) |
| **API Gateway throttling** | Request throttling (1 req/s sustained, burst 5, 100 000 req/month per self-service key — task 0157; 200 req/s per method stage-wide) |
| **Lambda** | Rust binary with `lambda_runtime`. Sub-millisecond cold starts. Stateless, auto-scales to concurrency limit. No VPC, so no ENI provisioning latency on cold start |
| **Database client (`clickhouse` crate)** | Warm connection pool reused across Lambda invocations to amortise mTLS handshake (~80-130 ms cross-cloud RTT to Caddy). Per-request payloads batched per-ledger so a typical invocation issues 1–2 INSERTs, not one per trade |
| **Sort key & partitioning** | Per-granularity tables sorted by `(asset_id, quote_asset_id, source, timestamp)`; monthly partitions on `timestamp`. Partition pruning + sort-key skip eliminate irrelevant months and assets on hot reads |
Expand Down Expand Up @@ -1275,6 +1275,10 @@ for the full local-CLI metrics.
1. All 7 endpoint groups return correct, schema-valid responses for at least 20 major assets
2. Load test (k6 or Locust, script provided): 100 req/s sustained for 5 minutes on
`GET /assets/{id}/price` → p95 latency <200ms, error rate <0.1%
— the target is unchanged, but since task 0157 no key in the account can sustain
it: the default plan is 1 req/s (§6). The run needs a usage plan created for it,
per `docs/runbooks/manual-api-key-tier.md`; the report must state which plan the
key was on (task 0121)
3. Cache confirmed: consecutive identical requests within TTL window return `X-Cache: Hit` header
4. VWAP calculation verifiable against raw `price_ohlcv` rows for at least 3 assets
5. `GET /backfill/status` shows `earliest_data_available` ≤ 2022-01-01
Expand Down Expand Up @@ -1332,7 +1336,8 @@ post-delivery monitoring.
2. OpenAPI spec passes `openapi-validator` lint with no errors; Swagger UI deployed
3. Onboarding portal accessible; self-service API key request flow functional
4. Integration test suite: all tests pass on CI (GitHub Actions link provided)
5. Load test report: p95 <100ms at 100 req/s confirmed
5. Load test report: p95 <100ms at 100 req/s confirmed — same caveat as Tranche 2
AC 2: a purpose-built usage plan is required, and the report names it
6. Security checklist signed off: no wildcard IAM, ClickHouse endpoint reachable only via
mTLS through Caddy:443, mTLS cert + key in Secrets Manager (not env vars), all inputs
validated
Expand Down
Loading
Loading