···1+# ATCR Hold Service
2+3+> The storage backend component of ATCR (ATProto Container Registry)
4+5+## Overview
6+7+**Hold Service** is the storage backend component of ATCR. It enables BYOS (Bring Your Own Storage) - users can store their own container image layers in their own S3, Storj, Minio, or filesystem storage. Each hold runs as a full ATProto user with an embedded PDS, exposing both standard ATProto sync endpoints and custom XRPC endpoints for OCI multipart blob uploads.
8+9+### What Hold Service Does
10+11+Hold Service is the storage layer that:
12+13+- **Bring Your Own Storage (BYOS)** - Store your own container image layers in your own S3, Storj, Minio, or filesystem
14+- **Embedded ATProto PDS** - Each hold is a full ATProto user with its own DID, repository, and identity
15+- **Custom XRPC Endpoints** - OCI-compatible multipart upload endpoints (`io.atcr.hold.*`) for blob operations
16+- **Presigned URL Generation** - Creates time-limited S3 URLs for direct client-to-storage transfers (~99% bandwidth reduction)
17+- **Crew Management** - Controls access via captain and crew records stored in the hold's embedded PDS
18+- **Standard ATProto Sync** - Exposes com.atproto.sync.* endpoints for repository synchronization and firehose
19+- **Multi-Backend Support** - Works with S3, Storj, Minio, filesystem, Azure, GCS via distribution's driver system
20+- **Bluesky Integration** - Optional: Posts container image push notifications from the hold's identity to Bluesky
21+22+### The ATCR Ecosystem
23+24+Hold Service is the **storage backend** of a multi-component architecture:
25+26+1. **[AppView](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API + web interface
27+2. **Hold Service** (this component) - Storage backend with embedded PDS
28+3. **Credential Helper** - Client-side tool for ATProto OAuth authentication
29+30+**Data flow:**
31+```
32+Docker Client → AppView (resolves identity) → User's PDS (stores manifest)
33+ ↓
34+ Hold Service (generates presigned URL)
35+ ↓
36+ S3/Storj/etc. (client uploads/downloads blobs directly)
37+```
38+39+Manifests (small JSON metadata) live in users' ATProto PDS, while blobs (large binary layers) live in hold services. AppView orchestrates the routing, and hold services provide presigned URLs to eliminate bandwidth bottlenecks.
40+41+## When to Run Your Own Hold
42+43+Most users can push to the default hold at **https://hold01.atcr.io** - you don't need to run your own hold.
44+45+**Run your own hold if you want to:**
46+- Control where your container layer data is stored (own S3 bucket, Storj, etc.)
47+- Manage access for a team or organization via crew membership
48+- Reduce bandwidth costs by using presigned URLs for direct S3 transfers
49+- Run a shared hold for a community or project
50+- Maintain data sovereignty (keep blobs in specific geographic regions)
51+52+**Prerequisites:**
53+- S3-compatible storage (AWS S3, Storj, Minio, UpCloud, etc.) OR filesystem storage
54+- (Optional) Domain name with SSL/TLS certificates for production
55+- ATProto DID for hold owner (get from: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`)
56+57+## Quick Start
58+59+### Using Docker Compose
60+61+The fastest way to run Hold service with S3 storage:
62+63+```bash
64+# Clone repository
65+git clone https://tangled.org/@evan.jarrett.net/at-container-registry
66+cd atcr
67+68+# Copy and configure environment
69+cp .env.hold.example .env.hold
70+# Edit .env.hold - set HOLD_PUBLIC_URL, HOLD_OWNER, S3 credentials (see Configuration below)
71+72+# Start hold service
73+docker-compose -f docker-compose.hold.yml up -d
74+75+# Verify
76+curl http://localhost:8080/.well-known/did.json
77+```
78+79+### Minimal Configuration
80+81+At minimum, you must set:
82+83+```bash
84+# Required: Public URL (generates did:web identity)
85+HOLD_PUBLIC_URL=https://hold.example.com
86+87+# Required: Your ATProto DID (for captain record)
88+HOLD_OWNER=did:plc:your-did-here
89+90+# Required: Storage driver type
91+STORAGE_DRIVER=s3
92+93+# Required for S3: Credentials and bucket
94+AWS_ACCESS_KEY_ID=your-access-key
95+AWS_SECRET_ACCESS_KEY=your-secret-key
96+S3_BUCKET=your-bucket-name
97+98+# Recommended: Database directory for embedded PDS
99+HOLD_DATABASE_DIR=/var/lib/atcr-hold
100+```
101+102+See **Configuration Reference** below for all options.
103+104+## Configuration Reference
105+106+Hold Service is configured entirely via environment variables. Load them with:
107+```bash
108+source .env.hold
109+./bin/atcr-hold
110+```
111+112+Or via Docker Compose (recommended).
113+114+### Server Configuration
115+116+#### `HOLD_PUBLIC_URL` ⚠️ REQUIRED
117+- **Default:** None (required)
118+- **Description:** Public URL of this hold service. Used to generate the hold's did:web identity. The hostname becomes the hold's DID.
119+- **Format:** `https://hold.example.com` or `http://127.0.0.1:8080` (development)
120+- **Example:** `https://hold01.atcr.io` → DID is `did:web:hold01.atcr.io`
121+- **Note:** This URL must be reachable by AppView and Docker clients
122+123+#### `HOLD_SERVER_ADDR`
124+- **Default:** `:8080`
125+- **Description:** HTTP listen address for XRPC endpoints
126+- **Example:** `:8080`, `:9000`, `0.0.0.0:8080`
127+128+#### `HOLD_PUBLIC`
129+- **Default:** `false`
130+- **Description:** Allow public blob reads (pulls) without authentication. Writes always require crew membership.
131+- **Use cases:**
132+ - `true`: Public registry (anyone can pull, authenticated users can push if crew)
133+ - `false`: Private registry (authentication required for both push and pull)
134+135+### Storage Configuration
136+137+#### `STORAGE_DRIVER`
138+- **Default:** `s3`
139+- **Options:** `s3`, `filesystem`
140+- **Description:** Storage backend type. S3 enables presigned URLs for direct client-to-storage transfers (~99% bandwidth reduction). Filesystem stores blobs locally (development/testing).
141+142+#### S3 Storage (when `STORAGE_DRIVER=s3`)
143+144+##### `AWS_ACCESS_KEY_ID` ⚠️ REQUIRED for S3
145+- **Description:** S3 access key ID for authentication
146+- **Example:** `AKIAIOSFODNN7EXAMPLE`
147+148+##### `AWS_SECRET_ACCESS_KEY` ⚠️ REQUIRED for S3
149+- **Description:** S3 secret access key for authentication
150+- **Example:** `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`
151+152+##### `AWS_REGION`
153+- **Default:** `us-east-1`
154+- **Description:** S3 region
155+- **AWS regions:** `us-east-1`, `us-west-2`, `eu-west-1`, etc.
156+- **UpCloud regions:** `us-chi1`, `us-nyc1`, `de-fra1`, `uk-lon1`, `sg-sin1`
157+158+##### `S3_BUCKET` ⚠️ REQUIRED for S3
159+- **Description:** S3 bucket name where blobs will be stored
160+- **Example:** `atcr-blobs`, `my-company-registry-blobs`
161+- **Note:** Bucket must already exist
162+163+##### `S3_ENDPOINT`
164+- **Default:** None (uses AWS S3)
165+- **Description:** S3-compatible endpoint URL for non-AWS providers
166+- **Storj:** `https://gateway.storjshare.io`
167+- **UpCloud:** `https://[bucket-id].upcloudobjects.com`
168+- **Minio:** `http://minio:9000`
169+- **Note:** Leave empty for AWS S3
170+171+#### Filesystem Storage (when `STORAGE_DRIVER=filesystem`)
172+173+##### `STORAGE_ROOT_DIR`
174+- **Default:** `/var/lib/atcr/hold`
175+- **Description:** Directory path where blobs will be stored on local filesystem
176+- **Use case:** Development, testing, or single-server deployments
177+- **Note:** Presigned URLs are not available with filesystem driver (hold proxies all blob transfers)
178+179+### Embedded PDS Configuration
180+181+#### `HOLD_DATABASE_DIR`
182+- **Default:** `/var/lib/atcr-hold`
183+- **Description:** Directory path for embedded PDS carstore (SQLite database). Carstore creates `db.sqlite3` inside this directory.
184+- **Note:** This must be a directory path, NOT a file path. If empty, embedded PDS is disabled (not recommended - hold authorization requires PDS).
185+186+#### `HOLD_KEY_PATH`
187+- **Default:** `{HOLD_DATABASE_DIR}/signing.key`
188+- **Description:** Path to hold's signing key (secp256k1). Auto-generated on first run if missing.
189+- **Note:** Keep this secure - it's used to sign ATProto commits in the hold's repository
190+191+### Access Control
192+193+#### `HOLD_OWNER`
194+- **Default:** None
195+- **Description:** Your ATProto DID. Used to create the captain record and add you as the first crew member with admin role.
196+- **Get your DID:** `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`
197+- **Example:** `did:plc:abc123xyz789`
198+- **Note:** If set, the hold will initialize with your DID as owner on first run
199+200+#### `HOLD_ALLOW_ALL_CREW`
201+- **Default:** `false`
202+- **Description:** Allow any authenticated ATCR user to write to this hold (treat all as crew)
203+- **Security model:**
204+ - `true`: Any authenticated user can push images (useful for shared/community holds)
205+ - `false`: Only hold owner and explicit crew members can push (verified via crew records in hold's PDS)
206+- **Use cases:**
207+ - Public registry: `HOLD_PUBLIC=true, HOLD_ALLOW_ALL_CREW=true`
208+ - ATProto users only: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=true`
209+ - Private hold: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=false` (default)
210+211+### Bluesky Integration
212+213+#### `HOLD_BLUESKY_POSTS_ENABLED`
214+- **Default:** `false`
215+- **Description:** Create Bluesky posts when users push container images. Posts include image name, tag, size, and layer count.
216+- **Note:** Posts are created from the hold's embedded PDS identity (did:web). Requires hold to be crawled by Bluesky relay.
217+- **Enable relay crawl:** `./deploy/request-crawl.sh hold.example.com`
218+219+#### `HOLD_PROFILE_AVATAR`
220+- **Default:** `https://imgs.blue/evan.jarrett.net/1TpTOdtS60GdJWBYEqtK22y688jajbQ9a5kbYRFtwuqrkBAE`
221+- **Description:** URL to download avatar image for hold's Bluesky profile. Downloaded and uploaded as blob during bootstrap.
222+- **Note:** Avatar is stored in hold's PDS and displayed on Bluesky profile
223+224+### Advanced Configuration
225+226+#### `TEST_MODE`
227+- **Default:** `false`
228+- **Description:** Enable test mode (skips some validations). Do not use in production.
229+230+#### `DISABLE_PRESIGNED_URLS`
231+- **Default:** `false`
232+- **Description:** Force proxy mode even with S3 configured (for testing). Disables presigned URL generation and routes all blob transfers through the hold service.
233+- **Use case:** Testing, debugging, or environments where presigned URLs don't work
234+235+## XRPC Endpoints
236+237+Hold Service exposes two types of XRPC endpoints:
238+239+### ATProto Sync Endpoints (Standard)
240+- `GET /.well-known/did.json` - DID document (did:web resolution)
241+- `GET /xrpc/com.atproto.sync.getRepo` - Download full repository as CAR file
242+- `GET /xrpc/com.atproto.sync.getBlob` - Get blob or presigned download URL
243+- `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events
244+- `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS)
245+- `GET /xrpc/com.atproto.repo.describeRepo` - Repository metadata
246+- `GET /xrpc/com.atproto.repo.getRecord` - Get record by collection and rkey
247+- `GET /xrpc/com.atproto.repo.listRecords` - List records in collection
248+- `POST /xrpc/com.atproto.repo.deleteRecord` - Delete record (owner/crew admin only)
249+250+### OCI Multipart Upload Endpoints (Custom)
251+- `POST /xrpc/io.atcr.hold.initiateUpload` - Start multipart upload session
252+- `POST /xrpc/io.atcr.hold.getPartUploadUrl` - Get presigned URL for uploading a part
253+- `PUT /xrpc/io.atcr.hold.uploadPart` - Direct buffered part upload (alternative to presigned URLs)
254+- `POST /xrpc/io.atcr.hold.completeUpload` - Finalize multipart upload
255+- `POST /xrpc/io.atcr.hold.abortUpload` - Cancel multipart upload
256+- `POST /xrpc/io.atcr.hold.notifyManifest` - Notify hold of manifest upload (creates layer records, Bluesky posts)
257+258+## Authorization Model
259+260+Hold Service uses crew membership records in its embedded PDS for access control:
261+262+### Read Access (Blob Downloads)
263+264+**Public Hold** (`HOLD_PUBLIC=true`):
265+- Anonymous users: ✅ Allowed
266+- Authenticated users: ✅ Allowed
267+268+**Private Hold** (`HOLD_PUBLIC=false`):
269+- Anonymous users: ❌ Forbidden
270+- Authenticated users with crew membership: ✅ Allowed
271+- Crew must have `blob:read` permission
272+273+### Write Access (Blob Uploads)
274+275+Regardless of `HOLD_PUBLIC` setting:
276+- Hold owner (from captain record): ✅ Allowed
277+- Crew members with `blob:write` permission: ✅ Allowed
278+- Non-crew authenticated users: Depends on `HOLD_ALLOW_ALL_CREW`
279+ - `HOLD_ALLOW_ALL_CREW=true`: ✅ Allowed
280+ - `HOLD_ALLOW_ALL_CREW=false`: ❌ Forbidden
281+282+### Authentication Method
283+284+AppView uses **service tokens** from user's PDS to authenticate with hold service:
285+1. AppView calls user's PDS: `com.atproto.server.getServiceAuth` with hold DID
286+2. User's PDS returns a service token scoped to the hold DID
287+3. AppView includes service token in XRPC requests to hold
288+4. Hold validates token and checks crew membership in its embedded PDS
289+290+## Deployment Scenarios
291+292+### Personal Hold (Single User)
293+294+Your own storage for your images:
295+296+```bash
297+# Hold config
298+HOLD_PUBLIC_URL=https://hold.alice.com
299+HOLD_OWNER=did:plc:alice-did
300+HOLD_PUBLIC=false # Private (only you can pull)
301+HOLD_ALLOW_ALL_CREW=false # Only you can push
302+HOLD_DATABASE_DIR=/var/lib/atcr-hold
303+304+# S3 storage
305+STORAGE_DRIVER=s3
306+AWS_ACCESS_KEY_ID=your-key
307+AWS_SECRET_ACCESS_KEY=your-secret
308+S3_BUCKET=alice-container-registry
309+S3_ENDPOINT=https://gateway.storjshare.io # Using Storj
310+```
311+312+### Shared Hold (Team/Organization)
313+314+Shared storage for a team with crew members:
315+316+```bash
317+# Hold config
318+HOLD_PUBLIC_URL=https://hold.acme.corp
319+HOLD_OWNER=did:plc:acme-org-did
320+HOLD_PUBLIC=false # Private reads (crew only)
321+HOLD_ALLOW_ALL_CREW=false # Explicit crew membership required
322+HOLD_DATABASE_DIR=/var/lib/atcr-hold
323+324+# S3 storage
325+STORAGE_DRIVER=s3
326+AWS_ACCESS_KEY_ID=your-key
327+AWS_SECRET_ACCESS_KEY=your-secret
328+S3_BUCKET=acme-registry-blobs
329+```
330+331+Then add crew members via XRPC or hold PDS records.
332+333+### Public Hold (Community Registry)
334+335+Open storage allowing anyone to push and pull:
336+337+```bash
338+# Hold config
339+HOLD_PUBLIC_URL=https://hold.community.io
340+HOLD_OWNER=did:plc:community-did
341+HOLD_PUBLIC=true # Public reads (anyone can pull)
342+HOLD_ALLOW_ALL_CREW=true # Any authenticated user can push
343+HOLD_DATABASE_DIR=/var/lib/atcr-hold
344+345+# S3 storage
346+STORAGE_DRIVER=s3
347+AWS_ACCESS_KEY_ID=your-key
348+AWS_SECRET_ACCESS_KEY=your-secret
349+S3_BUCKET=community-registry-blobs
350+```
351+352+### Development/Testing
353+354+Local filesystem storage for testing:
355+356+```bash
357+# Hold config
358+HOLD_PUBLIC_URL=http://127.0.0.1:8080
359+HOLD_OWNER=did:plc:your-test-did
360+HOLD_PUBLIC=true
361+HOLD_ALLOW_ALL_CREW=true
362+HOLD_DATABASE_DIR=/tmp/atcr-hold
363+364+# Filesystem storage
365+STORAGE_DRIVER=filesystem
366+STORAGE_ROOT_DIR=/tmp/atcr-hold-blobs
367+```
368+369+## Production Deployment
370+371+For production deployments with:
372+- SSL/TLS certificates
373+- S3 storage with presigned URLs
374+- Proper access control
375+- Systemd service files
376+- Monitoring
377+378+See **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** for comprehensive production deployment guide.
379+380+### Quick Production Checklist
381+382+Before going to production:
383+384+- [ ] Set `HOLD_PUBLIC_URL` to your public HTTPS URL
385+- [ ] Set `HOLD_OWNER` to your ATProto DID
386+- [ ] Configure S3 storage (`STORAGE_DRIVER=s3`)
387+- [ ] Set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET`, `S3_ENDPOINT`
388+- [ ] Set `HOLD_DATABASE_DIR` to persistent directory
389+- [ ] Configure `HOLD_PUBLIC` and `HOLD_ALLOW_ALL_CREW` for desired access model
390+- [ ] Configure SSL/TLS termination (Caddy/nginx/Cloudflare)
391+- [ ] Verify DID document: `curl https://hold.example.com/.well-known/did.json`
392+- [ ] Test presigned URLs: Check logs for "presigned URL" messages during push
393+- [ ] Monitor crew membership: `curl https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo={holdDID}&collection=io.atcr.hold.crew`
394+- [ ] (Optional) Enable Bluesky posts: `HOLD_BLUESKY_POSTS_ENABLED=true`
395+- [ ] (Optional) Request relay crawl: `./deploy/request-crawl.sh hold.example.com`
396+397+## Configuration Files Reference
398+399+- **[.env.hold.example](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/.env.hold.example)** - All available environment variables with documentation
400+- **[deploy/.env.prod.template](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/.env.prod.template)** - Production configuration template (includes both AppView and Hold)
401+- **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** - Production deployment guide
402+- **[AppView Documentation](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API server setup
403+- **[BYOS Architecture](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/docs/BYOS.md)** - Bring Your Own Storage technical design