An encrypted personal cloud built on the AT Protocol.
at main 183 lines 5.4 kB view raw view rendered
1<!-- 2 NOTE TO EDITORS: 3 Opake uses a dual-documentation system. If you modify the AppView service 4 details or indexing logic in this file, you MUST also update the 5 corresponding MDX content in `web/src/content/` to prevent documentation drift. 6--> 7 8# AppView: API & Deployment 9 10The AppView indexes `app.opake.grant` and `app.opake.keyring` records from the AT Protocol firehose and serves them via a REST API. It enables the `inbox` command — "what's been shared with me?" — without scanning every PDS in the network. 11 12Built with Elixir/Phoenix. Source lives in `appview/`. 13 14## Running Modes 15 16Controlled by environment variables, not subcommands: 17 18| Mode | Config | Effect | 19|------|--------|--------| 20| `run` (default) | — | Indexer + API | 21| `serve` | `INDEXER_ENABLED=false` | API only (no Jetstream) | 22| `index` | `PHX_SERVER=false` | Indexer only (no HTTP) | 23 24Status check via release eval: 25 26```bash 27bin/opake_appview eval "OpakeAppview.Release.status()" 28``` 29 30## Development 31 32```bash 33cd appview 34docker compose up -d # start postgres 35mix setup # deps + create DB + migrate 36mix phx.server # dev server on :6100 37mix test # run tests 38``` 39 40## Production (Docker) 41 42```bash 43cd appview 44docker compose --profile full up --build 45``` 46 47This starts postgres and the appview container. The entrypoint auto-creates the database and runs migrations. 48 49## Configuration 50 51### Development 52 53`config/dev.exs` — defaults to local postgres (`postgres:postgres@localhost/opake_appview_dev`) and the public Jetstream relay. 54 55### Production (environment variables) 56 57| Variable | Required | Default | Notes | 58|----------|----------|---------|-------| 59| `DATABASE_URL` | yes | — | Ecto URL, e.g. `ecto://user:pass@host/db` | 60| `SECRET_KEY_BASE` | yes | — | 64+ char random string | 61| `JETSTREAM_URL` | no | dev default | Must start with `ws://` or `wss://` | 62| `PORT` | no | `6100` | HTTP listen port | 63| `PHX_HOST` | no | `localhost` | Hostname for URL generation | 64| `PHX_SERVER` | no | `true` | Set to `false` to disable HTTP | 65| `INDEXER_ENABLED` | no | `true` | Set to `false` to disable Jetstream consumer | 66| `POOL_SIZE` | no | `10` | Postgres connection pool size | 67| `ECTO_IPV6` | no | `false` | Enable IPv6 for Postgres connections | 68 69## Authentication 70 71All API endpoints except `/api/health` require authentication via DID-scoped Ed25519 signatures. 72 73**Header format:** 74``` 75Authorization: Opake-Ed25519 <did>:<unix-timestamp>:<base64(signature)> 76``` 77 78**Signature covers:** 79``` 80<METHOD>:<path>:<timestamp>:<did> 81``` 82 83Example: 84``` 85GET:/api/inbox:1709330400:did:plc:abc123 86``` 87 88**Verification flow:** 891. Parse header — extract DID, timestamp, signature (split from right, DIDs contain colons) 902. Reject if timestamp is >60 seconds from now (replay protection) 913. Reject if `?did=` parameter doesn't match authenticated DID (scope enforcement) 924. Fetch `app.opake.publicKey/self` from the user's PDS 935. Extract `signingKey` (Ed25519) from the record 946. Verify signature with Erlang `:crypto` (Ed25519) 957. Cache verified key in ETS for 5 minutes 96 97## API Endpoints 98 99### `GET /api/health` 100 101Always unauthenticated. Returns indexer status only — no aggregate data. 102 103```json 104{ 105 "indexerConnected": true, 106 "cursorTime": "2026-03-02T12:00:00.000000Z", 107 "cursorAgeSecs": 5 108} 109``` 110 111### `GET /api/inbox?did=<did>&limit=<n>&cursor=<cursor>` 112 113Returns grants where `did` is the recipient. Newest first. 114 115| Param | Required | Default | Max | 116|-------|----------|---------|-----| 117| `did` | yes | — | — | 118| `limit` | no | 50 | 100 | 119| `cursor` | no | — | — | 120 121```json 122{ 123 "grants": [ 124 { 125 "uri": "at://did:plc:owner/app.opake.grant/3abc", 126 "ownerDid": "did:plc:owner", 127 "documentUri": "at://did:plc:owner/app.opake.document/3xyz", 128 "createdAt": "2026-03-01T12:00:00Z" 129 } 130 ], 131 "cursor": "2026-03-01T12:00:01.000000Z::at://did:plc:owner/app.opake.grant/3abc" 132} 133``` 134 135### `GET /api/keyrings?did=<did>&limit=<n>&cursor=<cursor>` 136 137Returns keyrings where `did` is a member. 138 139```json 140{ 141 "keyrings": [ 142 { 143 "uri": "at://did:plc:owner/app.opake.keyring/3def", 144 "ownerDid": "did:plc:owner", 145 "indexedAt": "2026-03-01T12:00:00.000000Z" 146 } 147 ], 148 "cursor": "..." 149} 150``` 151 152## Rate Limiting 153 154All endpoints are rate-limited per IP via Hammer (ETS backend). Limits: 30 requests/second burst. Requests beyond the limit receive `429 Too Many Requests`. 155 156IP extraction checks `X-Forwarded-For`, `X-Real-Ip`, and falls back to peer address — works correctly behind reverse proxies. 157 158## Horizontal Scaling 159 160PostgreSQL supports concurrent reads and writes natively. For horizontal scaling: 161 162- **One process with `INDEXER_ENABLED=true`** — consumes the firehose 163- **N processes with `INDEXER_ENABLED=false`** — read-only API servers behind a load balancer 164 165All processes share the same `DATABASE_URL`. 166 167## Release Tasks 168 169Available via `bin/opake_appview eval`: 170 171```bash 172# Create the database 173bin/opake_appview eval "OpakeAppview.Release.create_db()" 174 175# Run pending migrations 176bin/opake_appview eval "OpakeAppview.Release.migrate()" 177 178# Print cursor position, lag, and indexed record counts 179bin/opake_appview eval "OpakeAppview.Release.status()" 180 181# Rollback to a specific migration version 182bin/opake_appview eval "OpakeAppview.Release.rollback(OpakeAppview.Repo, 20260310000001)" 183```