···11+# ATCR Hold Service
22+33+> The storage backend component of ATCR (ATProto Container Registry)
44+55+## Overview
66+77+**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.
88+99+### What Hold Service Does
1010+1111+Hold Service is the storage layer that:
1212+1313+- **Bring Your Own Storage (BYOS)** - Store your own container image layers in your own S3, Storj, Minio, or filesystem
1414+- **Embedded ATProto PDS** - Each hold is a full ATProto user with its own DID, repository, and identity
1515+- **Custom XRPC Endpoints** - OCI-compatible multipart upload endpoints (`io.atcr.hold.*`) for blob operations
1616+- **Presigned URL Generation** - Creates time-limited S3 URLs for direct client-to-storage transfers (~99% bandwidth reduction)
1717+- **Crew Management** - Controls access via captain and crew records stored in the hold's embedded PDS
1818+- **Standard ATProto Sync** - Exposes com.atproto.sync.* endpoints for repository synchronization and firehose
1919+- **Multi-Backend Support** - Works with S3, Storj, Minio, filesystem, Azure, GCS via distribution's driver system
2020+- **Bluesky Integration** - Optional: Posts container image push notifications from the hold's identity to Bluesky
2121+2222+### The ATCR Ecosystem
2323+2424+Hold Service is the **storage backend** of a multi-component architecture:
2525+2626+1. **[AppView](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API + web interface
2727+2. **Hold Service** (this component) - Storage backend with embedded PDS
2828+3. **Credential Helper** - Client-side tool for ATProto OAuth authentication
2929+3030+**Data flow:**
3131+```
3232+Docker Client → AppView (resolves identity) → User's PDS (stores manifest)
3333+ ↓
3434+ Hold Service (generates presigned URL)
3535+ ↓
3636+ S3/Storj/etc. (client uploads/downloads blobs directly)
3737+```
3838+3939+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.
4040+4141+## When to Run Your Own Hold
4242+4343+Most users can push to the default hold at **https://hold01.atcr.io** - you don't need to run your own hold.
4444+4545+**Run your own hold if you want to:**
4646+- Control where your container layer data is stored (own S3 bucket, Storj, etc.)
4747+- Manage access for a team or organization via crew membership
4848+- Reduce bandwidth costs by using presigned URLs for direct S3 transfers
4949+- Run a shared hold for a community or project
5050+- Maintain data sovereignty (keep blobs in specific geographic regions)
5151+5252+**Prerequisites:**
5353+- S3-compatible storage (AWS S3, Storj, Minio, UpCloud, etc.) OR filesystem storage
5454+- (Optional) Domain name with SSL/TLS certificates for production
5555+- ATProto DID for hold owner (get from: `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`)
5656+5757+## Quick Start
5858+5959+### Using Docker Compose
6060+6161+The fastest way to run Hold service with S3 storage:
6262+6363+```bash
6464+# Clone repository
6565+git clone https://tangled.org/@evan.jarrett.net/at-container-registry
6666+cd atcr
6767+6868+# Copy and configure environment
6969+cp .env.hold.example .env.hold
7070+# Edit .env.hold - set HOLD_PUBLIC_URL, HOLD_OWNER, S3 credentials (see Configuration below)
7171+7272+# Start hold service
7373+docker-compose -f docker-compose.hold.yml up -d
7474+7575+# Verify
7676+curl http://localhost:8080/.well-known/did.json
7777+```
7878+7979+### Minimal Configuration
8080+8181+At minimum, you must set:
8282+8383+```bash
8484+# Required: Public URL (generates did:web identity)
8585+HOLD_PUBLIC_URL=https://hold.example.com
8686+8787+# Required: Your ATProto DID (for captain record)
8888+HOLD_OWNER=did:plc:your-did-here
8989+9090+# Required: Storage driver type
9191+STORAGE_DRIVER=s3
9292+9393+# Required for S3: Credentials and bucket
9494+AWS_ACCESS_KEY_ID=your-access-key
9595+AWS_SECRET_ACCESS_KEY=your-secret-key
9696+S3_BUCKET=your-bucket-name
9797+9898+# Recommended: Database directory for embedded PDS
9999+HOLD_DATABASE_DIR=/var/lib/atcr-hold
100100+```
101101+102102+See **Configuration Reference** below for all options.
103103+104104+## Configuration Reference
105105+106106+Hold Service is configured entirely via environment variables. Load them with:
107107+```bash
108108+source .env.hold
109109+./bin/atcr-hold
110110+```
111111+112112+Or via Docker Compose (recommended).
113113+114114+### Server Configuration
115115+116116+#### `HOLD_PUBLIC_URL` ⚠️ REQUIRED
117117+- **Default:** None (required)
118118+- **Description:** Public URL of this hold service. Used to generate the hold's did:web identity. The hostname becomes the hold's DID.
119119+- **Format:** `https://hold.example.com` or `http://127.0.0.1:8080` (development)
120120+- **Example:** `https://hold01.atcr.io` → DID is `did:web:hold01.atcr.io`
121121+- **Note:** This URL must be reachable by AppView and Docker clients
122122+123123+#### `HOLD_SERVER_ADDR`
124124+- **Default:** `:8080`
125125+- **Description:** HTTP listen address for XRPC endpoints
126126+- **Example:** `:8080`, `:9000`, `0.0.0.0:8080`
127127+128128+#### `HOLD_PUBLIC`
129129+- **Default:** `false`
130130+- **Description:** Allow public blob reads (pulls) without authentication. Writes always require crew membership.
131131+- **Use cases:**
132132+ - `true`: Public registry (anyone can pull, authenticated users can push if crew)
133133+ - `false`: Private registry (authentication required for both push and pull)
134134+135135+### Storage Configuration
136136+137137+#### `STORAGE_DRIVER`
138138+- **Default:** `s3`
139139+- **Options:** `s3`, `filesystem`
140140+- **Description:** Storage backend type. S3 enables presigned URLs for direct client-to-storage transfers (~99% bandwidth reduction). Filesystem stores blobs locally (development/testing).
141141+142142+#### S3 Storage (when `STORAGE_DRIVER=s3`)
143143+144144+##### `AWS_ACCESS_KEY_ID` ⚠️ REQUIRED for S3
145145+- **Description:** S3 access key ID for authentication
146146+- **Example:** `AKIAIOSFODNN7EXAMPLE`
147147+148148+##### `AWS_SECRET_ACCESS_KEY` ⚠️ REQUIRED for S3
149149+- **Description:** S3 secret access key for authentication
150150+- **Example:** `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY`
151151+152152+##### `AWS_REGION`
153153+- **Default:** `us-east-1`
154154+- **Description:** S3 region
155155+- **AWS regions:** `us-east-1`, `us-west-2`, `eu-west-1`, etc.
156156+- **UpCloud regions:** `us-chi1`, `us-nyc1`, `de-fra1`, `uk-lon1`, `sg-sin1`
157157+158158+##### `S3_BUCKET` ⚠️ REQUIRED for S3
159159+- **Description:** S3 bucket name where blobs will be stored
160160+- **Example:** `atcr-blobs`, `my-company-registry-blobs`
161161+- **Note:** Bucket must already exist
162162+163163+##### `S3_ENDPOINT`
164164+- **Default:** None (uses AWS S3)
165165+- **Description:** S3-compatible endpoint URL for non-AWS providers
166166+- **Storj:** `https://gateway.storjshare.io`
167167+- **UpCloud:** `https://[bucket-id].upcloudobjects.com`
168168+- **Minio:** `http://minio:9000`
169169+- **Note:** Leave empty for AWS S3
170170+171171+#### Filesystem Storage (when `STORAGE_DRIVER=filesystem`)
172172+173173+##### `STORAGE_ROOT_DIR`
174174+- **Default:** `/var/lib/atcr/hold`
175175+- **Description:** Directory path where blobs will be stored on local filesystem
176176+- **Use case:** Development, testing, or single-server deployments
177177+- **Note:** Presigned URLs are not available with filesystem driver (hold proxies all blob transfers)
178178+179179+### Embedded PDS Configuration
180180+181181+#### `HOLD_DATABASE_DIR`
182182+- **Default:** `/var/lib/atcr-hold`
183183+- **Description:** Directory path for embedded PDS carstore (SQLite database). Carstore creates `db.sqlite3` inside this directory.
184184+- **Note:** This must be a directory path, NOT a file path. If empty, embedded PDS is disabled (not recommended - hold authorization requires PDS).
185185+186186+#### `HOLD_KEY_PATH`
187187+- **Default:** `{HOLD_DATABASE_DIR}/signing.key`
188188+- **Description:** Path to hold's signing key (secp256k1). Auto-generated on first run if missing.
189189+- **Note:** Keep this secure - it's used to sign ATProto commits in the hold's repository
190190+191191+### Access Control
192192+193193+#### `HOLD_OWNER`
194194+- **Default:** None
195195+- **Description:** Your ATProto DID. Used to create the captain record and add you as the first crew member with admin role.
196196+- **Get your DID:** `https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle=yourhandle.bsky.social`
197197+- **Example:** `did:plc:abc123xyz789`
198198+- **Note:** If set, the hold will initialize with your DID as owner on first run
199199+200200+#### `HOLD_ALLOW_ALL_CREW`
201201+- **Default:** `false`
202202+- **Description:** Allow any authenticated ATCR user to write to this hold (treat all as crew)
203203+- **Security model:**
204204+ - `true`: Any authenticated user can push images (useful for shared/community holds)
205205+ - `false`: Only hold owner and explicit crew members can push (verified via crew records in hold's PDS)
206206+- **Use cases:**
207207+ - Public registry: `HOLD_PUBLIC=true, HOLD_ALLOW_ALL_CREW=true`
208208+ - ATProto users only: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=true`
209209+ - Private hold: `HOLD_PUBLIC=false, HOLD_ALLOW_ALL_CREW=false` (default)
210210+211211+### Bluesky Integration
212212+213213+#### `HOLD_BLUESKY_POSTS_ENABLED`
214214+- **Default:** `false`
215215+- **Description:** Create Bluesky posts when users push container images. Posts include image name, tag, size, and layer count.
216216+- **Note:** Posts are created from the hold's embedded PDS identity (did:web). Requires hold to be crawled by Bluesky relay.
217217+- **Enable relay crawl:** `./deploy/request-crawl.sh hold.example.com`
218218+219219+#### `HOLD_PROFILE_AVATAR`
220220+- **Default:** `https://imgs.blue/evan.jarrett.net/1TpTOdtS60GdJWBYEqtK22y688jajbQ9a5kbYRFtwuqrkBAE`
221221+- **Description:** URL to download avatar image for hold's Bluesky profile. Downloaded and uploaded as blob during bootstrap.
222222+- **Note:** Avatar is stored in hold's PDS and displayed on Bluesky profile
223223+224224+### Advanced Configuration
225225+226226+#### `TEST_MODE`
227227+- **Default:** `false`
228228+- **Description:** Enable test mode (skips some validations). Do not use in production.
229229+230230+#### `DISABLE_PRESIGNED_URLS`
231231+- **Default:** `false`
232232+- **Description:** Force proxy mode even with S3 configured (for testing). Disables presigned URL generation and routes all blob transfers through the hold service.
233233+- **Use case:** Testing, debugging, or environments where presigned URLs don't work
234234+235235+## XRPC Endpoints
236236+237237+Hold Service exposes two types of XRPC endpoints:
238238+239239+### ATProto Sync Endpoints (Standard)
240240+- `GET /.well-known/did.json` - DID document (did:web resolution)
241241+- `GET /xrpc/com.atproto.sync.getRepo` - Download full repository as CAR file
242242+- `GET /xrpc/com.atproto.sync.getBlob` - Get blob or presigned download URL
243243+- `GET /xrpc/com.atproto.sync.subscribeRepos` - WebSocket firehose for real-time events
244244+- `GET /xrpc/com.atproto.sync.listRepos` - List all repositories (single-user PDS)
245245+- `GET /xrpc/com.atproto.repo.describeRepo` - Repository metadata
246246+- `GET /xrpc/com.atproto.repo.getRecord` - Get record by collection and rkey
247247+- `GET /xrpc/com.atproto.repo.listRecords` - List records in collection
248248+- `POST /xrpc/com.atproto.repo.deleteRecord` - Delete record (owner/crew admin only)
249249+250250+### OCI Multipart Upload Endpoints (Custom)
251251+- `POST /xrpc/io.atcr.hold.initiateUpload` - Start multipart upload session
252252+- `POST /xrpc/io.atcr.hold.getPartUploadUrl` - Get presigned URL for uploading a part
253253+- `PUT /xrpc/io.atcr.hold.uploadPart` - Direct buffered part upload (alternative to presigned URLs)
254254+- `POST /xrpc/io.atcr.hold.completeUpload` - Finalize multipart upload
255255+- `POST /xrpc/io.atcr.hold.abortUpload` - Cancel multipart upload
256256+- `POST /xrpc/io.atcr.hold.notifyManifest` - Notify hold of manifest upload (creates layer records, Bluesky posts)
257257+258258+## Authorization Model
259259+260260+Hold Service uses crew membership records in its embedded PDS for access control:
261261+262262+### Read Access (Blob Downloads)
263263+264264+**Public Hold** (`HOLD_PUBLIC=true`):
265265+- Anonymous users: ✅ Allowed
266266+- Authenticated users: ✅ Allowed
267267+268268+**Private Hold** (`HOLD_PUBLIC=false`):
269269+- Anonymous users: ❌ Forbidden
270270+- Authenticated users with crew membership: ✅ Allowed
271271+- Crew must have `blob:read` permission
272272+273273+### Write Access (Blob Uploads)
274274+275275+Regardless of `HOLD_PUBLIC` setting:
276276+- Hold owner (from captain record): ✅ Allowed
277277+- Crew members with `blob:write` permission: ✅ Allowed
278278+- Non-crew authenticated users: Depends on `HOLD_ALLOW_ALL_CREW`
279279+ - `HOLD_ALLOW_ALL_CREW=true`: ✅ Allowed
280280+ - `HOLD_ALLOW_ALL_CREW=false`: ❌ Forbidden
281281+282282+### Authentication Method
283283+284284+AppView uses **service tokens** from user's PDS to authenticate with hold service:
285285+1. AppView calls user's PDS: `com.atproto.server.getServiceAuth` with hold DID
286286+2. User's PDS returns a service token scoped to the hold DID
287287+3. AppView includes service token in XRPC requests to hold
288288+4. Hold validates token and checks crew membership in its embedded PDS
289289+290290+## Deployment Scenarios
291291+292292+### Personal Hold (Single User)
293293+294294+Your own storage for your images:
295295+296296+```bash
297297+# Hold config
298298+HOLD_PUBLIC_URL=https://hold.alice.com
299299+HOLD_OWNER=did:plc:alice-did
300300+HOLD_PUBLIC=false # Private (only you can pull)
301301+HOLD_ALLOW_ALL_CREW=false # Only you can push
302302+HOLD_DATABASE_DIR=/var/lib/atcr-hold
303303+304304+# S3 storage
305305+STORAGE_DRIVER=s3
306306+AWS_ACCESS_KEY_ID=your-key
307307+AWS_SECRET_ACCESS_KEY=your-secret
308308+S3_BUCKET=alice-container-registry
309309+S3_ENDPOINT=https://gateway.storjshare.io # Using Storj
310310+```
311311+312312+### Shared Hold (Team/Organization)
313313+314314+Shared storage for a team with crew members:
315315+316316+```bash
317317+# Hold config
318318+HOLD_PUBLIC_URL=https://hold.acme.corp
319319+HOLD_OWNER=did:plc:acme-org-did
320320+HOLD_PUBLIC=false # Private reads (crew only)
321321+HOLD_ALLOW_ALL_CREW=false # Explicit crew membership required
322322+HOLD_DATABASE_DIR=/var/lib/atcr-hold
323323+324324+# S3 storage
325325+STORAGE_DRIVER=s3
326326+AWS_ACCESS_KEY_ID=your-key
327327+AWS_SECRET_ACCESS_KEY=your-secret
328328+S3_BUCKET=acme-registry-blobs
329329+```
330330+331331+Then add crew members via XRPC or hold PDS records.
332332+333333+### Public Hold (Community Registry)
334334+335335+Open storage allowing anyone to push and pull:
336336+337337+```bash
338338+# Hold config
339339+HOLD_PUBLIC_URL=https://hold.community.io
340340+HOLD_OWNER=did:plc:community-did
341341+HOLD_PUBLIC=true # Public reads (anyone can pull)
342342+HOLD_ALLOW_ALL_CREW=true # Any authenticated user can push
343343+HOLD_DATABASE_DIR=/var/lib/atcr-hold
344344+345345+# S3 storage
346346+STORAGE_DRIVER=s3
347347+AWS_ACCESS_KEY_ID=your-key
348348+AWS_SECRET_ACCESS_KEY=your-secret
349349+S3_BUCKET=community-registry-blobs
350350+```
351351+352352+### Development/Testing
353353+354354+Local filesystem storage for testing:
355355+356356+```bash
357357+# Hold config
358358+HOLD_PUBLIC_URL=http://127.0.0.1:8080
359359+HOLD_OWNER=did:plc:your-test-did
360360+HOLD_PUBLIC=true
361361+HOLD_ALLOW_ALL_CREW=true
362362+HOLD_DATABASE_DIR=/tmp/atcr-hold
363363+364364+# Filesystem storage
365365+STORAGE_DRIVER=filesystem
366366+STORAGE_ROOT_DIR=/tmp/atcr-hold-blobs
367367+```
368368+369369+## Production Deployment
370370+371371+For production deployments with:
372372+- SSL/TLS certificates
373373+- S3 storage with presigned URLs
374374+- Proper access control
375375+- Systemd service files
376376+- Monitoring
377377+378378+See **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** for comprehensive production deployment guide.
379379+380380+### Quick Production Checklist
381381+382382+Before going to production:
383383+384384+- [ ] Set `HOLD_PUBLIC_URL` to your public HTTPS URL
385385+- [ ] Set `HOLD_OWNER` to your ATProto DID
386386+- [ ] Configure S3 storage (`STORAGE_DRIVER=s3`)
387387+- [ ] Set `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_BUCKET`, `S3_ENDPOINT`
388388+- [ ] Set `HOLD_DATABASE_DIR` to persistent directory
389389+- [ ] Configure `HOLD_PUBLIC` and `HOLD_ALLOW_ALL_CREW` for desired access model
390390+- [ ] Configure SSL/TLS termination (Caddy/nginx/Cloudflare)
391391+- [ ] Verify DID document: `curl https://hold.example.com/.well-known/did.json`
392392+- [ ] Test presigned URLs: Check logs for "presigned URL" messages during push
393393+- [ ] Monitor crew membership: `curl https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo={holdDID}&collection=io.atcr.hold.crew`
394394+- [ ] (Optional) Enable Bluesky posts: `HOLD_BLUESKY_POSTS_ENABLED=true`
395395+- [ ] (Optional) Request relay crawl: `./deploy/request-crawl.sh hold.example.com`
396396+397397+## Configuration Files Reference
398398+399399+- **[.env.hold.example](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/.env.hold.example)** - All available environment variables with documentation
400400+- **[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)
401401+- **[deploy/README.md](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/deploy/README.md)** - Production deployment guide
402402+- **[AppView Documentation](https://atcr.io/r/evan.jarrett.net/atcr-appview)** - Registry API server setup
403403+- **[BYOS Architecture](https://tangled.org/@evan.jarrett.net/at-container-registry/blob/main/docs/BYOS.md)** - Bring Your Own Storage technical design