Search by

rishi-raj-jain / flysystem-neon

rishi-raj-jain

Flysystem adapter for Neon Object Storage (branchable, S3-compatible storage on Neon).

Package info

github.com/rishi-raj-jain/flysystem-neon

pkg:composer/rishi-raj-jain/flysystem-neon

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.0.0 2026-11-05 08:34 UTC

This package is auto-updated.

Last update: 2026-11-05 08:39:04 UTC


README

CI Latest Version License

A Flysystem v3 adapter for Neon Object Storage: S3-compatible object storage that branches along with your Neon Postgres database.

It is built on Flysystem's official AWS S3 adapter and adds what Neon needs:

  • an S3 client preconfigured for Neon (path-style addressing, SigV4, checksums only when required);
  • visibility that follows Neon's bucket-level access levels (private / public_read), since Neon has no per-object ACLs;
  • fromEnvironment(), which reads the AWS_* variables that neon env pull, neon deploy and Neon Functions provide.

Installation

composer require rishi-raj-jain/flysystem-neon

Requires PHP 8.2+ and league/flysystem 3.x.

Get credentials

You need a Neon project in a region with Object Storage (AWS us-east-1, us-east-2, eu-central-1 or ap-southeast-1), a bucket, and a credential with the storage:read and storage:write scopes.

neon buckets create uploads            # add --access-level public_read for a public bucket
neon env pull --file .env              # writes the AWS_* variables below
AWS_ENDPOINT_URL_S3=https://br-cool-darkness-a1b2c3d4.storage.c-1.us-east-2.aws.neon.tech
AWS_ACCESS_KEY_ID=nak_live_...
AWS_SECRET_ACCESS_KEY=nsk_live_...
AWS_REGION=us-east-2

You can also find these values under Connect > Storage in the Neon Console. See Get started with Object Storage and Authentication.

Usage

use League\Flysystem\Filesystem;
use RishiRajJain\FlysystemNeon\NeonStorageAdapter;

// Reads AWS_ENDPOINT_URL_S3, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION.
$adapter = NeonStorageAdapter::fromEnvironment(bucket: 'uploads');

$filesystem = new Filesystem($adapter);

$filesystem->write('avatars/rishi.txt', 'Hello from Neon!');
echo $filesystem->read('avatars/rishi.txt');

foreach ($filesystem->listContents('avatars', deep: true) as $item) {
    echo $item->path(), PHP_EOL;
}

// Presigned download link for a private bucket.
$url = $filesystem->temporaryUrl('avatars/rishi.txt', new DateTimeImmutable('+10 minutes'));

Passing configuration explicitly

$adapter = NeonStorageAdapter::fromConfig([
    'endpoint'   => 'https://br-cool-darkness-a1b2c3d4.storage.c-1.us-east-2.aws.neon.tech',
    'key'        => 'nak_live_...',
    'secret'     => 'nsk_live_...',
    'region'     => 'us-east-2',   // optional, defaults to us-east-2
    'bucket'     => 'uploads',
    'prefix'     => 'tenant-42',   // optional, scopes every path ("root" also works)
    'visibility' => 'private',     // the bucket's access level: "private" or "public"
    'client_options' => [],        // optional, extra Aws\S3\S3Client options
]);

Or bring your own client:

$client = NeonStorageAdapter::createClient($endpoint, $accessKeyId, $secretAccessKey, 'us-east-2');
$adapter = new NeonStorageAdapter($client, 'uploads', prefix: 'tenant-42');

Visibility

Neon sets access per bucket, not per object. A bucket is either private or public_read, and you choose this when you create it in the Console, API or CLI. Tell the adapter which one it is talking to:

$public = NeonStorageAdapter::fromEnvironment('public-assets', bucketVisibility: 'public');

The adapter then behaves like this:

Operation Behavior
write() / writeStream() with any visibility Succeeds. The object takes the bucket's access level.
visibility() Returns the bucket's access level.
setVisibility() with the bucket's level No-op.
setVisibility() with the other level Throws UnableToSetVisibility instead of silently doing nothing.
publicUrl() on a public_read bucket Path-style URL that anyone can read.
publicUrl() on a private bucket Throws UnableToGeneratePublicUrl. Use temporaryUrl() instead.

To serve some files publicly and keep others private, use two buckets (and two adapters, or Flysystem's MountManager).

Branches

Each Neon branch has its own storage endpoint, and a new branch starts with a copy-on-write view of its parent's objects. Point the adapter at a branch's endpoint to work on that branch:

$preview = NeonStorageAdapter::fromConfig([
    'endpoint' => 'https://br-preview-xyz.storage.c-1.us-east-2.aws.neon.tech', // the preview branch
    'key'      => getenv('AWS_ACCESS_KEY_ID'),     // a credential from main also works on child branches
    'secret'   => getenv('AWS_SECRET_ACCESS_KEY'),
    'bucket'   => 'uploads',
]);

Files written on the preview branch don't show up on main, and changes on the preview don't affect the parent's objects. That makes preview environments and test runs with real uploads safe. Get a branch's endpoint with neon env pull while linked to that branch, or from the GET /projects/{project_id}/branches/{branch_id}/storage API.

Why not just use the AWS S3 adapter?

Neon speaks the S3 API, so league/flysystem-aws-s3-v3 mostly works once it's configured with the right endpoint and use_path_style_endpoint. These parts don't, verified against a live Neon branch:

With the plain S3 adapter With this adapter
write(..., ['visibility' => 'public']) fails with 501 NotImplemented (public canned ACLs aren't supported) Write succeeds
setVisibility() always fails with 501 (PutObjectAcl isn't supported) No-op or a clear error
visibility() reports private even for objects in a public_read bucket Reports the bucket's access level
Leaving out use_path_style_endpoint breaks every request Preconfigured
An ACL option in the write config is forwarded and rejected Not forwarded

All other S3 adapter features keep working: streaming reads, multipart uploads for large files, ContentType / CacheControl / ContentDisposition / Metadata options, server-side copy and move, deep listings, presigned temporary URLs and ETag checksums.

Testing

The adapter runs Flysystem's shared adapter test suite (league/flysystem-adapter-test-utilities) against two live Neon buckets, one private and one public_read. Each run writes under a random prefix and cleans up after itself.

composer install
vendor/bin/phpunit --testsuite unit          # no network needed
cp .env.example .env                         # fill in a branch's credentials and two bucket names
vendor/bin/phpunit --testsuite integration   # runs against Neon
vendor/bin/phpstan analyse

A few upstream scenarios assume per-object ACLs or "no mime type". The test classes override those to assert Neon's actual behavior; each override has a comment explaining it.

License

MIT. See LICENSE.