Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
caca1f4
feat(templates): add Next.js templates (JavaScript + TypeScript)
dawsontoth Jul 21, 2026
09dd623
chore(templates): skip Studio build/publish for Next.js templates
dawsontoth Jul 21, 2026
d2bb8ff
fix(templates): address Next.js review feedback (age 0, useTransition)
dawsontoth Jul 21, 2026
e6f048a
fix(templates): don't import 'harper' in Next.js server code
dawsontoth Jul 21, 2026
56ed077
refactor(templates): slim Next.js templates to a persisted-counter st…
dawsontoth Jul 21, 2026
98355e2
ci(integration): cover Next.js variants in the integration matrix
dawsontoth Jul 21, 2026
742d52e
fix(templates): make Next.js templates install + lint under pnpm and …
dawsontoth Jul 21, 2026
3a8706d
fix(templates): pre-approve pnpm builds via pnpm-workspace.yaml allow…
dawsontoth Jul 21, 2026
1738243
fix(templates): deploy Next.js prebuilt to survive multi-thread clusters
dawsontoth Jul 21, 2026
6bd7435
fix(templates): use Harper's atomic addTo for the counter increment
dawsontoth Jul 22, 2026
ff0d37d
fix(templates): mark Next.js templates as SSR in the catalog (ssr: true)
dawsontoth Jul 22, 2026
f8e4e35
refactor(templates): drop the prebuilt deploy workaround for Next.js
dawsontoth Jul 24, 2026
cb67812
test(nextjs): exercise the increment server action in the runtime smoke
dawsontoth Jul 24, 2026
d1a5466
fix(templates): ship Next.js prebuilt — on-cluster build fails (nextj…
dawsontoth Jul 24, 2026
e3615e1
docs(ci): describe the prebuilt Next.js smoke flow in integration.yaml
dawsontoth Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 30 additions & 7 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ jobs:
fail-fast: false
matrix:
pkg-manager: [npm, yarn, pnpm]
template: [vanilla, vanilla-ts, react, react-ts, vue, vue-ts]
template: [
vanilla,
vanilla-ts,
react,
react-ts,
vue,
vue-ts,
nextjs,
nextjs-ts,
]
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down Expand Up @@ -136,24 +145,38 @@ jobs:
echo "Running format check..."
${{ matrix.pkg-manager }} run format

echo "Running tests..."
${{ matrix.pkg-manager }} run test
# Run tests if the template defines a test script (the Next.js templates don't ship
# unit tests — their runtime smoke below is their coverage).
if grep -q '"test":' package.json; then
echo "Running tests..."
${{ matrix.pkg-manager }} run test
fi

# Check for build script and run it if exists
if grep -q '"build":' package.json; then
echo "Running build..."
${{ matrix.pkg-manager }} run build
fi

# Boot the generated app under a real Harper instance and verify both the REST
# resources and the frontend are reachable (guards against the static handler
# swallowing REST GETs). Once per template — the package manager doesn't matter here.
# Boot the generated app under a real Harper instance and verify its HTTP surface. Once
# per template — the package manager doesn't matter here. The Next.js templates route
# through the @harperfast/nextjs plugin (no REST resources), so they use a dedicated
# smoke that serves the prebuilt build (produced by the `build` step above) and checks the
# Harper-backed counter reads and increments; every other template uses runtimeSmoke,
# which also guards against the static handler swallowing REST GETs.
if [ "${{ matrix.pkg-manager }}" = "npm" ]; then
echo "Installing Harper..."
npm install --global harper

echo "Running runtime smoke test..."
node "$REPO_PATH/template.tests/runtimeSmoke.js" .
case "${{ matrix.template }}" in
nextjs | nextjs-ts)
node "$REPO_PATH/template.tests/nextSmoke.js" .
;;
*)
node "$REPO_PATH/template.tests/runtimeSmoke.js" .
;;
esac
fi

echo "Integration test passed for ${{ matrix.pkg-manager }} with ${{ matrix.template }}!"
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ Currently supported template presets include:
- `vanilla-ts`
- `react`
- `react-ts`
- `nextjs`
- `nextjs-ts`

You can use `.` for the project name to scaffold in the current directory.

Expand Down
2 changes: 2 additions & 0 deletions lib/constants/frameworks.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const {
blue,
cyan,
green,
magenta,
yellow,
} = colors;

Expand All @@ -28,6 +29,7 @@ const frameworkMeta = {
vanilla: { display: 'Vanilla', color: yellow },
react: { display: 'React', color: cyan },
vue: { display: 'Vue', color: green },
nextjs: { display: 'Next.js', color: magenta },
};

