Skip to main content

File Storage

SpacetimeDB can store binary data directly in table columns, making it suitable for files, images, and other blobs that need to participate in transactions and subscriptions.

Storing Binary Data Inline

Store binary data using Vec<u8> (Rust), List<byte> (C#), or t.array(t.u8()) (TypeScript). This approach keeps data within the database, ensuring it participates in transactions and real-time updates.

import { table, t, schema } from 'spacetimedb/server';

const userAvatar = table(
  { name: 'user_avatar', public: true },
  {
    userId: t.u64().primaryKey(),
    mimeType: t.string(),
    data: t.array(t.u8()),  // Binary data stored inline
    uploadedAt: t.timestamp(),
  }
);

const spacetimedb = schema(userAvatar);

spacetimedb.reducer('upload_avatar', {
  userId: t.u64(),
  mimeType: t.string(),
  data: t.array(t.u8()),
}, (ctx, { userId, mimeType, data }) => {
  // Delete existing avatar if present
  ctx.db.userAvatar.userId.delete(userId);

  // Insert new avatar
  ctx.db.userAvatar.insert({
    userId,
    mimeType,
    data,
    uploadedAt: ctx.timestamp,
  });
});

When to Use Inline Storage

Inline storage works well for:

  • Files up to ~100MB
  • Data that changes with other row fields (e.g., user profile with avatar)
  • Data requiring transactional consistency (file updates atomic with metadata)
  • Data clients need through subscriptions (real-time avatar updates)

Size Considerations

Very large binary data affects:

  • Memory usage: Rows are held in memory during reducer execution
  • Network bandwidth: Large rows increase subscription traffic
  • Transaction size: Large rows slow down transaction commits

For very large files (over 100MB), consider external storage.

External Storage with References

For large files or data that changes independently, store the file externally and keep a reference in the database. This pattern separates bulk storage from metadata management.

import { table, t, schema } from 'spacetimedb/server';

const document = table(
  { name: 'document', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    ownerId: t.identity().index('btree'),
    filename: t.string(),
    mimeType: t.string(),
    sizeBytes: t.u64(),
    storageUrl: t.string(),  // Reference to external storage
    uploadedAt: t.timestamp(),
  }
);

const spacetimedb = schema(document);

// Called after uploading file to external storage
spacetimedb.reducer('register_document', {
  filename: t.string(),
  mimeType: t.string(),
  sizeBytes: t.u64(),
  storageUrl: t.string(),
}, (ctx, { filename, mimeType, sizeBytes, storageUrl }) => {
  ctx.db.document.insert({
    id: 0,  // auto-increment
    ownerId: ctx.sender,
    filename,
    mimeType,
    sizeBytes,
    storageUrl,
    uploadedAt: ctx.timestamp,
  });
});

External Storage Options

Common external storage solutions include:

ServiceUse Case
AWS S3 / Google Cloud Storage / Azure BlobGeneral-purpose object storage
Cloudflare R2S3-compatible with no egress fees
CDN (CloudFront, Cloudflare)Static assets with global distribution
Self-hosted (MinIO)On-premises or custom deployments

Upload Flow

A typical external storage flow:

  1. Client requests upload URL: Call a procedure or reducer to generate a pre-signed upload URL
  2. Client uploads directly: Upload the file to external storage using the pre-signed URL
  3. Client registers metadata: Call a reducer with the storage URL and file metadata
  4. Database tracks reference: The table stores the URL for later retrieval

This pattern keeps large files out of SpacetimeDB while maintaining metadata in the database for queries and subscriptions.

Example: Uploading to S3 from a Procedure

Procedures can make HTTP requests, enabling direct uploads to external storage services like S3. This example shows uploading a file to S3 and storing the metadata in SpacetimeDB:

import { table, t, schema, SenderError } from 'spacetimedb/server';

const document = table(
  { name: 'document', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    ownerId: t.identity(),
    filename: t.string(),
    s3Key: t.string(),
    uploadedAt: t.timestamp(),
  }
);

const spacetimedb = schema(document);

// Upload file to S3 and register in database
spacetimedb.procedure(
  'upload_to_s3',
  {
    filename: t.string(),
    contentType: t.string(),
    data: t.array(t.u8()),
    s3Bucket: t.string(),
    s3Region: t.string(),
  },
  t.string(),  // Returns the S3 key
  (ctx, { filename, contentType, data, s3Bucket, s3Region }) => {
    // Generate a unique S3 key
    const s3Key = `uploads/${Date.now()}-${filename}`;
    const url = `https://${s3Bucket}.s3.${s3Region}.amazonaws.com/${s3Key}`;

    // Upload to S3 (simplified - add AWS4 signature in production)
    const response = ctx.http.fetch(url, {
      method: 'PUT',
      headers: {
        'Content-Type': contentType,
        'x-amz-content-sha256': 'UNSIGNED-PAYLOAD',
        // Add Authorization header with AWS4 signature
      },
      body: new Uint8Array(data),
    });

    if (response.status !== 200) {
      throw new SenderError(`S3 upload failed: ${response.status}`);
    }

    // Store metadata in database
    ctx.withTx(txCtx => {
      txCtx.db.document.insert({
        id: 0n,
        ownerId: txCtx.sender,
        filename,
        s3Key,
        uploadedAt: txCtx.timestamp,
      });
    });

    return s3Key;
  }
);
AWS Authentication

The example above is simplified. Production S3 uploads require proper AWS Signature Version 4 authentication.

Alternative: Pre-signed URL Flow

For larger files, generate a pre-signed URL and let the client upload directly:

// Procedure returns a pre-signed URL for client-side upload
spacetimedb.procedure(
  'get_upload_url',
  { filename: t.string(), contentType: t.string() },
  t.object('UploadInfo', { uploadUrl: t.string(), s3Key: t.string() }),
  (ctx, { filename, contentType }) => {
    const s3Key = `uploads/${Date.now()}-${filename}`;

    // Generate pre-signed URL (requires AWS credentials and signing logic)
    const uploadUrl = generatePresignedUrl(s3Key, contentType);

    return { uploadUrl, s3Key };
  }
);

// Client uploads directly to S3 using the pre-signed URL, then calls:
spacetimedb.reducer('confirm_upload', { filename: t.string(), s3Key: t.string() }, (ctx, { filename, s3Key }) => {
  ctx.db.document.insert({
    id: 0n,
    ownerId: ctx.sender,
    filename,
    s3Key,
    uploadedAt: ctx.timestamp,
  });
});

The pre-signed URL approach is preferred for large files because:

  • No size limits: Files don't pass through SpacetimeDB
  • Better performance: Direct client-to-S3 transfer
  • Reduced load: SpacetimeDB only handles metadata

Hybrid Approach: Thumbnails and Originals

For images, store small thumbnails inline for fast access while keeping originals in external storage:

import { table, t, schema } from 'spacetimedb/server';

const image = table(
  { name: 'image', public: true },
  {
    id: t.u64().primaryKey().autoInc(),
    ownerId: t.identity().index('btree'),
    thumbnail: t.array(t.u8()),      // Small preview stored inline
    originalUrl: t.string(),          // Large original in external storage
    width: t.u32(),
    height: t.u32(),
    uploadedAt: t.timestamp(),
  }
);

This approach provides:

  • Fast thumbnail access through subscriptions (no extra network requests)
  • Efficient storage for large originals (external storage optimized for blobs)
  • Real-time updates for metadata changes through SpacetimeDB subscriptions

Choosing a Strategy

ScenarioRecommended Approach
User avatars (< 10MB)Inline storage
Chat attachments (< 50MB)Inline storage
Document uploads (< 100MB)Inline storage
Large files (> 100MB)External storage with reference
Video filesExternal storage with CDN
Images with previewsHybrid (inline thumbnail + external original)

SpacetimeDB storage costs approximately $1/GB compared to cheaper blob storage options like AWS S3. For large files that don't need atomic updates with other data, external storage may be more economical.

The right choice depends on your file sizes, access patterns, and whether the data needs to participate in real-time subscriptions.