npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@filelayer/core

v0.5.3

Published

Multi-tenant file authorization middleware and lifecycle for Postgres: a single decision point for public and private files, revocable URLs, delegated share links and a hash-chained audit trail.

Readme

Filelayer

CI npm node license

⚠️ Alpha — developer preview. Not production software.

Should you depend on this? — the real numbers, including the ones that are zero, and exactly what would change them.

We would rather you trust us later for good reasons than trust us now for bad ones, so here is the honest state of this project.

What is tested. The authorization engine is the part we stand behind. It carries a property suite covering cross-tenant isolation, revocation, delegation attenuation, download caps, audit tamper-evidence and the full role matrix, plus a differential test asserting that the set query and the point check agree exactly, plus an adversarial suite of attacks it must survive. All of it runs in CI on every commit, against real PostgreSQL. A release gate packs the tarball, installs it into an empty directory and drives the whole lifecycle as a stranger would.

What has never run against live infrastructure. The S3/R2 storage adapter has never been executed against real AWS or Cloudflare credentials. It is exercised against a local implementation that verifies SigV4 signatures, which is not the same thing and we will not pretend it is. A live-credential suite exists and runs automatically once FILELAYER_TEST_S3_* is in the environment; nobody has supplied it yet.

What may break. Nobody has deployed this. There is no production usage, no hosted service, no CLI, and no operational track record — so the failure modes that only appear under real traffic, real object stores and real connection pools are unmeasured. Expect to be the person who finds them.

The schema may change before 1.0. It has already had one breaking change. Any 0.x → 0.(x+1) may break the API, the schema, or both. Every break is in the changelog and every schema break ships with SQL in MIGRATIONS.md, but there is no long-term support branch and no backporting.

Use it to evaluate the model, to build something that is not yet carrying customer data, or to tell us where it breaks. Do not use it as the file layer under a production system you would be embarrassed to lose. The full list of known gaps is in Limitations below; nothing there is hidden in an appendix.

The file layer for SaaS applications. Public files and private files, with one authorization model behind both.

You tell Filelayer who the caller is. Filelayer decides what they may do with a file, serves the bytes with the right headers, and writes the audit event. For file access that goes through Filelayer, that decision is made in one place — packages/core/src/authz.ts — so you write no ownership checks in route handlers, no presigned-URL expiry logic, and no per-route access rules of your own.

Filelayer is authorization middleware, not row-level security. It runs in your application process, in front of Postgres and your bucket. Code that queries these tables directly does not go through it. The schema does enforce a set of invariants against every writer — cross-tenant grants, cross-project identities, and delegation that amplifies authority or subject breadth are refused by constraints and triggers, so a migration or a psql session cannot write them. But there is no RLS policy in schema.sql, and a direct SELECT is not filtered by anything. If you need enforcement that survives arbitrary database clients, use database-level enforcement; Filelayer is the layer above it, and the two compose.

// Public avatar
const { url } = await fl.files.put(bytes, { public: true });

// Private, user-owned
const { id } = await fl.files.put(bytes, { owner: 'user_123' });
const file  = await fl.files.get(id, { as: 'user_123' });

// Multi-tenant, role-controlled
await fl.files.put(bytes, { org: 'acme', owner: 'user_123' });

// Shared with an expiry, a password and a download cap — and revocable
const share = await fl.shares.create(id, {
  as: 'user_123', expiresIn: 3600, maxDownloads: 3, password: 'hunter2',
});
await fl.shares.revoke(share.grantId, { as: 'user_123' });   // takes effect now

Start at whichever line matches your problem. Complexity is incremental: each tier adds one concept, and no tier makes you pay for a concept you are not using. → docs/QUICKSTART.md


Status: pre-release. Read this part.

