Last updated 2026-09-04Development preview

Store files

Understand object keys and prefixes, grant precise access, and stream files through a generated bucket client.


Use a bucket when your application needs to keep file contents: an uploaded image, a report, or an exported archive. Each object has a key and a body. The key identifies it; the body is the sequence of bytes you store.

A bucket is not a directory mounted inside your runtime. Your code reads and writes objects through operations such as Get, Create, and Put. Fluffy generates those operations only for the bucket actions you grant to that runtime.

#Understand objects and access

#Keys are names, not filesystem paths

An object might be named uploads/report.txt. The slash is part of its key. A prefix such as uploads/ selects keys that start with those characters.

A runtime granted that prefix can access uploads/report.txt but not private/report.txt. Do not interpret .. segments as filesystem traversal or depend on them being normalized: keys are object names. Choose clear keys and enforce your application's naming rules.

How access works includes an experiment where you add a bucket grant, generate the client, apply the simulated deployment, and try keys inside and outside the prefix.

#Reading, creating, and replacing are different operations

Get reads an object's body. The same get grant also supplies Stat, which reads metadata. A missing object is different from an existing object whose body is empty.

Create stores a new object only when that key does not already exist. Put may replace an existing object. If overwriting would be a mistake, grant and call Create; do not rely on a separate existence check followed by Put, because another writer can act between those requests.

List and Delete are separate grants. Read/write access does not implicitly provide them.

#Large objects are streamed

Objects can be larger than your runtime's available memory. Streaming lets the application send or receive a bounded piece at a time. Fluffy's object clients use gRPC streaming; body chunks must be no larger than 64 KiB.

For an upload, send metadata first, then body chunks, and finish the stream. For a download, keep receiving chunks until the stream ends. Closing an upload is part of the operation: check its final response rather than treating the last successful send as proof of storage.

#Configure bucket access

#Declare a bucket and grant access

This excerpt adds a bucket named files and grants an existing api runtime permission to read and write keys under uploads/:

yaml
buckets:
  files: {}
runtimes:
  api:
    uses:
      buckets:
        - name: files
          prefix: uploads/
          actions: [get, put]

Add the entries to fluffy-chainsaw.yaml, keeping the runtime's artifact and other settings. The bucket declaration and its grant are separate: removing the grant does not remove the bucket declaration.

#Choose the storage product

Workspace policy selects the product and its settings. In fluffy-chainsaw.workspace.yaml, an existing application provider can supply this storage type:

yaml
resourceTypes:
  buckets:
    object-storage:
      product: gcp.storage
      addons:
        gcp:
          storageClass: STANDARD
          versioning: true
          softDeleteRetentionDays: 7
environments:
  prod:
    buckets:
      defaults:
        type: object-storage
        provider: application

The provider's project and region must already be configured. The type is reusable: every bucket selecting it receives its policy unless environment policy overrides the relevant settings. Use a separate type when buckets need different lifecycle behavior.

Local execution supplies the portable object API through a local service. It does not require a Cloud Storage bucket and is not a simulation of every Cloud Storage lifecycle feature.

#Upload a small file

Generate the runtime SDK after adding the grant. These commands assume an application named example-app; substitute your application's name:

fluffy-chainsaw generate --application example-app --runtime api --language typescript

Use the sidecar connection setup for your language. Imports assume application example-app; adjust them to your source location.

import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as objectsv1 from './.fluffy/generated/runtimes/api/typescript/fluffy/objects/v1/objects.js'

const client = new runtimev1.BucketsFilesClient(address, transport)
await new Promise<void>((resolve, reject) => {
  const stream = client.put({ deadline: Date.now() + 5000 }, (error) => {
    if (error) reject(error)
    else resolve()
  })
  stream.on('error', reject)
  stream.write(objectsv1.PutRequest.fromPartial({ key: 'uploads/hello.txt' }))
  stream.end(objectsv1.PutRequest.fromPartial({ body: Buffer.from('Hello from your application') }))
})

For a larger file, read from your source in chunks of at most 64 KiB and send each chunk. Use a request deadline and propagate read, send, and close errors.

#Read the file back

Read the object as a stream. This verification discards the body; replace that step with writing each chunk to your destination:

import * as runtimev1 from './.fluffy/generated/runtimes/api/typescript/fluffy/runtime/example-app/api.js'
import * as objectsv1 from './.fluffy/generated/runtimes/api/typescript/fluffy/objects/v1/objects.js'

const client = new runtimev1.BucketsFilesClient(address, transport)
const stream = client.get(objectsv1.GetRequest.fromPartial({ key: 'uploads/hello.txt' }), {
  deadline: Date.now() + 5000,
})
for await (const part of stream) {
  // This verification reads and discards each chunk. Write it to your destination here.
  console.log(`Received ${part.body.length} bytes`)
}

#Let a browser upload or download

A browser does not receive a runtime bucket client or cloud credentials. It asks your backend for access. Your backend verifies the user and, if appropriate, calls a supported delegated operation to obtain a temporary upload/download URL.

delegateGet, delegateCreate, and delegatePut are separate grants. Choose the operation that matches the intended action, keep its lifetime bounded, and pass only the delegated result to the authorized caller. A normal get or put grant does not automatically add delegated methods.

#Run and verify

Start the application locally after regenerating its SDK:

Terminal
fluffy-chainsaw local --application example-app

Exercise your upload/download handler. Upload a known small file, download it, and compare the bytes. Then request a missing key and a key outside uploads/: those should not look like successful empty files. With Create, also try the same key twice and verify the second creation fails.

#Reference: bucket operations and policy

GrantGenerated methodBoundary
getGet, StatReads body or metadata; key must be within the prefix
createCreateNew objects only; fails when the key already exists
putPutCan replace an object at the same key
listListLists the allowed prefix; pagination does not grant broader access
deleteDeleteDeletes an allowed key, subject to provider behavior
delegateGetDelegateGetDelegates a bounded read
delegateCreateDelegateCreateDelegates create-only access
delegatePutDelegatePutDelegates a write that may replace an object

#Rejected configuration and requests

Bucket grants require a nonempty prefix, a declared bucket name, and a nonempty list of supported actions. Duplicate actions and duplicate grants to the same bucket within one runtime are rejected. Use get, not a guessed read bucket action.

An object key outside the edge's prefix is rejected by the object adapter. A request can still fail with a valid key because the object is absent, a create precondition fails, or the provider is unavailable. Handle those conditions separately.

#Provider behavior

Cloud Storage policy can select storage class, versioning, soft-delete retention, lifecycle deletion, and a KMS key through its supported addons. These are bucket policy settings, not runtime action grants. See Manifest reference for their fields and accepted values.

The generated Unwrap and Credential methods are native access paths. The local object product does not implement either; portable Get, Create, and Put work locally. The GCP product can issue downscoped native credentials for the bucket/prefix/actions. Keep those credentials on the server.

Continue with Read secrets, or compare file storage with relational data in Databases.