Webhook delivery was neither idempotent nor order-safe, and every failure
returned 400, which Stripe does not retry. A transient DB or hold error
therefore dropped a subscription change silently and permanently.
- New stripe_processed_events table: event_id as primary key dedups
redelivery, and event_created per customer drops stale out-of-order
deliveries.
- HandleWebhook distinguishes ErrWebhookSignature (400, no retry) from
ErrWebhookProcessing (500, Stripe redelivers). The event handlers
return errors instead of swallowing them. ErrBillingDisabled maps to
400: the route is mounted but billing is off, so redelivery can never
succeed and Stripe should stop rather than retry to exhaustion.
- Refuse to boot when billing is enabled with an empty
STRIPE_WEBHOOK_SECRET. Stripe HMACs with the empty key, so an
attacker can reproduce the signature and the endpoint is forgeable.
- UpdateCrewTierOnAllHolds retries each hold (3 attempts, linear
backoff, 5s per request) and returns a joined error so the webhook
can fail and let Stripe redeliver.
The fan-out contacts holds concurrently rather than in sequence. Serially,
one unreachable hold burns the caller's entire 10s budget on its own
retries (3 x 5s plus backoff) and the holds after it are never contacted;
because Stripe redelivers in the same order, a persistently-down first
hold means the rest are never updated at all.
On the hold, the signature-validated sub claim is now the source of truth
for updateCrewTier: a mismatched body userDid is rejected with 403 rather
than retargeting the grant to another DID. "Not crew on this hold" is a
200 no-op, since the appview fans updates out to every managed hold and a
subscriber is not crew everywhere.
That no-op has to be told apart from a storage failure. GetCrewMember
collapsed both into one generic error, so a CAR-store failure read as
"not a member", answered 200, and let the appview record the event as
processed — losing the tier grant permanently, which is exactly the
failure mode this commit exists to prevent. Missing records now carry an
ErrCrewMemberNotFound sentinel, and anything else returns 500 so Stripe
redelivers.
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.