A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at lexgen 367 lines 11 kB view raw view rendered
1# Bring Your Own Storage (BYOS) 2 3## Overview 4 5ATCR supports "Bring Your Own Storage" (BYOS) for blob storage. Users can: 6- Deploy their own hold service with embedded PDS 7- Control access via crew membership in the hold's PDS 8- Keep blob data in their own S3/Storj/Minio while manifests stay in their user PDS 9 10## Architecture 11 12``` 13┌──────────────────────────────────────────┐ 14│ ATCR AppView (API) │ 15│ - Manifests → User's PDS │ 16│ - Auth & service token management │ 17│ - Blob routing via XRPC │ 18│ - Profile management │ 19└────────────┬─────────────────────────────┘ 20 21 │ Hold discovery priority: 22 │ 1. io.atcr.sailor.profile.defaultHold (DID) 23 │ 2. io.atcr.hold records (legacy) 24 │ 3. AppView default_hold_did 25 26┌──────────────────────────────────────────┐ 27│ User's PDS │ 28│ - io.atcr.sailor.profile (hold DID) │ 29│ - io.atcr.manifest (with holdDid) │ 30└────────────┬─────────────────────────────┘ 31 32 │ Service token from user's PDS 33 34┌──────────────────────────────────────────┐ 35│ Hold Service (did:web:hold.example.com) │ 36│ ├── Embedded PDS │ 37│ │ ├── Captain record (ownership) │ 38│ │ └── Crew records (access control) │ 39│ ├── XRPC multipart upload endpoints │ 40│ └── Storage driver (S3/Storj/etc.) │ 41└──────────────────────────────────────────┘ 42``` 43 44## Hold Service Components 45 46Each hold is a full ATProto actor with: 47- **DID**: `did:web:hold.example.com` (hold's identity) 48- **Embedded PDS**: Stores captain + crew records (shared data) 49- **Storage backend**: S3, Storj, Minio, filesystem, etc. 50- **XRPC endpoints**: Standard ATProto + custom OCI multipart upload 51 52### Records in Hold's PDS 53 54**Captain record** (`io.atcr.hold.captain/self`): 55```json 56{ 57 "$type": "io.atcr.hold.captain", 58 "owner": "did:plc:alice123", 59 "public": false, 60 "deployedAt": "2025-10-14T...", 61 "region": "iad", 62 "provider": "fly.io" 63} 64``` 65 66**Crew records** (`io.atcr.hold.crew/{rkey}`): 67```json 68{ 69 "$type": "io.atcr.hold.crew", 70 "member": "did:plc:bob456", 71 "role": "admin", 72 "permissions": ["blob:read", "blob:write"], 73 "addedAt": "2025-10-14T..." 74} 75``` 76 77### Sailor Profile (User's PDS) 78 79Users set their preferred hold in their sailor profile: 80 81```json 82{ 83 "$type": "io.atcr.sailor.profile", 84 "defaultHold": "did:web:hold.example.com", 85 "createdAt": "2025-10-02T...", 86 "updatedAt": "2025-10-02T..." 87} 88``` 89 90## Deployment 91 92### Configuration 93 94Hold service is configured entirely via environment variables: 95 96```bash 97# Hold identity (REQUIRED) 98HOLD_PUBLIC_URL=https://hold.example.com 99HOLD_OWNER=did:plc:your-did-here 100 101# Storage backend 102STORAGE_DRIVER=s3 103AWS_ACCESS_KEY_ID=your_access_key 104AWS_SECRET_ACCESS_KEY=your_secret_key 105AWS_REGION=us-east-1 106S3_BUCKET=my-blobs 107 108# Access control 109HOLD_PUBLIC=false # Require authentication for reads 110HOLD_ALLOW_ALL_CREW=false # Only explicit crew members can write 111 112# Embedded PDS 113HOLD_DATABASE_PATH=/var/lib/atcr-hold/hold.db 114HOLD_DATABASE_KEY_PATH=/var/lib/atcr-hold/keys 115``` 116 117### Running Locally 118 119```bash 120# Build 121go build -o bin/atcr-hold ./cmd/hold 122 123# Run (with env vars or .env file) 124export HOLD_PUBLIC_URL=http://localhost:8080 125export HOLD_OWNER=did:plc:your-did-here 126export STORAGE_DRIVER=filesystem 127export STORAGE_ROOT_DIR=/tmp/atcr-hold 128export HOLD_DATABASE_PATH=/tmp/atcr-hold/hold.db 129 130./bin/atcr-hold 131``` 132 133On first run, the hold service creates: 134- Captain record in embedded PDS (making you the owner) 135- Crew record for owner with all permissions 136- DID document at `/.well-known/did.json` 137 138### Deploy to Fly.io 139 140```bash 141# Create fly.toml 142cat > fly.toml <<EOF 143app = "my-atcr-hold" 144primary_region = "ord" 145 146[env] 147 HOLD_PUBLIC_URL = "https://my-atcr-hold.fly.dev" 148 STORAGE_DRIVER = "s3" 149 AWS_REGION = "us-east-1" 150 S3_BUCKET = "my-blobs" 151 HOLD_PUBLIC = "false" 152 HOLD_ALLOW_ALL_CREW = "false" 153 154[http_service] 155 internal_port = 8080 156 force_https = true 157 auto_stop_machines = true 158 auto_start_machines = true 159 min_machines_running = 0 160 161[[vm]] 162 cpu_kind = "shared" 163 cpus = 1 164 memory_mb = 256 165EOF 166 167# Deploy 168fly launch 169fly deploy 170 171# Set secrets 172fly secrets set AWS_ACCESS_KEY_ID=... 173fly secrets set AWS_SECRET_ACCESS_KEY=... 174fly secrets set HOLD_OWNER=did:plc:your-did-here 175``` 176 177## Request Flow 178 179### Push with BYOS 180 181``` 1821. Client: docker push atcr.io/alice/myapp:latest 183 1842. AppView resolves alice → did:plc:alice123 185 1863. AppView discovers hold DID: 187 - Check alice's sailor profile for defaultHold 188 - Returns: "did:web:alice-storage.fly.dev" 189 1904. AppView gets service token from alice's PDS: 191 GET /xrpc/com.atproto.server.getServiceAuth?aud=did:web:alice-storage.fly.dev 192 Response: { "token": "eyJ..." } 193 1945. AppView initiates multipart upload to hold: 195 POST https://alice-storage.fly.dev/xrpc/io.atcr.hold.initiateUpload 196 Authorization: Bearer {serviceToken} 197 Body: { "digest": "sha256:abc..." } 198 Response: { "uploadId": "xyz" } 199 2006. For each part: 201 - AppView: POST /xrpc/io.atcr.hold.getPartUploadUrl 202 - Hold validates service token, checks crew membership 203 - Hold returns: { "url": "https://s3.../presigned" } 204 - Client uploads directly to S3 presigned URL 205 2067. AppView completes upload: 207 POST /xrpc/io.atcr.hold.completeUpload 208 Body: { "uploadId": "xyz", "digest": "sha256:abc...", "parts": [...] } 209 2108. Manifest stored in alice's PDS: 211 - holdDid: "did:web:alice-storage.fly.dev" 212 - holdEndpoint: "https://alice-storage.fly.dev" (backward compat) 213``` 214 215### Pull with BYOS 216 217``` 2181. Client: docker pull atcr.io/alice/myapp:latest 219 2202. AppView fetches manifest from alice's PDS 221 2223. Manifest contains: 223 - holdDid: "did:web:alice-storage.fly.dev" 224 2254. AppView caches hold DID for 10 minutes (covers pull operation) 226 2275. Client requests blob: GET /v2/alice/myapp/blobs/sha256:abc123 228 2296. AppView uses cached hold DID from manifest 230 2317. AppView gets service token from alice's PDS 232 2338. AppView calls hold XRPC: 234 GET /xrpc/com.atproto.sync.getBlob?did={userDID}&cid=sha256:abc123 235 Authorization: Bearer {serviceToken} 236 Response: { "url": "https://s3.../presigned-download" } 237 2389. AppView redirects client to presigned S3 URL 239 24010. Client downloads directly from S3 241``` 242 243**Key insight:** Pull uses the `holdDid` stored in the manifest, ensuring blobs are fetched from where they were originally pushed. 244 245## Access Control 246 247### Read Access 248 249- **Public hold** (`HOLD_PUBLIC=true`): Anonymous + authenticated users 250- **Private hold** (`HOLD_PUBLIC=false`): Authenticated users with crew membership 251 252### Write Access 253 254- Hold owner (captain) OR crew members only 255- Verified via `io.atcr.hold.crew` records in hold's embedded PDS 256- Service token proves user identity (from user's PDS) 257 258### Authorization Flow 259 260```go 2611. AppView gets service token from user's PDS 2622. AppView sends request to hold with service token 2633. Hold validates service token (checks it's from user's PDS) 2644. Hold extracts user's DID from token 2655. Hold checks crew records in its embedded PDS 2666. If crew member found allow, else deny 267``` 268 269## Managing Crew Members 270 271### Add Crew Member 272 273Use ATProto client to create crew record in hold's PDS: 274 275```bash 276# Via XRPC (if hold supports it) 277POST https://hold.example.com/xrpc/io.atcr.hold.requestCrew 278Authorization: Bearer {userOAuthToken} 279 280# Or manually via captain's OAuth to hold's PDS 281atproto put-record \ 282 --pds https://hold.example.com \ 283 --collection io.atcr.hold.crew \ 284 --rkey "{memberDID}" \ 285 --value '{ 286 "$type": "io.atcr.hold.crew", 287 "member": "did:plc:bob456", 288 "role": "admin", 289 "permissions": ["blob:read", "blob:write"] 290 }' 291``` 292 293### Remove Crew Member 294 295```bash 296atproto delete-record \ 297 --pds https://hold.example.com \ 298 --collection io.atcr.hold.crew \ 299 --rkey "{memberDID}" 300``` 301 302## Storage Drivers 303 304Hold service supports all distribution storage drivers: 305- **S3** - AWS S3, Minio, Storj (via S3 gateway) 306- **Filesystem** - Local disk (for testing) 307- **Azure** - Azure Blob Storage 308- **GCS** - Google Cloud Storage 309- **Swift** - OpenStack Swift 310 311## Example: Team Hold 312 313```bash 314# 1. Deploy hold service 315export HOLD_PUBLIC_URL=https://team-hold.fly.dev 316export HOLD_OWNER=did:plc:admin 317export HOLD_PUBLIC=false # Private 318export STORAGE_DRIVER=s3 319export AWS_ACCESS_KEY_ID=... 320export S3_BUCKET=team-blobs 321 322fly deploy 323 324# 2. Hold auto-creates captain + crew records on first run 325 326# 3. Admin adds team members via hold's PDS (requires OAuth) 327# (TODO: Implement crew management UI/CLI) 328 329# 4. Team members set their sailor profile: 330atproto put-record \ 331 --collection io.atcr.sailor.profile \ 332 --rkey "self" \ 333 --value '{ 334 "$type": "io.atcr.sailor.profile", 335 "defaultHold": "did:web:team-hold.fly.dev" 336 }' 337 338# 5. Team members can now push/pull using team hold 339``` 340 341## Limitations 342 343### Current IAM Challenges 344 345See [EMBEDDED_PDS.md](./EMBEDDED_PDS.md#iam-challenges) for detailed discussion. 346 347**Known issues:** 3481. **RPC permission format**: Service tokens don't work with IP-based DIDs in local dev 3492. **Dynamic hold discovery**: AppView can't dynamically OAuth arbitrary holds from sailor profiles 3503. **Manual profile management**: No UI for updating sailor profile (must use ATProto client) 351 352**Workaround:** Use hostname-based DIDs (`did:web:hold.example.com`) and public holds for now. 353 354## Future Improvements 355 3561. **Crew management UI** - Web interface for adding/removing crew members 3572. **Dynamic OAuth** - Support for arbitrary BYOS holds without pre-configuration 3583. **Hold migration** - Tools for moving blobs between holds 3594. **Storage analytics** - Track usage per user/repository 3605. **Distributed cache** - Redis for hold DID cache in multi-instance deployments 361 362## References 363 364- [EMBEDDED_PDS.md](./EMBEDDED_PDS.md) - Embedded PDS architecture and IAM details 365- [ATProto Lexicon Spec](https://atproto.com/specs/lexicon) 366- [Distribution Storage Drivers](https://distribution.github.io/distribution/storage-drivers/) 367- [S3 Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/PresignedUrlUploadObject.html)