Skip to main content
Version: v5

Next.js Plugin (@harperfast/nextjs)

@harperfast/nextjs is a Harper Plugin for running Next.js applications directly on Harper. It supports Next.js v14, v15, and v16.

Source: github.com/HarperFast/nextjs — Example app

Setup​

1. Install​

npm install @harperfast/nextjs

2. Wrap your Next.js config​

Use withHarper() to wrap your existing Next.js config. All config formats are supported:

// next.config.js (CommonJS)
const { withHarper } = require('@harperfast/nextjs');

module.exports = withHarper({
// your existing Next.js config
});
// next.config.mjs (ESM)
import { withHarper } from '@harperfast/nextjs';

export default withHarper({
// your existing Next.js config
});
// next.config.ts (TypeScript)
import { withHarper } from '@harperfast/nextjs';

export default withHarper({
// your existing Next.js config
});

withHarper() automatically adds harper and harper-pro to serverExternalPackages so Harper's native dependencies are handled correctly by the Next.js bundler.

3. Configure in config.yaml​

'@harperfast/nextjs':
package: '@harperfast/nextjs'

4. Run​

harper run nextjs-app

Using Harper Globals​

Within any server-side code path, import harper to access Harper's global APIs and interact with Harper tables directly:

Ensure you are using withHarper(), or have manually added harper (or harper-pro) to the serverExternalPackages list in your Next.js config.

// app/actions.js
'use server';

import 'harper';

export async function listDogs() {
const dogs = [];
for await (const dog of tables.Dog.search()) {
dogs.push({ id: dog.id, name: dog.name });
}
return dogs;
}
// app/dogs/[id]/page.jsx
import { getDog, listDogs } from '@/app/actions';

export async function generateStaticParams() {
const dogs = await listDogs();
return dogs;
}

export default async function Dog({ params }) {
const dog = await getDog(params.id);

return (
<section>
<h1>{dog.name}</h1>
<p>Breed: {dog.get('breed')}</p>
</section>
);
}

Options​

All options are configured in config.yaml under the @harperfast/nextjs key. All options are optional.

bundler​

Type: 'webpack' | 'turbopack'

Selects the bundler used for building and serving the Next.js application. The default depends on the Next.js version:

  • Next.js v16: defaults to turbopack (matching the Next.js v16 default)
  • Next.js v15: defaults to webpack (matching the Next.js v15 default)
  • Next.js v14: always webpack (Turbopack is not supported)
'@harperfast/nextjs':
package: '@harperfast/nextjs'
bundler: webpack

dev​

Type: boolean

Default: false

Enables Next.js development mode with hot module replacement (HMR).

Dev mode relies on WebSockets. If you encounter an Invalid WebSocket frame: error, disable other WebSocket services running on the same port.

prebuilt​

Type: boolean

Default: false

When true, the plugin looks for an existing .next directory and skips the build step. Useful when the app is pre-built before deployment.

buildOnly​

Type: boolean

Default: false

Builds the Next.js application and then exits, including shutting down Harper. Useful as a CI build step.

port​

Type: number

Default: Harper default port (9926)

Custom HTTP port for the Next.js server.

securePort​

Type: number

Default: Harper default secure port

Custom HTTPS port for the Next.js server.

runFirst​

Type: boolean

Default: false

When true, the Next.js request handler runs before any other Harper HTTP middleware. Useful for scenarios where Next.js handles authentication directly.

Note: enabling runFirst conflicts with Harper's REST API on the same port. Use a dedicated port to avoid conflicts.

Versioned Filesystem Cache​

By default, Next.js writes its incremental render and data caches under .next inside the application directory. That directory is replaced on every deploy, and workers still running the previous build keep writing to it until they restart, so the new build can serve pages the old build rendered. See Writing Files in the Application Directory.

@harperfast/nextjs v2.4.0 and later can keep these caches on disk in a directory per build, outside the application directory. This is opt-in: select it with versionedCacheHandlerPath() in place of the default handler or the Harper-backed handler:

