Skip to content
Closed
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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
node_modules/
.DS_Store
.planning/
supabase/
/supabase/

# Credentials & runtime state
.env
Expand All @@ -14,3 +14,6 @@ sync-log.json

# Build artifacts
deno.lock
.next/
.svelte-kit/
dist/
6 changes: 5 additions & 1 deletion dashboards/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ https://github.com/user-attachments/assets/9454662f-2648-4928-8723-f7d52e94e9b8

Frontend templates you can host on Vercel or Netlify, pointed at your Supabase backend. Instant UI for your brain.

*No community dashboards yet — be the first to contribute one.*
| Dashboard | What It Does |
| --------- | ------------ |
| [Open Brain Dashboard](open-brain-dashboard/) | SvelteKit UI for browsing, searching, and capturing thoughts through MCP |
| [Open Brain Dashboard Next](open-brain-dashboard-next/) | Full-featured Next.js dashboard backed by the REST gateway |
| [Open Brain Auth Portal](open-brain-auth-portal/) | Minimal sign-in and consent app for MCP OAuth flows |

## Ideas

Expand Down
2 changes: 2 additions & 0 deletions dashboards/open-brain-auth-portal/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=your-publishable-key
57 changes: 57 additions & 0 deletions dashboards/open-brain-auth-portal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Open Brain Auth Portal

Standalone OAuth consent portal for Open Brain MCP servers.

## What it does

This app is the hosted sign-in and consent surface for the new Open Brain OAuth-based MCP flow. It gives Supabase Auth a real login page, a consent screen, and approve/deny callbacks that redirect back to Claude, ChatGPT, or any other remote MCP client.

## Prerequisites

- Working Open Brain setup ([guide](../../docs/01-getting-started.md))
- Supabase OAuth 2.1 enabled for your project
- A Supabase Auth owner user created for Open Brain
- Node.js 20.9+

## Quick Start

1. Install dependencies:

```bash
cd dashboards/open-brain-auth-portal
npm install
```

2. Copy envs:

```bash
cp .env.example .env.local
```

3. Fill in:

- `NEXT_PUBLIC_SUPABASE_URL`
- `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`

4. Run locally:

```bash
npm run dev
```

5. Deploy to Vercel and use the deployed `/oauth/consent` route as your Supabase OAuth authorization URL.

## Expected outcome

After deployment, a remote MCP client that hits your protected Open Brain server and receives `401 + WWW-Authenticate` should be redirected through Supabase Auth and land on this portal for sign-in and approval.

## Troubleshooting

**Issue: Consent screen says `Missing authorization_id`**
Solution: You opened the consent route directly without a real Supabase OAuth request. Use the root page preview or trigger the flow from an MCP client.

**Issue: Sign-in works but consent page loops back to login**
Solution: Verify the same `NEXT_PUBLIC_SUPABASE_URL` and publishable key are configured in both local env and deployed env. Mismatched projects cause session cookies to be ignored.

**Issue: Supabase returns an invalid authorization request**
Solution: Confirm Supabase OAuth 2.1 is enabled and that the authorization URL path points to `/oauth/consent` on this deployed app.
32 changes: 32 additions & 0 deletions dashboards/open-brain-auth-portal/app/api/oauth/decision/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextResponse } from "next/server";
import { createSupabaseServerClient } from "@/lib/supabase/server";

export async function POST(request: Request) {
const formData = await request.formData();
const decision = formData.get("decision");
const authorizationId = String(formData.get("authorization_id") || "");

if (!authorizationId) {
return NextResponse.json({ error: "Missing authorization_id" }, { status: 400 });
}

const supabase = await createSupabaseServerClient();

if (decision === "approve") {
const { data, error } = await supabase.auth.oauth.approveAuthorization(authorizationId);

if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}

return NextResponse.redirect(data.redirect_url);
}

const { data, error } = await supabase.auth.oauth.denyAuthorization(authorizationId);

if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}

return NextResponse.redirect(data.redirect_url);
}
209 changes: 209 additions & 0 deletions dashboards/open-brain-auth-portal/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
:root {
--bg: #f4efe4;
--card: rgba(255, 251, 244, 0.88);
--ink: #152033;
--muted: #5e697b;
--line: rgba(21, 32, 51, 0.12);
--accent: #c65d2d;
--accent-2: #157a8a;
--shadow: 0 24px 60px rgba(21, 32, 51, 0.12);
}

