Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions dashboards/open-brain-dashboard-next/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Provides 9 pages for managing your thoughts:
- **Node.js 18+** installed
- A **Vercel account** (free tier works) or any Node.js hosting

> **Need a backend?** [`integrations/cloudflare-rest-worker/`](../../integrations/cloudflare-rest-worker/) implements the `open-brain-rest` REST API this dashboard expects. Deploy it as a Cloudflare Worker, set `NEXT_PUBLIC_API_URL` to the Worker URL, and the four core pages (Dashboard, Browse, Detail, Search) work end-to-end. See that integration's README for setup + known limitations.

### Credential Tracker

| Credential | Where to get it | Where it goes |
Expand Down
6 changes: 6 additions & 0 deletions integrations/cloudflare-rest-worker/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.wrangler/
wrangler.toml
package-lock.json
*.log
.DS_Store
272 changes: 272 additions & 0 deletions integrations/cloudflare-rest-worker/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,272 @@
# Open Brain REST Gateway (Cloudflare Worker)

A small Cloudflare Worker that implements the REST API the [Next.js
dashboard](../../dashboards/open-brain-dashboard-next/) expects — `open-brain-rest`. The
dashboard's README references this service but no implementation ships in
the repo; this Worker fills that gap so the four core dashboard pages
(Dashboard, Browse, Detail, Search) work end-to-end.

## What It Does

Exposes a REST-shaped surface over your existing Open Brain Supabase project:

| Method | Path | Backed by |
|---|---|---|
| `GET` | `/health` | unauthenticated; used by the dashboard's login page to validate the API URL |
| `GET` | `/thoughts` | paginated `SELECT` with whitelisted `sort` + filters (type, source_type, importance_min, quality_score_max, status, exclude_restricted) |
| `GET` | `/thought/:id` | single-row read |
| `PUT` | `/thought/:id` | partial update of `{ content, type, importance, status }` (last one bumps `status_updated_at`) |
| `DELETE` | `/thought/:id` | hard delete |
| `POST` | `/search` | semantic (embedding → `match_thoughts` RPC → re-fetch full rows) or text mode (`search_thoughts_text` RPC) |
| `GET` | `/stats` | reshapes the existing `brain_stats_aggregate` RPC into the dashboard's StatsResponse shape |
| `POST` | `/capture` | extracts metadata + embeds in parallel, calls `upsert_thought`, returns `{thought_id, action, type, sensitivity_tier, content_fingerprint, message}` |
| `GET` | `/ingestion-jobs` | empty stub (smart-ingest is out of scope for v1) |
| `POST` | `/ingest`, `POST` `/ingestion-jobs/:id/execute` | 501 Not Implemented |

Auth: same `MCP_ACCESS_KEY` your `open-brain-mcp` already uses, sent as the
`x-brain-key` header (or `Authorization: Bearer …` / `?key=…`).

## Architecture

```
Browser
│ HTTPS (iron-session cookie set on dashboard /login)
Cloudflare Pages: open-brain-dashboard-next
│ HTTPS, server-side, x-brain-key from session cookie
Cloudflare Worker: open-brain-rest ← THIS WORKER
│ HTTPS, service-role JWT
Supabase (thoughts table + RPCs)
```

## Prerequisites

