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 addedharper(orharper-pro) to theserverExternalPackageslist 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 includesBUILD_IDand a digest of the build output. Set the plugin'scacheDirectoryoption inconfig.yamlto 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:
revalidateTag(tag)writes a{ tag, timestamp }row to thenextjs_cache_invalidationtable and updates an in-memory map in the calling worker.- Every other Harper worker subscribes to that table and updates its own map when the row is replicated - typically within milliseconds.
- On the next
cache.get(), if any of the cached entry's tags has an invalidation timestamp newer than the entry'slastModified, the handler returnsnulland 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:
| Table | Purpose |
|---|---|
nextjs_isr_cache | One row per cached entry. Stores data (the Next.js IncrementalCacheValue), tags, and lastModified. |
nextjs_cache_invalidation | One row per invalidated tag. id is the tag; timestamp is when revalidateTag was called. Auto-expires after 7 days. |