Filelayer is pre-1.0 and has not been deployed by anyone. This README is accurate rather than promotional, because you are more likely to be an AI agent reading it to write an integration than a human reading it to be persuaded, and an inaccurate README wastes your time and ours.

  • Licensed under Apache-2.0. You may use it, modify it, distribute it and ship it inside commercial software, with a patent grant. This was the single largest blocker to adoption and it is resolved: packages/core/package.json declares "license": "Apache-2.0", and both LICENSE and NOTICE ship inside the npm tarball. See License below.
  • The authorization core is tested: a property suite covering cross-tenant isolation, revocation, delegation attenuation, download caps, audit tamper-evidence and the full role matrix, plus a differential test that asserts the set query and the point check agree exactly.
  • The S3/R2 storage adapter is exercised against a signature-verifying local S3 implementation (packages/core/test/storage.test.ts). It has never been run against live AWS or Cloudflare credentials. A live-credential suite exists (packages/core/test/s3-live.test.ts) and runs automatically when FILELAYER_TEST_S3_* is present in the environment; nobody has supplied it.
  • There is no hosted service and no CLI. You run it against your own Postgres.
  • Versioning is pre-1.0: see Versioning below and packages/core/MIGRATIONS.md.

What it is for, and what it is not for

Worth the overhead when: files are private, belong to specific people or tenants, and their permissions change over time. Documents, contracts, attachments, exports, anything with a share link you might later want back.

Not worth the overhead when:

| You need | Use instead | Why | |---|---|---| | Public images at CDN volume | a CDN-backed bucket | Default delivery proxies every byte. Redirect delivery (below) removes the proxy but is opt-in and narrow. | | Video or audio seeking in a browser | a CDN / media service | The shipped HTTP route helpers do not answer Range requests. | | Direct browser → storage upload | Supabase / presigned S3 | Uploads go through your server. | | Thumbnails, transforms, format negotiation | Cloudinary / imgix | We have none. |

We publish the full comparison, including the cases we lose, in ARCHITECTURE-PROGRESSIVE.md §5. Short version: for a public avatar, Supabase is 10 lines and Filelayer is 16. If avatars are your whole problem, use Supabase.


The five security properties

These are the product. Each is enforced in the schema or the authorization engine, not by convention, and each has tests named after it.

| | Property | What it means in practice | |---|---|---| | P1 | Deny by default | There is no public boolean anywhere in the schema. Public delivery is an explicit, revocable, auditable grant row — there is no flag to leave on by accident. This is a property of the schema, not of your object store: Filelayer cannot make your bucket private, and a public bucket bypasses everything on this page. That is the one configuration step it cannot do for you — QUICKSTART §7. | | P2 | No ambient authority | Knowing an object key, a URL or a file id grants nothing. Storage location is never an input to a decision. | | P3 | Cross-tenant grants are structurally impossible to write | A grant's org_id must equal its file's org_id, enforced by the composite foreign key FOREIGN KEY (file_id, org_id) REFERENCES file (id, org_id). No writer — including a migration or a psql session — can create a grant pointing at another tenant's file. This is a write-side integrity constraint, not a read filter: it makes the row unrepresentable. Reads are scoped by the engine, not by the database. | | P4 | A URL never outlives its permission | Every signed URL embeds a grant id and is re-validated on every request, transitively through the whole delegation chain. Revocation beats a live URL. | | P5 | Every decision is audited, including denials | Hash-chained per tenant. Probes that cannot be attributed to a tenant go to a system chain rather than being dropped. The log answers in your identifiers — marco, contract.pdf, acme — alongside the internal ones, so "who accessed this?" needs no SQL of yours. |

Three more properties are enforced in the schema and documented in packages/core/SEMANTICS.md: atomic download counters (P6), deletion as a liveness predicate rather than a cascade (P7), and per-project identifier namespaces (P8).

Two consequences worth knowing before you adopt:

  • Revocation actually works. fl.files.unpublish(id) makes a URL that has been printed, indexed and pasted into a support ticket stop working on the next request — no deletion, no key rotation, no cache purge. Supabase's getPublicUrl() is offline string concatenation, so it has no request at which to make that decision; deleting the object is the only withdrawal.
  • This costs you a byte path. P4 is why the default is to serve bytes rather than hand out a presigned URL, and it is why the default path has no CDN in front of it. The two facts are the same fact. Redirect delivery trades a bounded revocation window for that CDN and is opt-in; see packages/core/SEMANTICS.md.

