> ## Documentation Index
> Fetch the complete documentation index at: https://docs.polycore.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Firebase Storage

> How a Polycore runner browses, summarizes, and shares Cloud Storage objects under a bucket allowlist and a signed-URL ceiling.

The Firebase Storage integration gives Polycore a native object surface: browse
buckets and prefixes, read object metadata, total up usage, pull small files
inline, and mint short-lived links to the ones a human needs to open. Behind an
explicit write grant and a human approval, it can also delete, copy, move,
retag, and re-permission objects.

The runner authenticates with Application Default Credentials inside your GCP
environment. Polycore cloud does not receive a service-account key.

## Why this exists

Uploaded photos, exports, attachments, and crash artifacts are usually the part
of a product that ops questions reach for last, because the answer normally
requires bucket access nobody wants to hand out for a one-off look.

## Two governance controls

Storage differs from the other families in having a blast radius worth tuning,
so it carries two controls of its own.

<Columns cols={2}>
  <Card title="Bucket allowlist" icon="list-check">
    A runtime service account routinely has object access to buckets that have
    nothing to do with the product: build artifacts, Terraform state, backups.
    `buckets` names the ones in scope, so the capability surface is narrower
    than the IAM grant behind it.
  </Card>

  <Card title="Signed-URL ceiling" icon="clock">
    `storage.getSignedUrl` mints a bearer credential that outlives the audited
    call that created it. `limits.maxSignedUrlMinutes` caps its life, and
    setting it to `0` withdraws the capability entirely.
  </Card>
</Columns>

## 1. Bind the runtime identity

Grant the runner's service account `roles/storage.objectViewer` on the buckets
in scope. Add `roles/storage.objectAdmin` only if you intend to enable mutations
with ambient credentials.

## 2. Name the default bucket

```sh theme={"system"}
FIREBASE_STORAGE_BUCKET=your-project.firebasestorage.app
```

This is required, and the runner never guesses it. Newer Firebase projects
default to `<project-id>.firebasestorage.app` and older ones to
`<project-id>.appspot.com`; a wrong guess does not fail loudly, it reads to a
caller as an empty bucket.

## 3. Enable Storage queries

```json polycore.json theme={"system"}
{
  "integrations": {
    "firebase": {
      "storage": {
        "buckets": ["your-project.firebasestorage.app", "your-project-exports"],
        "limits": {
          "maxSignedUrlMinutes": 15,
          "maxDownloadBytes": 1048576
        }
      }
    }
  }
}
```

`"storage": {}` enables the read surface across every bucket the service
account can reach, at default ceilings. `buckets` and `limits` narrow it.

| Limit                 | Default | What it bounds                                                           |
| --------------------- | ------- | ------------------------------------------------------------------------ |
| `maxSignedUrlMinutes` | 60      | Longest life of a signed read URL. `0` withdraws `storage.getSignedUrl`. |
| `maxDownloadBytes`    | 1 MiB   | Largest object returned inline by `storage.download`.                    |
| `maxUploadBytes`      | 1 MiB   | Largest inline object `storage.upload` accepts.                          |
| `maxObjectsPerCall`   | 1,000   | Objects one `storage.summarize` walk covers.                             |

## Available capabilities

<ResponseField name="storage.listBuckets" type="read">
  Lists the buckets this runner can reach, marking the project default. Under an
  allowlist, the list **is** the complete scope.
</ResponseField>

<ResponseField name="storage.getBucketMetadata" type="read">
  Reads a bucket's location, storage class, versioning, lifecycle rules, and
  public-access prevention.
</ResponseField>

<ResponseField name="storage.listFiles" type="read">
  Lists objects under a prefix. With `delimiter: "/"` it browses one level at a
  time, returning folder-like prefixes alongside objects.
</ResponseField>

<ResponseField name="storage.getMetadata" type="read">
  Reads one object's size, content type, timestamps, md5, and custom metadata.
  Returns `null` when nothing is at that path, which doubles as the existence
  check.
</ResponseField>

<ResponseField name="storage.summarize" type="read">
  Totals objects under a prefix without listing them: count, bytes, per
  content-type breakdown, oldest and newest timestamps.
</ResponseField>

<ResponseField name="storage.download" type="read">
  Returns a small object's content inline, as text or base64.
</ResponseField>

<ResponseField name="storage.getSignedUrl" type="read">
  Mints a temporary read-only link to one object. Present only when
  `maxSignedUrlMinutes` is above zero.
</ResponseField>

<Note>
  Cloud Storage has no server-side aggregation, so `storage.summarize` walks
  objects and stops at `maxObjectsPerCall`, reporting `complete: false` when it
  did. It never presents a partial total as a complete one.
</Note>

<Tip>
  Object paths are flat strings — "folders" are just a shared prefix. A trailing
  slash is what makes a prefix behave like one.
</Tip>

## Optional: generic mutations

Mutations are off until you grant them explicitly.

```json polycore.json theme={"system"}
{
  "integrations": {
    "firebase": {
      "storage": {
        "buckets": ["your-project.firebasestorage.app"],
        "write": { "keyEnv": "STORAGE_WRITE_KEY" }
      }
    }
  }
}
```

Every mutation is `write`-hazard, resolves its bucket through the **same
allowlist as reads** — a write can never reach a bucket a read could not — and
names its objects by exact path.

<ResponseField name="storage.delete" type="write">
  Permanently deletes one object by exact path.
</ResponseField>

<ResponseField name="storage.deleteMany" type="write">
  Deletes up to 100 objects by explicit path. Every path appears in the
  approval.
</ResponseField>

<ResponseField name="storage.copy" type="write">
  Copies one object to another path, optionally into another in-scope bucket.
</ResponseField>

<ResponseField name="storage.move" type="write">
  Renames or relocates one object within a bucket.
</ResponseField>

<ResponseField name="storage.setMetadata" type="write">
  Changes content type, cache control, disposition, or custom metadata. The
  bytes are untouched.
</ResponseField>

<ResponseField name="storage.makePublic" type="write">
  Grants `allUsers` read access to one object.
</ResponseField>

<ResponseField name="storage.makePrivate" type="write">
  Removes public read access from one object.
</ResponseField>

<ResponseField name="storage.upload" type="write">
  Writes a small file from inline content. Overwrites an existing object at that
  path.
</ResponseField>

<Warning>
  `storage.makePublic` is the only capability in the runner that can expose
  customer data to the open internet, with no sign-in and no expiry. It is
  described that way in the approval so a reviewer reads it as the
  data-disclosure decision it is. To let one person see one file, use
  `storage.getSignedUrl` instead — it expires.
</Warning>

### No prefix deletes

There is deliberately no "delete everything under this prefix". That set would
resolve *after* approval, so the approver could not have reviewed it — and a
prefix matching three objects when the agent looked can match three thousand by
the time a human clicks approve. Bulk work lists the paths first, with no
approval needed, and proposes them explicitly.
