;
+ readOnly?: boolean;
}) {
const [editing, setEditing] = useState(false);
const router = useRouter();
+ if (readOnly) {
+ return (
+
+
+
+ Content
+
+ Read only
+
+
+ {thought.content}
+
+
+ );
+ }
+
if (!editing) {
return (
diff --git a/dashboards/open-brain-dashboard-next/lib/api.ts b/dashboards/open-brain-dashboard-next/lib/api.ts
index 846a1200..0a1c7410 100644
--- a/dashboards/open-brain-dashboard-next/lib/api.ts
+++ b/dashboards/open-brain-dashboard-next/lib/api.ts
@@ -6,8 +6,12 @@ import type {
Reflection,
IngestionJob,
} from "./types";
+import { KANBAN_TYPES } from "./types";
+import { mcpFetchThought, mcpThoughtStats } from "./openBrainMcp";
const API_URL = process.env.NEXT_PUBLIC_API_URL!;
+const MCP_URL = process.env.OPEN_BRAIN_MCP_URL;
+const LEGACY_MCP_SERVER_URL = MCP_URL?.replace(/open-brain-mcp\/?$/, "mcp-server");
export class ApiError extends Error {
constructor(message: string, public status: number) {
@@ -23,6 +27,151 @@ function headers(apiKey: string): HeadersInit {
};
}
+type LegacyThoughtRecord = {
+ id: string;
+ content: string;
+ metadata?: Record;
+ created_at: string;
+ updated_at: string;
+};
+
+function normalizeThoughtType(metadata: Record | undefined): string {
+ const type = metadata?.type;
+ return typeof type === "string" && type.length > 0 ? type : "reference";
+}
+
+function normalizeImportance(metadata: Record | undefined): number {
+ const raw = metadata?.importance;
+ if (typeof raw === "number") {
+ return Math.min(Math.max(Math.round(raw), 1), 5);
+ }
+ return 0;
+}
+
+function toThought(record: LegacyThoughtRecord): Thought {
+ const metadata = record.metadata ?? {};
+ const sensitivityTier = metadata.sensitivity_tier;
+ const status = metadata.status;
+ const sourceType = metadata.source;
+
+ return {
+ id: record.id,
+ content: record.content,
+ type: normalizeThoughtType(metadata),
+ source_type: typeof sourceType === "string" ? sourceType : "",
+ importance: normalizeImportance(metadata),
+ quality_score: 0,
+ sensitivity_tier:
+ typeof sensitivityTier === "string" ? sensitivityTier : "standard",
+ metadata,
+ created_at: record.created_at,
+ updated_at: record.updated_at,
+ status: typeof status === "string" ? status : null,
+ status_updated_at: null,
+ };
+}
+
+function summarizeThoughts(thoughts: Thought[]): StatsResponse {
+ const types: Record = {};
+ const topics: Record = {};
+
+ for (const thought of thoughts) {
+ types[thought.type] = (types[thought.type] ?? 0) + 1;
+
+ const thoughtTopics = thought.metadata.topics;
+ if (Array.isArray(thoughtTopics)) {
+ for (const topic of thoughtTopics) {
+ if (typeof topic === "string" && topic.trim()) {
+ topics[topic] = (topics[topic] ?? 0) + 1;
+ }
+ }
+ }
+ }
+
+ return {
+ total_thoughts: thoughts.length,
+ window_days: "all",
+ types,
+ top_topics: Object.entries(topics)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .map(([topic, count]) => ({ topic, count })),
+ };
+}
+
+function parseMcpStatsReport(report: string): StatsResponse {
+ const lines = report.split(/\r?\n/);
+ const stats: StatsResponse = {
+ total_thoughts: 0,
+ window_days: "all",
+ types: {},
+ top_topics: [],
+ };
+
+ let section: "types" | "topics" | "people" | null = null;
+ for (const rawLine of lines) {
+ const line = rawLine.trim();
+ if (!line) continue;
+
+ const totalMatch = line.match(/^Total thoughts:\s*(\d+)/i);
+ if (totalMatch) {
+ stats.total_thoughts = Number(totalMatch[1]);
+ continue;
+ }
+
+ if (/^Types:/i.test(line)) {
+ section = "types";
+ continue;
+ }
+ if (/^Top topics:/i.test(line)) {
+ section = "topics";
+ continue;
+ }
+ if (/^People mentioned:/i.test(line)) {
+ section = "people";
+ continue;
+ }
+
+ const entryMatch = line.match(/^(.+):\s*(\d+)$/);
+ if (!entryMatch) continue;
+
+ const label = entryMatch[1].trim();
+ const count = Number(entryMatch[2]);
+ if (section === "types") {
+ stats.types[label] = count;
+ }
+ if (section === "topics") {
+ stats.top_topics.push({ topic: label, count });
+ }
+ }
+
+ return stats;
+}
+
+async function fetchLegacyRecentThoughts(apiKey: string): Promise {
+ if (!LEGACY_MCP_SERVER_URL) {
+ throw new ApiError("Legacy MCP browse endpoint is not configured", 500);
+ }
+
+ const url = new URL(LEGACY_MCP_SERVER_URL);
+ url.searchParams.set("key", apiKey);
+
+ const res = await fetch(url.toString(), {
+ headers: {
+ Accept: "application/json",
+ },
+ cache: "no-store",
+ });
+
+ if (!res.ok) {
+ const text = await res.text().catch(() => "");
+ throw new ApiError(`API ${res.status}: ${text || res.statusText}`, res.status);
+ }
+
+ const data = (await res.json()) as LegacyThoughtRecord[];
+ return data.map(toThought);
+}
+
async function apiFetch(
apiKey: string,
path: string,
@@ -54,6 +203,33 @@ export async function fetchThoughts(
exclude_restricted?: boolean;
}
): Promise {
+ if (MCP_URL) {
+ const recent = await fetchLegacyRecentThoughts(apiKey);
+ const filtered = recent.filter((thought) => {
+ if (params?.type && thought.type !== params.type) return false;
+ if (params?.source_type && thought.source_type !== params.source_type) return false;
+ if (
+ params?.importance_min !== undefined &&
+ thought.importance < params.importance_min
+ ) {
+ return false;
+ }
+ return true;
+ });
+
+ const page = params?.page ?? 1;
+ const perPage = params?.per_page ?? 25;
+ const start = (page - 1) * perPage;
+ const data = filtered.slice(start, start + perPage);
+
+ return {
+ data,
+ total: filtered.length,
+ page,
+ per_page: perPage,
+ };
+ }
+
const sp = new URLSearchParams();
if (params?.page) sp.set("page", String(params.page));
if (params?.per_page) sp.set("per_page", String(params.per_page));
@@ -73,9 +249,21 @@ export async function fetchThoughts(
export async function fetchThought(
apiKey: string,
- id: number,
+ id: string | number,
excludeRestricted: boolean = true
): Promise {
+ if (MCP_URL) {
+ const thought = await mcpFetchThought(apiKey, String(id));
+ return toThought({
+ id: thought.id,
+ content: thought.text,
+ metadata: thought.metadata,
+ created_at: thought.metadata.created_at ?? new Date().toISOString(),
+ updated_at:
+ thought.metadata.updated_at ?? thought.metadata.created_at ?? new Date().toISOString(),
+ });
+ }
+
const qs = excludeRestricted ? "" : "?exclude_restricted=false";
return apiFetch(apiKey, `/thought/${id}${qs}`);
}
@@ -102,6 +290,20 @@ export async function fetchKanbanThoughts(
exclude_restricted?: boolean;
}
): Promise {
+ if (MCP_URL) {
+ const allowedStatuses = new Set(
+ params?.status?.split(",").map((status) => status.trim()).filter(Boolean)
+ );
+ const recent = await fetchLegacyRecentThoughts(apiKey);
+ return recent
+ .filter((thought) => KANBAN_TYPES.includes(thought.type))
+ .filter((thought) => {
+ if (allowedStatuses.size === 0) return true;
+ return allowedStatuses.has(thought.status ?? "new");
+ })
+ .sort((a, b) => b.importance - a.importance);
+ }
+
// Fetch tasks and ideas separately (API only supports single type filter)
const results: Thought[] = [];
for (const thoughtType of ["task", "idea"]) {
@@ -178,6 +380,14 @@ export async function fetchStats(
days?: number,
excludeRestricted: boolean = true
): Promise {
+ if (MCP_URL) {
+ try {
+ return parseMcpStatsReport(await mcpThoughtStats(apiKey));
+ } catch {
+ return summarizeThoughts(await fetchLegacyRecentThoughts(apiKey));
+ }
+ }
+
const sp = new URLSearchParams();
if (days) sp.set("days", String(days));
if (!excludeRestricted) sp.set("exclude_restricted", "false");
diff --git a/dashboards/open-brain-dashboard-next/lib/auth.ts b/dashboards/open-brain-dashboard-next/lib/auth.ts
index 84ef7d60..16253c70 100644
--- a/dashboards/open-brain-dashboard-next/lib/auth.ts
+++ b/dashboards/open-brain-dashboard-next/lib/auth.ts
@@ -3,7 +3,8 @@ import { cookies } from "next/headers";
import { redirect } from "next/navigation";
export interface SessionData {
- apiKey?: string;
+ userId?: string;
+ email?: string;
loggedIn?: boolean;
restrictedUnlocked?: boolean;
}
@@ -45,11 +46,17 @@ export async function getSession() {
* Call BEFORE parsing request body so unauthed requests get 401, not 400.
*/
export async function requireSession(): Promise<{ apiKey: string }> {
+ const apiKey =
+ process.env.OPEN_BRAIN_MCP_ACCESS_KEY || process.env.MCP_ACCESS_KEY;
+ if (!apiKey) {
+ throw new Error("OPEN_BRAIN_MCP_ACCESS_KEY env var is required");
+ }
+
const session = await getSession();
- if (!session.loggedIn || !session.apiKey) {
+ if (!session.loggedIn || !session.userId) {
throw new AuthError();
}
- return { apiKey: session.apiKey };
+ return { apiKey };
}
/**
@@ -58,9 +65,15 @@ export async function requireSession(): Promise<{ apiKey: string }> {
export async function requireSessionOrRedirect(): Promise<{
apiKey: string;
}> {
+ const apiKey =
+ process.env.OPEN_BRAIN_MCP_ACCESS_KEY || process.env.MCP_ACCESS_KEY;
+ if (!apiKey) {
+ throw new Error("OPEN_BRAIN_MCP_ACCESS_KEY env var is required");
+ }
+
const session = await getSession();
- if (!session.loggedIn || !session.apiKey) {
+ if (!session.loggedIn || !session.userId) {
redirect("/login");
}
- return { apiKey: session.apiKey };
+ return { apiKey };
}
diff --git a/dashboards/open-brain-dashboard-next/lib/openBrainMcp.ts b/dashboards/open-brain-dashboard-next/lib/openBrainMcp.ts
new file mode 100644
index 00000000..b3540ed7
--- /dev/null
+++ b/dashboards/open-brain-dashboard-next/lib/openBrainMcp.ts
@@ -0,0 +1,183 @@
+import "server-only";
+
+type JsonRpcSuccess = {
+ jsonrpc: "2.0";
+ id?: string | number | null;
+ result: T;
+};
+
+type JsonRpcError = {
+ jsonrpc: "2.0";
+ id?: string | number | null;
+ error: {
+ code: number;
+ message: string;
+ data?: unknown;
+ };
+};
+
+type JsonRpcEnvelope = JsonRpcSuccess | JsonRpcError;
+
+type ToolCallResult = {
+ content?: Array<{
+ type: string;
+ text?: string;
+ }>;
+ isError?: boolean;
+};
+
+type SearchHit = {
+ id: string;
+ title: string;
+ url?: string;
+};
+
+export type McpThoughtDocument = {
+ id: string;
+ title: string;
+ text: string;
+ url?: string;
+ metadata: Record & {
+ created_at?: string;
+ updated_at?: string | null;
+ };
+};
+
+const MCP_URL = process.env.OPEN_BRAIN_MCP_URL;
+
+function requireMcpUrl() {
+ if (!MCP_URL) {
+ throw new Error("OPEN_BRAIN_MCP_URL is not configured");
+ }
+ return MCP_URL;
+}
+
+function buildHeaders(apiKey: string): HeadersInit {
+ return {
+ Accept: "application/json, text/event-stream",
+ "Content-Type": "application/json",
+ "x-brain-key": apiKey,
+ };
+}
+
+function parseEventStreamPayload(body: string): JsonRpcEnvelope {
+ const dataLines = body
+ .split(/\r?\n/)
+ .filter((line) => line.startsWith("data:"))
+ .map((line) => line.slice(5).trim())
+ .filter(Boolean);
+
+ if (dataLines.length === 0) {
+ throw new Error("MCP event stream returned no data payload");
+ }
+
+ return JSON.parse(dataLines.join("\n")) as JsonRpcEnvelope;
+}
+
+async function parseJsonRpc(response: Response): Promise {
+ const contentType = response.headers.get("content-type") ?? "";
+ const raw = await response.text();
+
+ const payload = contentType.includes("text/event-stream")
+ ? parseEventStreamPayload(raw)
+ : (JSON.parse(raw) as JsonRpcEnvelope);
+
+ if ("error" in payload) {
+ throw new Error(payload.error.message);
+ }
+
+ return payload.result;
+}
+
+async function mcpPost({
+ apiKey,
+ body,
+}: {
+ apiKey: string;
+ body: Record;
+}): Promise {
+ const response = await fetch(requireMcpUrl(), {
+ method: "POST",
+ headers: buildHeaders(apiKey),
+ body: JSON.stringify(body),
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ const text = await response.text().catch(() => "");
+ throw new Error(text || `MCP request failed (${response.status})`);
+ }
+
+ return parseJsonRpc(response);
+}
+
+function extractToolText(result: ToolCallResult): string {
+ const firstText = result.content?.find((item) => item.type === "text")?.text;
+ if (!firstText) {
+ throw new Error("MCP tool returned no text payload");
+ }
+ if (result.isError) {
+ throw new Error(firstText);
+ }
+ return firstText;
+}
+
+async function callToolText(
+ apiKey: string,
+ name: string,
+ args: Record
+): Promise {
+ const result = await mcpPost({
+ apiKey,
+ body: {
+ jsonrpc: "2.0",
+ id: crypto.randomUUID(),
+ method: "tools/call",
+ params: {
+ name,
+ arguments: args,
+ },
+ },
+ });
+
+ return extractToolText(result);
+}
+
+async function callTool(
+ apiKey: string,
+ name: string,
+ args: Record
+): Promise {
+ return JSON.parse(await callToolText(apiKey, name, args)) as T;
+}
+
+export async function validateMcpKey(apiKey: string): Promise {
+ await callTool<{ results: SearchHit[] }>(apiKey, "search", {
+ query: "test",
+ });
+}
+
+export async function mcpSearchThoughts(
+ apiKey: string,
+ query: string
+): Promise {
+ const search = await callTool<{ results: SearchHit[] }>(apiKey, "search", {
+ query,
+ });
+
+ const hits = search.results ?? [];
+ return Promise.all(
+ hits.slice(0, 10).map((hit) => callTool(apiKey, "fetch", { id: hit.id }))
+ );
+}
+
+export async function mcpFetchThought(
+ apiKey: string,
+ id: string
+): Promise {
+ return callTool(apiKey, "fetch", { id });
+}
+
+export async function mcpThoughtStats(apiKey: string): Promise {
+ return callToolText(apiKey, "thought_stats", {});
+}
diff --git a/dashboards/open-brain-dashboard-next/lib/supabaseAuth.ts b/dashboards/open-brain-dashboard-next/lib/supabaseAuth.ts
new file mode 100644
index 00000000..bb648fd9
--- /dev/null
+++ b/dashboards/open-brain-dashboard-next/lib/supabaseAuth.ts
@@ -0,0 +1,157 @@
+import "server-only";
+
+import {
+ createClient,
+ type EmailOtpType,
+ type Provider,
+ type User,
+} from "@supabase/supabase-js";
+import { createSupabaseServerAuthClient } from "@/lib/supabaseServerAuth";
+
+function requireEnv(name: string): string {
+ const value = process.env[name];
+ if (!value) {
+ throw new Error(`${name} env var is required`);
+ }
+ return value;
+}
+
+function normalizeBaseUrl(value: string) {
+ const trimmed = value.trim().replace(/\/$/, "");
+ if (/^https?:\/\//i.test(trimmed)) {
+ return trimmed;
+ }
+ if (
+ trimmed.startsWith("localhost") ||
+ trimmed.startsWith("127.0.0.1") ||
+ trimmed.startsWith("[::1]")
+ ) {
+ return `http://${trimmed}`;
+ }
+ return `https://${trimmed}`;
+}
+
+function getBaseUrl() {
+ return normalizeBaseUrl(
+ process.env.NEXT_PUBLIC_APP_URL ||
+ process.env.VERCEL_PROJECT_PRODUCTION_URL ||
+ process.env.VERCEL_URL ||
+ "http://127.0.0.1:3001"
+ );
+}
+
+function createSupabaseAuthClient() {
+ return createClient(
+ requireEnv("NEXT_PUBLIC_SUPABASE_URL"),
+ requireEnv("NEXT_PUBLIC_SUPABASE_ANON_KEY"),
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false,
+ },
+ }
+ );
+}
+
+export function getMagicLinkRedirectUrl(next = "/") {
+ return `${getBaseUrl()}/auth/callback?next=${encodeURIComponent(next)}`;
+}
+
+async function getOAuthSignInUrl(provider: Provider, next = "/") {
+ const supabase = await createSupabaseServerAuthClient();
+ const { data, error } = await supabase.auth.signInWithOAuth({
+ provider,
+ options: {
+ redirectTo: getMagicLinkRedirectUrl(next),
+ },
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ if (!data.url) {
+ throw new Error(`No ${provider} sign-in URL returned from Supabase Auth`);
+ }
+
+ return data.url;
+}
+
+export async function getGithubSignInUrl(next = "/") {
+ return getOAuthSignInUrl("github", next);
+}
+
+export async function getGoogleSignInUrl(next = "/") {
+ return getOAuthSignInUrl("google", next);
+}
+
+export async function signInWithPassword(email: string, password: string) {
+ const supabase = createSupabaseAuthClient();
+ const { data, error } = await supabase.auth.signInWithPassword({
+ email,
+ password,
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ if (!data.user) {
+ throw new Error("No user returned from Supabase Auth");
+ }
+
+ return data.user;
+}
+
+export async function signUpWithPassword(email: string, password: string) {
+ const supabase = createSupabaseAuthClient();
+ const { data, error } = await supabase.auth.signUp({
+ email,
+ password,
+ options: {
+ emailRedirectTo: getMagicLinkRedirectUrl("/"),
+ },
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return data;
+}
+
+export async function verifyMagicLink(
+ tokenHash: string,
+ type: EmailOtpType
+): Promise {
+ const supabase = createSupabaseAuthClient();
+ const { data, error } = await supabase.auth.verifyOtp({
+ token_hash: tokenHash,
+ type,
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ if (!data.user) {
+ throw new Error("No user returned from Supabase Auth");
+ }
+
+ return data.user;
+}
+
+export async function exchangeAuthCodeForUser(code: string): Promise {
+ const supabase = await createSupabaseServerAuthClient();
+ const { data, error } = await supabase.auth.exchangeCodeForSession(code);
+
+ if (error) {
+ throw error;
+ }
+
+ if (!data.user) {
+ throw new Error("No user returned from Supabase Auth");
+ }
+
+ return data.user;
+}
diff --git a/dashboards/open-brain-dashboard-next/lib/supabaseServerAuth.ts b/dashboards/open-brain-dashboard-next/lib/supabaseServerAuth.ts
new file mode 100644
index 00000000..a085b031
--- /dev/null
+++ b/dashboards/open-brain-dashboard-next/lib/supabaseServerAuth.ts
@@ -0,0 +1,33 @@
+import "server-only";
+
+import { createServerClient } from "@supabase/ssr";
+import { cookies } from "next/headers";
+
+function requireEnv(name: string): string {
+ const value = process.env[name];
+ if (!value) {
+ throw new Error(`${name} env var is required`);
+ }
+ return value;
+}
+
+export async function createSupabaseServerAuthClient() {
+ const cookieStore = await cookies();
+
+ return createServerClient(
+ requireEnv("NEXT_PUBLIC_SUPABASE_URL"),
+ requireEnv("NEXT_PUBLIC_SUPABASE_ANON_KEY"),
+ {
+ cookies: {
+ getAll() {
+ return cookieStore.getAll();
+ },
+ setAll(cookiesToSet) {
+ for (const { name, value, options } of cookiesToSet) {
+ cookieStore.set(name, value, options);
+ }
+ },
+ },
+ }
+ );
+}
diff --git a/dashboards/open-brain-dashboard-next/lib/types.ts b/dashboards/open-brain-dashboard-next/lib/types.ts
index f58a1cbe..660c462d 100644
--- a/dashboards/open-brain-dashboard-next/lib/types.ts
+++ b/dashboards/open-brain-dashboard-next/lib/types.ts
@@ -1,5 +1,5 @@
export interface Thought {
- id: number;
+ id: string | number;
uuid?: string;
content: string;
type: string;
@@ -170,3 +170,36 @@ export interface AddToBrainResult {
extracted_count?: number | null;
message: string;
}
+
+export interface GraphNode {
+ id: string;
+ type: string;
+ importance: number;
+ content: string;
+ preview: string;
+ created_at: string;
+ metadata: {
+ topics?: string[];
+ people?: string[];
+ };
+ ring: 0 | 1 | 2;
+ similarity?: number;
+ overlap_count?: number;
+}
+
+export interface GraphEdge {
+ source: string;
+ target: string;
+ similarity?: number;
+ overlap_count?: number;
+ shared_topics?: string[];
+ shared_people?: string[];
+}
+
+export interface GraphResponse {
+ centerId: string;
+ depth: 1 | 2;
+ truncated: boolean;
+ nodes: GraphNode[];
+ edges: GraphEdge[];
+}
diff --git a/dashboards/open-brain-dashboard-next/middleware.ts b/dashboards/open-brain-dashboard-next/middleware.ts
index 6ad1714b..c8b3e6dd 100644
--- a/dashboards/open-brain-dashboard-next/middleware.ts
+++ b/dashboards/open-brain-dashboard-next/middleware.ts
@@ -6,6 +6,7 @@ export function middleware(request: NextRequest) {
// Allow login page, API routes, and static assets
if (
pathname === "/login" ||
+ pathname.startsWith("/auth") ||
pathname.startsWith("/api") ||
pathname.startsWith("/_next") ||
pathname.startsWith("/favicon")
diff --git a/dashboards/open-brain-dashboard-next/package-lock.json b/dashboards/open-brain-dashboard-next/package-lock.json
index 2608fa68..a73e1fb0 100644
--- a/dashboards/open-brain-dashboard-next/package-lock.json
+++ b/dashboards/open-brain-dashboard-next/package-lock.json
@@ -11,6 +11,8 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@supabase/ssr": "^0.10.3",
+ "@supabase/supabase-js": "^2.105.4",
"iron-session": "^8.0.4",
"next": "16.2.1",
"react": "19.2.4",
@@ -1291,6 +1293,115 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@supabase/auth-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.105.4.tgz",
+ "integrity": "sha512-Ejfa37M5xoIwoxVebxRahnwubPo8g22qkXQ4p50+N9MIvU9UZoN+A8dwVPtczzGf8oV/YXN80ZPxK4aWXuSN/A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/functions-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.105.4.tgz",
+ "integrity": "sha512-JVNKbBft3Qkja+WlGaE026AJ2AH9K0UTsxsfvEIHgd4zFrBor4BYRCrYFrv9IDsvVqkF72wKDsODJl5GY/C4tA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/phoenix": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.2.tgz",
+ "integrity": "sha512-YSAGnmDAfuleFCVt3CeurQZAhxRfXWeZIIkwp7NhYzQ1UwW6ePSnzsFAiUm/mbCkfoCf70QQHKW/K6RKh52a4A==",
+ "license": "MIT"
+ },
+ "node_modules/@supabase/postgrest-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.105.4.tgz",
+ "integrity": "sha512-SppIyLo/kTwIlz1qpv2HN1EQqBg0GVktrDDFsXygYROha3MgVn4rT7p5EjFHFqXQm2rdRGb/BI7bc+jr10m91w==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/realtime-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.105.4.tgz",
+ "integrity": "sha512-6ov6c59+8D9h7q4M4Gy/uDJlC0Akxl9/714Y+6vJ+Sijuc16TS/p5DwhfRCLNcIhNiej1gEt+CQUwsjiPt4PxQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/phoenix": "^0.4.2",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/ssr": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.10.3.tgz",
+ "integrity": "sha512-ux2CJgX89h0Fz2lY7ZNafNG2SkXpyRc5dz77K9eKeBLPdtywQixKwIuetDeIViAJBp/buOUVmgj8PVesOklNpw==",
+ "license": "MIT",
+ "dependencies": {
+ "cookie": "^1.0.2"
+ },
+ "peerDependencies": {
+ "@supabase/supabase-js": "^2.105.3"
+ }
+ },
+ "node_modules/@supabase/ssr/node_modules/cookie": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
+ "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/@supabase/storage-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.105.4.tgz",
+ "integrity": "sha512-Jx+pzMP1Whjof2PWHoVBUA75/p7PQE9CqKBzn1oXVyJDOggMLSH2OzVWwsXYaxEpdC1K/KltwmOX44nL3LHl9g==",
+ "license": "MIT",
+ "dependencies": {
+ "iceberg-js": "^0.8.1",
+ "tslib": "2.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@supabase/supabase-js": {
+ "version": "2.105.4",
+ "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.105.4.tgz",
+ "integrity": "sha512-cEnx+k49knU+qdIP7rXwR6fqEXPHZs+74xFK1R0S8MgQ7v9tbePVdGxvO03n3bPympMdJWVLadARBfU4TgNHCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@supabase/auth-js": "2.105.4",
+ "@supabase/functions-js": "2.105.4",
+ "@supabase/postgrest-js": "2.105.4",
+ "@supabase/realtime-js": "2.105.4",
+ "@supabase/storage-js": "2.105.4"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@@ -3993,6 +4104,15 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/iceberg-js": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
+ "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
diff --git a/dashboards/open-brain-dashboard-next/package.json b/dashboards/open-brain-dashboard-next/package.json
index b369d301..eb7cac33 100644
--- a/dashboards/open-brain-dashboard-next/package.json
+++ b/dashboards/open-brain-dashboard-next/package.json
@@ -12,6 +12,8 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
+ "@supabase/ssr": "^0.10.3",
+ "@supabase/supabase-js": "^2.105.4",
"iron-session": "^8.0.4",
"next": "16.2.1",
"react": "19.2.4",