import { withHarper, versionedCacheHandlerPath } from '@harperfast/nextjs';

export default withHarper({
cacheHandler: versionedCacheHandlerPath(import.meta.dirname),
});

For CommonJS configs, use require('@harperfast/nextjs') and pass __dirname.

  • For <rootPath>/components/my-app, entries go to <rootPath>/.nextjs-cache/<app-path-hash>/<build-hash>/. The build identity includes BUILD_ID and a digest of the build output. Set the plugin's cacheDirectory option in config.yaml to an absolute path outside the application directory to use another location.
  • Workers of the same build on the same node share the cache, and it survives worker restarts. It does not replicate between nodes.
  • Before the first deploy that uses this handler, stop workers running the stock handler: they can still overwrite the new build's seed files. Later rolling deploys are safe once the outgoing workers use this handler too.
  • Old build directories are not removed automatically. Delete them while no worker of that build is running.

See the plugin README for the full behavior, including cacheMaxMemorySize when several apps share a Next.js installation.

Caching​

@harperfast/nextjs includes a Harper-backed cache handler for Next.js Incremental Static Regeneration (ISR), the Data Cache, and unstable_cache.

Cached entries live in Harper instead of the worker's local filesystem, so a cache write on one cluster node is immediately visible to every other node.

Note: The cache handler is currently a work in progress.

Enabling​

Use the cacheHandlerPath() helper to set the cacheHandler path. This helper resolves the path relative to your config file, which is required for Turbopack compatibility:

// next.config.js (CommonJS)
const { withHarper, cacheHandlerPath } = require('@harperfast/nextjs');

module.exports = withHarper({
cacheHandler: cacheHandlerPath(__dirname),
});
// next.config.mjs (ESM)
import { withHarper, cacheHandlerPath } from '@harperfast/nextjs';

export default withHarper({
cacheHandler: cacheHandlerPath(import.meta.dirname),
});
// next.config.ts (TypeScript)
import { withHarper, cacheHandlerPath } from '@harperfast/nextjs';

export default withHarper({
cacheHandler: cacheHandlerPath(import.meta.dirname),
});

Tag Invalidation​

revalidateTag() is supported and propagates across the cluster automatically. A typical flow:

// app/products/[id]/page.js
import { unstable_cache } from 'next/cache';

const getProduct = unstable_cache(
async (id) => {
const res = await fetch(`https://api.example.com/products/${id}`);
return res.json();
},
['product'],
{ tags: ['products'], revalidate: 3600 }
);

export default async function ProductPage({ params }) {
const product = await getProduct(params.id);
return <h1>{product.name}</h1>;
}
// app/api/revalidate/route.js
import { revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request) {
const tag = new URL(request.url).searchParams.get('tag');
revalidateTag(tag);
return NextResponse.json({ revalidated: true });
}

fetch() calls with next: { tags: [...] } and the 'use cache' directive (with cacheTag()) are also supported - anywhere Next.js attaches tags to a cached value, the handler picks them up.

How Invalidation Works​

The cache handler uses a soft-invalidation model:

  1. revalidateTag(tag) writes a { tag, timestamp } row to the nextjs_cache_invalidation table and updates an in-memory map in the calling worker.
  2. Every other Harper worker subscribes to that table and updates its own map when the row is replicated - typically within milliseconds.
  3. On the next cache.get(), if any of the cached entry's tags has an invalidation timestamp newer than the entry's lastModified, the handler returns null and Next.js regenerates the entry.

The nextjs_cache_invalidation rows expire after 7 days so abandoned tags do not accumulate indefinitely.

Cache Tables​

Enabling the cache handler adds two tables to the harperfast_nextjs database:

TablePurpose
nextjs_isr_cacheOne row per cached entry. Stores data (the Next.js IncrementalCacheValue), tags, and lastModified.
nextjs_cache_invalidationOne row per invalidated tag. id is the tag; timestamp is when revalidateTag was called. Auto-expires after 7 days.

See Also​