/**
Expand Down
20 changes: 18 additions & 2 deletions lib/constants/templates.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Framework = 'vanilla' | 'react' | 'vue';
export type Framework = 'vanilla' | 'react' | 'vue' | 'nextjs';

export type TemplateName =
| 'vanilla-ts'
Expand All @@ -10,7 +10,9 @@ export type TemplateName =
| 'vue-ts'
| 'vue'
| 'vue-ts-ssr'
| 'vue-ssr';
| 'vue-ssr'
| 'nextjs-ts'
| 'nextjs';

export interface TemplateInfo {
/** The canonical template name (e.g. 'vanilla', 'react-ts'). Used to scaffold. */
Expand All @@ -27,6 +29,11 @@ export interface TemplateInfo {
typescript: boolean;
/** Whether the template is server-side rendered. */
ssr: boolean;
/**
* Whether a Studio template package is built and published for this template. Defaults to true;
* set to false for templates that don't (yet) run in the Studio's deploy-only model.
*/
studio?: boolean;
/** The published Studio template package for this template. */
npmPackage: string;
/** A link to the template's source on GitHub. */
Expand All @@ -51,4 +58,13 @@ export declare const templateNames: readonly [
'vue',
'vue-ts-ssr',
'vue-ssr',
'nextjs-ts',
'nextjs',
];

/**
* The subset of {@link templateNames} for which a Studio template package is built and published
* (every template whose catalog entry does not set `studio: false`). Consumed by the Studio
* build/publish scripts.
*/
export declare const studioTemplateNames: readonly TemplateName[];
39 changes: 38 additions & 1 deletion lib/constants/templates.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
const CREATE_HARPER_TREE = 'https://github.com/HarperFast/create-harper/tree/main';

/**
* @typedef {'vanilla' | 'react' | 'vue'} Framework
* @typedef {'vanilla' | 'react' | 'vue' | 'nextjs'} Framework
*/

/**
Expand All @@ -13,6 +13,9 @@ const CREATE_HARPER_TREE = 'https://github.com/HarperFast/create-harper/tree/mai
* @property {string[]} tags - Tags describing the template's stack.
* @property {boolean} typescript - Whether the template uses TypeScript.
* @property {boolean} ssr - Whether the template is server-side rendered.
* @property {boolean} [studio] - Whether a Studio template package is built and published for this
* template. Defaults to true; set to false for templates that don't (yet) run in the Studio's
* deploy-only model. See {@link studioTemplateNames}.
* @property {string} npmPackage - The published Studio template package for this template.
* @property {string} githubUrl - A link to the template's source on GitHub.
*/
Expand Down Expand Up @@ -131,6 +134,31 @@ export const templates = [
typescript: false,
ssr: true,
}),
template({
name: 'nextjs-ts',
framework: 'nextjs',
title: 'Next.js + TypeScript',
description: 'A type-safe Next.js app that reads and writes Harper tables directly from server actions.',
tags: ['Next.js', 'TypeScript', 'React', 'App Router'],
typescript: true,
// Next.js renders on the server (the app is force-dynamic and reads Harper per request), so
// this is SSR even though the name has no `-ssr` suffix (that suffix marks the Vite variants).
ssr: true,
// Not built/published as a Studio template: the plugin needs a prebuilt deploy (its on-startup
// build races on multi-thread clusters — HarperFast/nextjs#52) and Studio's edit-in-place
// model doesn't rebuild, so it isn't a fit today.
studio: false,
Comment thread
dawsontoth marked this conversation as resolved.
}),
template({
name: 'nextjs',
framework: 'nextjs',
title: 'Next.js',
description: "A Next.js app wired to Harper's Resource API, reading and writing tables from server actions.",
tags: ['Next.js', 'React', 'App Router'],
typescript: false,
ssr: true,
studio: false,
}),
];

/**
Expand All @@ -140,3 +168,12 @@ export const templates = [
* @type {string[]}
*/
export const templateNames = templates.map((t) => t.name);

/**
* The subset of {@link templateNames} for which a Studio template package is built and published
* (i.e. every template except those with `studio: false`). The Studio build/publish scripts iterate
* this instead of {@link templateNames} so opt-out templates are skipped.
*
* @type {string[]}
*/
export const studioTemplateNames = templates.filter((t) => t.studio !== false).map((t) => t.name);
40 changes: 35 additions & 5 deletions lib/constants/templates.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import colors from 'picocolors';
import { describe, expect, test } from 'vitest';
import { frameworks } from './frameworks.js';
import { templateNames, templates as catalog } from './templates.js';
import { studioTemplateNames, templateNames, templates as catalog } from './templates.js';

const { blue, cyan, green, yellow } = colors;
const { blue, cyan, green, magenta, yellow } = colors;

