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

# Buckets

> Creating a bucket, pointing an S3 client at it, and what deleting one takes.

## Creating a bucket

<Tabs>
  <Tab title="Console">
    Go to **Storage → Buckets** and choose **Create Bucket**. **Bucket Name**
    is the only thing you have to fill in; **Versioning**, **Default
    encryption**, **Deletion protection** and **Tags** are on the same form.

    Those extras are applied as separate calls once the bucket exists, so the
    bucket is created even if one of them fails, and the console tells you
    which one did.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    POST https://storage.sa-saopaulo-1.basaltic.sh/v1/buckets
    { "name": "my-app-assets" }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage bucket create --name my-app-assets
    ```

    Add `--object-lock-enabled` here if you need it — it can only ride on the
    call that creates the bucket.
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    b, err := storage.New(cfg).CreateBucket(ctx, &storage.CreateBucketRequest{
        Name: "my-app-assets",
    })
    ```
  </Tab>
</Tabs>

Bucket names follow the S3 rules, and they are checked in full:

<ResponseField name="name" type="3–63 characters" required>
  Lowercase letters, digits and hyphens. Must start and end with a letter or
  digit, must not contain a **double hyphen** (`--`), and must not be shaped
  like an IP address.
</ResponseField>

<Warning>
  **A bucket name is unique across the whole region, not just your account.**
  A name another account already holds comes back `409`. Creating a bucket you
  already own is a no-op that succeeds, so a repeated create is safe.
</Warning>

Bucket count is capped by your organization's `buckets` quota; exhausting it is
also a `409` on create.

### Object Lock has to be decided here

```bash theme={null}
POST /v1/buckets
{ "name": "audit-archive", "object_lock_enabled": true }
```

<Warning>
  **Object Lock can only be enabled at creation.** There is no call that turns
  it on later — you would have to create a new bucket and copy the objects
  across. Enabling it also turns versioning on, because a lock has nothing to
  hold onto without versions.
</Warning>

`PUT /v1/buckets/{bucket}/object-lock` updates the *default retention rule* on a
bucket that already has Object Lock enabled. Against a bucket that does not, it
is a `409` — `object lock must be enabled at bucket creation`.

<Note>
  **Turning Object Lock on is API only.** It has to ride on the call that
  creates the bucket, and the console's **Create Bucket** form does not send it
  — the bucket is created without Object Lock, and the follow-up configuration
  is refused with that same `409`. Create the bucket through the API when you
  need Object Lock.

  On a bucket that already has it, the console does edit the rule: the **Object
  Lock** card on the bucket's **Settings** tab carries the **Default retention
  rule**, with a **Mode** and a **Retention period**.
</Note>

## Pointing an S3 client at it

Set a custom endpoint and sign with your Basaltic access key. Nothing else about
the client changes.

<CodeGroup>
  ```python boto3 theme={null}
  import boto3
  from botocore.config import Config

  s3 = boto3.client(
      "s3",
      endpoint_url="https://objects.sa-saopaulo-1.basaltic.cloud",
      aws_access_key_id=ACCESS_KEY_ID,
      aws_secret_access_key=SECRET_ACCESS_KEY,
      region_name="sa-saopaulo-1",
      config=Config(signature_version="s3v4"),
  )

  s3.put_object(Bucket="my-app-assets", Key="images/logo.png", Body=data)
  ```

  ```bash AWS CLI theme={null}
  aws --endpoint-url https://objects.sa-saopaulo-1.basaltic.cloud \
      s3 cp ./logo.png s3://my-app-assets/images/logo.png
  ```

  ```python Temporary credentials theme={null}
  s3 = boto3.client(
      "s3",
      endpoint_url="https://objects.sa-saopaulo-1.basaltic.cloud",
      aws_access_key_id=creds.access_key_id,
      aws_secret_access_key=creds.secret_access_key,
      aws_session_token=creds.session_token,   # required for STS credentials
      region_name="sa-saopaulo-1",
      config=Config(signature_version="s3v4"),
  )
  ```
</CodeGroup>

