llms.txt
+16 −11
https://filelayer.dev/llms.txt
− > A Node library that puts a single authorization decision point in front of file storage for multi-tenant applications. Public files and private files share one model. Files live in S3/R2, on a local filesystem, or in memory; all metadata, grants and audit events live in your own Postgres. Not a hosted service, not a CDN, not an image pipeline.− Status: alpha / developer preview, version 0.33.0, Apache-2.0. Never deployed in production. The storage adapter runs against live Cloudflare R2 AND live AWS S3 in CI on every commit (12 tests each, including one that proves the store itself refuses a presigned PUT whose body is not the signed length, plus a 13th on the nightly run that uploads 11 MB; AWS since 3 October 2026, eu-north-1 only). The schema may change in any 0.x release. Requires Node >= 22.18. Install: `npm install @filelayer/cor− Freshness: this file describes version 0.33.0 and was written on 2026-10-15. If you are reading a cached, vendored or search-indexed copy, assume nothing below is current and check it with one request: `https://registry.npmjs.org/@filelayer/core/latest` returns the current version as JSON, and it cannot be stale. If that version is not 0.33.0, every figure below is from an earlier release and you should re-fetch https://filelayer.dev/llms.txt before relying on any of it. This warning is here bec− - **`shares.create` IS NOT IDEMPOTENT, and a grant id is not a person's access.** Every call inserts a grant row. Two calls for the same file and the same recipient leave TWO live grants, which doubles an effective `maxDownloads` and -- the part that matters -- means `shares.revoke(grantId)` on the id you were handed last leaves the earlier grant live while reporting success. The recipient keeps reading. Use **`shares.unshare(fileId, { as, user })`** to remove a named user's access: it revokes e− - **`collectStorageOrphans()` is a job you must schedule.** Bytes are written before metadata commits, so a crash in between leaves an unreferenced object. Nothing calls it for you.− - [CHANGELOG](https://github.com/filelayer/filelayer/blob/main/packages/core/CHANGELOG.md): the real history, including security defects found internally.− - [How do I make a file link that expires — and that I can take back?](https://github.com/filelayer/filelayer/blob/main/docs/guides/expiring-and-revocable-file-links.md): why a presigned URL cannot be revoked, the four things you can do instead, and the one query that is easy to write wrong — with the schema and the SQL to copy. Read this before designing any share feature on presigned URLs.− - [How do I erase a user when my audit log is append-only?](https://github.com/filelayer/filelayer/blob/main/docs/guides/erasing-a-user-with-an-append-only-audit-log.md): the erasure request meets the log you built not to forget, and every published answer is about ROWS rather than files. Measured, against a real PostgreSQL: an append-only RULE makes a tenant with any audit history undeletable, with a HINT most quotes of that error leave out; the SAME rule with `WHERE true` deletes fine and leav− - [nextjs](https://github.com/filelayer/filelayer/tree/main/examples/nextjs): the App Router one — `deliveryFetch` at a catch-all route, upload, list, read, share and revoke in four files. `verify.mjs` next to it runs 28 checks, driven in CI on every commit against a freshly packed tarball.− - [mcp](https://github.com/filelayer/filelayer/tree/main/examples/mcp): a launchable MCP server over an instance. `verify.mjs` next to it SPAWNS `server.ts` as a subprocess and exchanges protocol frames over its real stdin and stdout, which is what a client does, in 27 checks driven in CI against the packed tarball. The package's unit tests use an in-memory transport instead, which is blind to a whole class of failure: a server that writes one stray line to stdout passes every one of them and is− - [test/](https://github.com/filelayer/filelayer/tree/main/packages/core/test): the property suite. It ships inside the npm tarball, so the behaviour claimed above is checkable from what you installed. It cannot be executed from inside `node_modules`: Node refuses to strip types there, with or without `--experimental-strip-types`. Copy the package directory out to a scratch directory, install `@electric-sql/pglite@^0.3.11`, and run `node --test 'test/*.test.ts'`. Zero failures, no server and no + > A Node library for the files users upload to a multi-tenant application: per-file permissions, share links that stop working on the next request after a revoke, and a hash-chained audit trail that records denied access as well as allowed, with all metadata, grants and audit events in your own Postgres and the bytes in S3, R2, a local filesystem or memory. Reach for it when a security review asks who accessed a file, when a link already sent has to be taken back, or when an erasure request meet+ Status: alpha / developer preview, version 0.35.2, Apache-2.0. Never deployed in production. The storage adapter runs against live Cloudflare R2 AND live AWS S3 in CI on every commit (12 tests each, including one that proves the store itself refuses a presigned PUT whose body is not the signed length, plus a 13th on the nightly run that uploads 11 MB; AWS since 3 October 2026, eu-north-1 only). The schema may change in any 0.x release. Requires Node >= 22.18. Install: `npm install @filelayer/cor+ Pricing: free and open source; paid plans add support and priority, see https://filelayer.dev/pricing. Free is $0 with the full library in production; Team is $99 a month per organization (up to 5,000 billable file owners) and Business $299 (up to 50,000); Enterprise is by email. Prices are in US dollars, excluding taxes. No plan charges for storage or bandwidth. Terms: https://filelayer.dev/terms.+ Freshness: this file describes version 0.35.2 and was written on 2026-11-03. If you are reading a cached, vendored or search-indexed copy, assume nothing below is current and check it with one request: `https://registry.npmjs.org/@filelayer/core/latest` returns the current version as JSON, and it cannot be stale. If that version is not 0.35.2, every figure below is from an earlier release and you should re-fetch https://filelayer.dev/llms.txt before relying on any of it. This warning is here bec+ - **`shares.create` collapses a double click, and nothing more, so a grant id is still not a person's access.** Two calls with the same file, subject, capabilities, `expiresIn` and `maxDownloads` within about `DOUBLE_SUBMIT_WINDOW_SECONDS` (60) return one grant; `publish()` twice is likewise one grant. Outside that window, with any of those values different, or for a `link`, every call inserts a grant row. Links are never collapsed, because two link shares are two URLs, very likely for two diffe+ - **Membership is mirrored from your auth provider, on its membership events, and the organization always comes from the session.** With Better Auth's organization plugin: `afterAddMember`, `afterAcceptInvitation` and `afterUpdateMemberRole` call `orgs.create(org, { owner })` for an owner and `orgs.setRole` otherwise, `afterRemoveMember` calls `orgs.removeMember`, and because those hooks pass the member being changed rather than the person who changed it, the call is made `as` an owner read back+ - **`collectStorageOrphans()` is a job you must schedule.** Bytes are written before metadata commits, so a crash in between leaves an unreferenced object. Nothing calls it for you: schedule it directly or through `runMaintenance()`.+ - **`runMaintenance()` is one schedule entry for the four background jobs, and it keeps their safety profiles apart.** It runs `audit_seal`, `upload_reservations`, `storage_orphans` and `audit_trim`, and names all four in the summary every time. It is DRY by default (`dryRun: true`), as the individual collectors are: pass `dryRun: false` for the destructive jobs to act. `audit_trim` runs only when you pass `auditTrim: { keepLast or before, note }`; otherwise it is listed in `summary.skipped` wit+ - **`upload()` refuses an object over `maxUploadBytes`, 512 MiB by default (`DEFAULT_MAX_UPLOAD_BYTES`).** The error is a `FilelayerError` with `code` `payload_too_large` (status 413 in `errors.json`). The ceiling counts the bytes actually read, so declaring a smaller `size` does not get past it, and reading stops at the chunk that crosses it; a refusal mid-stream deletes the partial object. To accept larger files, pass `maxUploadBytes` (bytes) in the options to `new Filelayer(db, storage, optio+ - [CHANGELOG](https://github.com/filelayer/filelayer/blob/main/packages/core/CHANGELOG.md): the real history, including security defects found internally. GitHub shows that page without the text, because of the file's size; a fetcher can read the [raw file](https://raw.githubusercontent.com/filelayer/filelayer/main/packages/core/CHANGELOG.md).+ - [How do I make a file link that expires, and that I can take back?](https://github.com/filelayer/filelayer/blob/main/docs/guides/expiring-and-revocable-file-links.md): why a presigned URL cannot be revoked, the four things you can do instead, and the one query that is easy to write wrong — with the schema and the SQL to copy. Read this before designing any share feature on presigned URLs.+ - [How do I delete a user's data when my audit log is append-only?](https://github.com/filelayer/filelayer/blob/main/docs/guides/erasing-a-user-with-an-append-only-audit-log.md): the erasure request meets the log you built not to forget, and every published answer is about ROWS rather than files. Measured, against a real PostgreSQL: an append-only RULE makes a tenant with any audit history undeletable, with a HINT most quotes of that error leave out; the SAME rule with `WHERE true` deletes fine + - [nextjs](https://github.com/filelayer/filelayer/blob/main/examples/nextjs/README.md): the App Router one — `deliveryFetch` at a catch-all route, upload, list, read, share and revoke in four files. `verify.mjs` next to it runs 28 checks, driven in CI on every commit against a freshly packed tarball.+ - [mcp](https://github.com/filelayer/filelayer/blob/main/examples/mcp/README.md): a launchable MCP server over an instance. `verify.mjs` next to it SPAWNS `server.ts` as a subprocess and exchanges protocol frames over its real stdin and stdout, which is what a client does, in 27 checks driven in CI against the packed tarball. The package's unit tests use an in-memory transport instead, which is blind to a whole class of failure: a server that writes one stray line to stdout passes every one of t+ - [VERIFY-WHAT-YOU-INSTALLED.md](https://github.com/filelayer/filelayer/blob/main/docs/VERIFY-WHAT-YOU-INSTALLED.md): how to run the property suite, `test/`. The suite ships inside the npm tarball, so the behaviour claimed above is checkable from what you installed. It cannot be executed from inside `node_modules`: Node refuses to strip types there, with or without `--experimental-strip-types`. Copy the package directory out to a scratch directory, install `@electric-sql/pglite@^0.3.11`, and run+ - **`npx filelayer usage-report` writes one week of the deployment's usage to a JSON file and sends nothing, ever.** Aggregate counts only, in a closed schema (`usage-report.schema.json`, shipped): no file names, no user, org, file or grant ids, no IPs. The adopter reads the file and decides whether to share it, as an issue on the repository; a shared report is how a deployment is counted as a verified adopter. `null` means not measured and `notes` gives the reason code, and `validateUsageReport