# Bring Your Own Storage (BYOS) ## Overview ATCR supports "Bring Your Own Storage" (BYOS) for blob storage. Users can: - Deploy their own hold service with embedded PDS - Control access via crew membership in the hold's PDS - Keep blob data in their own S3-compatible storage (AWS S3, Storj, Minio, UpCloud, etc.) while manifests stay in their user PDS ## Architecture ``` ┌──────────────────────────────────────────┐ │ ATCR AppView (API) │ │ - Manifests → User's PDS │ │ - Auth & service token management │ │ - Blob routing via XRPC │ │ - Profile management │ └────────────┬─────────────────────────────┘ │ │ Hold discovery (findHoldDIDAndProfile): │ 1. io.atcr.sailor.profile.defaultHold (DID) │ 2. AppView default hold (server.managed_holds[0]) │ │ Then resolveSuccessor: if the chosen hold's │ captain record sets a successor DID, apply a │ single-hop redirect to it (hold migration). ▼ ┌──────────────────────────────────────────┐ │ User's PDS │ │ - io.atcr.sailor.profile (hold DID) │ │ - io.atcr.manifest (with holdDid) │ └────────────┬─────────────────────────────┘ │ │ Service token from user's PDS ▼ ┌──────────────────────────────────────────┐ │ Hold Service (did:web:hold.example.com) │ │ ├── Embedded PDS │ │ │ ├── Captain record (ownership) │ │ │ └── Crew records (access control) │ │ ├── XRPC multipart upload endpoints │ │ └── Storage driver (S3/Storj/etc.) │ └──────────────────────────────────────────┘ ``` ## Hold Service Components Each hold is a full ATProto actor with: - **DID**: `did:web:hold.example.com` (hold's identity) - **Embedded PDS**: Stores captain + crew records (shared data) - **Storage backend**: S3-compatible (AWS S3, Storj, Minio, UpCloud, etc.) - **XRPC endpoints**: Standard ATProto + custom OCI multipart upload ### Records in Hold's PDS **Captain record** (`io.atcr.hold.captain/self`): ```json { "$type": "io.atcr.hold.captain", "owner": "did:plc:alice123", "public": false, "allowAllCrew": false, "enableBlueskyPosts": false, "deployedAt": "2025-10-14T...", "region": "iad", "successor": "" } ``` `region` and `successor` are optional. `successor` holds the DID of a replacement hold; when set, the AppView applies a single-hop redirect to it during hold discovery (see the Architecture diagram above). **Crew records** (`io.atcr.hold.crew/{rkey}`): ```json { "$type": "io.atcr.hold.crew", "member": "did:plc:bob456", "role": "captain", "permissions": ["blob:read", "blob:write"], "tier": "bosun", "plankowner": false, "addedAt": "2025-10-14T..." } ``` Authorization is driven by the `permissions` array (`blob:read`, `blob:write`, `crew:admin`), not the `role` string. `blob:write` implicitly grants `blob:read` (you can't push without being able to pull). `tier` and `plankowner` are optional and feed quota limits. ### Sailor Profile (User's PDS) Users set their preferred hold in their sailor profile: ```json { "$type": "io.atcr.sailor.profile", "defaultHold": "did:web:hold.example.com", "createdAt": "2025-10-02T...", "updatedAt": "2025-10-02T..." } ``` ## Deployment ### Configuration The hold service is configured with Viper: a YAML file is the primary source, and environment variables override individual fields. Env var names are `HOLD_` plus the YAML path with `_` separators (e.g. `server.public_url` → `HOLD_SERVER_PUBLIC_URL`). S3 credentials use the standard AWS names. Generate a fully commented config and run with it: ```bash ./bin/atcr-hold config init config-hold.yaml # edit config-hold.yaml, then: ./bin/atcr-hold serve --config config-hold.yaml ``` Key fields (YAML on the left, env override on the right): ```yaml server: public_url: https://hold.example.com # HOLD_SERVER_PUBLIC_URL (REQUIRED) public: false # HOLD_SERVER_PUBLIC (allow anonymous reads) registration: owner_did: did:plc:your-did-here # HOLD_REGISTRATION_OWNER_DID allow_all_crew: false # HOLD_REGISTRATION_ALLOW_ALL_CREW database: path: /var/lib/atcr-hold # HOLD_DATABASE_PATH (carstore + SQLite) key_path: "" # HOLD_DATABASE_KEY_PATH (defaults to {path}/signing.key) storage: bucket: my-blobs # S3_BUCKET (REQUIRED) region: us-east-1 # AWS_REGION endpoint: "" # S3_ENDPOINT (for non-AWS providers) ``` S3 credentials are read from the standard AWS env vars (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`). ### Running Locally For local development, use Minio as an S3-compatible storage: ```bash # Start Minio (in separate terminal) docker run -p 9000:9000 -p 9001:9001 minio/minio server /data --console-address ":9001" # Build go build -o bin/atcr-hold ./cmd/hold # Run (env overrides shown; a YAML config works too) export HOLD_SERVER_PUBLIC_URL=http://localhost:8080 export HOLD_REGISTRATION_OWNER_DID=did:plc:your-did-here export AWS_ACCESS_KEY_ID=minioadmin export AWS_SECRET_ACCESS_KEY=minioadmin export S3_BUCKET=test export S3_ENDPOINT=http://localhost:9000 export HOLD_DATABASE_PATH=/tmp/atcr-hold ./bin/atcr-hold serve ``` On first run, the hold service creates: - Captain record in embedded PDS (making you the owner) - Crew record for owner with all permissions - DID document at `/.well-known/did.json` ### Deploy to Fly.io ```bash # Create fly.toml cat > fly.toml <