Install

npm install @filelayer/core

Requires Node ≥ 22.18. The package ships compiled JavaScript and type declarations in dist/; the TypeScript source and the full test suite are in the tarball as well, so every claim on this page is inspectable from what you installed.

That install has zero runtime dependencies. Filelayer talks to your Postgres and your bucket, so it ships neither. The throwaway instance below is the one exception: quickstart() runs on an embedded WebAssembly Postgres, declared as an optional peer dependency so that it never lands in a production node_modules. Add it if you want the throwaway instance, and skip it otherwise — createTestDb() will tell you, by name, if you need it:

npm install --save-dev "@electric-sql/pglite@^0.3.11"

Install PGlite with that version constraint. Filelayer supports the 0.3.x line, which is what peerDependencies declares and what the suite runs against. 0.5.x is not supported: we ran the full suite against 0.5.8 and it does not pass, so the range has not been widened. PGlite's latest on npm is a 0.5.x release, so omitting the constraint can install a version outside the supported range — and npm then refuses the whole tree with ERESOLVE. The quotes are for your shell, not for npm: ^ is a glob operator under zsh with extendedglob and an escape character in cmd.exe.

import { Filelayer } from '@filelayer/core';

const fl2 = await Filelayer.quickstart();          // PGlite + in-memory bytes
const { url: avatarUrl } = await fl2.files.put(bytes, { public: true });

quickstart() is ephemeral — everything is lost when the process exits. Production is three configuration steps and is not hidden — the third is confirming your bucket is private: docs/QUICKSTART.md §7.

Running the suite

The tests are in the tarball but they cannot be executed from inside node_modules: Node refuses to strip types from files under node_modules, and the tests import the TypeScript source directly. To run them, clone the repository — tests run against PGlite, PostgreSQL 17 compiled to WebAssembly and running in-process, so there is no daemon and no Docker:

git clone https://github.com/filelayer/filelayer && cd filelayer
npm run bootstrap        # npm ci in packages/core
npm test                 # the security property suite, 330 tests
npm run typecheck
npm run verify           # typecheck + tests + build + doc and language checks
npm run example:tier1    # a public avatar, on :3000
npm run example:vault    # the full Vault app, on :8787

Versioning

Pre-1.0. The version is 0.MINOR.PATCH and the promise is deliberately narrow:

  • 0.x → 0.(x+1) may break the API, the schema, or both. Every break is in packages/core/CHANGELOG.md, and every schema break has a migration in packages/core/MIGRATIONS.md.
  • 0.x.y → 0.x.(y+1) is additive or a fix. No schema change that requires action, no signature change.
  • There is no long-term support branch and no backporting before 1.0.

The schema has already had one breaking change (per-project identifier namespaces). It is written up, with the SQL, as the first entry in packages/core/MIGRATIONS.md.


Layout

| Path | What it is | |---|---| | packages/core/schema.sql | The data model. Every security property is commented at the constraint that enforces it. Read this first if you are reviewing us. | | packages/core/src/authz.ts | The authorization engine. Two functions, one decision core. | | packages/core/src/simple.ts | The tiered API (files, orgs, shares). No authorization logic — a facade. | | packages/core/src/delivery.ts | Byte delivery. Owns the response headers so you cannot get them wrong. | | packages/core/test/ | The property suite. Ships in the tarball. | | packages/core/dev/ | Diagnostic scripts. Not published. | | examples/tier1-avatar … tier3-org-roles | One runnable example per tier | | examples/vault | A full B2B document workspace over HTTP |

Documents

