Lantern changes where your AI route gets its model access. Without Lantern, your server calls a provider with a credential owned by your app. With Lantern, the user first connects your site to their Mac. Your server can then send each supported AI SDK call through the CLI they are already signed into.

What changes

Your app still owns its interface, prompts, and server route. The user supplies the Codex or Claude account and must keep their Mac awake, Lantern running, and the CLI used for the request signed in.

This is a good fit for interactive features used while the user is present: chat, rewriting, summarizing, and image actions. It is not a replacement for an always-on model API used by background jobs or scheduled work.

i

What the user sees: they install Lantern, choose which CLIs approved sites may use, and approve your exact site address. Approval covers later requests from that address; Lantern does not ask again for every prompt.

Set up with a coding agent

Lantern includes an open Agent Skill for Codex, Claude Code, Cursor, GitHub Copilot, and other compatible coding agents. Install it in your Next.js project:

Terminal
npx skills add sunbeam-za/lantern --skill add-lantern

Then ask your agent: Add Lantern to this Next.js app. Reuse its existing AI route and put the connection control next to the feature.

The skill traces the existing feature, adds the signed server helper and pairing route, composes the connection UI, and routes supported AI SDK calls through lantern.server(request). Review its changes and add the generated LANTERN_APP_SECRET to your deployment environment.

Install

Use Node.js 22 or newer. Lantern's Next.js routes need the Node.js runtime; the Edge runtime is not supported.

Terminal
npm install @sunbeam-za/lantern ai @ai-sdk/react

Create the helper

Create one server-only module and give it a random secret. LANTERN_APP_SECRET identifies your app to Lantern and protects the browser's saved connection. It is not a Codex or Claude credential.

.env.local
LANTERN_APP_SECRET=replace-with-at-least-32-random-characters

Generate it once, keep it server-side, and use the same value after deployment. Changing it gives your app a new identity, so users have to approve it again.

lib/lantern.ts
import { createLanternApp } from "@sunbeam-za/lantern/next";

export const lantern = createLanternApp({
  appName: "Tiny Weather",
  secret: process.env.LANTERN_APP_SECRET!,
});

Now expose its handlers from a Next.js route. GET reports the saved connection and whether its Lantern is reachable through the relay, POST saves a completed connection, and DELETE forgets it in this browser.

app/api/lantern/route.ts
import { lantern } from "@/lib/lantern";

export const { GET, POST, DELETE } = lantern;

Add the connect button

Render LanternConnect where the user starts the connection. The button first explains what your app wants to do. It then opens Lantern on the user's Mac, where they see your app name and exact web address before approving it.

app/providers.tsx
"use client";

import { LanternConnect, LanternProvider } from "@sunbeam-za/lantern/react";

export function Providers({ children }) {
  return (
    <LanternProvider>
      <LanternConnect appName="Tiny Weather" />
      {children}
    </LanternProvider>
  );
}

After approval, the route saves the selected model, capabilities, and relay destination in an HttpOnly browser cookie signed by your app. The cookie lasts for up to 30 days by default.

The default button opens a compact connection dialog. Use appearance="panel" to put the same lit/unlit lamp interface directly inside an empty state. The app side uses its /favicon.ico automatically; pass appIconURL only when the feature represents a different service.

The compact button uses Lantern's circular mark. Inside the connection interface, the lamp on its stand represents the actual Mac endpoint: lit when reachable, unlit when disconnected. Keep those roles separate when composing nearby product UI.

Inline connection state
<LanternConnect
  appName="Tiny Weather"
  appearance="panel"
/>

Add it to your marketing site

WorksWithLanternBadge and WorksWithLanternSection let visitors know about the integration before they reach the AI feature. They are standalone React components: neither one needs LanternProvider, browser state, or a client component boundary.

Compact badge
import { WorksWithLanternBadge } from "@sunbeam-za/lantern/react";

<WorksWithLanternBadge />
Works with Lantern

Bring your own AI with Lantern

Use this app with the Codex or Claude setup already signed in on your Mac. Lantern pairs this site to your computer and routes each request without asking you for an API key.

