@ebay-dt/bonfire-next
v0.1.0
Published
Next.js server integration for Bonfire.
Keywords
Readme
@ebay-dt/bonfire-next
Next.js integration for Bonfire. Server APIs are exposed from
@ebay-dt/bonfire-next/server, and the Next-specific lifecycle client adapter is
exposed from @ebay-dt/bonfire-next/client.
It provides HS256 development sessions, the development authentication proxy,
login/status/logout handlers, and server-side login-page model composition.
Production remains public and does not require GitHub CLI or
BF_SESSION_SECRET.
Installation and instance setup
Install @ebay-dt/bonfire-next, @ebay-dt/bonfire-core, and @ebay-dt/bonfire-react in the
application. Install @ebay-dt/bonfire-cli as a development dependency.
Create src/bonfire.ts. Keep the application configuration and Bonfire
composition together in this server-only entry point:
import {
createBonfireNext,
createDeploymentProvider,
} from "@ebay-dt/bonfire-next/server";
const deploymentProvider =
process.env.NODE_ENV !== "production" &&
process.env.BONFIRE_DEPLOYMENT_ENDPOINT !== undefined
? createDeploymentProvider({
endpoint: process.env.BONFIRE_DEPLOYMENT_ENDPOINT,
})
: undefined;
export const bonfire = createBonfireNext({
appId: "my-app",
name: "My App",
auth: {
github: {
hostname: "github.com",
},
},
repositoryRoot: process.cwd(),
primaryBranch: process.env.BF_PROTOTYPE_BASE_BRANCH,
deploymentProvider,
sessionSecret: process.env.BF_SESSION_SECRET,
disableAutoBranchSwitch: process.env.BF_DISABLE_AUTO_BRANCH_SWITCH,
});Theme registry configuration
Theme configuration is optional. Add a themes property to the
createBonfireNext options; the registry is validated by
@ebay-dt/bonfire-core and included in prototype route models:
export const bonfire = createBonfireNext({
appId: "my-app",
name: "My App",
auth: { github: { hostname: "github.com" } },
themes: {
definitions: [
{
id: "alpha",
label: "Alpha",
allowedColorSchemes: ["system", "light", "dark"],
},
{ id: "beta", label: "Beta", allowedColorSchemes: ["dark"] },
],
defaultTheme: "alpha",
defaultColorScheme: "system",
},
repositoryRoot: process.cwd(),
});Prototype metadata can provide a validated theme policy. If it is omitted,
Bonfire applies the instance defaults and registered capabilities. The server
integration passes the normalized policy and registry to the host; it does not
scan metadata or construct a theme map in the application. The preferred host
composition passes prototype.themeRegistry directly to
PrototypeEnvironment alongside prototype.theme.
The host owns theme CSS. Use the provider's data-theme and
data-color-scheme attributes in an instance stylesheet. Keep a system
selector and a prefers-color-scheme media rule when a theme supports System;
Bonfire retains system as the attribute value and exposes the resolved Light
or Dark value separately through React.
The deployment endpoint is complete, has no embedded default, and must remain in
server-only configuration. When configured, the provider sends only
{ applicationId, login }; it does not send owner or branch values. The
applicationId (normally appId) must exactly match the application's
DT deployment-service registration. The service resolves the repository,
project, deployment target, and credentials from the application ID and login.
HTTP 201 confirms deployment creation, while readyState reports the
provider's current state; Bonfire preserves the deploymentId but does not
poll it. deploymentAvailable exposes configured development capability to the
lifecycle provider, not authorization.
A registration mismatch is intentionally surfaced as a safe deployment failure; Bonfire does not send the provider's response details to the browser. Diagnose it server-side by comparing the configured application ID with the registered value. Any host-facing message should remain generic, such as “Deployment is not configured for this application.”
Run bonfire prepare before development so the secret exists in
.env.local. An externally supplied BF_SESSION_SECRET takes precedence.
The explicit repository root enables request-time discovery under
src/app/(prototypes) without relying on package-internal process.cwd().
BF_PROTOTYPE_BASE_BRANCH can override the default main branch used as the
base for identity branches. The configured branch must exist; a missing base
branch fails preparation rather than silently using the current branch.
A development instance can also receive a trusted server-only
preparePrototypeFiles callback. It runs after Bonfire scaffolds or
duplicates the destination and before the initial commit, allowing the host to
write prototype-local files. Callback failures clean the newly owned
destination; commit or push failures preserve completed output. Do not supply
this callback together with a prototypeLifecycle override.
Next host contract
Next.js cannot install filesystem routes for a package, so each application adds these small bindings. Keep authentication logic in the package.
src/app/layout.tsx should own the document and long-lived providers, but not
route-specific application chrome. Apply htmlProps to the root <html>
element:
import "@ebay-dt/bonfire-react/styles/defaults.css";
import { bonfire } from "@/bonfire";
export default async function RootLayout({ children }: LayoutProps<"/">) {
const { htmlProps } = await bonfire.auth.getAppLayoutModel();
return (
<html lang="en" {...htmlProps}>
<body>{children}</body>
</html>
);
}getAppLayoutModel() returns a paired set of opaque document props and
serializable header props. Spread htmlProps on <html> and headerProps on
<AppHeader>; do not omit the document props when rendering the header. Both
values come from the same getAppLayoutModel() result. Render AppHeader in
the route layout that owns it. Keep the dashboard header in a system layout and
the prototype header inside the prototype environment, so the header can
consume the environment context:
// src/app/(system)/layout.tsx
const { headerProps } = await bonfire.auth.getAppLayoutModel();
return (
<>
<AppHeader {...headerProps} />
{children}
</>
);Prototype environment composition
Use getCurrentRouteModel() from a shared prototype route-group layout when the
application enters prototypes from the dashboard and does not support soft
navigation between prototypes. The model includes the configured registry when
themes are enabled, so the host does not scan metadata or rebuild a
themesByPath map:
// src/app/(prototypes)/layout.tsx
import { notFound } from "next/navigation";
import { AppHeader, PrototypeEnvironment } from "@ebay-dt/bonfire-react";
import { openInClaudeCode, openInCursor } from "@/actions/agents";
import { bonfire } from "@/bonfire";
export default async function PrototypeLayout({ children }: LayoutProps<"/">) {
const [{ headerProps }, prototype] = await Promise.all([
bonfire.auth.getAppLayoutModel(),
bonfire.prototypes.getCurrentRouteModel(),
]);
if (prototype === null) notFound();
const agentActions = { openInClaudeCode, openInCursor };
const header = <AppHeader {...headerProps} agentActions={agentActions} />;
if (prototype.theme === undefined)
return (
<>
{header}
{children}
</>
);
return (
<PrototypeEnvironment
prototypeUrl={prototype.url}
theme={prototype.theme}
themeRegistry={prototype.themeRegistry}
agentActions={agentActions}
>
{header}
{children}
</PrototypeEnvironment>
);
}Keep this layout scoped to prototype routes. Dashboard, login, and other
application-level pages should remain outside it. If the application later
adds soft navigation between prototypes, move the boundary to the route
segment that identifies one prototype (for example [login]/[slug]/layout.tsx)
and use getRouteModel({ login, slug }) there. That is what allows navigation
within one prototype to preserve state while navigation to another remounts it.
Prototype lifecycle composition
The lifecycle-aware dashboard uses the framework-neutral controller and React
components from @ebay-dt/bonfire-react/lifecycle. The Next-specific provider
supplies router integration, dirty-state observation, and Server Action
bindings, while LifecycleProvider owns the React controller lifecycle. The
dirty endpoint is a long-lived SSE route and must run on the Node runtime. Bind
it directly; do not wrap it in another handler:
// src/app/api/prototype-dirty/route.ts
import { bonfire } from "@/bonfire";
export const runtime = "nodejs";
export const GET = bonfire.prototypes.routes.dirty;Then mount the provider and lifecycle-aware header from the root layout:
import { NextLifecycleProvider } from "@ebay-dt/bonfire-next/client";
import { AppHeader } from "@ebay-dt/bonfire-react";
import {
createPrototype,
deletePrototype,
discardPrototype,
duplicatePrototype,
editPrototype,
deployPrototype,
savePrototype,
} from "@/actions/prototypes";
import { bonfire } from "@/bonfire";
const { headerProps } = await bonfire.auth.getAppLayoutModel();
<NextLifecycleProvider
enabled={process.env.NODE_ENV !== "production"}
user={headerProps.user}
deploymentAvailable={bonfire.prototypes.capabilities.deployment}
dirtyEndpoint="/api/prototype-dirty"
actions={{
create: createPrototype,
duplicate: duplicatePrototype,
edit: editPrototype,
delete: deletePrototype,
save: savePrototype,
discard: discardPrototype,
...(process.env.NODE_ENV !== "production"
? { deploy: deployPrototype }
: {}),
}}
>
<AppHeader {...headerProps} />
{children}
</NextLifecycleProvider>;Mount it from the root layout so the controller survives dashboard/prototype
navigation. Render the regular PrototypeDashboard from the page and pass the
resolved user; it consumes lifecycle context directly. Production can leave
the provider disabled, in which case the dashboard renders its public
read-only view. The action bindings must include create, duplicate, edit, delete,
save, discard, and deploy when deployment is enabled. Edit/delete/deploy
are authorized by the server-side lifecycle adapter; UI visibility is not an
authorization boundary.
The provider performs one host refresh after a successful edit or delete.
Agent launch bindings follow the same thin host-action pattern. Add a
server-only wrapper module; it must delegate to bonfire.prototypes.agents and
must not reproduce authentication, ownership, process, or filesystem logic:
// src/actions/agents.ts
"use server";
import { bonfire } from "@/bonfire";
import type {
OpenInAgentInput,
OpenInClaudeCodeInput,
} from "@ebay-dt/bonfire-core";
export async function openInCursor(input: OpenInAgentInput) {
return bonfire.prototypes.agents.openInCursor(input);
}
export async function openInClaudeCode(input: OpenInClaudeCodeInput) {
return bonfire.prototypes.agents.openInClaudeCode(input);
}Pass these Server Action references to both packaged surfaces. The dashboard page can provide them to owner cards:
<PrototypeDashboard
{...model}
agentActions={{ openInClaudeCode, openInCursor }}
user={user}
/>The root header accepts the same bindings:
<AppHeader {...headerProps} agentActions={{ openInClaudeCode, openInCursor }} />The card controls are owner-only icon buttons with tooltips. A successful card launch navigates into the selected prototype; preflight and launch failures remain on the current page so their dialogs can be shown. The header controls are text menu rows and close the account popover when clicked. Both surfaces use the validated prototype reference on the server; browser props contain no absolute filesystem path or shell command.
Cursor requires cursor and Claude Code requires claude on the PATH inherited
by the Bonfire server process. On macOS, Claude Code also requires
osascript. A GUI-launched Node process may have a different PATH from an
interactive shell, so restart Bonfire after changing a shell profile. Claude
Code terminal preferences support Terminal.app, iTerm2, and Warp. The first
launch without a preference presents the packaged terminal-selection dialog;
the selected value is written server-side and then used for the launch.
Preferences are stored at <prototypeRoot>/<login>/preferences.json. This is a
local preference file, not prototype source, and hosts must ignore it in Git.
For the conventional integration prototype root, use:
/src/app/(prototypes)/*/preferences.jsonNo Claude Code skill, clipboard fallback, copy/paste setup command, or unauthenticated preferences route is required. Production remains public and read-only: it does not construct the local agent service, launch a process, or read or write preferences.
Migration from the Sandbox reference
This flow adapts the Sandbox reference's direct Cursor/Claude Code launching, owner checks, terminal selection, and actionable failure UI. Bonfire keeps those responsibilities in Core, Node, Next adapters, and packaged React controls; it does not copy the reference application's absolute paths, local Next actions, client setup hook, skills, clipboard behavior, or unauthenticated preferences API.
src/app/login/page.tsx:
import { GitHubCliLoginPage } from "@ebay-dt/bonfire-react";
import { bonfire } from "@/bonfire";
type LoginPageProps = {
searchParams?: Promise<{
auth?: string;
code?: string;
error?: string;
}>;
};
export default async function LoginPage({ searchParams }: LoginPageProps) {
const model = await bonfire.auth.getLoginPageModel(searchParams);
return <GitHubCliLoginPage {...model} />;
}src/app/auth/login/route.ts:
import { bonfire } from "@/bonfire";
export const runtime = "nodejs";
export const GET = bonfire.auth.routes.login;src/app/auth/status/route.ts:
import { bonfire } from "@/bonfire";
export const runtime = "nodejs";
export const GET = bonfire.auth.routes.status;src/app/auth/logout/route.ts:
import { bonfire } from "@/bonfire";
export const POST = bonfire.auth.routes.logout;src/proxy.ts:
import { bonfire } from "./bonfire";
export const proxy = bonfire.auth.proxy;
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};Behavior and security
In development, the proxy requires a valid app-specific, HTTP-only session
cookie. Login reads the local GitHub CLI identity, and status refreshes the
session while device authentication is in progress. Logout clears the cookie
and redirects to /login. In production, the proxy passes requests through,
login and status return 404, and logout clears stale state before redirecting to
/.
The session secret must contain at least 32 UTF-8 bytes. Tokens use HS256 and contain only the validated identity and expiration. Invalid or tampered tokens are treated as unauthenticated. Secrets are not included in login models, errors, logs, or redirects.
The configured GitHub hostname controls both CLI commands and the device activation URL. For GitHub Enterprise, set the hostname in configuration rather than changing the host bindings.
Prototype dashboard
The configured instance exposes read-only catalog methods:
import { PrototypeDashboard } from "@ebay-dt/bonfire-react";
import { bonfire } from "@/bonfire";
type HomeProps = {
searchParams: Promise<Record<string, string | string[] | undefined>>;
};
export default async function Home({ searchParams }: HomeProps) {
const [model, user] = await Promise.all([
bonfire.prototypes.getDashboardModel(searchParams),
bonfire.auth.getCurrentUser(),
]);
return <PrototypeDashboard {...model} user={user} />;
}The catalog is discovered at request time from
src/app/(prototypes)/<author>/<slug>. Production is public and does not
include the development-only mine option. Catalog reads are read-only and do
not invoke GitHub CLI, Git, or the network.
Prototype mutation actions
Create, duplicate, save, and discard actions are returned as typed results and do not perform redirects. Bind them from a small host Server Action module. Save and discard accept only a validated target reference and never push changes:
// src/actions/prototypes.ts
"use server";
import type {
CreatePrototypeInput,
DuplicatePrototypeInput,
PrototypeCommandInput,
} from "@ebay-dt/bonfire-core";
import { bonfire } from "@/bonfire";
export async function createPrototype(input: CreatePrototypeInput) {
return bonfire.prototypes.actions.create(input);
}
export async function duplicatePrototype(input: DuplicatePrototypeInput) {
return bonfire.prototypes.actions.duplicate(input);
}
export async function savePrototype(input: PrototypeCommandInput) {
return bonfire.prototypes.actions.save(input);
}
export async function discardPrototype(input: PrototypeCommandInput) {
return bonfire.prototypes.actions.discard(input);
}
export async function deployPrototype(input: PrototypeCommandInput) {
return bonfire.prototypes.actions.deploy(input);
}Keep these wrappers thin: form validation, session lookup, lifecycle errors,
and filesystem/Git operations remain in Bonfire. Save creates a local,
path-scoped commit and discard restores/removes content only inside that path;
neither operation pushes. The host should pass the Server Action references to
NextLifecycleProvider from @ebay-dt/bonfire-next/client. In development, the
provider protects dirty internal navigation and reload/close attempts; in
production it is disabled and the application remains public/read-only.
Supported versions
- Node.js 24 or newer
- Next.js 16.2 through 16.x
- React 19.2 through 19.x
- ESM-only package output
If gh is missing or unauthenticated, install GitHub CLI and run
gh auth login --hostname <hostname>. If device login does not open a browser,
use the link and code shown on the login page. The eventual migration of an
existing application changes the cookie name and secret contract, so existing
sessions will need to authenticate again.
