Delegations allow you to grant other users or services access to your space. They enable secure, scoped, and time-limited sharing of data and capabilities.
What Delegations Enable
- Share data: Let other users read or write to paths in your space
- Grant permissions: Allow services to perform actions on your behalf
- Chain access: Recipients can create sub-delegations to others
- Time-bound access: Delegations automatically expire
Creating a Delegation
Grant another user access to your space.
Critical: Always use pkhDid (PKH DID format) for the delegateDID parameter, NOT the did property. Using the wrong DID format will cause server validation to fail.
// Alice grants Bob read access to her documents
const delegation = await tc.storage.delegate({
delegateDID: bobPkhDid, // Use PKH DID!
actions: ['read'],
paths: ['documents/*'],
expiresIn: '7d',
});
// Send delegation to Bob
console.log(delegation.portable); // Serialized for transport
// Grant full access to a specific path
const delegation = await tc.createDelegation({
delegateDID: recipient.pkhDid,
actions: ['read', 'write', 'delete'],
paths: ['shared/*'],
});
const delegation = await tc.createDelegation({
// The recipient's PKH DID (did:pkh:eip155:{chainId}:{address})
delegateDID: recipient.pkhDid,
// Actions the recipient can perform
// Options: 'read', 'write', 'delete', 'list'
actions: ['read', 'write', 'delete'],
// Path patterns the delegation covers
// Use * for wildcards: 'docs/*' matches 'docs/file1', 'docs/folder/file2'
paths: ['shared/*'],
// When the delegation expires (optional)
// Formats: '1h', '7d', '30d', or ISO date string
expiresIn: '7d',
// Earliest time the delegation becomes valid (optional)
notBefore: new Date().toISOString(),
});
Using a Received Delegation
When you receive a delegation, use it to access another user’s space.
// Bob received a delegation from Alice
const portableDelegation = /* received from Alice */;
// Load the delegation
await tc.storage.useDelegation(portableDelegation);
// Now Bob can access Alice's documents
const docs = await tc.storage.list('documents/', {
space: aliceSpaceId,
});
Sub-Delegations
Recipients can further delegate their access to others, with constraints.
// Bob creates a sub-delegation for Carol
// Bob can only delegate permissions he has
const subDelegation = await tc.storage.delegate({
delegateDID: carolPkhDid,
actions: ['read'], // Must be subset of Bob's actions
paths: ['documents/public/*'], // Must be within Bob's paths
expiresIn: '1d', // Must expire before Bob's delegation
});
Delegation Constraints
Delegations must follow these rules:
| Constraint | Rule |
|---|
| Actions | Must be a subset of parent’s actions |
| Paths | Must be within parent’s path scope |
| Expiry | Must expire at or before parent’s expiry |
| Not Before | Must be at or after parent’s not_before |
// Parent delegation: read + write to 'data/*', expires in 7 days
// Valid sub-delegation
await tc.createSubDelegation({
parentDelegation,
delegateDID: recipient.pkhDid,
actions: ['read'], // Subset of [read, write]
paths: ['data/public/*'], // Within data/*
expiresIn: '3d', // Before 7 days
});
// Invalid - would be rejected
await tc.createSubDelegation({
parentDelegation,
delegateDID: recipient.pkhDid,
actions: ['read', 'delete'], // 'delete' not in parent
paths: ['other/*'], // Outside parent scope
expiresIn: '30d', // After parent expires
});
Delegations are serialized as PortableDelegation for transport between users.
// Create delegation
const delegation = await tc.createDelegation({
delegateDID: recipient.pkhDid,
actions: ['read'],
paths: ['shared/*'],
});
// Serialize for transport (JSON string)
const portable = delegation.portable;
// Send via any channel: API, email, QR code, etc.
sendToRecipient(portable);
// Recipient loads it
const received = await tc.useDelegation(portable);
The portable format is a JSON string containing the signed delegation chain, which can be verified cryptographically by the server.
Understanding DID formats is critical for delegations to work correctly.
TinyCloud uses two DID formats:
| Format | Property | Example | Use Case |
|---|
| PKH DID | pkhDid | did:pkh:eip155:1:0x1234... | Delegations, user identity |
| Key DID | did | did:key:z6Mk...#z6Mk... | Internal session keys |
// Correct - use PKH DID for delegations
await tc.createDelegation({
delegateDID: recipient.pkhDid, // did:pkh:eip155:1:0x...
});
// Wrong - session key DID will fail
await tc.createDelegation({
delegateDID: recipient.did, // did:key:z6Mk...
});
Complete Example
Alice shares a document folder with Bob, who then shares a subset with Carol.
Alice creates delegation for Bob
// Alice's client
const aliceToBob = await alice.createDelegation({
delegateDID: bob.pkhDid,
actions: ['read', 'write'],
paths: ['documents/*'],
expiresIn: '30d',
});
// Send to Bob
sendToUser(bob.address, aliceToBob.portable);
Bob receives and uses the delegation
// Bob's client
const received = await bob.useDelegation(aliceToBobPortable);
// Bob can now read Alice's documents
const files = await bob.kv.list('documents/', {
space: alice.spaceId,
});
Bob creates sub-delegation for Carol
// Bob sub-delegates read-only access to Carol
const bobToCarol = await bob.createSubDelegation({
parentDelegation: received,
delegateDID: carol.pkhDid,
actions: ['read'], // Only read, not write
paths: ['documents/public/*'], // Narrower scope
expiresIn: '7d', // Shorter duration
});
sendToUser(carol.address, bobToCarol.portable);
Carol accesses the data
// Carol's client
await carol.useDelegation(bobToCarolPortable);
// Carol can read Alice's public documents
const doc = await carol.kv.get('documents/public/readme.txt', {
space: alice.spaceId,
});
Revoking Delegations
Currently, delegations expire automatically based on their expiresIn value. For immediate revocation, use short-lived delegations and refresh them as needed.
// Short-lived delegation that must be refreshed
const delegation = await tc.createDelegation({
delegateDID: recipient.pkhDid,
actions: ['read'],
paths: ['shared/*'],
expiresIn: '1h', // Expires in 1 hour
});
// To "revoke", simply don't issue a new delegation