@@ -12,6 +12,8 @@ Returns a cursor-paginated list of registered users, ordered newest-first
1212| ` limit ` | number | no | ` 20 ` | 1-100 | Number of rows to return per page. |
1313| ` cursor ` | string | no | -- | opaque token | Cursor from the previous page's ` nextCursor ` . Absent = page 1. |
1414
15+ Unknown query parameters are rejected with ` 400 validation_error ` .
16+
1517### Pagination
1618
1719This endpoint uses ** keyset (cursor) pagination** on ` (createdAt DESC, id DESC) ` .
@@ -21,6 +23,49 @@ This endpoint uses **keyset (cursor) pagination** on `(createdAt DESC, id DESC)`
2123- Cursors are versioned. A stale or tampered cursor is safely ignored (the
2224 response restarts from page 1) rather than causing a 500 or a wrong offset.
2325
26+ ### Performance
27+
28+ Migration ` 0025_users_filter_idx ` adds a composite B-tree index:
29+
30+ ``` sql
31+ CREATE INDEX CONCURRENTLY IF NOT EXISTS users_created_at_id_idx
32+ ON users (created_at DESC , id DESC );
33+ ```
34+
35+ ** Without the index** the PostgreSQL planner produces:
36+ ```
37+ Seq Scan on users (cost=0.00..N rows=N)
38+ → Sort (cost=.. rows=N width=.. Sort Method: quicksort)
39+ ```
40+ This is O(n) I/O that degrades linearly as the user count grows.
41+
42+ ** With the index** the planner switches to:
43+ ```
44+ Index Scan Backward using users_created_at_id_idx on users
45+ (cost=0.29..8.31 rows=21 width=56)
46+ ```
47+ Key benefits:
48+ - O(log n + page_size) I/O instead of O(n) — scales with table size.
49+ - No sort step — the index delivers rows in the required order.
50+ - ` CONCURRENTLY ` creation — no ` ACCESS EXCLUSIVE ` lock, zero downtime.
51+
52+ ** Column order rationale:**
53+ 1 . ` created_at DESC ` — the dominant sort key; satisfies the keyset ` WHERE created_at < cursor_time ` .
54+ 2 . ` id DESC ` — the tie-breaker; satisfies ` id < cursor_id ` when two users share the same millisecond.
55+
56+ ` stellar_address ` already has an implicit B-tree index via the ` UNIQUE ` constraint, so
57+ ` getUserByAddress ` lookups are O(log n) without any additional index.
58+
59+ ### Rollback
60+
61+ To remove the index without downtime:
62+
63+ ``` sql
64+ DROP INDEX CONCURRENTLY IF EXISTS users_created_at_id_idx;
65+ ```
66+
67+ ` CONCURRENTLY ` cannot run inside a transaction block; execute it directly.
68+
2469### Response
2570
2671` 200 OK `
@@ -40,7 +85,7 @@ This endpoint uses **keyset (cursor) pagination** on `(createdAt DESC, id DESC)`
4085
4186### Errors
4287
43- - ` 400 validation_error ` - invalid query parameters
88+ - ` 400 validation_error ` — invalid or unknown query parameters
4489
4590### Conditional requests and caching
4691
@@ -56,16 +101,31 @@ GET /api/users
56101If-None-Match: "<etag>"
57102```
58103
104+ ---
105+
59106## ` GET /api/users/me `
60107
61108Returns the authenticated user's own profile. Requires a valid JWT.
62109
63110Supports strong ETag / ` 304 ` conditional GET on the ` { data: profile } ` payload.
64111
112+ ### Authentication
113+
114+ Bearer JWT via the ` Authorization: Bearer <token> ` header. A missing or
115+ invalid token returns ` 403 Forbidden ` .
116+
117+ ---
118+
65119## ` GET /api/users/:address/predictions `
66120
67121Returns a cursor-paginated list of predictions for the given Stellar address.
68122
123+ ### Path Parameters
124+
125+ | Parameter | Type | Description |
126+ | ------------| --------| --------------------------------------|
127+ | ` :address ` | string | A valid 56-character G… Stellar address |
128+
69129### Query Parameters
70130
71131| Parameter | Type | Required | Default | Constraints | Description |
@@ -82,6 +142,8 @@ Supports strong ETag / `304` conditional GET on the `{ data, nextCursor }` paylo
82142- ` 400 validation_error ` — query params fail validation
83143- ` 404 not_found ` — no user row for that address
84144
145+ ---
146+
85147## ` GET /api/users/:stellarAddress/profile `
86148
87149Returns the public profile for any Stellar address.
@@ -92,3 +154,19 @@ Supports strong ETag / `304` conditional GET on the `{ data: profile }` payload.
92154
93155- ` 400 validation_error ` — invalid Stellar address
94156- ` 404 not_found ` — no matching user row
157+
158+ ---
159+
160+ ## Rate Limiting
161+
162+ All ` /api/users ` routes apply a shared per-user rate limiter (60 req/min).
163+ Authenticated requests key by ` users:{id} ` ; anonymous requests key by
164+ ` users:ip:{ip} ` . Rate limit headers follow IETF draft-7 (` RateLimit-* ` ).
165+
166+ ## Structured Logging
167+
168+ Every request emits a structured log entry via ` accessLog ` middleware including:
169+ - ` correlationId ` — resolved from ` X-Correlation-Id ` → ` X-Request-Id ` → generated UUID
170+ - Route-specific events: ` users_list_request ` , ` users_list_served ` , etc.
171+
172+ Pass ` X-Correlation-Id: <uuid> ` to correlate log entries with a specific request.
0 commit comments