- Working Open Brain setup ([guide](../../docs/01-getting-started.md)) — gives
you `thoughts`, `match_thoughts`, `upsert_thought`
- [`schemas/enhanced-thoughts/`](../../schemas/enhanced-thoughts/) applied —
required for `/stats`, `/search?mode=text`, and the
`type / sensitivity_tier / importance / quality_score / source_type` columns
- A Cloudflare account (free tier works) — sign up at
[dash.cloudflare.com](https://dash.cloudflare.com)
- `wrangler` CLI installed (`npm install -g wrangler`) and authenticated
(`wrangler login`)
- Node.js 20+

## Credential Tracker

```text
OPEN BRAIN REST -- CREDENTIAL TRACKER
--------------------------------------

SUPABASE (from your Open Brain setup)
Project URL: ____________
Service role key: ____________
MCP access key (reused): ____________
OpenRouter API key: ____________

WORKER (filled in after deploy)
Worker URL: ____________

--------------------------------------
```

## Setup

### Step 1 — Configure

```bash
cd integrations/cloudflare-rest-worker
cp wrangler.toml.example wrangler.toml
```

The default `wrangler.toml` deploys as `ob-rest`. Rename via `[name]` if you
want a different subdomain.

### Step 2 — Install

```bash
npm install
```

### Step 3 — Set secrets

`wrangler secret put` is interactive — it prompts for the value, no shell
history. Set all four:

```bash
wrangler secret put SUPABASE_URL
wrangler secret put SUPABASE_SERVICE_ROLE_KEY
wrangler secret put MCP_ACCESS_KEY
wrangler secret put OPENROUTER_API_KEY
```

`MCP_ACCESS_KEY` is the same value already set on your `open-brain-mcp`
function — the dashboard reuses it. `OPENROUTER_API_KEY` powers the
`/search?mode=semantic` and `/capture` endpoints; same key as core.

### Step 4 — Deploy

```bash
wrangler deploy
```

Wrangler prints the published URL: `https://ob-rest.<your-cf-subdomain>.workers.dev`.
Save it as `WORKER_URL` in the credential tracker.

### Step 5 — Verify

```bash
# Unauthenticated health check
curl -sS "${WORKER_URL}/health"
# → {"status":"ok","service":"open-brain-rest","version":"0.1.0"}

# Auth enforcement
curl -sS -X GET "${WORKER_URL}/thoughts"
# → {"error":"Unauthorized"} 401

# Authenticated list
curl -sS "${WORKER_URL}/thoughts?per_page=3" \
-H "x-brain-key: ${MCP_ACCESS_KEY}"
# → {"data":[…],"total":N,"page":1,"per_page":3}

# Stats
curl -sS "${WORKER_URL}/stats?days=7" \
-H "x-brain-key: ${MCP_ACCESS_KEY}"

# Semantic search
curl -sS -X POST "${WORKER_URL}/search" \
-H "x-brain-key: ${MCP_ACCESS_KEY}" \
-H "content-type: application/json" \
-d '{"query":"my thoughts on X","mode":"semantic","limit":5,"page":1,"exclude_restricted":true}'

# Capture
curl -sS -X POST "${WORKER_URL}/capture" \
-H "x-brain-key: ${MCP_ACCESS_KEY}" \
-H "content-type: application/json" \
-d '{"content":"Test thought from REST gateway"}'
```

## Wiring the Dashboard

In the dashboard's `.env` (or Cloudflare Pages env vars):

```
NEXT_PUBLIC_API_URL=https://ob-rest.<your-cf-subdomain>.workers.dev
SESSION_SECRET=<openssl rand -hex 32>
```

Then run the dashboard locally (`npm run dev` from
`dashboards/open-brain-dashboard-next/`) or deploy it (next section). At the
login page, paste your `MCP_ACCESS_KEY` — the dashboard validates it against
this Worker's `/health`, then encrypts it into an HTTP-only session cookie
for the rest of the session.

## Deploying the Dashboard to Cloudflare

The dashboard is a Next.js app. Cloudflare's current path for Next.js 15+ is
the [`@opennextjs/cloudflare`](https://opennext.js.org/cloudflare) adapter,
which deploys the app as a Cloudflare Worker (with static assets attached).
The older `@cloudflare/next-on-pages` adapter caps at Next 15.5.x and won't
work with this dashboard's Next 16.x.

```bash
cd dashboards/open-brain-dashboard-next
npm install
npm install -D @opennextjs/cloudflare wrangler

# Scaffold open-next.config.ts and wrangler.jsonc — the dashboard's
# README has the templates and walks through the deploy.

npx opennextjs-cloudflare build
npx opennextjs-cloudflare deploy
wrangler secret put SESSION_SECRET --name <your-worker-name>
```

`NEXT_PUBLIC_API_URL` is read from the dashboard's `.env` at *build* time and
baked into the bundle, so set it before running `build`. `SESSION_SECRET` is
a *runtime* secret set on the Worker after first deploy.

The dashboard ends up at
`https://<your-worker-name>.<your-cf-subdomain>.workers.dev`. Custom domain
optional.

## Known Limitations (v1)

These are real impedance mismatches between the dashboard's expectations and
the upstream schema. The Worker is correct; resolving these requires upstream
changes that are out of scope for this PR:

1. **`Thought.id` type mismatch.** The dashboard's TypeScript types declare
`id: number` and call `parseInt(id, 10)` on URL params
(`app/thoughts/[id]/page.tsx:29`). The actual `thoughts.id` column is
`UUID`. The Worker returns UUIDs as strings. Until the dashboard's `id`
type is widened to `string | number`, the Detail page won't navigate to
individual rows. **A separate small follow-up PR can fix the dashboard
types.**

2. **`importance` scale mismatch.** The dashboard's `PRIORITY_LEVELS`
expects 0–100 (Critical = 80+). The `enhanced-thoughts` schema defaults
`importance` to 3 with no documented upper bound; the entity-extraction
worker emits 0–6. Existing data will render as "Low" priority in the
dashboard. Not a Worker bug.

3. **No `reflections` table.** The dashboard's Detail page calls
`/thought/:id/reflection`. No schema in the repo creates a `reflections`
table. The Worker doesn't implement this endpoint; the page will surface
an error, the rest of the dashboard works.

4. **No smart-ingest integration.** `/ingest`, `/ingestion-jobs/:id`, and
`/ingestion-jobs/:id/execute` return 501. The dashboard's Add to Brain
"extract" mode and the Ingestion Jobs detail view will surface errors;
single-thought capture via `/capture` works.

## Troubleshooting

**`wrangler deploy` errors with "not authenticated"**
Run `wrangler login`. The CLI opens a browser window for OAuth.

**Health check returns 200 but `/thoughts` returns 401**
You sent a key that doesn't match the Worker's `MCP_ACCESS_KEY` secret.
Verify with `wrangler secret list` (it shows names + when each was set, not
values). If you rotated the key on Supabase, also run
`wrangler secret put MCP_ACCESS_KEY` to keep them in sync.

**`/search?mode=semantic` returns 500 with "OpenRouter embedding failed"**
The `OPENROUTER_API_KEY` secret is missing, expired, or out of credits.
`wrangler secret put OPENROUTER_API_KEY` to refresh.

**`/search?mode=text` returns 500 with "function search_thoughts_text does not exist"**
The `enhanced-thoughts` schema isn't applied. Run
`schemas/enhanced-thoughts/schema.sql` in your Supabase SQL Editor.

**`/stats` returns 500 with "function brain_stats_aggregate does not exist"**
Same fix as above — apply `schemas/enhanced-thoughts/schema.sql`.

**Dashboard logs in successfully but Browse shows zero rows**
Check that `NEXT_PUBLIC_API_URL` is the Worker URL, not the Supabase MCP
function URL. The MCP function speaks JSON-RPC, not REST, and won't return
`{ data: [...] }`.

**Capture returns 200 but `embedding` column stays null**
The Worker calls `upsert_thought` (which writes the row) and then a
follow-up `UPDATE` (which writes the embedding). If your `service_role` is
missing `UPDATE` grants on `thoughts`, that follow-up fails silently into
500 — check Worker Logs in the Cloudflare dashboard for the Postgres error.

## What This Worker Doesn't Do

- **Workflow kanban endpoints** (P1) — needs `workflow-status` schema +
status-flow status transitions; can be a follow-up.
- **Audit bulk delete, Duplicates** (P2) — `quality_score`-based bulk
operations.
- **Reflections** (P2) — needs a `reflections` table that no schema
currently creates.
- **Smart ingest extract / execute** (P2) — large feature; needs its own
integration.

Future PRs can add these incrementally.
17 changes: 17 additions & 0 deletions integrations/cloudflare-rest-worker/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "Open Brain REST Gateway (Cloudflare Worker)",
"description": "REST API gateway that backs dashboards/open-brain-dashboard-next. Implements the endpoints the dashboard's lib/api.ts expects (thoughts CRUD, search, stats, capture) on top of the existing Supabase schema and RPCs. Deploys as a Cloudflare Worker.",
"category": "integrations",
"author": { "name": "Travis Swicegood", "github": "tswicegood" },
"version": "0.1.0",
"requires": {
"open_brain": true,
"services": ["Cloudflare Workers", "Supabase"],
"tools": ["wrangler", "Node.js 20+"]
},
"tags": ["rest", "dashboard", "cloudflare", "worker", "gateway"],
"difficulty": "intermediate",
"estimated_time": "20 minutes",
"created": "2026-04-25",
"updated": "2026-04-25"
}
21 changes: 21 additions & 0 deletions integrations/cloudflare-rest-worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "open-brain-rest",
"version": "0.1.0",
"private": true,
"description": "REST gateway Worker that backs dashboards/open-brain-dashboard-next.",
"type": "module",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"types": "wrangler types"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20250420.0",
"typescript": "^5.6.0",
"wrangler": "^3.95.0"
},
"dependencies": {
"@supabase/supabase-js": "^2.47.10",
"hono": "^4.9.2"
}
}
63 changes: 63 additions & 0 deletions integrations/cloudflare-rest-worker/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* open-brain-rest — Cloudflare Worker REST gateway.
*
* Backs the Next.js dashboard at dashboards/open-brain-dashboard-next/ by
* implementing the endpoints its lib/api.ts expects. Reads from / writes to
* the existing Open Brain Supabase schema using the service-role key.
*
* Tech: Hono on Cloudflare Workers. Auth: x-brain-key (or Authorization:
* Bearer / ?key=) — same shared secret used by open-brain-mcp.
*/