<AccordionGroup>
  <Accordion title="Credentials" icon="key-round">
    The same access keys you use everywhere else. A service account's long-lived
    key needs nothing extra; temporary credentials from STS — a role session or
    a user session — must also carry the session token, and are rejected
    without it.

    See [authentication](/authentication) for how to obtain each.
  </Accordion>

  <Accordion title="Addressing style" icon="route">
    Both styles work. Virtual-hosted
    (`https://my-app-assets.objects.sa-saopaulo-1.basaltic.cloud/key`) is what
    most SDKs default to; path-style
    (`https://objects.sa-saopaulo-1.basaltic.cloud/my-app-assets/key`) is
    available through your client's addressing-style option.
  </Accordion>

  <Accordion title="The region string" icon="globe">
    Set `region_name` to the Basaltic region code. The value is not checked
    against the region serving the request — it only has to match what your
    client signed with — so a tool hard-wired to `us-east-1` still works.
    `GetBucketLocation` reports the real region.
  </Accordion>

  <Accordion title="Clock skew and presigned URLs" icon="clock">
    A signed request must be within **15 minutes** of the server's clock, or it
    is rejected as too skewed. Presigned URLs are supported with an expiry
    between 1 second and **7 days**, and a URL dated in the future beyond the
    skew tolerance is refused rather than becoming valid later.
  </Accordion>
</AccordionGroup>

### What the S3 endpoint serves

The endpoint is verified against a real AWS SDK rather than a specification of
our own — if boto3 can do it and gets S3's error codes back, it works. What is
routed today:

<Columns cols={2}>
  <Card title="Buckets" icon="boxes">
    ListBuckets, CreateBucket, HeadBucket, DeleteBucket, GetBucketLocation, and
    the `?policy`, `?cors`, `?lifecycle`, `?versioning`, `?encryption`,
    `?tagging`, `?object-lock` and `?acl` sub-resources.
  </Card>

  <Card title="Objects" icon="file">
    PutObject, GetObject (including range requests), HeadObject, DeleteObject,
    DeleteObjects, CopyObject, ListObjects, ListObjectsV2, ListObjectVersions,
    and the `?tagging`, `?retention`, `?legal-hold` and `?acl` sub-resources.
  </Card>

  <Card title="Multipart" icon="layers">
    CreateMultipartUpload, UploadPart, UploadPartCopy, ListParts,
    ListMultipartUploads, CompleteMultipartUpload, AbortMultipartUpload.
  </Card>

  <Card title="Payload signing" icon="shield">
    Signed payloads, `UNSIGNED-PAYLOAD`, and signed `aws-chunked` streaming
    uploads. The body is re-hashed as it streams, so a body that does not match
    what was signed is rejected mid-flight.
  </Card>
</Columns>

Anything outside that list answers `NotImplemented`. Preflight `OPTIONS`
requests are answered without a signature, because browsers never sign them.

## Deleting a bucket

`DELETE /v1/buckets/{bucket}` does two quite different things depending on
whether deletion protection is on:

<Tabs>
  <Tab title="Protection off (default)">
    The bucket must be **empty**. Any remaining objects, versions or in-flight
    multipart uploads make it a `409 BucketNotEmpty`. An empty bucket is
    deleted immediately and the quota slot is released.
  </Tab>

  <Tab title="Protection on">
    The delete is **scheduled** for the end of the recovery window rather than
    performed, and it is accepted whether or not the bucket is empty.
    `scheduled_deletion_at` appears on the bucket, and
    `POST /v1/buckets/{bucket}/restore` cancels it any time before that
    deadline — in the console, the scheduled bucket carries a **Cancel
    deletion** action that does the same thing. Calling delete again while a
    deletion is already scheduled is a no-op, not a second window.

    <Warning>
      When the deadline passes, the bucket is emptied and purged — **its
      objects go with it**. Protection buys you a window to change your mind,
      not a refusal to delete a bucket with data in it.
    </Warning>
  </Tab>
</Tabs>

<Tabs>
  <Tab title="Console">
    The **Deletion protection** card on the bucket's **Settings** tab is a
    switch; turning it on reveals **Recovery window (days)**, and **Save**
    applies both.
  </Tab>

  <Tab title="API">
    ```bash theme={null}
    PUT /v1/buckets/{bucket}/deletion-protection
    { "enabled": true, "recovery_days": 14 }
    ```
  </Tab>

  <Tab title="CLI">
    ```bash theme={null}
    basaltic storage bucket set-deletion-protection <bucket> \
      --enabled --recovery-days 14
    ```
  </Tab>

  <Tab title="Go">
    ```go theme={null}
    err := storage.New(cfg).PutBucketDeletionProtection(ctx, bucket,
        &storage.PutBucketDeletionProtectionRequest{
            Enabled:      true,
            RecoveryDays: basaltic.Int(14),
        })
    ```
  </Tab>
</Tabs>

`recovery_days` is clamped to **1–30**; `0` uses the default of **7 days**.