Full section
import { WorksWithLanternSection } from "@sunbeam-za/lantern/react";

<WorksWithLanternSection
  tone="dark"
  title="Use the AI subscription you already have"
  description="Connect this app to Codex or Claude on your Mac with Lantern."
/>

Both components ship with responsive inline styles. Use className and style for layout changes. The section accepts custom title, description, badge, primaryAction, and secondaryAction props; pass null for either action to hide it. Set href={null} on the badge to render a non-linking span.

Choose a CLI

Setup on the Mac can make Codex, Claude, or both available. LanternConnect asks for text support by default. It filters the available CLIs by capability, uses preferredModel when that CLI is compatible, and otherwise uses the first compatible CLI Lantern reports.

Prefer Claude when available
<LanternConnect appName="Tiny Weather" preferredModel="claude" />

After connecting, useLantern().connection.model is the CLI chosen for requests. connection.models lists the compatible CLI model names saved with that connection.

Handle connection state

useLantern() separates the saved browser connection from live reachability. connection is the saved session; status is checking, not-connected, unreachable, or connected. The provider checks on load, every 15 seconds, and when the window regains focus.

Connected state
"use client";

import { LanternConnect, useLantern } from "@sunbeam-za/lantern/react";

export function RewriteAccess() {
  const { connection, disconnect, status } = useLantern();

  if (status !== "connected" || !connection) {
    return <LanternConnect appName="Tiny Weather" appearance="panel" />;
  }

  return (
    <div>
      <button>Rewrite with {connection.model}</button>
      <button onClick={() => void disconnect()}>Forget this browser connection</button>
    </div>
  );
}

disconnect() deletes this browser's cookie only. It does not remove the site from Lantern's Approved apps list or affect another browser. Stopping Lantern pauses every approved site.

i

Connected means the paired Mac is present on Lantern's relay. Continue to show request failures: the chosen CLI can still be signed out, busy, or unavailable.

Send a request

Call lantern.server(request) inside your AI route. It reads the browser cookie, selects that user's Lantern, and returns an AI SDK provider using the model chosen during connection.

app/api/chat/route.ts
import { streamText } from "ai";
import { lantern } from "@/lib/lantern";

export async function POST(request: Request) {
  const local = lantern.server(request);
  const { prompt } = await request.json();

  return streamText({
    model: local.provider(local.model),
    prompt,
  }).toTextStreamResponse();
}

generateText, streamText, cancellation, and the AI SDK text response helpers work through this provider.

i

Current scope: text, streaming text, and Codex-backed images. Client-defined tools and structured response formats are not supported yet.

Generate images

Set capability="image" on LanternConnect when the feature needs images. This makes the connection choose a provider that reports image support.

Image connection button
<LanternConnect appName="Tiny Weather" capability="image" />

Then use the same provider with generateImage. Images require image generation to work in the signed-in Codex setup. Lantern returns one PNG per call. Edits accept up to eight GIF, JPEG, PNG, or WebP files, with a 20 MB limit per file and 64 MB total.

Server-side image generation
import { generateImage } from "ai";
import { lantern } from "@/lib/lantern";

export async function POST(request: Request) {
  const local = lantern.server(request);
  const { prompt } = await request.json();

  const { image } = await generateImage({
    model: local.provider.imageModel(local.model),
    prompt,
    size: "1024x1024",
  });

  return Response.json({ mediaType: image.mediaType, base64: image.base64 });
}

Test and deploy

Approval is bound to the exact web origin. http://localhost:3000, a preview deployment, and your production domain are different origins, so the user approves each one separately.

Use the same stable LANTERN_APP_SECRET in every deployment that should represent the same app. Before testing a request, confirm that the user's Mac is awake, Lantern is running and online, and the CLI chosen for the connection is still signed in.

Where requests and data go

Lantern routes each call to the Mac that user approved. Their Codex or Claude login stays on that Mac. Prompts, attached images, and replies pass through your app, Lantern's relay, Lantern on the Mac, and the AI provider.

Read How Lantern works for the relay, signing, browser session, and data boundaries. For lower-level local calls, use the local API reference.