Docs
Concepts
Architecture options

Possible architectures using web3.storage to upload

UCAN opens up a number of options in how to integrate with w3up: Should you, the developer, own the Space? Should you delegate permissions to your users? Or should your user own their own Space? Broadly, there are three ways to integrate:

  • Client-server: You (the developer) own the space, your users upload data to your application server, and you upload it to web3.storage from there.
  • Delegated: You own the Space, but you provide your user with a delegation that allows them to upload directly to the service. This avoids proxying the upload through your backend (no egress from your infrastructure).
  • User-owned: Your user owns the Space and registers it and they use it to upload directly with the service; if you want to instrument visibility into what they're uploading, you'll have to write separate code in your app for it.

In the How-tos section of the docs, we focused on the first two options, as they are the most typical today. However, you can implement each of these in a number of ways, but we talk through some considerations when implementing a given option.

Client-server

In this set up, you (the developer) own the space, your users upload data to your application server, and you upload it to web3.storage from there.

You'll need a registered Space, and your client in the backend to have a delegation to use the Space.

💡

Recommended: It's easiest to create and register your Space using w3cli (opens in a new tab), then create an identity for your server and delegate it the ability to upload to your Space. The bring your own delegations section shows how to generate an identity, create a delegation and then use it on the server.

After this, once your user uploads data to your backend, you can run any of the upload methods.

For your backend to be scalable, you might consider using serverless workers or a queue in front of a server.

Delegated

In this set up, you own the Space, but you provide your user with a delegation that allows them to upload directly to the service. This avoids proxying the upload through your backend (no egress from your infrastructure).

Typically w3up-client will be running in your end-user's client code, as well as backend code that's able to generate UCANs that delegate the ability to upload and pass them to your users (e.g., w3up-client running in a serverless worker).

You'll need a registered Space, and your client in the backend to have a delegation to use the Space.

💡

Recommended: It's easiest to create and register your Space using w3cli (opens in a new tab), then create an identity for your server and delegate it the ability to upload to your Space. The bring your own delegations section shows how to generate an identity, create a delegation and then use it on the server.

Your backend code can then re-delegate it's capabilities to your users. Your user does not need a registered Space - just an Agent with a delegation from your Space.

When your user is ready to upload, they should request a delegation from your backend to upload to your Space. They must send their Agent DID so that the delegation is restricted to be used by only them.

In your backend, you can call client.createDelegation(...) (opens in a new tab) passing in the Agent object from client.agent() in your end user's instance, and passing params to limit the scope of the delegation (e.g., store/add, upload/add, expiration time)

You can serialize the delegation using delegation.archive() and send it to your user.

The client the user is using does not need to call client.login(email), as it is not claiming any delegations via email address (but instead getting a delegation from your backend).

When your user receives the delegation, they can deserialize it using Delegation.extract() (opens in a new tab) and pass it in using client.addSpace(), and from there they can run any of the upload methods.

This does not provide visibility over which users are uploading what; to track this, you'll need to share that information separately (e.g., once they've run upload and get back a content CID, you can have them send that CID to you for tracking).

A code example that does this can be found below:

Backend

import { CarReader } from '@ipld/car'
import * as DID from '@ipld/dag-ucan/did'
import * as Delegation from '@ucanto/core/delegation'
import * as Signer from '@ucanto/principal/ed25519'
import * as Client from '@web3-storage/w3up-client'
import { StoreMemory } from '@web3-storage/w3up-client/stores/memory'
 
async function backend(did) {
  // Load client with specific private key
  const principal = Signer.parse(process.env.KEY)
  const store = new StoreMemory()
  const client = await Client.create({ principal, store })
 
  // Add proof that this agent has been delegated capabilities on the space
  const proof = await parseProof(process.env.PROOF)
  const space = await client.addSpace(proof)
  await client.setCurrentSpace(space.did())
 
  // Create a delegation for a specific DID
  const audience = DID.parse(did)
  const abilities = ['store/add', 'upload/add']
  const expiration = Math.floor(Date.now() / 1000) + (60 * 60 * 24) // 24 hours from now
  const delegation = await client.createDelegation(audience, abilities, { expiration })
 
  // Serialize the delegation and send it to the client
  const archive = await delegation.archive()
  return archive.ok
}
 
/** @param {string} data Base64 encoded CAR file */
async function parseProof(data) {
  const blocks = []
  const reader = await CarReader.fromBytes(Buffer.from(data, 'base64'))
  for await (const block of reader.blocks()) {
    blocks.push(block)
  }
  return Delegation.importDAG(blocks)
}

Frontend

import * as Delegation from '@ucanto/core/delegation'
import * as Client from '@web3-storage/w3up-client'
 
async function frontend() {
  // Create a new client
  const client = await Client.create()
 
  // Fetch the delegation from the backend
  const apiUrl = `/api/w3up-delegation/${client.agent().did()}`
  const response = await fetch(apiUrl)
  const data = await response.arrayBuffer()
 
  // Deserialize the delegation
  const delegation = await Delegation.extract(new Uint8Array(data))
  if (!delegation.ok) {
    throw new Error('Failed to extract delegation', { cause: delegation.error })
  }
 
  // Add proof that this agent has been delegated capabilities on the space
  const space = await client.addSpace(delegation.ok)
  client.setCurrentSpace(space.did())
 
  // READY to go!
}

User-owned

In this set up your user owns the Space and registers it and they use it to upload directly with the service; if you want to instrument visibility into what they're uploading, you'll have to write separate code in your app for it.

If you want your user to own their own Space, you'll typically be relying on the w3up-client methods to create a Space, authorize the Space, and authorize the Agent on the user-side; afterwards they can run any of the upload methods.

This does not provide visibility over which users are uploading what; to track this, you'll need to share that information separately (e.g., once they've run upload and get back a content CID, you can have them send that CID to you for tracking).

There is a world of possibilities when users "bring their own" Space; you could explore how crypto wallet private keys, Apple Passkey, and more might map to Space DIDs and have the client use those. Your users could delegate capabilities for their space to your backend application or share spaces between friends by delegating to each other.


If you have code snippet(s) for any of the above architectural options, please share them in a PR or Github issue (opens in a new tab) and we'll link them here!