<!--
Demo / Public Sandbox Playbook

Part of the free Claude Code template library from BILT AI — https://biltai.io/claude-templates

These are the actual playbooks used to build and ship production SaaS apps with
Claude Code. They're written to be handed to an agent as-is: point Claude at one
and it has the full pattern, the schema, the gotchas, and the "don't do this"
list. Paths, project refs, and org names have been replaced with placeholders
(<your-app>, <supabase-project-ref>, <your-github-org>) — search for "<" and
fill in your own before running.

Free to use, modify, and ship. No attribution required.
-->

# Demo / Public Sandbox Playbook

**Portable spec for reducing onboarding friction with a "try before signup" surface — anonymous sandbox tier, public-chat endpoints, sample-input flows, and demo-image manifest.**

Built from the BKE / Kompozy implementation. Reference repo: `reference app`.

---

## 0. When to use this spec

**Use it when:** your app has high-friction onboarding — non-trivial configuration (persona briefs, API keys, voice cloning, brand setup) before the first useful output. High-intent visitors bounce when they hit a signup wall before seeing the product work.

**Skip it when:** the product's "wow" is post-account (multi-tenant collaboration, long-running automations, paid-only outputs). Demo surfaces add maintenance load; only ship them where the trial-conversion lift pays for the upkeep.

**The core thesis:** "Try one generation, see the output, then sign up to keep going" outperforms "sign up to try." But every demo surface that drifts from the authenticated path becomes a marketing-only artifact that lies about the product.

---

## 1. Three patterns (pick at least one)

### Pattern A — Interactive marketing pages (free tools at /tools)
Standalone single-purpose mini-tools served at `/tools/<slug>`. Each tool runs through the shared anonymous endpoint, exercises the same LLM stack as authenticated paths, and ranks for long-tail SEO queries. Covered in detail in TOPICAL-AUTHORITY-PLAYBOOK §4. Kompozy ships 21 of these.

Reference: `src/app/api/tools/llm/route.ts` — shared LLM endpoint with per-tool system prompts + Zod schemas, discriminator-keyed.

### Pattern B — Public chat / Q&A bot (Rozy AI at /api/public-chat)
Anonymous pre-sale assistant on the marketing site. Answers from a static corpus (features, pricing, integrations, FAQs). No per-user context. Streams SSE. Per-IP + global rate limit. Lead-capture explicitly forbidden — signup is the conversion event, not email capture.

Reference: `src/app/api/public-chat/route.ts`.

### Pattern C — Sample-input flows (homepage demo)
Homepage shows a "try this with sample inputs" CTA that exercises the same generator path as logged-in users but seeds curated demo content (sample brand, sample topic, sample format). Renders the real output in the real UI shell. Adds a soft signup prompt below the result.

Reference: `src/app/api/onboarding/sample/route.ts` — text-only by design; image gen too expensive for disposable demos.

---

## 2. Architecture

```
src/
├── proxy.ts
│   └── PUBLIC_API_PREFIXES = [
│         '/api/public-chat',
│         '/api/tools',
│         '/api/public-sandbox',   # if you add Pattern C as anon
│       ]                          # default-deny posture; explicit allowlist only
│
├── lib/
│   ├── demo-image-manifest.json       # pre-uploaded sample images (Pattern C)
│   ├── admin-keys-server.ts           # getSandboxTextKeys() — admin enterprise keys
│   ├── rate-limit.ts                  # Upstash sliding-window per-bucket
│   └── help/public-corpus.ts          # static Q&A corpus (Pattern B)
│
├── app/
│   ├── api/
│   │   ├── public-chat/route.ts       # Pattern B
│   │   ├── tools/llm/route.ts         # Pattern A
│   │   └── onboarding/sample/route.ts # Pattern C (or /api/public-sandbox)
│   │
│   ├── tools/                         # /tools/<slug> marketing pages
│   └── (marketing)/page.tsx           # homepage embeds Pattern C CTA
```

---

## 3. Schema considerations

**Option 1 — separate demo_runs table (recommended for high-volume marketing sites):**

```sql
create table demo_runs (
  id uuid primary key default gen_random_uuid(),
  ip_hash text not null,                       -- sha256(ip + salt)
  tool text not null,                           -- 'persona-brief' | 'public-chat' | ...
  input jsonb not null,
  output text,
  provider text,
  model text,
  created_at timestamptz default now(),
  expires_at timestamptz default (now() + interval '24 hours')
);

create index on demo_runs(expires_at);
create index on demo_runs(ip_hash, created_at desc);

-- service-role-only — RLS denies all
alter table demo_runs enable row level security;
-- (no policies = no access for anon/authenticated)
```