Every link above points at GitHub, and an installed consumer may have no network and no browser. The npm package therefore carries the same files on disk, under node_modules/@filelayer/core/: this README, llms.txt, openapi.json, schema.sql, SEMANTICS.md, MIGRATIONS.md, CHANGELOG.md, LICENSE and NOTICE, alongside src/ and test/. Three of them also resolve as subpath imports — @filelayer/core/llms.txt, @filelayer/core/openapi.json and @filelayer/core/schema.sql — so a reader does not have to guess at the layout of node_modules.

License

Apache-2.0. Commercial use, modification, distribution and private use are permitted, with an express patent grant and a patent-retaliation termination clause. You must preserve the copyright and licence notices and state significant changes; there is no copyleft obligation on your own code.

SPDX-License-Identifier: Apache-2.0
Copyright 2026 Technology Pro Bono S.L.

LICENSE is the unmodified licence text from apache.org. NOTICE carries the attribution notice required by section 4(d); both are inside the published npm tarball, so the grant travels with the artifact rather than only with the repository. There are no per-file licence headers — the grant is carried by LICENSE, NOTICE and the license field of every package.json.

Dependencies. Filelayer has no runtime dependencies. Its one third-party package, @electric-sql/pglite (Apache-2.0), is a devDependency and an optional peer dependency: the test suite and quickstart() run on it, and a production install does not contain it. It is installed from the registry rather than vendored, ships no NOTICE file of its own, and is recorded in ours for convenience. No third-party code is copied or embedded anywhere in this repository.

Contributing, security and conduct

  • CONTRIBUTING.md — how to set up, what npm run verify covers, and what a pull request needs.
  • SECURITY.md — report vulnerabilities privately, never as a public issue. Includes our disclosure timetable and what is in and out of scope.
  • CODE_OF_CONDUCT.md — Contributor Covenant 2.1.

Bugs, questions and "this document is wrong" reports go to GitHub Issues. Given the alpha status at the top of this page, a report that the product does not do what this README says is the most valuable thing you can send us.

Limitations

Restated here so they are not only in an appendix. Each one is current as of 0.5.3; where a limitation has been lifted since an earlier release, the changelog says so.

  1. No Range responses from the shipped HTTP routes. fileDownloadRoute() and shareDownloadRoute() do not parse the Range request header, so they never return 206 and a browser cannot seek. Do not use the shipped routes for video or audio. The layer underneath is complete: readStream() and redeemStream() take a byte range, the S3 adapter honours it, and sendNodeStream() / toStreamResponse() emit 206 with Content-Range whenever a range was served. Parsing the request header is the part you write — see QUICKSTART §6 for the whole thing.
  2. The tiered facade fl.files.put() takes a Uint8Array, so a file put through it is fully resident in memory. The core fl.upload() accepts a ReadableStream; use that above a few tens of megabytes.
  3. No direct browser → storage upload. Upload bytes go through your server.
  4. Org admins and owners can read private files. Deliberate — retention and legal hold are their responsibility — but if you need to exclude the operator, you need envelope encryption and we do not have it.
  5. Identifiers are unique per project, not per org. actor.external_id and org.external_id are scoped to a project (one customer application). Two orgs inside one project cannot both have a user called alice meaning different people.
  6. The S3/R2 adapter has never run against live credentials. It is tested against a local implementation that verifies SigV4 signatures, which is not the same thing.
  7. Unauthenticated callers can still append denial events to the audit chain of a tenant inside a project they can reach. That is P5 working as designed — denials are the events worth recording — but it is a load-bearing reason to rate-limit at ingest. orgExists is project-scoped, so the reach is bounded to a project the caller is already authenticated for.
  8. Orphan collection is a job you have to run. Bytes are written before the metadata commits, so a crash in between leaves an unreferenced object. collectStorageOrphans() cleans them up and nothing calls it for you.
  9. Redirect delivery has a revocation window. If you enable it, a presigned URL stays valid for up to its TTL after the grant is revoked. It is off by default, defaults to anonymous grants only, and requires passing a verbatim acknowledgement string. That string is the point.