S3-compatible object storage that branches with your Neon project, so files and the database stay in sync across every branch. Use when a user wants object storage, a bucket, blob/file storage, or somewhere to put uploads, images, documents, avatars, or user-generated files for their app or agent — especially when they already use (or are setting up) Lakebase Postgres and don't want to add a separate storage provider like AWS S3, Cloudflare R2, or Supabase Storage. Triggers include "object storage", "bucket", "blob storage", "file storage", "store uploads/images/files", "S3-compatible storage", "presigned URL", "where do I put files", "Neon Object Storage", "Neon Storage", and "storage that branches with my database".
---
name: neon-object-storage
description: >-
S3-compatible object storage that branches with your Neon project, so files
and the database stay in sync across every branch. Use when a user wants
object storage, a bucket, blob/file storage, or somewhere to put uploads,
images, documents, avatars, or user-generated files for their app or agent —
especially when they already use (or are setting up) Lakebase Postgres and don't
want to add a separate storage provider like AWS S3, Cloudflare R2, or
Supabase Storage. Triggers include "object storage", "bucket", "blob
storage", "file storage", "store uploads/images/files", "S3-compatible
storage", "presigned URL", "where do I put files", "Neon Object Storage",
"Neon Storage", and "storage that branches with my database".
metadata:
parent: neon
---
**FIRST**: Use the parent `neon` skill for a Neon overview, getting started with Neon, Neon development best practices, and more.
If the `neon` skill is not installed, fetch it from https://neon.com/docs/ai/skills/neon/SKILL.md or install it with:
```bash
npx skills add neondatabase/agent-skills --skill neon
```
# Neon Object Storage
This is a public beta feature and only available in `us-east-2`.
Neon Object Storage is S3-compatible object storage that branches with your projects: every branch gets its own isolated storage state, so files and database rows stay in sync across dev, preview, staging, and production.
Use this skill to help the user store and serve files that branch alongside their database. Deliver a working bucket and upload/download flow, a branch-aware S3 client wired to the injected env vars, or a precise answer from the official Neon docs.
## When to Use
Reach for Neon Object Storage when the user needs to store files (images, uploads, generated assets, documents, backups) and any of the following are true:
- **They already use Lakebase Postgres and don't want a second provider.** One backend, one bill, one CLI, one set of branches — instead of standing up and wiring a separate AWS S3 / R2 / Supabase Storage account. The same Neon credential that backs the database backs storage.
- **Files must stay in sync with the database across environments.** Storage branches _together with_ your Postgres data. Fork a branch and the child instantly inherits the parent's buckets and objects at that point in time — copy-on-write, so no data is duplicated. This is what makes agent, dev, preview, and test environments seamless: a preview branch gets a consistent snapshot of _both_ the rows and the files they reference, and writes on the child never touch the parent.
- **They want safe, throwaway environments.** Upload, overwrite, and delete files in a preview/CI branch without any risk to production data, then drop the branch.
- **They want standard S3 tooling.** It's built on S3 semantics and speaks the S3 API, so the AWS SDKs, `boto3`, the AWS CLI, and presigned URLs all work — reliable and familiar, with no proprietary client.
If the user has no Neon project, isn't on Postgres, and just needs a standalone CDN-backed asset store, a dedicated object store may fit better — but the moment branch-consistent files + rows matter, this is the reason to use it.
## What It Does
- **S3-compatible** — Works with existing S3 SDKs, `boto3`, the AWS CLI, and presigned URLs. Path-style addressing and SigV4 only.
- **Branches with your database** — Every Neon branch gets its own isolated, copy-on-write storage state. Forking copies no data.
- **Two access modes** — `private` buckets require a credential for every operation; `public_read` buckets allow anonymous reads with authenticated writes.
- **One credential system** — The same Neon credential system used by Functions and the AI Gateway.
## Availability
Check this precondition before setting anything up: Neon Object Storage is a public beta feature available only on new projects in the `us-east-2` region. Confirm the user's Neon project is a new project in `us-east-2` before proceeding; it can't be enabled on existing projects.
## Setup
Object storage is part of the `neon.ts` infrastructure-as-code config (see the `neon` skill for the branch-first workflow, `link`/`checkout`, and `neon.ts` basics). Declare buckets under `preview.buckets`, keyed by bucket name:
```typescript
// neon.ts
import { defineConfig } from "@neon/config/v1";
export default defineConfig({
preview: {
buckets: {
images: {}, // private by default
"public-assets": { access: "public_read" },
},
},
});
```
Provision the declared buckets on the linked branch:
```bash
neon deploy # alias for `neon config apply`
```
## Neon Infrastructure as Code (`neon.ts`)
The `preview.buckets` block above is part of `neon.ts`, Neon's infrastructure-as-code file — one TypeScript file declares your buckets alongside every other service the branch should have (see the `neon` skill for the full reference). Reconcile the declaration against a branch the Terraform way:
```bash
neon config status # print the branch's live config (which buckets exist)
neon config plan # dry-run diff of what apply would change
neon config apply # create the declared buckets (neon deploy is an alias)
```
Buckets are **branch-scoped**: when a `neon.ts` is present, `neon checkout` applies the policy as it _creates_ a branch, so a fresh preview/CI branch comes up with its buckets already provisioned (and copy-on-write objects inherited from the parent). Checking out an _existing_ branch doesn't reconcile it — run `neon deploy` to apply changes. Provisioning (`config apply` / `deploy`), `link`, and `checkout` also pull the branch's S3 credentials into your local `.env.local`, so the same `env pull` step shown below happens for you on those commands.
## Environment Variables
When `preview.buckets` is declared, Neon injects **AWS-standard** S3 env vars so the AWS SDKs work from the environment with zero extra config. Inside a deployed Neon Function these are injected automatically; locally, pull them onto disk (or inject them at runtime) via the CLI:
```bash
neon env pull # writes the branch's vars into .env (or .env.local)
# or, without writing a file, inject at runtime:
neon-env run -- <your dev command>
```
| Variable | Meaning |
| ----------------------- | --------------------------------------------------- |
| `AWS_ACCESS_KEY_ID` | S3 Access Key ID (the branch credential's token id) |
| `AWS_SECRET_ACCESS_KEY` | S3 Secret Access Key |
| `AWS_ENDPOINT_URL_S3` | Branch S3 endpoint URL |
| `AWS_REGION` | Region, e.g. `us-east-2` |
Because the names are AWS-standard, the AWS SDK picks up the credentials, endpoint, and region from the environment automatically. Credentials are branch-scoped and valid for that branch and all its descendants.
For typed, validated access to these credentials instead of reading `process.env` directly, pass the same `neon.ts` config object to `parseEnv` from `@neon/env` — it returns an `env.storage` namespace (`accessKeyId`, `secretAccessKey`, `endpoint`, `region`) derived from your config. See the `neon` skill.
## Working with Objects: the Files SDK (Recommended)
The simplest, most portable way to read and write objects is the [Files SDK](https://files-sdk.dev) with its `neon` adapter — a small, unified storage API (`upload`, `download`, `url`, `list`, `exists`, `copy`, `delete`, `signedUploadUrl`) over web-standard I/O. It uses the AWS S3 client under the hood, configured appropriately for Neon, and relabels errors as `Neon error` — so there's nothing to misconfigure. Reach for this first.
Install it alongside the AWS S3 peer dependencies the adapter uses internally:
```bash
npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner
```
The adapter resolves its endpoint, region, and credentials from the same injected `AWS_*` env vars — pass only the bucket name:
```typescript
import { Files } from "files-sdk";
import { neon } from "files-sdk/neon";
const files = new Files({ adapter: neon({ bucket: "images" }) });
// Upload — body may be a Buffer, Uint8Array, Blob, File, ReadableStream, or string
await files.upload("generated/cat.jpg", fileBuffer, { contentType: "image/jpeg" });
// Download
const file = await files.download("generated/cat.jpg");
const bytes = new Uint8Array(await file.arrayBuffer());
// Presigned GET — share without exposing credentials (defaults to a 1h expiry)
const url = await files.url("generated/cat.jpg", { expiresIn: 3600 });
// Plus: files.exists(), files.list({ prefix }), files.copy(), files.delete(), files.signedUploadUrl()
```
Swap the adapter import (`files-sdk/s3`, `files-sdk/r2`, `files-sdk/gcs`, …) and the rest of your code is unchanged.
## Working with Objects: the AWS S3 Client (Alternative)
Neon speaks the S3 API directly, so you can drop down to the AWS SDK whenever you prefer the native client or already depend on it. The credentials, endpoint, and region are read from the standard AWS env chain, so the only setting you pass is `forcePathStyle: true` — Neon requires path-style addressing, so the S3 client **must** set it:
```typescript
import { S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({
forcePathStyle: true, // required: Neon uses path-style addressing
});
```
Then upload, download, and presign with the raw command objects:
```typescript
import { PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const BUCKET = "images";
// Upload
await s3.send(
new PutObjectCommand({
Bucket: BUCKET,
Key: "generated/cat.jpg",
Body: fileBuffer,
ContentType: "image/jpeg",
}),
);
// Download
const res = await s3.send(
new GetObjectCommand({ Bucket: BUCKET, Key: "generated/cat.jpg" }),
);
const bytes = await res.Body?.transformToByteArray();
// Presigned GET — share without exposing credentials
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: BUCKET, Key: "generated/cat.jpg" }),
{ expiresIn: 3600 },
);
```
## Pairing Storage with the Database on a Branch
The canonical pattern: an agent generates an image → `PutObject` into the `images` bucket → a row is inserted in Postgres → a presigned URL is returned on read. Store the bucket **key** (not the bytes) in a Postgres column, and presign on read. Because both the row and the object live on the same branch, they branch together and never drift.
## CLI Bucket and Object Commands
`neon` also has first-class bucket/object commands (`neon bucket create|list|delete`, `neon bucket object put|get|list|delete`) for scripting and one-off operations.
## Neon Documentation
The Neon documentation is the source of truth and Object Storage is evolving rapidly, so always verify against the official docs. Any doc page can be fetched as markdown by appending `.md` to the URL or by requesting `Accept: text/markdown`. Find the right page from the docs index (https://neon.com/docs/llms.txt) and the changelog announcements.
## Further Reading
- https://neon.com/docs/storage/overview.md
- https://neon.com/docs/storage/get-started.md
- https://neon.com/docs/storage/buckets.md
- https://neon.com/docs/storage/objects.md
- https://neon.com/docs/storage/authentication.md
- https://neon.com/docs/storage/s3-compatibility.md
- https://neon.com/docs/storage/troubleshooting.md
- https://files-sdk.dev — Files SDK docs (the `neon` adapter)
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit inputs section documenting neon auth, aws sdk peers, and env var injection; restructured procedure into 8 numbered steps with clear input/output for each; formalized decision points covering region eligibility, client choice, presigned urls, public/private buckets, function deployments, error handling, copy-on-write isolation, and config reconciliation; clarified output contract with bucket status checks, env var validation, and file/url operations; added outcome signals for bucket listing, upload/download, presigned sharing, database sync, branch forking, and isolation.
Neon Object Storage is S3-compatible object storage that branches with your Neon project: every branch gets its own isolated storage state, so files and database rows stay in sync across dev, preview, staging, and production. Use this skill when the user needs to store and serve files (images, uploads, generated assets, documents, backups) that must stay consistent with database state across branches, or when they already use Neon Postgres and want one backend, one credential system, and one set of branches instead of juggling a separate storage provider. Deliver a working bucket and upload/download flow, a branch-aware S3 client wired to the injected env vars, or a precise answer from the official Neon docs.
us-east-2 region. Neon Object Storage is public beta and not available on existing projects or other regions.npm install -g neondb or brew install neon).neon.ts config file in the project root (see the neon skill for setup).neon skill for Neon overview, getting started, and branch-first workflow. If not installed, fetch from https://neon.com/docs/ai/skills/neon/SKILL.md or install with npx skills add neondatabase/agent-skills --skill neon.~/.config/neon/credentials.json or NEON_API_KEY env var. Set up via neon auth or https://console.neon.tech.@aws-sdk/client-s3, @aws-sdk/s3-presigned-post, and @aws-sdk/s3-request-presigner to be installed.files-sdk package with the neon adapter for a simpler, unified storage API.When preview.buckets is declared in neon.ts, Neon injects AWS-standard S3 env vars. Pull them locally via neon env pull or neon-env run:
AWS_ACCESS_KEY_ID: S3 Access Key ID (branch credential token id).AWS_SECRET_ACCESS_KEY: S3 Secret Access Key.AWS_ENDPOINT_URL_S3: Branch S3 endpoint URL.AWS_REGION: Region, e.g., us-east-2.In Neon Functions, these are injected automatically. For typed, validated access, use parseEnv from @neon/env with your neon.ts config object to get an env.storage namespace.
Input: User's Neon project details. Steps:
us-east-2 region.us-east-2 project or wait for expanded availability.Output: Confirmed project is eligible to proceed.
Input: User's desired bucket names, access modes (private or public_read).
Steps:
neon.ts in the project root.preview.buckets block to the defineConfig object, keyed by bucket name. Set access: "public_read" for public-read buckets; omit the access key for private (default) buckets.import { defineConfig } from "@neon/config/v1";
export default defineConfig({
preview: {
buckets: {
images: {}, // private by default
"public-assets": { access: "public_read" },
},
},
});
Output: Updated neon.ts with bucket declarations.
Input: Completed neon.ts with preview.buckets declared.
Steps:
neon deploy (alias for neon config apply) to create the declared buckets on the linked branch.neon config status to view the branch's live config or neon config plan to dry-run the diff.Output: Buckets created on the branch; S3 credentials written to .env.local or pulled via neon env pull.
Input: Provisioned buckets on the branch. Steps:
neon env pull to write the branch's S3 credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, AWS_REGION) into .env or .env.local.neon-env run -- <your dev command>.echo $AWS_ACCESS_KEY_ID.Output: S3 credentials available in the environment.
Input: Project package.json.
Steps:
npm install files-sdk @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner.npm install @aws-sdk/client-s3 @aws-sdk/s3-presigned-post @aws-sdk/s3-request-presigner.Output: Dependencies installed; ready to import and use.
Input: Bucket name, file buffer/Blob/stream, optional bucket config. Steps:
Files from files-sdk and the neon adapter.Files client with the neon adapter, passing only the bucket name (credentials and endpoint are read from AWS_* env vars).files.upload(key, body, options) to upload, files.download(key) to download, files.url(key, options) to generate presigned GET URLs, or other methods (list, exists, copy, delete, signedUploadUrl).import { Files } from "files-sdk";
import { neon } from "files-sdk/neon";
const files = new Files({ adapter: neon({ bucket: "images" }) });
await files.upload("generated/cat.jpg", fileBuffer, { contentType: "image/jpeg" });
const file = await files.download("generated/cat.jpg");
const url = await files.url("generated/cat.jpg", { expiresIn: 3600 });
Output: Files uploaded, downloaded, or presigned URLs generated.
Input: Bucket name, file buffer, S3 commands. Steps:
S3Client from @aws-sdk/client-s3.forcePathStyle: true (required for Neon path-style addressing).PutObjectCommand, GetObjectCommand) with the client to upload, download, or manage objects.getSignedUrl from @aws-sdk/s3-request-presigner to generate presigned URLs.import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({ forcePathStyle: true });
await s3.send(new PutObjectCommand({
Bucket: "images",
Key: "generated/cat.jpg",
Body: fileBuffer,
ContentType: "image/jpeg",
}));
const res = await s3.send(new GetObjectCommand({
Bucket: "images",
Key: "generated/cat.jpg",
}));
const bytes = await res.Body?.transformToByteArray();
const url = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: "images", Key: "generated/cat.jpg" }),
{ expiresIn: 3600 },
);
Output: Files uploaded, downloaded, or presigned URLs generated.
Input: Uploaded object key, Postgres connection. Steps:
"generated/cat.jpg"), not the file bytes.files.url() or getSignedUrl() to share without exposing credentials.Output: Row inserted with object key; presigned URL returned on read.
Input: Bucket names, object keys. Steps:
neon bucket create, neon bucket list, neon bucket delete.neon bucket object put, neon bucket object get, neon bucket object list, neon bucket object delete.neon bucket object put images generated/cat.jpg --file ./cat.jpg.Output: Buckets and objects managed via CLI.
if the user's Neon project is existing or not in us-east-2, then stop setup. Neon Object Storage is available only on new projects in us-east-2. Advise the user to create a new project in that region or check the Neon changelog for expanded availability.
if the user wants a simple, unified storage API with minimal config, then use Files SDK with the neon adapter (step 6a). The adapter handles endpoint, region, and credential resolution from env vars automatically.
if the user already depends on the AWS SDK or prefers native S3 semantics, then use the AWS S3 client (step 6b). Remember to set forcePathStyle: true , Neon requires path-style addressing and will reject virtual-hosted-style requests.
if the user needs to share files without exposing S3 credentials, then generate presigned URLs via files.url() (Files SDK) or getSignedUrl() (AWS S3 client). Presigned URLs default to 1 hour expiry; customize via the expiresIn option (in seconds).
if the bucket is declared as public_read, then anonymous users can GET objects without a presigned URL, but PutObject and DeleteObject still require credentials. Private buckets require a credential for all operations.
if the user is deploying to a Neon Function, then do not call neon env pull in the function code. Credentials are injected automatically at runtime by the Neon platform.
if a presigned URL or S3 operation fails with a 403 Forbidden or credential error, then check that AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_ENDPOINT_URL_S3, and AWS_REGION are set and valid. Run neon env pull again to refresh them (credentials may have been rotated or the branch was created after a platform update).
if an S3 operation times out or returns a 503 Service Unavailable, then this is a transient network or Neon backend issue. Retry with exponential backoff (e.g., 100ms, 200ms, 400ms base with jitter). Do not assume the operation failed.
if the user wants to copy objects from parent to child on branch fork, then confirm that copy-on-write isolation works: child branch gets a read-only snapshot of parent objects at fork time. Writes on the child create new object versions that do not affect the parent.
if an neon checkout or neon deploy command is run but the bucket declarations in neon.ts were not applied, then this is expected. Checking out an existing branch does not reconcile its config. Run neon deploy or neon config apply to apply pending changes.
Success looks like:
neon config status shows the declared buckets under buckets: with their names, access modes, and creation timestamps.echo $AWS_ACCESS_KEY_ID and other AWS_* vars are non-empty and present in the shell environment.files.upload() or s3.send(new PutObjectCommand(...)) completes without error and returns metadata (e.g., ETag, object size).files.download() or s3.send(new GetObjectCommand(...)) returns a readable stream or buffer with the correct file content and MIME type.files.url() or getSignedUrl() returns an HTTPS URL with a valid SigV4 signature, query params, and expiry timestamp. The URL can be shared and accessed without credentials.The user knows the skill worked when:
neon config status and see their declared buckets listed with metadata.neon bucket object list or the Files SDK files.list() method.