# Bring Your Own Storage (BYOS) ## Overview ATCR supports "Bring Your Own Storage" (BYOS) for blob storage. This allows users to: - Deploy their own storage service backed by S3/Storj/Minio/filesystem - Control who can use their storage (public or private) - Keep blob data in their own infrastructure while manifests remain in their ATProto PDS ## Architecture ``` ┌─────────────────────────────────────────────┐ │ ATCR AppView (API) │ │ - Manifests → ATProto PDS │ │ - Auth & token validation │ │ - Blob routing (issues redirects) │ │ - Profile management │ └─────────────────┬───────────────────────────┘ │ │ Hold discovery priority: │ 1. io.atcr.sailor.profile.defaultHold │ 2. io.atcr.hold records │ 3. AppView default_storage_endpoint ▼ ┌─────────────────────────────────────────────┐ │ User's PDS │ │ - io.atcr.sailor.profile (hold preference) │ │ - io.atcr.hold records (own holds) │ │ - io.atcr.manifest records (with holdEP) │ └─────────────────┬───────────────────────────┘ │ │ Redirects to hold ▼ ┌─────────────────────────────────────────────┐ │ Storage Service (Hold) │ │ - Blob storage (S3/Storj/Minio/filesystem) │ │ - Presigned URL generation │ │ - Authorization (DID-based) │ └─────────────────────────────────────────────┘ ``` ## ATProto Records ### io.atcr.sailor.profile **NEW:** User profile for hold selection preferences. Created automatically on first authentication. ```json { "$type": "io.atcr.sailor.profile", "defaultHold": "https://team-hold.example.com", "createdAt": "2025-10-02T12:00:00Z", "updatedAt": "2025-10-02T12:00:00Z" } ``` **Record key:** Always `"self"` (only one profile per user) **Behavior:** - Created automatically when user first authenticates (OAuth or Basic Auth) - If AppView has `default_storage_endpoint`, profile gets that as initial `defaultHold` - User can update to join shared holds or use their own hold - Set `defaultHold` to `null` to opt out of defaults (use own hold or AppView default) **This solves the multi-hold problem:** Users who are crew members of multiple holds can explicitly choose which one to use via their profile. ### io.atcr.hold Users create a hold record in their PDS to configure their own storage: ```json { "$type": "io.atcr.hold", "endpoint": "https://alice-storage.example.com", "owner": "did:plc:alice123", "public": false, "createdAt": "2025-10-01T12:00:00Z" } ``` ### io.atcr.hold.crew Hold owners can add crew members (for shared storage): ```json { "$type": "io.atcr.hold.crew", "hold": "at://did:plc:alice/io.atcr.hold/my-storage", "member": "did:plc:bob456", "role": "write", "addedAt": "2025-10-01T12:00:00Z" } ``` **Note:** Crew records are stored in the **hold owner's PDS**, not the crew member's PDS. This ensures the hold owner maintains full control over access. ## Storage Service ### Deployment The storage service is a lightweight HTTP server that: 1. Accepts presigned URL requests 2. Verifies DID authorization 3. Generates presigned URLs for S3/Storj/etc 4. Returns URLs to AppView for client redirect ### Configuration The hold service is configured entirely via environment variables. See `.env.example` for all options. **Required environment variables:** ```bash # Hold service public URL (REQUIRED) HOLD_PUBLIC_URL=https://storage.example.com # Storage driver type STORAGE_DRIVER=s3 # For S3/Minio AWS_ACCESS_KEY_ID=your_access_key AWS_SECRET_ACCESS_KEY=your_secret_key AWS_REGION=us-east-1 S3_BUCKET=my-blobs # For Storj (optional - custom S3 endpoint) # S3_ENDPOINT=https://gateway.storjshare.io # For filesystem storage # STORAGE_DRIVER=filesystem # STORAGE_ROOT_DIR=/var/lib/atcr-storage ``` **Authorization:** ATCR follows ATProto's public-by-default model with gated anonymous access: **Read Access:** - **Public hold** (`HOLD_PUBLIC=true`): Anonymous reads allowed (no authentication) - **Private hold** (`HOLD_PUBLIC=false`): Requires authentication (any ATCR user with sailor.profile) **Write Access:** - Always requires authentication - Must be hold owner OR crew member (verified via `io.atcr.hold.crew` records in owner's PDS) **Key Points:** - "Private" just means "no anonymous access" - not "limited user access" - Any authenticated ATCR user can read from private holds - Crew membership only controls WRITE access, not READ access - This aligns with ATProto's public records model (no private PDS records yet) ### Running ```bash # Build go build -o atcr-hold ./cmd/hold # Set environment variables (or use .env file) export HOLD_PUBLIC_URL=https://storage.example.com export STORAGE_DRIVER=s3 export AWS_ACCESS_KEY_ID=... export AWS_SECRET_ACCESS_KEY=... export AWS_REGION=us-east-1 export S3_BUCKET=my-blobs # Run ./atcr-hold ``` **Registration (required):** The hold service must be registered in a PDS to be discoverable by the AppView. **Standard registration workflow:** 1. Set `HOLD_OWNER` to your DID: ```bash export HOLD_OWNER=did:plc:your-did-here ``` 2. Start the hold service: ```bash ./atcr-hold ``` 3. **Check the logs** for the OAuth authorization URL: ``` ================================================================================ OAUTH AUTHORIZATION REQUIRED ================================================================================ Please visit this URL to authorize the hold service: https://bsky.app/authorize?client_id=... Waiting for authorization... ================================================================================ ``` 4. Visit the URL in your browser and authorize 5. The hold service will: - Exchange the authorization code for a token - Create `io.atcr.hold` record in your PDS - Create `io.atcr.hold.crew` record (making you the owner) - Save registration state 6. On subsequent runs, the service checks if already registered and skips OAuth **Alternative methods:** - **Manual API registration**: Call `POST /register` with your own OAuth token - **Completely manual**: Create PDS records yourself using any ATProto client ### Deploy to Fly.io ```bash # Create fly.toml cat > fly.toml <