a7569a7added credential-less pulls of public images. Three things about it were wrong, all of them in how the appview handled the decision that belongs to the hold. **Scope handling was all-or-nothing.** IsPullOnlyScope required every requested action to already be "pull", but clients routinely ask for more than the operation needs — pull,push is common for a plain read, and some ask for pull,push,delete up front. Those were rejected and challenged, leaving a credential-less client no way to pull even a public image, which is the entire feature. NarrowToPullOnly drops the write actions and issues a token carrying "pull" and nothing else. Granting a subset is what the distribution token spec expects. The allowlist property is preserved: "pull" is the only action that survives, and "*" is deliberately not expanded into it, since a wildcard request is not evidence the caller wants a read. **The appview-side read gate was inert.** checkReadAccess passed p.ctx.DID, the DID of the repository *owner*, not the requester. Any non-empty DID satisfies a private hold's check, and the owner's is never empty, so it asked "may the owner read their own hold", answered yes, and admitted everyone. Worse, CheckReadAccessWithCaptain admitted any authenticated DID to a private hold at all, on an explicitly-MVP assumption that holding a DID was close enough to being a sailor. Every doc says otherwise (docs/hold.md:109 "Crew with blob:read", CLAUDE.md:140, docs/BYOS.md:280) and so does the hold (ValidateBlobReadAccess: owner, or crew carrying blob:read/blob:write). It now takes isCrew and requires owner-or-crew, and callers only pay for the crew lookup when it can change the answer — a public hold or an anonymous caller is decided by the captain record alone. Nothing here loosens access; it brings the local gate into agreement with the authority. **Denials could not reach the client.** distribution's blobHandler.GetBlob maps everything except ErrBlobUnknown to ErrorCodeUnknown, so a 401 raised in the blob store left as a 500 — misreporting an auth failure as a server fault, and giving BearerChallenge no 401 to attach WWW-Authenticate to, so Docker was told "server error" instead of being prompted for credentials. Clients that retry 5xx looped: 4.1s per case in the matrix, now 0.01s. The check moves to Repository(), where an errcode.Error is passed through verbatim by the registry app — the same mechanisma7569a7used for NAME_UNKNOWN. It fails open on a lookup error, since the hold is the authority and a transient failure should not break public pulls. Removes auth.allow_anonymous_pull. It could only ever withhold — captain.Public is what grants — so it was a second flag for a decision the hold already owns, and gating it appview-side was never the intent. Layer bytes 307 straight to S3, so the appview is not even in the path whose cost might have justified an operator-side lever. Tests: TestAuthMatrix only ever ran against a public hold, and its pull cases never fetched a layer — crane.Pull is lazy and img.Digest() needs only the manifest, which ATCR serves from the user's PDS where it is world-readable, so no pull row in the matrix touched blob authorization at all. Pulls now materialize layer bytes, and testharness.WithPrivateHold plus TestAuthMatrixPrivateHold cover public:false + allow_all_crew:true — the production shape, where anyone with an account pulls and pushes and anonymous gets nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ATCR - ATProto Container Registry
https://atcr.io
An OCI-compliant container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
What is ATCR?
ATCR integrates container registries with the AT Protocol ecosystem. Container image manifests are stored as ATProto records in your Personal Data Server (PDS), while layers are stored in S3-compatible storage.
Image names use your ATProto identity:
atcr.io/alice.bsky.social/myapp:latest
atcr.io/did:plc:xyz123/myapp:latest
Architecture
Three components:
-
AppView - Registry API + web UI
- Serves OCI Distribution API (Docker push/pull)
- Resolves handles/DIDs to PDS endpoints
- Routes manifests to user's PDS, blobs to hold services
- Web interface for browsing/search
-
Hold Service - Storage service with embedded PDS (optional BYOS)
- Each hold has a full ATProto PDS for access control (captain + crew records)
- Identified by did:web (e.g.,
did:web:hold01.atcr.io) - Generates presigned URLs for S3/Storj/Minio/etc.
- Users can deploy their own storage and control access via crew membership
-
Credential Helper - Client authentication
- ATProto OAuth (DPoP handled transparently)
- Automatic authentication on first push/pull
Storage model:
- Manifests → ATProto records in user's PDS (small JSON, includes
holdDidreference) - Blobs → Hold services via XRPC multipart upload (large binaries, stored in S3/etc.)
- AppView uses service tokens to communicate with holds on behalf of users
Features
- ✅ OCI-compliant - Works with Docker, containerd, podman
- ✅ Decentralized - You own your manifest data via your PDS
- ✅ ATProto OAuth - Secure authentication (DPoP-compliant)
- ✅ BYOS - Deploy your own storage service
- ✅ Web UI - Browse, search, star repositories
- ✅ Multi-backend - S3, Storj, Minio, Azure, GCS, filesystem
Quick Start
Using the Registry
1. Install credential helper:
curl -fsSL https://atcr.io/static/install.sh | bash
2. Configure Docker (add to ~/.docker/config.json):
{
"credHelpers": {
"atcr.io": "atcr"
}
}
3. Push/pull images:
docker tag myapp:latest atcr.io/yourhandle/myapp:latest
docker push atcr.io/yourhandle/myapp:latest # Authenticates automatically
docker pull atcr.io/yourhandle/myapp:latest
See INSTALLATION.md for detailed installation instructions.
Running Your Own AppView
# Build
go build -o bin/atcr-appview ./cmd/appview
# Generate a config file with all defaults
./bin/atcr-appview config init config-appview.yaml
# Edit config-appview.yaml — set server.default_hold_did at minimum
# Run
./bin/atcr-appview serve --config config-appview.yaml
Using Docker:
docker build -f Dockerfile.appview -t atcr-appview:latest .
docker run -d -p 5000:5000 \
-v ./config-appview.yaml:/config.yaml:ro \
-v atcr-data:/var/lib/atcr \
atcr-appview:latest serve --config /config.yaml
See deploy/README.md for production deployment.
Running Your Own Hold (BYOS Storage)
See docs/hold.md for deploying your own storage backend.
Development
Building from Source
# Build all binaries
go build -o bin/atcr-appview ./cmd/appview
go build -o bin/atcr-hold ./cmd/hold
go build -o bin/docker-credential-atcr ./cmd/credential-helper
# Run tests
go test ./...
go test -race ./...
Project Structure
cmd/
├── appview/ # Registry server + web UI
├── hold/ # Storage service (BYOS)
├── credential-helper/ # Docker credential helper
├── oauth-helper/ # OAuth debug tool
├── healthcheck/ # HTTP health check (for Docker)
├── db-migrate/ # SQLite → libsql migration
├── usage-report/ # Hold storage usage report
├── record-query/ # Query ATProto relay by collection
└── s3-test/ # S3 connectivity test
pkg/
├── appview/
│ ├── db/ # SQLite database (migrations, queries, stores)
│ ├── handlers/ # HTTP handlers (home, repo, search, auth, settings)
│ ├── holdhealth/ # Hold service health checker
│ ├── jetstream/ # ATProto Jetstream consumer
│ ├── middleware/ # Auth & registry middleware
│ ├── ogcard/ # OpenGraph image generation
│ ├── readme/ # Repository README fetcher
│ ├── routes/ # HTTP route registration
│ ├── storage/ # Storage routing (blob proxy, manifest store)
│ ├── public/ # Static assets (JS, CSS, install scripts)
│ └── templates/ # HTML templates
├── atproto/ # ATProto client, records, manifest/tag stores
├── auth/
│ ├── oauth/ # OAuth client, refresher, storage
│ ├── token/ # JWT issuer, validator, claims
│ └── holdlocal/ # Local hold authorization
├── config/ # Config marshaling (commented YAML)
├── hold/
│ ├── admin/ # Admin web UI
│ ├── billing/ # Stripe billing integration
│ ├── db/ # Vendored carstore (go-libsql)
│ ├── gc/ # Garbage collection
│ ├── oci/ # OCI upload endpoints
│ ├── pds/ # Embedded PDS (DID, captain, crew, stats, scans)
│ └── quota/ # Storage quotas
├── logging/ # Structured logging + remote shipping
└── s3/ # S3 client utilities
License
MIT
Contributing
Contributions welcome! Please open an issue or PR.