BrandApp Playground

Dev Server

Run a local in-memory API server for offline development. No remote connection required.

Quick Start

Run your app with the dev server using a single command.

terminal
# Run with dev mode (in-memory server, no remote connection needed)
REOPT_DEV_MODE=true next dev

# Or use the npm script
pnpm dev:local

createDevServer()

Create an in-memory API server with your schema definition.

example.ts
import { createDevServer } from "@reopt-ai/brandapp-sdk/dev";

const devServer = await createDevServer({
  port: 4300,
  schema: {
    entities: {
      contacts: {
        name: "contacts",
        description: "Contact management",
        icon: "👤",
        attributes: {
          name: {
            label: "Name",
            dataType: "string",
            isRequired: true,
          },
          email: {
            label: "Email",
            dataType: "email",
            isRequired: true,
            isUnique: true,
          },
        },
      },
    },
  },
  dataPath: ".reopt/dev-runtime.json",
  verbose: true,
});

console.log("Dev server running at:", devServer.url);

Seed Data

Define seed data that is automatically loaded when the dev server starts.

reopt.seed.ts
// reopt.seed.ts — automatically applied when dev server starts
export default {
  contacts: [
    { values: { name: "Alice Johnson", email: "alice@example.com", status: "active" } },
    { values: { name: "Bob Smith", email: "bob@example.com", status: "active" } },
    { values: { name: "Carol Davis", email: "carol@example.com", status: "inactive" } },
  ],
};

Instrumentation

Auto-start the dev server when your Next.js app boots in dev mode.

instrumentation.ts
// instrumentation.ts — auto-start dev server on app startup
export async function register() {
  // Gate on NEXT_RUNTIME === "nodejs": the dev server imports Node built-ins
  // (http / fs / path), so this must NOT run on the Edge runtime.
  if (
    process.env.NEXT_RUNTIME === "nodejs" &&
    process.env.REOPT_DEV_MODE === "true"
  ) {
    const { startDevServer } = await import("./lib/dev-server");
    await startDevServer();
  }
}

Bootstrap Module

The dev server bootstrap module overrides environment variables so existing SDK clients automatically connect to the local server.

lib/dev-server.ts
// lib/dev-server.ts — Dev Server bootstrap
import type { DevServer, DevSchema } from "@reopt-ai/brandapp-sdk/dev";

let devServer: DevServer | null = null;

export async function startDevServer() {
  if (devServer) return devServer;

  const { createDevServer } = await import("@reopt-ai/brandapp-sdk/dev");
  const schema = (await import("./eav.schema")).default;

  // Convert the defineSchema() output to the DevSchema shape createDevServer()
  // expects: flatten each entity's attributes into label/dataType/isRequired/
  // isUnique/options, and carry over name/description/icon per entity.
  const devSchema: DevSchema = { entities: {} };
  for (const [key, entity] of Object.entries(schema.entities)) {
    const attrs: DevSchema["entities"][string]["attributes"] = {};
    for (const [attrKey, attr] of Object.entries(entity.attributes)) {
      const a = attr as Record<string, unknown>;
      attrs[attrKey] = {
        label: (a.label as string) ?? attrKey,
        dataType: (a.dataType as string) ?? "text",
        isRequired: (a.isRequired as boolean) ?? false,
        isUnique: (a.isUnique as boolean) ?? false,
        options: a.options as DevSchema["entities"][string]["attributes"][string]["options"],
      };
    }
    devSchema.entities[key] = {
      name: entity.name ?? key,
      description: entity.description,
      icon: entity.icon,
      attributes: attrs,
    };
  }

  devServer = await createDevServer({
    port: 4300,
    schema: devSchema,
    dataPath: ".reopt/dev-runtime.json",
    verbose: true,
  });

  // Override env for existing SDK clients
  process.env.REOPT_BASE_URL = devServer.url;
  process.env.BRANDAPP_CLIENT_ID = "dev";
  process.env.BRANDAPP_CLIENT_SECRET = "dev";
  process.env.BRANDAPP_ID = "dev-brandapp";

  // Apply seed data
  const seedData = (await import("../reopt.seed")).default;
  await devServer.seed(seedData, { mode: "upsert" });

  return devServer;
}

Faithful OIDC mock

3.3.0

The dev server is a faithful OIDC identity provider: it signs real EdDSA tokens with an in-memory Ed25519 keypair, serves the matching JWKS at /api/auth/jwks, and mounts /api/auth/oauth2/end-session. dev.mintLogoutToken() drives an OIDC Single Logout receiver end-to-end with no reopt server — this is what powers the Single Logout demo.

logout.test.ts
import { createDevServer } from "@reopt-ai/brandapp-sdk/dev";
import { createBackchannelLogoutHandler } from "@reopt-ai/brandapp-sdk/logout";

const dev = await createDevServer({ brandappId: "dev" });

// The receiver verifies against the dev IdP's discovery + JWKS (idBaseUrl: dev.url).
const handler = createBackchannelLogoutHandler({
  clientId: "my-brandapp",
  idBaseUrl: dev.url,
  onLogout: async ({ sid }) => {
    await db.sessions.deleteMany({ where: { reoptSid: sid } });
  },
});

// Mint a spec-compliant, EdDSA-signed logout_token from the dev IdP …
const logoutToken = await dev.mintLogoutToken({
  clientId: "my-brandapp",
  sessionId: "idp-session-1",
  userId: "user-1",
  // issuedAtMs: Date.now() - 10 * 60_000, // backdate to test the max-age branch
});

// … then deliver it the way reopt does: form-encoded POST.
const res = await handler(
  new Request("http://localhost/api/auth/backchannel-logout", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({ logout_token: logoutToken }),
  }),
);
// res.status === 200 — the local sessions for sid "idp-session-1" are revoked.

// Requires Ed25519 Web Crypto (Node 20+). Where it's unavailable, discovery
// omits JWKS/EdDSA/end-session (reduced-but-coherent) instead of faking them.

Proxy Mode

Forward selected modules to the real Reopt server while keeping the rest in-memory. Here AI routes hit the real server (so you get real completions and credits) while EAV, CMS, Files, and Auth stay offline.

example.ts
import { createDevServer } from "@reopt-ai/brandapp-sdk/dev";
import schema from "./eav.schema";

const devServer = await createDevServer({
  schema,
  proxy: {
    // Where "passthrough" routes are forwarded
    remoteUrl: "https://brand.local.reopt.io:3443",
    // Credentials used for the proxied (real) calls
    remoteCredentials: {
      clientId: process.env.BRANDAPP_CLIENT_ID!,
      clientSecret: process.env.BRANDAPP_CLIENT_SECRET!,
      brandappId: process.env.BRANDAPP_ID!,
    },
    // Per-module strategy: "passthrough" hits the real server, "local" stays in-memory
    routes: {
      ai: "passthrough", // real AI completions / credits
      eav: "local",
      cms: "local",
      files: "local",
      auth: "local",
    },
  },
});

console.log("Dev server (AI proxied):", devServer.url);