Full test project

ResiResi — try the whole thing

A complete, runnable demo driven by one script. You play a reservation platform whose restaurant tenants need API keys and staff groups. Put APIblaze in front of the backend, drop in two widgets, write one plain-English rule — and watch John lose the ability to open Maria's reservation while his own still works.

What it is

One fictional company, so every APIblaze idea has somewhere concrete to land.

ResiResi is a restaurant-reservation platform. Its backend is open — no keys, no roles — on purpose: access control is the gateway's job, not the origin's. It has two tenants, Nino's Pizza and Gino's Pizza, whose software engineers self-serve API keys and manage users & groups from ResiResi's Developers page.

What you'll set up, in plain terms:

  • Start ResiResi's local backend on your computer.
  • Create an APIblaze proxy from its OpenAPI spec — a checkpoint every request passes through — and tunnel it back to your machine.
  • Add logins, API keys, and staff groups to ResiResi's app using APIblaze's ready-made widgets.
  • Prove it works: John could open Maria's reservation before — after one rule, he can't (but his own still works, and staff see everything).

Everything runs on your laptop. You'll need Node 20+ and a free APIblaze account (one browser login, for the step that gives your backend a public address).

1 · Clone the repo and run the lab

One script explains each step, prints the exact command it's about to run, waits for Enter, runs it, and shows the result.

terminal — the whole demo
git clone https://github.com/jayjaychicago/rr
cd rr
./start.sh

Windows: .\start.ps1 (if blocked: powershell -ExecutionPolicy Bypass -File .\start.ps1).

  • Every step shows its command before you press Enter — nothing runs blind.
  • Crash or Ctrl-C? Re-run ./start.sh and choose [r]esume — finished steps are skipped, and the tunnel self-heals if it drops.
  • Names are minted per run (resiresi0000 for the proxy, nino0000 for the tenant), so two people doing the lab never collide.

That's the whole demo. The sections below are the same sequence the script drives, written out — for reading along, or doing it by hand.

Create the APIblaze proxy

The same sequence the script drives, by hand. All calls use the dev environment — the gateway captures dev traffic as samples the AI agents learn from.

