Skip to main content
Customer-owned object storage lets a SaaS workspace keep a collection’s graph data in a bucket you control while Nebula continues to run the hosted API and graph engine.
This guide applies to hosted SaaS workspaces. Enterprise self-hosted deployments configure object storage at deployment time instead.

How It Works

A workspace owner or admin registers an S3 storage target, validates access, then creates collections bound to that target. After a collection is bound, Nebula writes that collection’s graph objects under:
s3://<bucket>/<prefix>/<collection_id>/
Collections without a storage target continue using Nebula-managed storage.

Prerequisites

  • An Enterprise-plan team workspace
  • An S3 bucket in the region you want to use
  • An IAM role Nebula can assume
  • Object access for the target prefix
For hosted SaaS, use AWS S3 and omit endpoint_url. Custom S3-compatible endpoints are only supported in trusted self-hosted deployments; hosted SaaS rejects custom endpoints.

Create the IAM Role

Create a role that trusts the Nebula-provided AWS principal and requires the external ID returned when you register the storage target. The trust policy shape is:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "NEBULA_AWS_PRINCIPAL_ARN"
      },
      "Action": "sts:AssumeRole",
      "Condition": {
        "StringEquals": {
          "sts:ExternalId": "EXTERNAL_ID_FROM_NEBULA"
        }
      }
    }
  ]
}
Grant object access to the target prefix:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::CUSTOMER_BUCKET",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "nebula/*"
          ]
        }
      }
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::CUSTOMER_BUCKET/nebula/*"
    }
  ]
}
If the bucket uses a customer-managed KMS key, also allow the role to encrypt, decrypt, and generate data keys for that key.

Register a Storage Target

Register the bucket on the workspace:
from nebula import Nebula

async with Nebula(api_key="YOUR_API_KEY") as client:
    target = await client.workspaces.create_storage_target(
        workspace_id="WORKSPACE_ID",
        body={
            "name": "production-s3",
            "bucket": "CUSTOMER_BUCKET",
            "prefix": "nebula",
            "region": "us-east-1",
            "role_arn": "arn:aws:iam::123456789012:role/NebulaStorageAccess",
            "kms_key_id": "OPTIONAL_KMS_KEY_ID",
        },
    )
The response includes:
FieldDescription
idStorage target ID used when creating collections
external_idExternal ID to require in the role trust policy
statuspending, active, validation_failed, or disabled
validation_errorLast validation error, if any
If you need the generated external_id before finalizing the IAM trust policy, register the target first, update the role trust policy with the returned value, then validate.

Validate Access

Validation confirms Nebula can access the bucket and prefix:
from nebula import Nebula

async with Nebula(api_key="YOUR_API_KEY") as client:
    target = await client.workspaces.validate_storage_target(
        workspace_id="WORKSPACE_ID",
        target_id="STORAGE_TARGET_ID",
    )
Nebula checks the bucket, writes a small validation object under the target prefix, reads it back, and deletes it. A successful validation sets the target to active.

Create a BYOC Collection

Create collections with both workspace_id and storage_target_id:
curl -X POST "https://api.zeroset.com/v1/collections" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "name": "customer-prod-memory",
    "workspace_id": "WORKSPACE_ID",
    "storage_target_id": "STORAGE_TARGET_ID"
  }'
Only active storage targets can be used for new collections. Disabling a target prevents future collection binds; existing collections keep their storage binding.

List or Disable Targets

List targets:
from nebula import Nebula

async with Nebula(api_key="YOUR_API_KEY") as client:
    targets = await client.workspaces.list_storage_targets(
        workspace_id="WORKSPACE_ID",
    )
Disable a target:
from nebula import Nebula

async with Nebula(api_key="YOUR_API_KEY") as client:
    target = await client.workspaces.disable_storage_target(
        workspace_id="WORKSPACE_ID",
        target_id="STORAGE_TARGET_ID",
    )

Operational Notes

  • Use one bucket prefix per environment, such as nebula/prod and nebula/staging.
  • Do not manually move or delete objects under a collection prefix while the collection is active.
  • Workspace deletion is blocked while BYOC-bound collections still exist.
  • To use signed URL transport for device memory snapshots, see Device Memory API.