* {
box-sizing: border-box;
}

html {
min-height: 100%;
}

body {
margin: 0;
min-height: 100vh;
font-family: var(--font-display), sans-serif;
color: var(--ink);
background:
radial-gradient(circle at top left, rgba(198, 93, 45, 0.18), transparent 32%),
radial-gradient(circle at top right, rgba(21, 122, 138, 0.2), transparent 28%),
linear-gradient(180deg, #fffaf1 0%, var(--bg) 100%);
}

a {
color: inherit;
}

.shell {
width: min(960px, calc(100% - 2rem));
margin: 0 auto;
padding: 2rem 0 4rem;
}

.hero {
display: grid;
gap: 1.5rem;
align-items: start;
}

.pill {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.45rem 0.8rem;
border-radius: 999px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.52);
font-family: var(--font-mono), monospace;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}

.grid {
display: grid;
gap: 1rem;
}

.cards {
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}

.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 28px;
padding: 1.25rem;
box-shadow: var(--shadow);
backdrop-filter: blur(14px);
}

.card h1,
.card h2,
.card h3,
.card p {
margin-top: 0;
}

.headline {
font-size: clamp(2.2rem, 4vw, 4.5rem);
line-height: 0.94;
letter-spacing: -0.04em;
margin-bottom: 1rem;
}

.lede,
.muted {
color: var(--muted);
}

.button-row {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}

.button,
.ghost-button,
button {
appearance: none;
border: none;
border-radius: 999px;
padding: 0.9rem 1.2rem;
font: inherit;
cursor: pointer;
transition: transform 160ms ease, box-shadow 160ms ease, opacity 160ms ease;
}

.button,
button[type="submit"] {
background: linear-gradient(135deg, var(--accent), #ea8a47);
color: white;
box-shadow: 0 16px 30px rgba(198, 93, 45, 0.22);
}

.ghost-button {
background: transparent;
color: var(--ink);
border: 1px solid var(--line);
}

.button:hover,
.ghost-button:hover,
button:hover {
transform: translateY(-1px);
}

.stack {
display: grid;
gap: 0.9rem;
}

.field {
display: grid;
gap: 0.35rem;
}

.field label {
font-family: var(--font-mono), monospace;
font-size: 0.82rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}

.field input {
width: 100%;
border-radius: 18px;
border: 1px solid var(--line);
background: rgba(255, 255, 255, 0.72);
padding: 0.95rem 1rem;
font: inherit;
}

.list {
display: grid;
gap: 0.6rem;
padding-left: 1.1rem;
}

.error {
border: 1px solid rgba(198, 93, 45, 0.26);
background: rgba(198, 93, 45, 0.08);
color: #8b3d1d;
border-radius: 18px;
padding: 0.9rem 1rem;
}

.meta {
display: grid;
gap: 0.7rem;
}

.meta-row {
display: grid;
gap: 0.2rem;
padding-bottom: 0.7rem;
border-bottom: 1px solid var(--line);
}

.meta-row:last-child {
border-bottom: none;
padding-bottom: 0;
}

.meta-row strong {
font-family: var(--font-mono), monospace;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.08em;
}

@media (max-width: 720px) {
.shell {
width: min(100%, calc(100% - 1rem));
padding-top: 1rem;
}

.card {
border-radius: 22px;
padding: 1rem;
}
}
31 changes: 31 additions & 0 deletions dashboards/open-brain-auth-portal/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { IBM_Plex_Mono, Space_Grotesk } from "next/font/google";
import "./globals.css";

const display = Space_Grotesk({
variable: "--font-display",
subsets: ["latin"],
});

const mono = IBM_Plex_Mono({
variable: "--font-mono",
subsets: ["latin"],
weight: ["400", "500"],
});

export const metadata: Metadata = {
title: "Open Brain Auth Portal",
description: "Sign in and approve MCP connector access for your Open Brain.",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body className={`${display.variable} ${mono.variable}`}>{children}</body>
</html>
);
}
Loading
Loading