import { Hono } from "hono";
import { cors } from "hono/cors";
import { health } from "./routes/health";
import { thoughts } from "./routes/thoughts";
import { search } from "./routes/search";
import { stats } from "./routes/stats";
import { capture } from "./routes/capture";
import { ingestionJobs } from "./routes/ingestion-jobs";
import { requireApiKey } from "./lib/auth";
import type { Env } from "./lib/types";

const app = new Hono<{ Bindings: Env }>();

// Open CORS — the dashboard's server-side fetches don't need it (they go
// from Worker to Worker), but local development hits the Worker directly
// from the browser via curl/devtools, and other clients (e.g. Insomnia)
// also benefit. Allow the headers we actually accept; reject by default
// at the auth layer instead.
app.use(
"*",
cors({
origin: "*",
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowHeaders: ["Content-Type", "Authorization", "x-brain-key"],
maxAge: 86400,
}),
);

// Mount /health pre-auth. The dashboard's login validates the API URL with
// an unauthenticated GET, then makes a second authenticated call to confirm
// the key — so /health must respond regardless of credentials.
app.route("/", health);

// Everything below this point requires a valid x-brain-key.
app.use("*", requireApiKey);

app.route("/", thoughts);
app.route("/", search);
app.route("/", stats);
app.route("/", capture);
app.route("/", ingestionJobs);

// Catch-all: anything unmatched returns 404. Hono's default is 200 with an
// empty body, which is more confusing than a clear miss.
app.notFound((c) => c.json({ error: "Not Found" }, 404));

app.onError((err, c) => {
console.error("Unhandled error:", err);
return c.json({ error: err.message || "Internal error" }, 500);
});

export default app;
Loading
Loading