A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
73
fork

Configure Feed

Select the types of activity you want to include in your feed.

1# ATCR AppView UI - Version 1 Specification 2 3## Overview 4 5The ATCR AppView UI provides a web interface for discovering, managing, and configuring container images in the ATCR registry. Version 1 focuses on three core pages that leverage existing functionality: 6 71. **Front Page** - Federated image discovery via firehose 82. **Settings Page** - Profile and hold configuration 93. **Personal Page** - Manage your images and tags 10 11## Architecture 12 13### Tech Stack 14 15- **Backend:** Go (existing AppView codebase) 16- **Frontend:** TBD (Go templates/Templ or separate SPA) 17- **Database:** SQLite (firehose data cache) 18- **Styling:** TBD (plain CSS, Tailwind, etc.) 19- **Authentication:** OAuth with DPoP (reuse existing implementation) 20 21### Components 22 23``` 24┌─────────────────────────────────────────────────────────────┐ 25│ Web UI (Browser) │ 26└─────────────────────────────────────────────────────────────┘ 27 28 29┌─────────────────────────────────────────────────────────────┐ 30│ AppView HTTP Server │ 31│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ 32│ │ UI Endpoints │ │ OCI API │ │ OAuth Server │ │ 33│ │ /ui/* │ │ /v2/* │ │ /auth/* │ │ 34│ └──────────────┘ └──────────────┘ └──────────────┘ │ 35└─────────────────────────────────────────────────────────────┘ 36 37 ┌─────────┴─────────┐ 38 ▼ ▼ 39 ┌──────────────────┐ ┌──────────────────┐ 40 │ SQLite Database │ │ ATProto Client │ 41 │ (Firehose cache) │ │ (PDS operations) │ 42 └──────────────────┘ └──────────────────┘ 43 44 ┌──────────────────┐ │ 45 │ Firehose Worker │───────────┘ 46 │ (Background) │ 47 └──────────────────┘ 48 49 50 ┌──────────────────┐ 51 │ ATProto Firehose │ 52 │ (Jetstream/Relay)│ 53 └──────────────────┘ 54``` 55 56## Database Schema 57 58SQLite database for caching firehose data and enabling fast queries. 59 60### Tables 61 62**users** 63```sql 64CREATE TABLE users ( 65 did TEXT PRIMARY KEY, 66 handle TEXT NOT NULL, 67 pds_endpoint TEXT NOT NULL, 68 last_seen TIMESTAMP NOT NULL, 69 UNIQUE(handle) 70); 71CREATE INDEX idx_users_handle ON users(handle); 72``` 73 74**manifests** 75```sql 76CREATE TABLE manifests ( 77 id INTEGER PRIMARY KEY AUTOINCREMENT, 78 did TEXT NOT NULL, 79 repository TEXT NOT NULL, 80 digest TEXT NOT NULL, 81 hold_endpoint TEXT NOT NULL, 82 schema_version INTEGER NOT NULL, 83 media_type TEXT NOT NULL, 84 config_digest TEXT, 85 config_size INTEGER, 86 raw_manifest TEXT NOT NULL, -- JSON blob 87 created_at TIMESTAMP NOT NULL, 88 UNIQUE(did, repository, digest), 89 FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE 90); 91CREATE INDEX idx_manifests_did_repo ON manifests(did, repository); 92CREATE INDEX idx_manifests_created_at ON manifests(created_at DESC); 93CREATE INDEX idx_manifests_digest ON manifests(digest); 94``` 95 96**layers** 97```sql 98CREATE TABLE layers ( 99 manifest_id INTEGER NOT NULL, 100 digest TEXT NOT NULL, 101 size INTEGER NOT NULL, 102 media_type TEXT NOT NULL, 103 layer_index INTEGER NOT NULL, 104 PRIMARY KEY(manifest_id, layer_index), 105 FOREIGN KEY(manifest_id) REFERENCES manifests(id) ON DELETE CASCADE 106); 107CREATE INDEX idx_layers_digest ON layers(digest); 108``` 109 110**tags** 111```sql 112CREATE TABLE tags ( 113 id INTEGER PRIMARY KEY AUTOINCREMENT, 114 did TEXT NOT NULL, 115 repository TEXT NOT NULL, 116 tag TEXT NOT NULL, 117 digest TEXT NOT NULL, 118 created_at TIMESTAMP NOT NULL, 119 UNIQUE(did, repository, tag), 120 FOREIGN KEY(did) REFERENCES users(did) ON DELETE CASCADE 121); 122CREATE INDEX idx_tags_did_repo ON tags(did, repository); 123``` 124 125**firehose_cursor** 126```sql 127CREATE TABLE firehose_cursor ( 128 id INTEGER PRIMARY KEY CHECK (id = 1), 129 cursor INTEGER NOT NULL, 130 updated_at TIMESTAMP NOT NULL 131); 132``` 133 134## Firehose Worker 135 136Background goroutine that subscribes to ATProto firehose and populates the database. 137 138### Implementation 139 140```go 141// pkg/ui/firehose/worker.go 142 143type Worker struct { 144 db *sql.DB 145 jetstream *JetstreamClient 146 resolver *atproto.Resolver 147 stopCh chan struct{} 148} 149 150func (w *Worker) Start() error { 151 // Load cursor from database 152 cursor := w.loadCursor() 153 154 // Subscribe to firehose 155 events := w.jetstream.Subscribe(cursor, []string{ 156 "io.atcr.manifest", 157 "io.atcr.tag", 158 }) 159 160 for { 161 select { 162 case event := <-events: 163 w.handleEvent(event) 164 case <-w.stopCh: 165 return nil 166 } 167 } 168} 169 170func (w *Worker) handleEvent(event FirehoseEvent) error { 171 switch event.Collection { 172 case "io.atcr.manifest": 173 return w.handleManifest(event) 174 case "io.atcr.tag": 175 return w.handleTag(event) 176 } 177 return nil 178} 179``` 180 181### Event Handling 182 183**Manifest create:** 184- Resolve DID → handle, PDS endpoint 185- Insert/update user record 186- Parse manifest JSON 187- Insert manifest record 188- Insert layer records 189 190**Tag create/update:** 191- Insert/update tag record 192- Link to existing manifest 193 194**Record deletion:** 195- Delete from database (cascade handles related records) 196 197### Firehose Connection 198 199Use Jetstream (bluesky-social/jetstream) or connect directly to relay: 200- **Jetstream:** Websocket to `wss://jetstream.atproto.tools/subscribe` 201- **Relay:** Websocket to relay (e.g., `wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos`) 202 203Jetstream is simpler and filters events server-side. 204 205## Page Specifications 206 207### 1. Front Page - Federated Discovery 208 209**URL:** `/ui/` or `/ui/explore` 210 211**Purpose:** Discover recently pushed images across all ATCR users. 212 213**Layout:** 214``` 215┌─────────────────────────────────────────────────────────────┐ 216│ ATCR [Search] [@handle] [Login] │ 217├─────────────────────────────────────────────────────────────┤ 218│ Recent Pushes [Filter ▼]│ 219│ │ 220│ ┌───────────────────────────────────────────────────────┐ │ 221│ │ alice.bsky.social/nginx:latest │ │ 222│ │ sha256:abc123... • hold1.alice.com • 2 hours ago │ │ 223│ │ [docker pull atcr.io/alice.bsky.social/nginx:latest] │ │ 224│ └───────────────────────────────────────────────────────┘ │ 225│ │ 226│ ┌───────────────────────────────────────────────────────┐ │ 227│ │ bob.dev/myapp:v1.2.3 │ │ 228│ │ sha256:def456... • atcr-storage.fly.dev • 5 hours ago │ │ 229│ │ [docker pull atcr.io/bob.dev/myapp:v1.2.3] │ │ 230│ └───────────────────────────────────────────────────────┘ │ 231│ │ 232│ [Load more...] │ 233└─────────────────────────────────────────────────────────────┘ 234``` 235 236**Features:** 237- List of recent pushes (manifests + tags) 238- Show: handle, repository, tag, digest (truncated), timestamp, hold endpoint 239- Copy-paste pull command with click-to-copy 240- Filter by user (click handle to filter) 241- Search by repository name or tag 242- Click manifest to view details (modal or dedicated page) 243- Pagination (50 items per page) 244 245**API Endpoint:** 246``` 247GET /ui/api/recent-pushes 248Query params: 249 - limit (default: 50) 250 - offset (default: 0) 251 - user (optional: filter by DID or handle) 252 - repository (optional: filter by repo name) 253 254Response: 255{ 256 "pushes": [ 257 { 258 "did": "did:plc:alice123", 259 "handle": "alice.bsky.social", 260 "repository": "nginx", 261 "tag": "latest", 262 "digest": "sha256:abc123...", 263 "hold_endpoint": "https://hold1.alice.com", 264 "created_at": "2025-10-05T12:34:56Z", 265 "pull_command": "docker pull atcr.io/alice.bsky.social/nginx:latest" 266 } 267 ], 268 "total": 1234, 269 "offset": 0, 270 "limit": 50 271} 272``` 273 274**Manifest Details Modal:** 275- Full manifest JSON (syntax highlighted) 276- Layer list with digests and sizes 277- Link to ATProto record (at://did/io.atcr.manifest/rkey) 278- Architecture, OS, labels 279- Creation timestamp 280 281### 2. Settings Page 282 283**URL:** `/ui/settings` 284 285**Auth:** Requires login (OAuth) 286 287**Purpose:** Configure profile and hold preferences. 288 289**Layout:** 290``` 291┌─────────────────────────────────────────────────────────────┐ 292│ ATCR [@alice] [⚙️] │ 293├─────────────────────────────────────────────────────────────┤ 294│ Settings │ 295│ │ 296│ ┌─ Identity ───────────────────────────────────────────┐ │ 297│ │ Handle: alice.bsky.social │ │ 298│ │ DID: did:plc:alice123abc (read-only) │ │ 299│ │ PDS: https://bsky.social (read-only) │ │ 300│ └───────────────────────────────────────────────────────┘ │ 301│ │ 302│ ┌─ Default Hold ──────────────────────────────────────┐ │ 303│ │ Current: https://hold1.alice.com │ │ 304│ │ │ │ 305│ │ [Dropdown: Select from your holds ▼] │ │ 306│ │ • https://hold1.alice.com (Your BYOS) │ │ 307│ │ • https://storage.atcr.io (AppView default) │ │ 308│ │ • [Custom URL...] │ │ 309│ │ │ │ 310│ │ Custom hold URL: [_____________________] │ │ 311│ │ │ │ 312│ │ [Save] │ │ 313│ └───────────────────────────────────────────────────────┘ │ 314│ │ 315│ ┌─ OAuth Session ─────────────────────────────────────┐ │ 316│ │ Logged in as: alice.bsky.social │ │ 317│ │ Session expires: 2025-10-06 14:23:00 UTC │ │ 318│ │ [Re-authenticate] │ │ 319│ └───────────────────────────────────────────────────────┘ │ 320└─────────────────────────────────────────────────────────────┘ 321``` 322 323**Features:** 324- Display current identity (handle, DID, PDS) 325- Default hold configuration: 326 - Dropdown showing user's `io.atcr.hold` records (query from PDS) 327 - Option to select AppView's default storage endpoint 328 - Manual entry for custom hold URL 329 - "Save" button updates `io.atcr.sailor.profile.defaultHold` 330- OAuth session status 331- Re-authenticate button (redirects to OAuth flow) 332 333**API Endpoints:** 334 335``` 336GET /ui/api/profile 337Auth: Required (session cookie) 338Response: 339{ 340 "did": "did:plc:alice123", 341 "handle": "alice.bsky.social", 342 "pds_endpoint": "https://bsky.social", 343 "default_hold": "https://hold1.alice.com", 344 "holds": [ 345 { 346 "endpoint": "https://hold1.alice.com", 347 "name": "My BYOS Storage", 348 "public": false 349 } 350 ], 351 "session_expires_at": "2025-10-06T14:23:00Z" 352} 353 354POST /ui/api/profile/default-hold 355Auth: Required 356Body: 357{ 358 "hold_endpoint": "https://hold1.alice.com" 359} 360Response: 361{ 362 "success": true 363} 364``` 365 366### 3. Personal Page - Your Images 367 368**URL:** `/ui/images` or `/ui/@{handle}` 369 370**Auth:** Requires login (OAuth) 371 372**Purpose:** Manage your container images and tags. 373 374**Layout:** 375``` 376┌─────────────────────────────────────────────────────────────┐ 377│ ATCR [@alice] [⚙️] │ 378├─────────────────────────────────────────────────────────────┤ 379│ Your Images │ 380│ │ 381│ ┌─ nginx ──────────────────────────────────────────────┐ │ 382│ │ 3 tags • 5 manifests • Last push: 2 hours ago │ │ 383│ │ │ │ 384│ │ Tags: │ │ 385│ │ ┌────────────────────────────────────────────────┐ │ │ 386│ │ │ latest → sha256:abc123... (2 hours ago) [✏️][🗑️]│ │ │ 387│ │ │ v1.25 → sha256:def456... (1 day ago) [✏️][🗑️]│ │ │ 388│ │ │ alpine → sha256:ghi789... (3 days ago) [✏️][🗑️]│ │ │ 389│ │ └────────────────────────────────────────────────┘ │ │ 390│ │ │ │ 391│ │ Manifests: │ │ 392│ │ ┌────────────────────────────────────────────────┐ │ │ 393│ │ │ sha256:abc123... • 45MB • hold1.alice.com │ │ │ 394│ │ │ linux/amd64 • 5 layers • [View] [Delete] │ │ │ 395│ │ │ sha256:def456... • 42MB • hold1.alice.com │ │ │ 396│ │ │ linux/amd64 • 5 layers • [View] [Delete] │ │ │ 397│ │ └────────────────────────────────────────────────┘ │ │ 398│ └───────────────────────────────────────────────────────┘ │ 399│ │ 400│ ┌─ myapp ──────────────────────────────────────────────┐ │ 401│ │ 2 tags • 2 manifests • Last push: 1 day ago │ │ 402│ │ [Expand ▼] │ │ 403│ └───────────────────────────────────────────────────────┘ │ 404└─────────────────────────────────────────────────────────────┘ 405``` 406 407**Features:** 408 409**Repository List:** 410- Group manifests by repository name 411- Show: tag count, manifest count, last push time 412- Collapsible/expandable repository cards 413 414**Repository Details (Expanded):** 415- **Tags:** Table showing tag → manifest digest → timestamp 416 - Edit tag: Modal to re-point tag to different manifest digest 417 - Delete tag: Confirm dialog, removes `io.atcr.tag` record from PDS 418- **Manifests:** List of all manifests in repository 419 - Show: digest (truncated), size, hold endpoint, architecture, layer count 420 - View: Open manifest details modal (same as front page) 421 - Delete: Confirm dialog with warning if manifest is tagged 422 423**Actions:** 424- Copy pull command for each tag 425- Edit tag (re-point to different digest) 426- Delete tag 427- Delete manifest (with validation) 428 429**API Endpoints:** 430 431``` 432GET /ui/api/images 433Auth: Required 434Response: 435{ 436 "repositories": [ 437 { 438 "name": "nginx", 439 "tag_count": 3, 440 "manifest_count": 5, 441 "last_push": "2025-10-05T10:23:45Z", 442 "tags": [ 443 { 444 "tag": "latest", 445 "digest": "sha256:abc123...", 446 "created_at": "2025-10-05T10:23:45Z" 447 } 448 ], 449 "manifests": [ 450 { 451 "digest": "sha256:abc123...", 452 "size": 47185920, 453 "hold_endpoint": "https://hold1.alice.com", 454 "architecture": "amd64", 455 "os": "linux", 456 "layer_count": 5, 457 "created_at": "2025-10-05T10:23:45Z", 458 "tagged": true 459 } 460 ] 461 } 462 ] 463} 464 465PUT /ui/api/images/{repository}/tags/{tag} 466Auth: Required 467Body: 468{ 469 "digest": "sha256:new-digest..." 470} 471Response: 472{ 473 "success": true 474} 475 476DELETE /ui/api/images/{repository}/tags/{tag} 477Auth: Required 478Response: 479{ 480 "success": true 481} 482 483DELETE /ui/api/images/{repository}/manifests/{digest} 484Auth: Required 485Response: 486{ 487 "success": true 488} 489``` 490 491## Authentication 492 493### OAuth Login Flow 494 495Reuse existing OAuth implementation from credential helper and AppView. 496 497**Login Endpoint:** `/auth/oauth/login` 498 499**Flow:** 5001. User clicks "Login" on UI 5012. Redirects to `/auth/oauth/login?return_to=/ui/images` 5023. User enters handle (e.g., "alice.bsky.social") 5034. Server resolves handle → DID → PDS → OAuth server 5045. Server initiates OAuth flow with PAR + DPoP 5056. User redirected to PDS for authorization 5067. OAuth callback to `/auth/oauth/callback` 5078. Server exchanges code for token, validates with PDS 5089. Server creates session cookie (secure, httpOnly, SameSite) 50910. Redirects to `return_to` URL or default `/ui/images` 510 511**Session Management:** 512- Session cookie: `atcr_session` (JWT or opaque token) 513- Session storage: In-memory map or SQLite table 514- Session duration: 24 hours (or match OAuth token expiry) 515- Refresh: Auto-refresh OAuth token when needed 516 517**Middleware:** 518```go 519// pkg/ui/middleware/auth.go 520 521func RequireAuth(next http.Handler) http.Handler { 522 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 523 session := getSession(r) 524 if session == nil { 525 http.Redirect(w, r, "/auth/oauth/login?return_to="+r.URL.Path, http.StatusFound) 526 return 527 } 528 529 // Add session info to context 530 ctx := context.WithValue(r.Context(), "session", session) 531 next.ServeHTTP(w, r.WithContext(ctx)) 532 }) 533} 534``` 535 536## Implementation Roadmap 537 538### Phase 1: Database & Firehose 5391. Define SQLite schema 5402. Implement database layer (pkg/ui/db/) 5413. Implement firehose worker (pkg/ui/firehose/) 5424. Test worker with real firehose 543 544### Phase 2: API Endpoints 5451. Implement `/ui/api/recent-pushes` (front page data) 5462. Implement `/ui/api/profile` (settings page data) 5473. Implement `/ui/api/images` (personal page data) 5484. Implement tag/manifest mutation endpoints 549 550### Phase 3: Authentication 5511. Implement OAuth login endpoint 5522. Implement session management 5533. Add auth middleware 5544. Test login flow 555 556### Phase 4: Frontend 5571. Choose framework (templates vs SPA) 5582. Implement front page 5593. Implement settings page 5604. Implement personal page 5615. Add styling 562 563### Phase 5: Polish 5641. Error handling 5652. Loading states 5663. Responsive design 5674. Testing 568 569## Open Questions 570 5711. **Framework choice:** Go templates (Templ?), HTMX, or SPA (React/Vue)? 5722. **Styling:** Tailwind, plain CSS, or component library? 5733. **Manifest details:** Modal vs dedicated page? 5744. **Search:** Full-text search on repository/tag names? Requires FTS in SQLite. 5755. **Real-time updates:** WebSocket for firehose events, or polling? 5766. **Image size calculation:** Sum of layer sizes, or read from manifest? 5777. **Public profiles:** Should `/ui/@alice` show public view of alice's images? 5788. **Firehose resilience:** Reconnect logic, backfill on downtime? 579 580## Dependencies 581 582New Go packages needed: 583- `github.com/mattn/go-sqlite3` - SQLite driver 584- `github.com/bluesky-social/jetstream` - Firehose client (or direct websocket) 585- Session management library (or custom implementation) 586- Frontend framework (TBD) 587 588## Configuration 589 590Add to `config/config.yml`: 591 592```yaml 593ui: 594 enabled: true 595 database_path: /var/lib/atcr/ui.db 596 firehose: 597 enabled: true 598 endpoint: wss://jetstream.atproto.tools/subscribe 599 collections: 600 - io.atcr.manifest 601 - io.atcr.tag 602 session: 603 duration: 24h 604 cookie_name: atcr_session 605 cookie_secure: true 606``` 607 608## Security Considerations 609 6101. **Session cookies:** Secure, HttpOnly, SameSite=Lax 6112. **CSRF protection:** For mutation endpoints (tag/manifest delete) 6123. **Rate limiting:** On API endpoints 6134. **Input validation:** Sanitize user input for search/filters 6145. **Authorization:** Verify authenticated user owns resources before mutation 6156. **SQL injection:** Use parameterized queries 616 617## Performance Considerations 618 6191. **Database indexes:** On DID, repository, created_at, digest 6202. **Pagination:** Limit query results to avoid large payloads 6213. **Caching:** Cache profile data, hold list, manifest details 6224. **Firehose buffering:** Batch database inserts 6235. **Connection pooling:** For SQLite and HTTP clients 624 625## Testing Strategy 626 6271. **Unit tests:** Database layer, API handlers 6282. **Integration tests:** Firehose worker with mock events 6293. **E2E tests:** Full login → browse → manage flow 6304. **Load testing:** Firehose worker with high event volume 6315. **Manual testing:** Real PDS, real images, real firehose