Prereqs (the lab's first steps): the local backend running — cd rr/resiresi-backend-lightweight && node server.js — and npx apiblaze login.

Create the APIblaze proxy from the resiresi OpenAPI spec. This creates resiresi0000 — the public address that will tunnel into your local backend. It's created from the spec, so the gateway knows the routes from day one (the AI agents use them later). --identified makes each request say which person it's for; --iam turns on users & groups. Tenant names are globally unique, so mint a per-run tenant:

terminal — run from the rr/ folder
npx apiblaze create --name resiresi0000 --openapi resiresi-backend-lightweight/openapi.yaml --tenant nino0000 --auth api_key --identified --iam

Note from the output: the dev API key (call it DPKEY) and the tenant it created.

Connect your backend to the gateway — your backend lives on your laptop; the gateway lives in the cloud. Keep this running:

terminal — keep open
npx apiblaze dev 8080 --project resiresi0000

Prove the proxy reaches your machine — the exact call a storefront makes: the API key says which app; X-End-User-Id says which person:

terminal
curl "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations" \
  -H "X-API-Key: $DPKEY" \
  -H "X-End-User-Id: john@nino.com"

Widgets & tenant admin

ResiResi's Developers page is where Nino's and Gino's engineers self-serve API keys and manage users & groups. Its backend needs one limited key.

Mint the key that powers the Developers page — a limited key that can only do those admin actions (issue keys, manage users & groups), not a full-control key. Shown once:

terminal
npx apiblaze apikeys mint --desc "resiresi widget key"

Install ResiResi's web app + drop in the key (the key stays on the server, never in the browser):

terminal
cd rr/resiresi-frontend
npm install
npm install apiblaze
rr/resiresi-frontend/.env.local (new file)
APIBLAZE_CP_KEY=<the key you minted>

Add the two widgets. This edits only files inside rr/resiresi-frontend/. Create file 1 of 3 — maps the signed-in restaurant + email to the identity APIblaze authorizes on (the tenant is the restaurant slug + your run's suffix):

rr/resiresi-frontend/lib/apiblaze-user.ts (new file)
import type { AppUser } from "apiblaze/server";
import { getTenantSlug } from "./tenant";
import { getUser } from "./user";

const TENANT_SUFFIX = "0000"; // your run's suffix — nino becomes nino0000

export function getApiblazeUser(): AppUser | null {
  const slug = getTenantSlug();
  const user = getUser();
  if (!slug || !user) return null;
  return { tenant: slug + TENANT_SUFFIX, userId: user.email, email: user.email };
}

Create file 2 of 3 — the API-key route (mkdir -p app/api/apiblaze/keys from inside rr/resiresi-frontend):

rr/resiresi-frontend/app/api/apiblaze/keys/route.ts (new file)
import { NextResponse } from "next/server";
import { createApiblazeKeys } from "apiblaze/server";
import { getApiblazeUser } from "@/lib/apiblaze-user";

let h: ReturnType<typeof createApiblazeKeys> | null = null;

function handler(req: Request) {
  const cpKey = process.env.APIBLAZE_CP_KEY;
  if (!cpKey) return NextResponse.json({ error: "APIBLAZE_CP_KEY not set" }, { status: 503 });
  h ??= createApiblazeKeys({ cpKey, getUser: () => getApiblazeUser() });
  return h.handler(req);
}

export const GET = handler;
export const POST = handler;

Create file 3 of 3 — the users-and-groups route (mkdir -p app/api/apiblaze/groups). Identical shape, different factory:

rr/resiresi-frontend/app/api/apiblaze/groups/route.ts (new file)
import { NextResponse } from "next/server";
import { createApiblazeGroups } from "apiblaze/server";
import { getApiblazeUser } from "@/lib/apiblaze-user";

let h: ReturnType<typeof createApiblazeGroups> | null = null;

function handler(req: Request) {
  const cpKey = process.env.APIBLAZE_CP_KEY;
  if (!cpKey) return NextResponse.json({ error: "APIBLAZE_CP_KEY not set" }, { status: 503 });
  h ??= createApiblazeGroups({ cpKey, getUser: () => getApiblazeUser() });
  return h.handler(req);
}

export const GET = handler;
export const POST = handler;

Edit one file — open rr/resiresi-frontend/app/developers/page.tsx. Add this import at the top:

rr/resiresi-frontend/app/developers/page.tsx — add at the top
import { ApiKeyWidget, UsersGroupsWidget } from "apiblaze/react";

Then replace the two <Placeholder>…</Placeholder> blocks (one under “API access”, one under “Users & Groups”) with:

replace the "API access" placeholder with
<ApiKeyWidget title="API keys" theme={{ accent: "#4f46e5" }} />
replace the "Users & Groups" placeholder with
<UsersGroupsWidget theme={{ accent: "#4f46e5" }} />

Make yourself the tenant admin and sign in — name the first admin before signing in, so the widgets are ready the moment you land:

terminal
npx apiblaze admins add owner@nino.com --tenant nino0000

Start the app (npm run dev — serves http://localhost:3003), open http://localhost:3003/developers, and sign in with any name and the email owner@nino.com. Both widgets appear, ready to use.

Put staff in a group — a group reservationists with maria@nino.com in it (she's staff; John stays just a diner). In the widget: + New user → maria@nino.com, + New group → reservationists, add maria — or from the terminal (unknown emails are created on the spot):

terminal
npx apiblaze group create reservationists --admin owner@nino.com --tenant nino0000
npx apiblaze group add-user maria@nino.com reservationists --tenant nino0000

Prove access control

BEFORE: John opens Maria's reservation. One plain-English rule later, he can't — but his own booking still works, and staff see everything.

BEFORE — find one of Maria's reservations (any id whose diner_external_id is maria's), then open it as John:

terminal — John reads MARIA'S booking. HTTP 200. Not right.
curl "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations/<MARIA_ID>" \
  -H "X-API-Key: $DPKEY" -H "X-End-User-Id: john@nino.com"

Write the access rules — one command, plain English. The AI designs the model and rules from your routes and traffic, saves them, and turns enforcement on:

terminal — one sentence becomes enforced authorization
npx apiblaze rule "Bookings belong to whoever makes them. A reservation may be opened by its owner or by members of the existing group \"reservationists\". The full reservations list is for \"reservationists\" only. Leave every other route open." resiresi0000 --enforce

Want to iterate on it conversationally instead? npx apiblaze agent authz resiresi0000 opens the same designer as a chat.

AFTER — same app key everywhere; the person now decides the result:

1 · John books a table → 201 (the proxy records him as the owner)
curl -X POST "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations" \
  -H "X-API-Key: $DPKEY" -H "X-End-User-Id: john@nino.com" -H "Content-Type: application/json" \
  -d '{"diner_name":"John Diner","diner_external_id":"john@nino.com","party_size":2,"starts_at":"2026-08-01T19:00:00Z"}'
2 · John opens HIS new reservation → 200
curl "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations/<JOHN_ID>" \
  -H "X-API-Key: $DPKEY" -H "X-End-User-Id: john@nino.com"
3 · John tries MARIA'S reservation → 403 · blocked
curl "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations/<MARIA_ID>" \
  -H "X-API-Key: $DPKEY" -H "X-End-User-Id: john@nino.com"
4 · Maria (staff) opens John's → 200 · reservationists see everything
curl "https://resiresi0000.abz.run/1.0.0/dev/v1/restaurants/nino/reservations/<JOHN_ID>" \
  -H "X-API-Key: $DPKEY" -H "X-End-User-Id: maria@nino.com"

One key, many people: owners see their own, staff see all.