Two corrections to the HTTP/2 change, both found by running it. UpCloud refuses http2_enabled on a backend that is not TLS: invalid_params_properties.http2_enabled='Tls must be enabled to enable HTTP2.' This load balancer reaches both origins over the private network in cleartext, so the option is not available here at all, and asking for it failed the whole provision run with a 400 after the frontend had already been changed. Backend HTTP/2 is removed rather than made conditional: it would need TLS terminated at the origins, which is a much larger change than it earns. The frontend is the leg that mattered, and it is enabled and confirmed live — all three domains now negotiate h2 by ALPN. The second correction is more interesting: enabling HTTP/2 did not fix the symptom it was aimed at, and briefly made it look worse. With the browser no longer rationing itself to ~6 connections, every row of the crew tab now reaches the server at once, and the 504s went from a flat 10s to a climbing ladder: [HTTP/2 504 10026ms] ... [HTTP/2 504 15230ms] ... [HTTP/2 504 25976ms] HTTP/1.1 had been hiding a server-side limit by throttling the client. The queue moved; it did not disappear. What is actually slow is resolveHandle, which calls live identity resolution per row with no bound of its own, so a DID whose resolution hangs — a did:web on a host that stopped answering, a PDS that accepts and then stalls — holds its request open until the proxy gives up. The crew tab issues one request per member, so a hold with hundreds of crew gets hundreds of chances to hit one. resolveHandle now bounds each lookup at 2s. A handle is decoration on a row whose DID is already rendered beside it, so waiting seconds for one and failing the row when it does not arrive trades something load-bearing for something cosmetic. Two seconds is far above a warm cached lookup and far below the proxy's 10s cut, so a slow DID costs its own row a handle and costs the page nothing. The quota lookup on the same handler was the other suspect and was measured out: records(collection, did) is indexed and the aggregation returns immediately on the production database. This does not remove the N+1 — the tab still issues one request per member, and a batch endpoint is the real fix. It removes the failure. Verified: the guard is covered by a test using a directory that hangs until its context is cancelled, asserting both that the call returns near the bound and that it did not return suspiciously early. make lint 0 issues, make test green across 44 packages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AV6Mk2AgghFsNo4HWQEaBV
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.