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

@vryzel/file-next

v0.4.5

Published

File-system abstraction over AWS S3 / Cloudflare R2 for Next.js, with a secondary metadata index and shadcn/ui components.

Readme

@vryzel/file-next

S3/R2 filesystem for Next.js. Bytes in your bucket. The folder tree in your SQLite or Postgres. Tenant comes from the server, never from the client.

Live demo

pnpm add @vryzel/file-next
# optional, depending on the store:
pnpm add better-sqlite3   # SQLite
pnpm add pg               # Postgres

Peer deps better-sqlite3 and pg are optional. Install only the one you use.

Quick path

// app/lib/file-next.ts
import {
  asTenantId,
  asUserId,
  createFileSystem,
  createSqliteStore,
} from "@vryzel/file-next";
import { createServerActions } from "@vryzel/file-next/server";
import { createWriteThrough } from "@vryzel/file-next/sync";

const store = createSqliteStore({ path: ".data/metadata.db" });
const fs = createFileSystem(
  {
    provider: "s3",
    bucket: process.env.FILE_NEXT_BUCKET!,
    region: process.env.FILE_NEXT_REGION ?? "us-east-1",
    credentials: {
      accessKeyId: process.env.FILE_NEXT_ACCESS_KEY_ID!,
      secretAccessKey: process.env.FILE_NEXT_SECRET_ACCESS_KEY!,
    },
  },
  { store },
);

export const actions = createServerActions({
  store,
  fs,
  writeThrough: createWriteThrough(fs, store),
  getAuth: () => ({
    tenantId: asTenantId("demo"),
    userId: asUserId("user-1"),
  }),
});
// app/lib/actions.ts
"use server";
import { actions } from "./file-next";

export const listFiles = (input: { parentId: string | null; cursor?: string; limit?: number }) =>
  actions.listFiles(input);

Replace getAuth with Clerk / Auth.js / your session. Import @vryzel/file-next/server only from server modules.

Use cases

1. SQLite (one process)

Default for a single Next.js server.

import { createSqliteStore } from "@vryzel/file-next";
const store = createSqliteStore({ path: ".data/metadata.db" });

Schema is created on first call. Path is relative to process.cwd().

2. Postgres (multi-instance + RLS)

import { createPostgresStore } from "@vryzel/file-next";
const store = createPostgresStore({
  connectionString: process.env.DATABASE_URL!,
});

Install pg. Tenant isolation uses SET LOCAL app.current_tenant + row-level security.

3. In-memory (tests / Storybook)

import { createMemoryStore, createMemoryFileSystem } from "@vryzel/file-next";

const store = createMemoryStore();
const fs = createMemoryFileSystem({ store });

No AWS. Data dies with the process.

4. Cloudflare R2

const fs = createFileSystem(
  {
    provider: "r2",
    bucket: process.env.FILE_NEXT_BUCKET!,
    endpoint: process.env.FILE_NEXT_ENDPOINT!, // https://<accountid>.r2.cloudflarestorage.com
    credentials: {
      accessKeyId: process.env.FILE_NEXT_ACCESS_KEY_ID!,
      secretAccessKey: process.env.FILE_NEXT_SECRET_ACCESS_KEY!,
    },
  },
  { store },
);

R2 is S3-compatible. Set bucket CORS for PUT/GET from your origin.

5. Env singleton

import { getFileSystem } from "@vryzel/file-next";
const fs = getFileSystem(); // memoized for the process

Reads:

| Var | Required | |---|---| | FILE_NEXT_PROVIDER | s3 or r2 | | FILE_NEXT_BUCKET | yes | | FILE_NEXT_REGION | S3 | | FILE_NEXT_ENDPOINT | R2 | | FILE_NEXT_ACCESS_KEY_ID | yes | | FILE_NEXT_SECRET_ACCESS_KEY | yes | | FILE_NEXT_FORCE_PATH_STYLE | optional |

Missing config throws FileSystemError (not retryable).

6. Multi-tenant keys

const scoped = fs.forTenant("demo");
// object keys are prefixed t/demo/{nodeId} unless prefixTenantKeys: false

Every store method also takes tenantId. Do not take tenant from the request body.

7. Server actions

createServerActions returns:

listFiles, searchFiles, listTrash, createFolder, deleteFile, moveFile, copyFile, setMetadata, prepareUpload, confirmUpload, restoreNode, createShare, resolveShare, revokeShare.

All return Result<T, FileSystemError>: { ok: true, value } or { ok: false, error }.

const listed = await actions.listFiles({ parentId: null, limit: 50 });
if (!listed.ok) throw listed.error;

listFiles / listTrash accept cursor + limit.

createShare returns { token, url } where url is /api/share/{token} (your domain, not the bucket). Mount:

// app/api/share/[token]/route.ts
import { createShareRouteHandler } from "@vryzel/file-next/server";
export const GET = createShareRouteHandler({ store, fs });

The handler streams the object through Next.js. Folders cannot be shared this way. Optional sharePathPrefix on createServerActions (default /api/share).

8. Route handlers (upload / download)

// app/api/upload/route.ts
import { createUploadRouteHandler } from "@vryzel/file-next/server";
import { getFileSystem } from "@vryzel/file-next";

export const PUT = createUploadRouteHandler({
  fs: getFileSystem(),
  maxBytes: 25 * 1024 * 1024,
  allowedContentTypes: ["image/*", "application/pdf"],
});

Many apps skip this and PUT to a signed URL / a thin writeThroughFile route (see the test app). Use whichever matches your upload flow.

9. Write-through (object first, then the tree)

import { createWriteThrough } from "@vryzel/file-next/sync";

const writeThrough = createWriteThrough(fs, store);
await writeThrough.writeThroughFile({
  tenantId,
  parentId: null,
  name: "photo.png",
  body,
  contentType: "image/png",
  ownerId,
});

If the metadata insert fails after the object is written, the key lands in pending_orphans. reconcile() drains them.

10. Result + errors

import { ok, err, unwrap, type Result } from "@vryzel/file-next";
import { FileSystemError } from "@vryzel/file-next/errors";

if (!result.ok) {
  if (result.error.code === "Conflict") { /* duplicate name */ }
  if (result.error.retryable) { /* backoff */ }
}

@vryzel/file-next/errors is the client-safe entry (no Node builtins).

Imports

| Entry | Use from | |---|---| | @vryzel/file-next | server or shared types | | @vryzel/file-next/server | Server Actions / Route Handlers only (server-only) | | @vryzel/file-next/sync | server | | @vryzel/file-next/errors | client or server |

Not this package

  • React hooks → @vryzel/file-next-headless
  • Ready-made explorer → @vryzel/file-next-ui
  • CLI → @vryzel/file-next-cli

Bucket CORS and IAM: provider-setup, security.