Pair with a Trigger.dev cron / pg_cron job that deletes rows past `expires_at` every hour.

**Option 2 — reuse generated_content with is_demo flag** (for B-tier products where demo volume is low):

```sql
alter table generated_content add column is_demo boolean default false;
create policy "demo rows blocked from anon" on generated_content
  for select using (is_demo = false or auth.uid() = user_id);
```

Default to Option 1. Mixing demo into the production user-state table risks RLS misconfiguration leaking real user content.

---

## 4. Anonymous rate limiting

**Two buckets, both required:**

1. **Per-IP sliding-window** — caps a single IP's burst. `enforceRateLimit('public-chat', ip)`.
2. **Global circuit breaker** — caps total anonymous traffic across all IPs. `enforceRateLimit('public-chat-global', 'all')`.

The global bucket is non-negotiable. Without it, a botnet rotating IPs drains admin enterprise keys (every demo path uses the operator's keys, not the visitor's). Kompozy's tools-llm global cap is 1500/hr — well above legitimate traffic, well below "kills the quota."

Reference: `src/app/api/tools/llm/route.ts:423-428`.

**Tighter than authenticated limits.** Anonymous = no accountability. Authenticated users can be suspended; visitors can't. Suggested defaults:
- Per-IP: 5-10 req/hr for generation, 30 req/hr for chat
- Global: 1500-3000 req/hr depending on cost per call

Use Upstash for production (persists across Lambda warm boots). Fall back to in-memory only in dev.

---

## 5. Demo-image manifest

Pre-uploaded sample images stored in a public bucket, indexed by a JSON manifest. Avoids cold-cache hit on demo page first-load and gives the demo a curated brand-correct look.

Reference: `src/lib/demo-image-manifest.json` — keyed by output type with explicit width/height per asset.

```json
{
  "carousel-post-1": { "width": 1024, "height": 1024 },
  "persona-photo":   { "width": 1080, "height": 1080 },
  "quote-graphic":   { "width": 1080, "height": 1080 }
}
```

Storage convention: `public-bucket/demo/<key>.png`. Manifest ships with the build; images live in object storage with a CDN in front. Eliminates demo-time image generation entirely for showcase galleries.

---

## 6. Sample-content seeding

For Pattern C, pre-generate a curated output for each demo path (one polished blog intro, one social post, one carousel) and serve as static assets. The visitor sees the demo "generate" with a typing animation, but the output is static.

This reduces marginal demo cost to near-zero for the showcase path. The actual LLM call only fires when the visitor changes the sample input — at which point per-IP + global rate limits gate cost.

Pair with a "regenerate with your own topic" CTA that calls the live endpoint. Visitor sees both: the polished sample (instant, free), then the live generation against their input (rate-limited, real).

---

## 7. Conversion gates

After N demo runs from the same IP, soft-prompt for signup. UI copy pattern:

> You've used 3 of 3 free runs. Sign up for unlimited generations — no credit card required.

**Soft, not hard.** Hard-block after N kills demo word-of-mouth. Soft-prompt keeps the visitor in the demo UI with a slightly degraded experience (longer cooldown, smaller output) and a persistent banner.

Threshold tuning: start at 3-5 runs per 24h before the prompt fires. Watch your `demo_runs → signup` conversion rate by IP. If conversion happens earlier (run 1 or 2), tighten the threshold to surface the prompt sooner.

---

## 8. Branding & UI consistency

**Demo outputs must feel exactly like authenticated outputs.** Same fonts, same UI shell, same brand voice in the generated content, same skeleton loaders, same error states. If the demo's "social post" output looks different from the authenticated one, the visitor learns they're being shown a marketing mockup — trust collapses.

Practical implications:
- Reuse the same React component for output rendering (`<GeneratedPost />`) in both demo and authenticated routes.
- Same fonts. Same color tokens. Same SVG icons.
- Same provider/model selection (admin keys for demo; user keys for auth — but the same model behind it).
- Same prompt structure (the system prompt that builds the output is identical; only the inputs differ).

**Watermarking is optional and usually a mistake.** Visible watermarks ("Generated by Kompozy — sign up to remove") tank conversion. The CTA below the output is enough.

---

## 9. API key cost management