describe('templates catalog', () => {
test('templateNames is the exact, ordered list of names (keeps templates.d.ts honest)', () => {
Expand All @@ -18,6 +18,8 @@ describe('templates catalog', () => {
'vue',
'vue-ts-ssr',
'vue-ssr',
'nextjs-ts',
'nextjs',
]);
});

Expand All @@ -32,17 +34,36 @@ describe('templates catalog', () => {
}
});

test('typescript and ssr flags match the template name suffixes', () => {
test('typescript and ssr flags are correct per template', () => {
for (const t of catalog) {
expect(t.typescript).toBe(t.name.includes('-ts'));
expect(t.ssr).toBe(t.name.endsWith('-ssr'));
// The `-ssr` suffix marks the Vite SSR variants; Next.js is always server-rendered.
const expectedSsr = t.framework === 'nextjs' ? true : t.name.endsWith('-ssr');
expect(t.ssr).toBe(expectedSsr);
}
});

test('studioTemplateNames excludes the studio: false templates (nextjs)', () => {
expect(studioTemplateNames).toEqual([
'vanilla-ts',
'vanilla',
'react-ts',
'react',
'react-ts-ssr',
'react-ssr',
'vue-ts',
'vue',
'vue-ts-ssr',
'vue-ssr',
]);
expect(studioTemplateNames).not.toContain('nextjs');
expect(studioTemplateNames).not.toContain('nextjs-ts');
});
});

describe('frameworks (derived from the catalog)', () => {
test('groups templates by framework in catalog order', () => {
expect(frameworks.map((f) => f.name)).toEqual(['vanilla', 'react', 'vue']);
expect(frameworks.map((f) => f.name)).toEqual(['vanilla', 'react', 'vue', 'nextjs']);
});

test('matches the expected display names and colors', () => {
Expand Down Expand Up @@ -78,6 +99,15 @@ describe('frameworks (derived from the catalog)', () => {
{ name: 'vue-ssr', display: 'JavaScript + SSR', color: yellow },
],
},
{
name: 'nextjs',
display: 'Next.js',
color: magenta,
variants: [
{ name: 'nextjs-ts', display: 'TypeScript + SSR', color: blue },
{ name: 'nextjs', display: 'JavaScript + SSR', color: yellow },
],
},
]);
});
});
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
"template-vue/",
"template-vue-ts/",
"template-vue-ssr/",
"template-vue-ts-ssr/"
"template-vue-ts-ssr/",
"template-nextjs/",
"template-nextjs-ts/"
],
"scripts": {
"commitlint": "commitlint --edit",
Expand Down
79 changes: 79 additions & 0 deletions template-nextjs-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# your-project-name-here

A type-safe [Next.js](https://nextjs.org) app running on Harper via [`@harperfast/nextjs`](https://github.com/HarperFast/nextjs). Your new app is now ready for development!

Because the app runs _inside_ Harper, server-side code (server actions and server components) reads and writes your database directly through the injected `tables` global — no separate API server and no network round-trip.

The starter ships one tiny end-to-end example: a counter stored in a Harper table, read by a server component and incremented by a server action.

## Installation

Make sure you have [installed Harper](https://docs.harperdb.io/docs/deployments/install-harper):

```sh
npm install -g harper
```

## Development

Start the app:

```sh
npm run dev
```

Then open [http://localhost:9926](http://localhost:9926) 🎉

Click the button — the count persists in Harper across reloads and restarts.

### Define Your Schema

Your tables live in [`schema.graphql`](./schema.graphql). The starter defines a single `Count` table; add your own `@table` types there, then mirror their shape in [`harper.d.ts`](./harper.d.ts) so your server code stays type-safe. (The `@harperfast/schema-codegen` component can also generate these types for you.)

### Access Harper From Server Code

Harper injects `tables` and `transaction` globals into server-side code, so server actions and server components read and write your database directly — no import needed (their types come from [`harper.d.ts`](./harper.d.ts)). Use an atomic `addTo` inside a `transaction` for writes that stay correct when requests overlap across worker threads and replicated nodes (a read-then-write would lose concurrent increments):

```ts
'use server';

export async function getCount(): Promise<number> {
const record = await tables.Count.get('count');
return record?.value ?? 0;
}

export async function increment(): Promise<void> {
await transaction(async () => {
const record = await tables.Count.update('count');
record.addTo('value', 1);
});
}
```

> **Don't** add a top-level `import 'harper'` in these modules. It runs during the Next.js production build (when Next collects page data) and conflicts with the running database — use the injected globals instead.

Put data access in **server actions** (see [`app/actions.ts`](./app/actions.ts)) so that both server _and_ client components can share the same functions. Any action a client can reach is a public endpoint, so add your own authorization checks before shipping mutations that matter.

## Deployment

When you are ready, head to [https://fabric.harper.fast/](https://fabric.harper.fast/), log in to your account, and create a cluster.

Come back and log your local CLI into your cluster:

```sh
harper login
```

Then deploy your app:

```sh
npm run deploy
```

`npm run deploy` runs `next build` locally and ships the prebuilt `.next` output, then Harper serves it — no build runs on the cluster. (Building on the cluster currently fails; see the note in [`config.yaml`](./config.yaml).)

## Keep Going!

For more on building Harper applications, see the [getting started guide](https://docs.harperdb.io/docs).

For more on Harper Components, see the [Components documentation](https://docs.harperdb.io/docs/reference/components).
1 change: 1 addition & 0 deletions template-nextjs-ts/_aiignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
11 changes: 11 additions & 0 deletions template-nextjs-ts/_claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "harper",
"runtimeExecutable": "your-package-manager-here",
"runtimeArgs": ["run", "dev"],
"port": 9926
}
]
}
1 change: 1 addition & 0 deletions template-nextjs-ts/_env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CLI_TARGET='your-fabric.harper.fast-cluster-url-here'
1 change: 1 addition & 0 deletions template-nextjs-ts/_env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CLI_TARGET='YOUR_FABRIC.HARPER.FAST_CLUSTER_URL_HERE'
Loading