···285285- Implements full `distribution.BlobStore` interface
286286- Used when user has `io.atcr.hold` record
287287288288+#### AppView Web UI (`pkg/appview/`)
289289+290290+The AppView includes a web interface for browsing the registry:
291291+292292+**Features:**
293293+- Repository browsing and search
294294+- Star/favorite repositories
295295+- Pull count tracking
296296+- User profiles and settings
297297+- OAuth-based authentication for web users
298298+299299+**Database Layer** (`pkg/appview/db/`):
300300+- SQLite database for metadata (stars, pulls, repository info)
301301+- Schema migrations via SQL files in `pkg/appview/db/schema.go`
302302+- Stores: OAuth sessions, device flows, repository metadata
303303+- **NOTE:** Simple SQLite for MVP. For production multi-instance: use PostgreSQL
304304+305305+**Jetstream Integration** (`pkg/appview/jetstream/`):
306306+- Consumes ATProto Jetstream for real-time updates
307307+- Backfills repository records from PDS
308308+- Indexes manifests, tags, and repository metadata
309309+- Worker processes incoming events
310310+311311+**Web Handlers** (`pkg/appview/handlers/`):
312312+- `home.go` - Landing page
313313+- `repository.go` - Repository detail pages
314314+- `search.go` - Search functionality
315315+- `auth.go` - OAuth login/logout for web
316316+- `settings.go` - User settings management
317317+- `api.go` - JSON API endpoints
318318+319319+**Static Assets** (`pkg/appview/static/`, `pkg/appview/templates/`):
320320+- Templates use Go html/template
321321+- JavaScript in `static/js/app.js`
322322+- Minimal CSS for clean UI
323323+288324#### Hold Service (`cmd/hold/`)
289325290326Lightweight standalone service for BYOS (Bring Your Own Storage):
···403439- Name resolver under `middleware.registry`
404440- Default storage endpoint: `middleware.registry.options.default_storage_endpoint`
405441- Auth token signing keys and expiration
442442+- Database path: `db.path` (SQLite database location)
443443+- Jetstream endpoint: `jetstream.endpoint` (for ATProto event streaming)
406444407445**Hold Service configuration** (environment variables):
408446- Storage driver config via env vars: `STORAGE_DRIVER`, `AWS_*`, `S3_*`
···4805183. For custom drivers: implement `storagedriver.StorageDriver` interface
4815194. Add case to `buildStorageConfig()` in `cmd/hold/main.go`
4825205. Update `.env.example` with new driver's env vars
521521+522522+**Working with the database**:
523523+- Schema defined in `pkg/appview/db/schema.go`
524524+- Queries in `pkg/appview/db/queries.go`
525525+- Stores for OAuth, devices, sessions in separate files
526526+- Run migrations automatically on startup
527527+- Database path configurable via config.yml
528528+529529+**Adding web UI features**:
530530+- Add handler in `pkg/appview/handlers/`
531531+- Register route in `cmd/appview/serve.go`
532532+- Create template in `pkg/appview/templates/pages/`
533533+- Use existing auth middleware for protected routes
534534+- API endpoints return JSON, pages return HTML
483535484536## Important Context Values
485537
+104-29
README.md
···8899### Architecture
10101111-- **Manifests**: Stored as ATProto records in user PDSs (small JSON metadata)
1212-- **Blobs/Layers**: Stored in S3 (large binary data)
1313-- **Name Resolution**: Supports both ATProto handles and DIDs
1111+ATCR consists of three main components:
1212+1313+1. **AppView** - OCI registry server + web UI
1414+ - Serves OCI Distribution API (Docker push/pull)
1515+ - Resolves identities (handle/DID → PDS endpoint)
1616+ - Routes manifests to user's PDS, blobs to storage
1717+ - Web interface for browsing and search
1818+ - SQLite database for stars, pulls, metadata
1919+2020+2. **Hold Service** - Optional storage service (BYOS)
2121+ - Lightweight HTTP server for presigned URLs
2222+ - Supports S3, Storj, Minio, filesystem, etc.
2323+ - Authorization via ATProto records
2424+ - Users can deploy their own hold
2525+2626+3. **Credential Helper** - Client-side OAuth
2727+ - ATProto OAuth with DPoP
2828+ - Exchanges OAuth token for registry JWT
2929+ - Seamless Docker integration
3030+3131+**Storage Model:**
3232+- **Manifests** → ATProto records in user PDSs (small JSON metadata)
3333+- **Blobs/Layers** → S3 or user's hold service (large binary data)
3434+- **Name Resolution** → Supports both handles and DIDs
1435 - `atcr.io/alice.bsky.social/myimage:latest`
1536 - `atcr.io/did:plc:xyz123/myimage:latest`
16371738## Features
18391919-- OCI Distribution Spec compliant
2020-- ATProto-native manifest storage
2121-- S3 blob storage for container layers
2222-- DID/handle resolution
2323-- Decentralized manifest ownership
4040+### Core Registry
4141+- **OCI Distribution Spec compliant** - Works with Docker, containerd, podman
4242+- **ATProto-native manifest storage** - Manifests stored as records in user PDSs
4343+- **Hybrid storage** - Small manifests in ATProto, large blobs in S3/BYOS
4444+- **DID/handle resolution** - Supports both handles and DIDs for image names
4545+- **Decentralized ownership** - Users own their manifest data via their PDS
4646+4747+### Web Interface
4848+- **Repository browser** - Browse and search container images
4949+- **Star repositories** - Favorite images for quick access
5050+- **Pull tracking** - View popularity and usage metrics
5151+- **OAuth authentication** - Sign in with your ATProto identity
5252+- **User profiles** - Manage your default storage hold
5353+5454+### Authentication
5555+- **ATProto OAuth with DPoP** - Cryptographic proof-of-possession tokens
5656+- **Docker credential helper** - Seamless `docker push/pull` workflow
5757+- **Token exchange** - OAuth tokens converted to registry JWTs
5858+5959+### Storage
6060+- **BYOS (Bring Your Own Storage)** - Deploy your own hold service
6161+- **Multi-backend support** - S3, Storj, Minio, Azure, GCS, filesystem
6262+- **Presigned URLs** - Direct client-to-storage uploads/downloads
6363+- **Hold discovery** - Automatic routing based on user preferences
24642565## Building
2666···3575docker build -f Dockerfile.hold -t atcr.io/hold:latest .
3676```
37773838-## Quick Start (Local Testing)
3939-4040-**Automated setup:**
4141-```bash
4242-# Run the test script (handles everything)
4343-./test-local.sh
4444-```
4545-4646-The script will:
4747-1. Create necessary directories (`/var/lib/atcr/*`)
4848-2. Build all binaries
4949-3. Start registry and hold service
5050-4. Show you how to test
5151-5278**Manual setup:**
5379```bash
5480# 1. Create directories
···192218193219## Usage
194220221221+### Configure Credential Helper (Recommended)
222222+223223+```bash
224224+# Build and configure the credential helper
225225+go build -o docker-credential-atcr ./cmd/credential-helper
226226+./docker-credential-atcr configure
227227+# Follow the OAuth flow in your browser
228228+229229+# Add to Docker config (~/.docker/config.json)
230230+{
231231+ "credHelpers": {
232232+ "atcr.io": "atcr"
233233+ }
234234+}
235235+```
236236+195237### Pushing an Image
196238197239```bash
198240# Tag your image
199241docker tag myapp:latest atcr.io/alice/myapp:latest
200242201201-# Push to ATCR
243243+# Push to ATCR (credential helper handles auth)
202244docker push atcr.io/alice/myapp:latest
203245```
204246···209251docker pull atcr.io/alice/myapp:latest
210252```
211253254254+### Web Interface
255255+256256+Visit the AppView URL (default: http://localhost:5000) to:
257257+- Browse repositories
258258+- Search for images
259259+- Star your favorites
260260+- View pull statistics
261261+- Manage your storage settings
262262+212263## Development
213264214265### Project Structure
215266216267```
217268atcr.io/
218218-├── cmd/appview/ # AppView entrypoint
269269+├── cmd/
270270+│ ├── appview/ # AppView entrypoint (registry + web UI)
271271+│ ├── hold/ # Hold service entrypoint (BYOS)
272272+│ └── credential-helper/ # Docker credential helper
219273├── pkg/
220220-│ ├── atproto/ # ATProto client and manifest store
221221-│ ├── storage/ # S3 blob store and routing
222222-│ ├── middleware/ # Registry and repository middleware
223223-│ └── server/ # HTTP handlers
224224-├── config/ # Configuration files
274274+│ ├── appview/ # Web UI components
275275+│ │ ├── handlers/ # HTTP handlers (home, repo, search, auth)
276276+│ │ ├── db/ # SQLite database layer
277277+│ │ ├── jetstream/ # ATProto Jetstream consumer
278278+│ │ ├── static/ # JS, CSS assets
279279+│ │ └── templates/ # HTML templates
280280+│ ├── atproto/ # ATProto integration
281281+│ │ ├── client.go # PDS client
282282+│ │ ├── resolver.go # DID/handle resolution
283283+│ │ ├── manifest_store.go # OCI manifest store
284284+│ │ ├── lexicon.go # ATProto record schemas
285285+│ │ └── profile.go # Sailor profile management
286286+│ ├── storage/ # Storage layer
287287+│ │ ├── routing_repository.go # Routes manifests/blobs
288288+│ │ ├── proxy_blob_store.go # BYOS proxy
289289+│ │ ├── s3_blob_store.go # S3 wrapper
290290+│ │ └── hold_cache.go # Hold endpoint cache
291291+│ ├── middleware/ # Registry middleware
292292+│ │ ├── registry.go # Name resolution
293293+│ │ └── repository.go # Storage routing
294294+│ └── auth/ # Authentication
295295+│ ├── oauth/ # ATProto OAuth with DPoP
296296+│ ├── token/ # JWT issuer/validator
297297+│ └── atproto/ # Session validation
298298+├── config/ # Configuration files
299299+├── docs/ # Documentation
225300└── Dockerfile
226301```
227302
···33import (
44 "database/sql"
55 "html/template"
66+ "log"
67 "net/http"
7889 "atcr.io/pkg/appview/db"
1010+ "atcr.io/pkg/appview/middleware"
1111+ "atcr.io/pkg/atproto"
1212+ "atcr.io/pkg/auth/oauth"
1313+ "github.com/bluesky-social/indigo/atproto/identity"
914 "github.com/gorilla/mux"
1015)
1116···1419 DB *sql.DB
1520 Templates *template.Template
1621 RegistryURL string
2222+ Directory identity.Directory
2323+ Refresher *oauth.Refresher
1724}
18251926func (h *RepositoryPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
···4552 return
4653 }
47545555+ // Fetch star count
5656+ stats, err := db.GetRepositoryStats(h.DB, owner.DID, repository)
5757+ if err != nil {
5858+ log.Printf("Failed to fetch repository stats: %v", err)
5959+ // Continue with zero stats on error
6060+ stats = &db.RepositoryStats{StarCount: 0}
6161+ }
6262+6363+ // Check if current user has starred this repo
6464+ isStarred := false
6565+ user := middleware.GetUser(r)
6666+ if user != nil && h.Refresher != nil && h.Directory != nil {
6767+ // Get OAuth session for the authenticated user
6868+ session, err := h.Refresher.GetSession(r.Context(), user.DID)
6969+ if err == nil {
7070+ // Get user's PDS client
7171+ apiClient := session.APIClient()
7272+ pdsClient := atproto.NewClientWithIndigoClient(user.PDSEndpoint, user.DID, apiClient)
7373+7474+ // Check if star record exists
7575+ rkey := atproto.StarRecordKey(owner.DID, repository)
7676+ _, err = pdsClient.GetRecord(r.Context(), atproto.StarCollection, rkey)
7777+ isStarred = (err == nil)
7878+ }
7979+ }
8080+4881 data := struct {
4982 PageData
5083 Owner *db.User // Repository owner
5184 Repository *db.Repository
8585+ StarCount int
8686+ IsStarred bool
5287 }{
5388 PageData: NewPageData(r, h.RegistryURL),
5489 Owner: owner,
5590 Repository: repo,
9191+ StarCount: stats.StarCount,
9292+ IsStarred: isStarred,
5693 }
57945895 if err := h.Templates.ExecuteTemplate(w, "repository", data); err != nil {
-3
pkg/appview/static/js/app.js
···115115 dropdownMenu.setAttribute('hidden', '');
116116 }
117117 }
118118-119119- // Load star status on repository page
120120- loadStarStatus();
121118});
122119123120// Toggle star on a repository