Platform

Storage providers

S3, R2, GCS, Azure Blob, MinIO, local disk — every blob storage substrate behind one canonical contract.

Vadyl's storage plane abstracts blob storage across providers with a typed capability matrix. Your product logic reads and writes objects through the canonical contract; the binding decides whether bytes land in S3, R2, GCS, Azure Blob, MinIO, or a local disk.

Capability matrix

StorageCapabilities declares per-provider support for 14 categories: core ops, streams, signed URLs, copy, metadata, directories, multi-part uploads, queries, versioning, batch ops, moves, access control, provider identity, and lifecycle policies. Vadyl's storage routing branches on capabilities — never on provider name strings.

Bind a provider

bindings: {
  storage: {
    type:   "s3",
    bucket: process.env.S3_BUCKET,
    region: "us-east-1",
    auth:   { kind: "iamRole" },   // or accessKey, profile, etc.
    quotas: {
      maxObjectBytes: 5 * 1024 ** 3,         // 5 GB
      maxTotalBytes:  500 * 1024 ** 3,       // 500 GB
    },
  },
},

Read and write

// Write
await ctx.storage.put("uploads/user-123/avatar.png", buffer, {
  contentType: "image/png",
  metadata:    { uploadedBy: ctx.actor.userId },
});

// Read
const stream = await ctx.storage.get("uploads/user-123/avatar.png");

// Stream from a connected source (e.g. Stripe payment receipts)
await ctx.storage.putStream("receipts/2026/01/abc.pdf",
  ctx.connections.stripe.getReceipt(chargeId));

Multi-part uploads

Large files use the provider's native multi-part API (S3 UploadPart, GCS resumable upload). Vadyl manages part initiation, upload, completion, and abort under the hood — your code calls a single putStream and the bytes get there.

Signed URLs

const url = await ctx.storage.signedUrl("uploads/user-123/avatar.png", {
  operation: "get",
  expiresIn: "1h",
});

Signed URLs delegate access without proxying bytes. The signing key is per-binding and resolved through the project's key ring — plaintext key material never appears on storage records.

Physical paths

Storage paths use immutable canonical project IDs as prefixes — not slugs (anti-pattern #32). A project's bytes always live at {projectId}/... in the bound bucket, regardless of slug renames. This means renaming a project never invalidates URLs or breaks references.

Quotas and metering

Storage operations emit usage events into the canonical billing substrate. Quotas are enforced through the same engine as compute, events, and entity operations: hard (throws), soft (warns), or monitor (no-op).

Lifecycle events

Object writes, deletes, and moves emit canonical platform events. Subscribe from event consumers to trigger downstream behavior — image resizing, virus scanning, indexing, audit logging.

// src/events/onAvatarUploaded.ts
export default event.consumer({
  filter: { kind: "storage.object.written", pathPrefix: "uploads/" },
  handler: async (ctx, evt) => {
    await ctx.workflows.processAvatar.start({ key: evt.payload.key });
  },
});