Demo paths use **admin enterprise keys** — never customer keys (visitors don't have any). Resolution lives in `getSandboxTextKeys()` returning a `[primary, fallback]` array.

Reference: `src/lib/admin-keys-server.ts::getSandboxTextKeys`.

Cost containment stack:
1. **Cheap model defaults** — Kompozy demo paths default to Haiku 4.5 (Anthropic) or gpt-4o-mini (OpenAI). Pin via `slot.selectedModel`.
2. **Tight `max_tokens` budgets** — Pattern B caps at 384, Pattern A at 600-1500 per tool, Pattern C at 700. Never let a demo call consume a full 4096-token budget.
3. **Global rate-limit ceiling** — 1500/hr across all IPs (the `tools-llm-global` pattern).
4. **Provider fallback chain** — primary fails → fallback fires. Don't loop indefinitely.

Cross-reference: see ADMIN-CONSOLE-PLAYBOOK for the admin key bridge that surfaces these slots to `getSandboxTextKeys()`.

---

## 10. Common pitfalls

**❌ Don't make the demo a separate codepath from authenticated generation.** Drift kills you. Use the same generators (`src/lib/generators/`), the same prompt builders, the same image models. The ONLY difference is auth at the edge + admin keys vs user keys. A demo that uses a different code path will silently diverge and start lying about the product.

**❌ Don't expose internal API keys via demo path.** Server-side proxy only. The client receives the streamed/JSON response; the key never leaves the server. If a frontend dev asks "can I call Anthropic directly from the demo page" the answer is always no.

**❌ Don't store demo outputs forever.** TTL purge (24h is the Kompozy default) keeps storage costs predictable and limits PII exposure if a visitor pastes something sensitive.

**❌ Don't allow file uploads in demo.** Abuse vector (CSAM, malware, copyright-infringing content) + storage cost explosion + moderation burden. Demo inputs = text only, or pre-canned image picks from the demo-image manifest. The thumbnail-analyzer tool at Kompozy (Pattern A) accepts images but is gated to authenticated users for exactly this reason.

**❌ Don't rate-limit by user_id.** Visitor has none. Use IP + global bucket pair. If you forget the global bucket, a botnet rotating IPs drains your admin keys overnight.

**❌ Don't gate demo with email capture.** Defeats the "frictionless try" purpose. The whole point of the demo surface is that the visitor sees output before any commitment. Email gates push conversion AFTER the value moment — Kompozy's data and the broader SaaS literature both show this hurts trial-to-paid.

**❌ Don't watermark too aggressively.** Visible watermarks ("Made with Kompozy — sign up to remove") tank conversion. If you must mark, mark via metadata (EXIF / a hidden DOM attribute), not pixels.

**❌ Don't share Stripe / billing state with the demo path.** Demo runs never touch `credit_transactions`. They never increment a usage counter on a user row (there is no user row). Mixing demo cost into the authenticated billing system creates audit-trail confusion and risks accidentally charging a visitor.

**❌ Don't forget `proxy.ts` allowlist.** Per the BKE proxy default-deny rule, every `/api/*` route is 401'd at the edge unless added to `PUBLIC_API_PREFIXES`. A demo route with bulletproof handler-level auth bypass is unreachable if the proxy still blocks it.

---

## 11. Kickoff prompt

> Execute the Demo / Public Sandbox Playbook at `<workspace>/templates/b-tier/DEMO-SANDBOX-PLAYBOOK.md` for this new app. Pick the pattern(s) that fit:
>
> - **Pattern A** (free tools at /tools) — if the product has clear single-purpose value props (analyzer, scorer, generator) that map to SEO queries.
> - **Pattern B** (public chat) — if visitors typically have pre-sale questions that the corpus can answer (pricing, integrations, comparisons).
> - **Pattern C** (sample-input flow) — if the product's "wow" is a single live generation that's hard to convey via static marketing copy.
>
> Inputs:
> - Admin enterprise API keys configured in `admin-keys-server.ts`
> - `UPSTASH_REDIS_REST_URL` + `UPSTASH_REDIS_REST_TOKEN` for rate limiting
> - Demo image manifest paths (if Pattern C)
> - Static corpus for Pattern B
>
> Reference the BKE files at:
> - `src/app/api/public-chat/route.ts`
> - `src/app/api/tools/llm/route.ts`
> - `src/app/api/onboarding/sample/route.ts`
> - `src/lib/demo-image-manifest.json`
>
> Pause after the first endpoint + rate-limit wiring lands for review.

---

**Reference files in BKE:**
- `src/app/api/public-chat/route.ts` — Pattern B (Rozy AI)
- `src/app/api/tools/llm/route.ts` — Pattern A (shared free-tools endpoint)
- `src/app/api/onboarding/sample/route.ts` — Pattern C (sample-input flow)
- `src/lib/demo-image-manifest.json` — pre-uploaded sample image index
- `src/lib/admin-keys-server.ts` — `getSandboxTextKeys()` admin key resolution
- `src/lib/rate-limit.ts` — Upstash sliding-window enforcer
- `src/lib/help/public-corpus.ts` — static Q&A corpus for Pattern B
- `src/proxy.ts` — `PUBLIC_API_PREFIXES` allowlist
