View Source Dynamic Configuration

This guide explains how to configure cloud storage dynamically at runtime, enabling multi-tenant applications and flexible storage strategies.

Overview

Dynamic configuration allows you to:

  • Support multi-tenant applications with per-tenant storage
  • Switch storage providers without restarting
  • Test with different configurations
  • Implement feature flags for storage

Basic Dynamic Configuration

Process-Scoped Configuration

Set configuration for the current process:

# Set dynamic config
MyApp.Cloud.put_dynamic_config([
  adapter: Buckets.Adapters.S3,
  bucket: "tenant-123-bucket",
  region: "us-west-2",
  access_key_id: "AKIA...",
  secret_access_key: "secret..."
])

# All subsequent operations use this config
{:ok, object} = MyApp.Cloud.insert(upload)

Scoped Configuration Blocks

Use configuration for a specific block of code:

config = [
  adapter: Buckets.Adapters.GCS,
  bucket: "temporary-bucket",
  service_account_credentials: credentials
]

result = MyApp.Cloud.with_config(config, fn ->
  # All operations in this block use the config
  {:ok, obj1} = MyApp.Cloud.insert(file1)
  {:ok, obj2} = MyApp.Cloud.insert(file2)
  
  [obj1, obj2]
end)

Multi-Tenant Implementation

Basic Multi-Tenant Setup

defmodule MyApp.TenantStorage do
  def with_tenant(tenant, fun) do
    config = build_config(tenant)
    MyApp.Cloud.with_config(config, fun)
  end
  
  defp build_config(tenant) do
    case tenant.storage_provider do
      "s3" ->
        [
          adapter: Buckets.Adapters.S3,
          bucket: tenant.s3_bucket,
          region: tenant.s3_region,
          access_key_id: decrypt(tenant.s3_access_key),
          secret_access_key: decrypt(tenant.s3_secret_key)
        ]
        
      "gcs" ->
        [
          adapter: Buckets.Adapters.GCS,
          bucket: tenant.gcs_bucket,
          service_account_credentials: decrypt(tenant.gcs_credentials)
        ]
        
      "volume" ->
        [
          adapter: Buckets.Adapters.Volume,
          bucket: "tenants/#{tenant.id}",
          base_url: tenant.base_url || "http://localhost:4000"
        ]
    end
  end
  
  defp decrypt(encrypted_value) do
    # Decrypt sensitive credentials
    MyApp.Crypto.decrypt(encrypted_value)
  end
end

Phoenix Integration

Use dynamic config in Phoenix controllers:

defmodule MyAppWeb.FileController do
  plug :load_tenant_config
  
  def upload(conn, %{"file" => upload}) do
    # Config is already set by plug
    object = Buckets.Object.from_upload(upload)
    
    case MyApp.Cloud.insert(object) do
      {:ok, stored} ->
        conn
        |> put_flash(:info, "File uploaded")
        |> redirect(to: ~p"/files")
        
      {:error, reason} ->
        conn
        |> put_flash(:error, "Upload failed: #{inspect(reason)}")
        |> redirect(to: ~p"/files/new")
    end
  end
  
  defp load_tenant_config(conn, _opts) do
    tenant = conn.assigns.current_tenant
    config = MyApp.TenantStorage.build_config(tenant)
    
    MyApp.Cloud.put_dynamic_config(config)
    conn
  end
end

LiveView Integration

defmodule MyAppWeb.UploadLive do
  use MyAppWeb, :live_view
  
  def mount(_params, session, socket) do
    tenant = get_tenant(session)
    config = MyApp.TenantStorage.build_config(tenant)
    
    # Set config for this LiveView process
    MyApp.Cloud.put_dynamic_config(config)
    
    {:ok,
     socket
     |> assign(:tenant, tenant)
     |> allow_upload(:files, accept: :any, external: &presign_upload/2)}
  end
  
  defp presign_upload(entry, socket) do
    # Uses the dynamic config set in mount
    {:ok, config} = MyApp.Cloud.live_upload(entry)
    {:ok, config, socket}
  end
end

Background Jobs

Oban Integration

defmodule MyApp.FileProcessor do
  use Oban.Worker
  
  @impl true
  def perform(%{args: %{"file_id" => file_id, "tenant_id" => tenant_id}}) do
    tenant = Tenants.get!(tenant_id)
    file = Files.get!(file_id)
    
    # Run with tenant's config
    MyApp.TenantStorage.with_tenant(tenant, fn ->
      # Load file data
      {:ok, object} = MyApp.Cloud.load(file.object)
      
      # Process file
      process_file(object)
      
      # Save results
      {:ok, processed} = MyApp.Cloud.insert(processed_object)
      
      Files.update(file, %{processed_path: processed.location.path})
    end)
    
    :ok
  end
end