Hold Crew Access Control#
Overview#
ATCR uses crew-based access control for hold (storage) services. Crew records are stored in the hold's embedded PDS (not the owner's or user's PDS), making the hold a self-contained ATProto actor with its own access control.
Current Implementation#
Records in Hold's PDS#
Captain record - Hold ownership (single record at io.atcr.hold.captain/self):
{
"$type": "io.atcr.hold.captain",
"owner": "did:plc:alice123",
"public": false,
"deployedAt": "2025-10-14T...",
"region": "iad",
"provider": "fly.io"
}
Crew records - Access control (one per member at io.atcr.hold.crew/{rkey}):
{
"$type": "io.atcr.hold.crew",
"member": "did:plc:bob456",
"role": "admin",
"permissions": ["blob:read", "blob:write"],
"addedAt": "2025-10-14T..."
}
Authorization Logic#
Write authorization follows this priority:
isAuthorizedWrite(userDID):
1. If userDID == captain.owner → ALLOW
2. If crew record exists for userDID → ALLOW
3. Default → DENY
Read authorization depends on HOLD_PUBLIC setting:
- Public hold (
HOLD_PUBLIC=true): Anonymous + all authenticated users can read - Private hold (
HOLD_PUBLIC=false): Requires crew membership for reads
Configuration#
# Access control environment variables
HOLD_PUBLIC=false # Require authentication for reads
HOLD_ALLOW_ALL_CREW=false # Only explicit crew members can write
Crew Management#
Crew records are managed by the hold captain (owner) using standard ATProto operations on the hold's embedded PDS:
Add crew member:
# Via hold's PDS (requires captain's OAuth)
atproto put-record \
--pds https://hold.example.com \
--collection io.atcr.hold.crew \
--rkey "{memberDID}" \
--value '{
"$type": "io.atcr.hold.crew",
"member": "did:plc:bob456",
"role": "admin",
"permissions": ["blob:read", "blob:write"],
"addedAt": "2025-10-14T12:00:00Z"
}'
Remove crew member:
atproto delete-record \
--pds https://hold.example.com \
--collection io.atcr.hold.crew \
--rkey "{memberDID}"
List crew members:
# Via XRPC
GET https://hold.example.com/xrpc/com.atproto.repo.listRecords?repo={holdDID}&collection=io.atcr.hold.crew
Authentication Flow#
1. User pushes image to atcr.io/alice/myapp
2. AppView gets service token from alice's PDS:
GET /xrpc/com.atproto.server.getServiceAuth?aud={holdDID}
Response: { "token": "..." }
3. AppView calls hold with service token:
POST /xrpc/io.atcr.hold.initiateUpload
Authorization: Bearer {serviceToken}
4. Hold validates service token:
- Checks token is from alice's PDS
- Extracts alice's DID from token
5. Hold checks crew membership:
- Queries its own PDS: com.atproto.repo.getRecord
- Collection: io.atcr.hold.crew
- Record key: alice's DID
6. If crew record found → allow upload
Else → deny with 403 Forbidden
Trust model: "Trust but verify"
- User OAuth'd to AppView (proves identity)
- Service token from user's PDS (proves AppView is acting on behalf of user)
- Crew record in hold's PDS (proves user has access to this hold)
Use Cases#
1. Personal Hold (Private)#
# Owner only
HOLD_PUBLIC=false
HOLD_ALLOW_ALL_CREW=false
# No additional crew records needed - captain has implicit access
2. Team Hold (Shared)#
# Multiple team members
HOLD_PUBLIC=false
HOLD_ALLOW_ALL_CREW=false
# Captain adds crew members:
# - did:plc:alice (admin)
# - did:plc:bob (member)
# - did:plc:charlie (member)
3. Public Hold (Community)#
# Allow any authenticated user (TODO: Implement HOLD_ALLOW_ALL_CREW)
HOLD_PUBLIC=true
HOLD_ALLOW_ALL_CREW=true
Planned Features#
Pattern-Based Access Control#
Status: Planned but not yet implemented.
Concept: Allow crew records with pattern matching instead of explicit DIDs:
{
"$type": "io.atcr.hold.crew",
"memberPattern": "*.example.com",
"role": "write"
}
Use cases:
"*"- Allow all authenticated users"*.company.com"- Allow all users from company domain"*.community.social"- Allow all community members
Implementation needed:
- Add
memberPatternfield to crew record schema (makememberoptional) - Add handle resolution (DID → handle lookup)
- Add pattern matching logic
- Update authorization to check patterns
Barred List (Access Revocation)#
Status: Planned but not yet implemented.
Concept: Explicit deny list that overrides crew membership:
{
"$type": "io.atcr.hold.crew.barred",
"member": "did:plc:former-employee",
"reason": "No longer with company",
"barredAt": "2025-10-13T12:00:00Z"
}
Priority: Barred list checked before crew list.
HOLD_ALLOW_ALL_CREW#
Status: Environment variable exists but full implementation pending.
Concept: Automatically create/manage wildcard crew record via env var:
HOLD_ALLOW_ALL_CREW=true # Creates crew record with memberPattern: "*"
Implementation needed:
- Auto-create wildcard crew record on startup if env=true
- Auto-delete wildcard crew record if env changes to false
- Use well-known rkey "allow-all" for managed record
Architecture Notes#
Why Hold's Embedded PDS?#
Key insight: Crew records are shared data about the hold, not user-specific data.
Benefits:
- Self-contained: Hold is independent ATProto actor
- Portable: Hold can move without coordinating with user PDSs
- Discoverable: Query hold's PDS to see who has access
- Standard: Uses normal ATProto sync endpoints (subscribeRepos, getRecord, listRecords)
Comparison:
- User's PDS: Stores user-specific data (manifests, sailor profile)
- Hold's PDS: Stores hold-specific data (captain, crew, configuration)
- Clear separation of concerns
Security Considerations#
- Public Records: Crew records are public (anyone can see who has access to a hold)
- Service Tokens: Hold trusts user's PDS to issue valid service tokens
- DID-Based: Crew membership is DID-based (permanent), not handle-based
- Captain Control: Only captain can modify crew records (via OAuth to hold's PDS)
Future Improvements#
- Crew management UI - Web interface for adding/removing crew members
- Pattern-based matching - Implement
memberPatternfield - Barred list - Implement access revocation
- Role-based permissions - Fine-grained permissions beyond read/write
- Temporary access - Time-limited crew membership (
expiresAtfield) - Audit logging - Track access grants/denials
References#
- EMBEDDED_PDS.md - Embedded PDS architecture details
- BYOS.md - BYOS deployment and usage
- ATProto Lexicon Spec