From 081833ed258e0084b9c89868dc03536d007587b8 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 27 Jun 2025 21:56:38 -0400 Subject: [PATCH 1/3] ENG-420-create-space-and-anonymous-account --- apps/roam/src/utils/supabaseContext.ts | 52 +++++++-- apps/website/app/api/supabase/space/route.ts | 110 +++++++++++++++--- .../20250627172412_upsert_space.sql | 17 +++ .../database/supabase/schemas/account.sql | 3 +- packages/database/supabase/schemas/space.sql | 18 +++ packages/database/types.gen.ts | 11 +- packages/ui/src/lib/supabase/client.ts | 20 ++-- packages/ui/src/lib/utils.ts | 3 + 8 files changed, 201 insertions(+), 33 deletions(-) create mode 100644 packages/database/supabase/migrations/20250627172412_upsert_space.sql diff --git a/apps/roam/src/utils/supabaseContext.ts b/apps/roam/src/utils/supabaseContext.ts index 31ab97c63..c45cdb9a4 100644 --- a/apps/roam/src/utils/supabaseContext.ts +++ b/apps/roam/src/utils/supabaseContext.ts @@ -4,14 +4,19 @@ import getCurrentUserDisplayName from "roamjs-components/queries/getCurrentUserD import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; import getRoamUrl from "roamjs-components/dom/getRoamUrl"; -import { Database } from "@repo/database/types.gen"; +import { Enums } from "@repo/database/types.gen"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getBlockProps from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; +import { + createClient, + type DGSupabaseClient, +} from "@repo/ui/lib/supabase/client"; +import { spaceAnonUserEmail } from "@repo/ui/lib/utils"; declare const crypto: { randomUUID: () => string }; -type Platform = Database["public"]["Enums"]["Platform"]; +type Platform = Enums<"Platform">; export type SupabaseContext = { platform: Platform; @@ -44,12 +49,15 @@ const getOrCreateSpacePassword = () => { return password; }; -// Note: Some of this will be more typesafe if rewritten with direct supabase access eventually. -// We're going through nextjs until we have settled security. +// Note: calls in this file will still use vercel endpoints. +// It is better if this is still at least protected by CORS. +// But calls anywhere else should use the supabase client directly. -const fetchOrCreateSpaceId = async (): Promise => { +const fetchOrCreateSpaceId = async ( + user_account_id: number, + anon_password: string, +): Promise => { const url = getRoamUrl(); - const urlParts = url.split("/"); const name = window.roamAlphaAPI.graph.name; const platform: Platform = "Roam"; const response = await fetch(base_url + "/space", { @@ -57,7 +65,11 @@ const fetchOrCreateSpaceId = async (): Promise => { headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ url, name, platform }), + body: JSON.stringify({ + space: { url, name, platform }, + password: anon_password, + account_id: user_account_id, + }), }); if (!response.ok) throw new Error( @@ -120,17 +132,22 @@ const fetchOrCreatePlatformAccount = async ({ export const getSupabaseContext = async (): Promise => { if (CONTEXT_CACHE === null) { try { - const spaceId = await fetchOrCreateSpaceId(); const accountLocalId = window.roamAlphaAPI.user.uid(); + const spacePassword = getOrCreateSpacePassword(); const personEmail = getCurrentUserEmail(); const personName = getCurrentUserDisplayName(); - const spacePassword = getOrCreateSpacePassword(); const userId = await fetchOrCreatePlatformAccount({ accountLocalId, personName, personEmail, }); - CONTEXT_CACHE = { platform: "Roam", spaceId, userId, spacePassword }; + const spaceId = await fetchOrCreateSpaceId(userId, spacePassword); + CONTEXT_CACHE = { + platform: "Roam", + spaceId, + userId, + spacePassword, + }; } catch (error) { console.error(error); return null; @@ -138,3 +155,18 @@ export const getSupabaseContext = async (): Promise => { } return CONTEXT_CACHE; }; + +let LOGGED_IN_CLIENT: DGSupabaseClient | null = null; + +export const getLoggedInClient = async (): Promise => { + if (LOGGED_IN_CLIENT === null) { + const context = await getSupabaseContext(); + if (context === null) throw new Error("Could not create context"); + LOGGED_IN_CLIENT = createClient(); + await LOGGED_IN_CLIENT.auth.signInWithPassword({ + email: spaceAnonUserEmail(context.platform, context.spaceId), + password: context.spacePassword, + }); + } + return LOGGED_IN_CLIENT; +}; diff --git a/apps/website/app/api/supabase/space/route.ts b/apps/website/app/api/supabase/space/route.ts index 87049ea1c..bf6ee1578 100644 --- a/apps/website/app/api/supabase/space/route.ts +++ b/apps/website/app/api/supabase/space/route.ts @@ -1,6 +1,10 @@ import { NextResponse, NextRequest } from "next/server"; -import type { PostgrestSingleResponse } from "@supabase/supabase-js"; - +import { + type PostgrestSingleResponse, + PostgrestError, + type User, + type SupabaseClient, +} from "@supabase/supabase-js"; import { createClient } from "~/utils/supabase/server"; import { getOrCreateEntity, ItemValidator } from "~/utils/supabase/dbUtils"; import { @@ -10,10 +14,17 @@ import { asPostgrestFailure, } from "~/utils/supabase/apiUtils"; import { Tables, TablesInsert } from "@repo/database/types.gen.ts"; +import { spaceAnonUserEmail } from "@repo/ui/lib/utils"; type SpaceDataInput = TablesInsert<"Space">; type SpaceRecord = Tables<"Space">; +type SpaceCreationInput = { + space: SpaceDataInput; + account_id: number; + password: string; +}; + const spaceValidator: ItemValidator = (space) => { if (!space || typeof space !== "object") return "Invalid request body: expected a JSON object."; @@ -30,27 +41,101 @@ const spaceValidator: ItemValidator = (space) => { const processAndGetOrCreateSpace = async ( supabasePromise: ReturnType, - data: SpaceDataInput, + data: SpaceCreationInput, ): Promise> => { - const { name, url, platform } = data; - const error = spaceValidator(data); - if (error !== null) return asPostgrestFailure(error, "invalid"); + const { space, account_id, password } = data; + const { name, url, platform } = space; + const error = spaceValidator(space); + if (error !== null) return asPostgrestFailure(error, "invalid space"); + if (!account_id || Number.isNaN(parseInt(account_id as any, 10))) + return asPostgrestFailure( + "account_id is not a number", + "invalid account_id", + ); - const normalizedUrl = url.trim().replace(/\/$/, ""); - const trimmedName = name.trim(); const supabase = await supabasePromise; const result = await getOrCreateEntity<"Space">({ supabase, tableName: "Space", insertData: { - name: trimmedName, - url: normalizedUrl, - platform: platform, + name: name.trim(), + url: url.trim().replace(/\/$/, ""), + platform, }, uniqueOn: ["url"], }); + if (result.error) return result; + const space_id = result.data.id; + + // this is related but each step is idempotent, so con retry w/o transaction + const email = spaceAnonUserEmail(platform, result.data.id); + let anonymousUser: User | null = null; + try { + const login = await supabase.auth.signInWithPassword({ email, password }); + anonymousUser = login.data.user; + } catch (error) { + // no user + } + if (anonymousUser === null) { + const resultCreateAnonymousUser = await supabase.auth.admin.createUser({ + email, + password, + email_confirm: true, + }); + if (resultCreateAnonymousUser.error) { + return { + count: null, + status: resultCreateAnonymousUser.error.status || -1, + statusText: resultCreateAnonymousUser.error.message, + data: null, + error: new PostgrestError({ + message: resultCreateAnonymousUser.error.message, + details: + typeof resultCreateAnonymousUser.error.cause === "string" + ? resultCreateAnonymousUser.error.cause + : "", + hint: "", + code: resultCreateAnonymousUser.error.code || "unknown", + }), + }; // space created but not its user, try again + } + anonymousUser = resultCreateAnonymousUser.data.user; + } + + const anonPlatformUserResult = await getOrCreateEntity<"PlatformAccount">({ + supabase, + tableName: "PlatformAccount", + insertData: { + platform, + account_local_id: email, + name: `Anonymous of space ${space_id}`, + agent_type: "anonymous", + dg_account: anonymousUser.id, + }, + uniqueOn: ["account_local_id", "platform"], + }); + if (anonPlatformUserResult.error) return anonPlatformUserResult; + const resultAnonUserSpaceAccess = await getOrCreateEntity<"SpaceAccess">({ + supabase, + tableName: "SpaceAccess", + insertData: { + space_id, + account_id: anonPlatformUserResult.data.id, + editor: true, + }, + uniqueOn: ["space_id", "account_id"], + }); + if (resultAnonUserSpaceAccess.error) return resultAnonUserSpaceAccess; // space created but not connected, try again + + const resultUserSpaceAccess = await getOrCreateEntity<"SpaceAccess">({ + supabase, + tableName: "SpaceAccess", + insertData: { space_id, account_id, editor: true }, + uniqueOn: ["space_id", "account_id"], + }); + if (resultUserSpaceAccess.error) return resultUserSpaceAccess; // space created but not connected, try again return result; }; @@ -58,8 +143,7 @@ export const POST = async (request: NextRequest): Promise => { const supabasePromise = createClient(); try { - const body: SpaceDataInput = await request.json(); - + const body: SpaceCreationInput = await request.json(); const result = await processAndGetOrCreateSpace(supabasePromise, body); return createApiResponse(request, result); } catch (e: unknown) { diff --git a/packages/database/supabase/migrations/20250627172412_upsert_space.sql b/packages/database/supabase/migrations/20250627172412_upsert_space.sql new file mode 100644 index 000000000..010f0d4d7 --- /dev/null +++ b/packages/database/supabase/migrations/20250627172412_upsert_space.sql @@ -0,0 +1,17 @@ +ALTER TYPE public."AgentType" ADD VALUE IF NOT EXISTS 'anonymous'; + +CREATE OR REPLACE FUNCTION public.get_space_anonymous_email(platform public."Platform", space_id BIGINT) RETURNS character varying LANGUAGE sql IMMUTABLE AS $$ + SELECT concat(lower(platform::text), '-', space_id, '-anon@database.discoursegraphs.com') +$$; + +CREATE OR REPLACE FUNCTION public.after_delete_space() RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + DELETE FROM auth.users WHERE email = public.get_space_anonymous_email(OLD.platform, OLD.id); + DELETE FROM public."PlatformAccount" + WHERE platform = OLD.platform + AND account_local_id = public.get_space_anonymous_email(OLD.platform, OLD.id); + RETURN NEW; +END; +$$; + +CREATE TRIGGER on_delete_space_trigger AFTER DELETE ON public."Space" FOR EACH ROW EXECUTE FUNCTION public.after_delete_space(); diff --git a/packages/database/supabase/schemas/account.sql b/packages/database/supabase/schemas/account.sql index 76c3df82b..844a00199 100644 --- a/packages/database/supabase/schemas/account.sql +++ b/packages/database/supabase/schemas/account.sql @@ -1,7 +1,8 @@ CREATE TYPE public."AgentType" AS ENUM ( 'person', 'organization', - 'automated_agent' + 'automated_agent', + 'anonymous' ); ALTER TYPE public."AgentType" OWNER TO postgres; diff --git a/packages/database/supabase/schemas/space.sql b/packages/database/supabase/schemas/space.sql index 2f37fa471..b3dd4df0a 100644 --- a/packages/database/supabase/schemas/space.sql +++ b/packages/database/supabase/schemas/space.sql @@ -27,3 +27,21 @@ ALTER TABLE public."Space" OWNER TO "postgres"; GRANT ALL ON TABLE public."Space" TO anon; GRANT ALL ON TABLE public."Space" TO authenticated; GRANT ALL ON TABLE public."Space" TO service_role; + +CREATE OR REPLACE FUNCTION public.get_space_anonymous_email(platform public."Platform", space_id BIGINT) RETURNS character varying LANGUAGE sql IMMUTABLE AS $$ + SELECT concat(platform, '-', space_id, '-anon@database.discoursegraphs.com') +$$; + + +-- TODO: on delete trigger anonymous user +CREATE OR REPLACE FUNCTION public.after_delete_space() RETURNS TRIGGER LANGUAGE plpgsql AS $$ +BEGIN + DELETE FROM auth.users WHERE email = public.get_space_anonymous_email(OLD.platform, OLD.id); + DELETE FROM public."PlatformAccount" + WHERE platform = OLD.platform + AND account_local_id = public.get_space_anonymous_email(OLD.platform, OLD.id); + RETURN NEW; +END; +$$; + +CREATE TRIGGER on_delete_space_trigger AFTER DELETE ON public."Space" FOR EACH ROW EXECUTE FUNCTION public.after_delete_space(); diff --git a/packages/database/types.gen.ts b/packages/database/types.gen.ts index 818de2cde..2f9d45394 100644 --- a/packages/database/types.gen.ts +++ b/packages/database/types.gen.ts @@ -612,6 +612,13 @@ export type Database = { uid_to_sync: string }[] } + get_space_anonymous_email: { + Args: { + platform: Database["public"]["Enums"]["Platform"] + space_id: number + } + Returns: string + } match_content_embeddings: { Args: { query_embedding: string @@ -696,7 +703,7 @@ export type Database = { } Enums: { AgentIdentifierType: "email" | "orcid" - AgentType: "person" | "organization" | "automated_agent" + AgentType: "person" | "organization" | "automated_agent" | "anonymous" EmbeddingName: | "openai_text_embedding_ada2_1536" | "openai_text_embedding_3_small_512" @@ -922,7 +929,7 @@ export const Constants = { public: { Enums: { AgentIdentifierType: ["email", "orcid"], - AgentType: ["person", "organization", "automated_agent"], + AgentType: ["person", "organization", "automated_agent", "anonymous"], EmbeddingName: [ "openai_text_embedding_ada2_1536", "openai_text_embedding_3_small_512", diff --git a/packages/ui/src/lib/supabase/client.ts b/packages/ui/src/lib/supabase/client.ts index a07491c13..521ed998e 100644 --- a/packages/ui/src/lib/supabase/client.ts +++ b/packages/ui/src/lib/supabase/client.ts @@ -1,14 +1,20 @@ -import { createClient as createSupabaseClient } from "@supabase/supabase-js"; +import { + type SupabaseClient, + createClient as createSupabaseClient, +} from "@supabase/supabase-js"; import { Database } from "@repo/database/types.gen.ts"; -declare const SUPABASE_URL: string; -declare const SUPABASE_ANON_KEY: string; - // Inspired by https://supabase.com/ui/docs/react/password-based-auth -export const createClient = () => { +export type DGSupabaseClient = SupabaseClient< + Database, + "public", + Database["public"] +>; + +export const createClient = (): DGSupabaseClient => { return createSupabaseClient( - SUPABASE_URL, - SUPABASE_ANON_KEY, + process.env.SUPABASE_URL!, + process.env.SUPABASE_ANON_KEY!, ); }; diff --git a/packages/ui/src/lib/utils.ts b/packages/ui/src/lib/utils.ts index 365058ceb..670e448cf 100644 --- a/packages/ui/src/lib/utils.ts +++ b/packages/ui/src/lib/utils.ts @@ -4,3 +4,6 @@ import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } + +export const spaceAnonUserEmail = (platform: string, space_id: number) => + `${platform.toLowerCase()}-${space_id}-anon@database.discoursegraphs.com`; From 3acac2d74715a38d30e56d422d5e33824b98fff1 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 29 Jun 2025 10:34:40 -0400 Subject: [PATCH 2/3] coderabbit suggestions --- apps/roam/src/utils/supabaseContext.ts | 15 +++++++++-- apps/website/app/api/supabase/space/route.ts | 26 +++++++++++++++----- packages/database/supabase/schemas/space.sql | 2 +- packages/ui/src/lib/supabase/client.ts | 12 ++++++--- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/apps/roam/src/utils/supabaseContext.ts b/apps/roam/src/utils/supabaseContext.ts index c45cdb9a4..12e57ef87 100644 --- a/apps/roam/src/utils/supabaseContext.ts +++ b/apps/roam/src/utils/supabaseContext.ts @@ -11,7 +11,7 @@ import setBlockProps from "~/utils/setBlockProps"; import { createClient, type DGSupabaseClient, -} from "@repo/ui/lib/supabase/client"; +} from "@repo/ui/src/lib/supabase/client"; import { spaceAnonUserEmail } from "@repo/ui/lib/utils"; declare const crypto: { randomUUID: () => string }; @@ -163,10 +163,21 @@ export const getLoggedInClient = async (): Promise => { const context = await getSupabaseContext(); if (context === null) throw new Error("Could not create context"); LOGGED_IN_CLIENT = createClient(); - await LOGGED_IN_CLIENT.auth.signInWithPassword({ + const { error } = await LOGGED_IN_CLIENT.auth.signInWithPassword({ email: spaceAnonUserEmail(context.platform, context.spaceId), password: context.spacePassword, }); + if (error) { + LOGGED_IN_CLIENT = null; + throw new Error(`Authentication failed: ${error.message}`); + } + } else { + // renew session + const { error } = await LOGGED_IN_CLIENT.auth.getSession(); + if (error) { + LOGGED_IN_CLIENT = null; + throw new Error(`Authentication expired: ${error.message}`); + } } return LOGGED_IN_CLIENT; }; diff --git a/apps/website/app/api/supabase/space/route.ts b/apps/website/app/api/supabase/space/route.ts index bf6ee1578..1dad957c5 100644 --- a/apps/website/app/api/supabase/space/route.ts +++ b/apps/website/app/api/supabase/space/route.ts @@ -47,11 +47,20 @@ const processAndGetOrCreateSpace = async ( const { name, url, platform } = space; const error = spaceValidator(space); if (error !== null) return asPostgrestFailure(error, "invalid space"); - if (!account_id || Number.isNaN(parseInt(account_id as any, 10))) + if ( + typeof account_id !== "number" || + !Number.isInteger(account_id) || + account_id <= 0 + ) return asPostgrestFailure( "account_id is not a number", "invalid account_id", ); + if (!password || typeof password !== "string" || password.length < 8) + return asPostgrestFailure( + "password must be at least 8 characters", + "invalid password", + ); const supabase = await supabasePromise; @@ -71,11 +80,16 @@ const processAndGetOrCreateSpace = async ( // this is related but each step is idempotent, so con retry w/o transaction const email = spaceAnonUserEmail(platform, result.data.id); let anonymousUser: User | null = null; - try { - const login = await supabase.auth.signInWithPassword({ email, password }); - anonymousUser = login.data.user; - } catch (error) { - // no user + { + const { error, data } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error && error.message !== "Invalid login credentials") { + // Handle unexpected errors + return asPostgrestFailure(error.message, "authentication_error"); + } + anonymousUser = data.user; } if (anonymousUser === null) { const resultCreateAnonymousUser = await supabase.auth.admin.createUser({ diff --git a/packages/database/supabase/schemas/space.sql b/packages/database/supabase/schemas/space.sql index b3dd4df0a..c448b28c0 100644 --- a/packages/database/supabase/schemas/space.sql +++ b/packages/database/supabase/schemas/space.sql @@ -29,7 +29,7 @@ GRANT ALL ON TABLE public."Space" TO authenticated; GRANT ALL ON TABLE public."Space" TO service_role; CREATE OR REPLACE FUNCTION public.get_space_anonymous_email(platform public."Platform", space_id BIGINT) RETURNS character varying LANGUAGE sql IMMUTABLE AS $$ - SELECT concat(platform, '-', space_id, '-anon@database.discoursegraphs.com') + SELECT concat(lower(platform), '-', space_id, '-anon@database.discoursegraphs.com') $$; diff --git a/packages/ui/src/lib/supabase/client.ts b/packages/ui/src/lib/supabase/client.ts index 521ed998e..faedae0b0 100644 --- a/packages/ui/src/lib/supabase/client.ts +++ b/packages/ui/src/lib/supabase/client.ts @@ -13,8 +13,12 @@ export type DGSupabaseClient = SupabaseClient< >; export const createClient = (): DGSupabaseClient => { - return createSupabaseClient( - process.env.SUPABASE_URL!, - process.env.SUPABASE_ANON_KEY!, - ); + const url = process.env.SUPABASE_URL; + const key = process.env.SUPABASE_ANON_KEY; + + if (!url || !key) { + throw new Error("Missing required Supabase environment variables"); + } + + return createSupabaseClient(url, key); }; From f5e5a29eb6a8b0a5d1f67234c33d07216458efe4 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sat, 5 Jul 2025 16:25:57 -0400 Subject: [PATCH 3/3] stylistic changes --- apps/roam/src/utils/supabaseContext.ts | 32 ++++++++++---------- apps/website/app/api/supabase/space/route.ts | 1 - 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/apps/roam/src/utils/supabaseContext.ts b/apps/roam/src/utils/supabaseContext.ts index 12e57ef87..91734d56f 100644 --- a/apps/roam/src/utils/supabaseContext.ts +++ b/apps/roam/src/utils/supabaseContext.ts @@ -25,7 +25,7 @@ export type SupabaseContext = { spacePassword: string; }; -let CONTEXT_CACHE: SupabaseContext | null = null; +let _contextCache: SupabaseContext | null = null; // TODO: This should be an util on its own. const base_url = @@ -54,8 +54,8 @@ const getOrCreateSpacePassword = () => { // But calls anywhere else should use the supabase client directly. const fetchOrCreateSpaceId = async ( - user_account_id: number, - anon_password: string, + account_id: number, + password: string, ): Promise => { const url = getRoamUrl(); const name = window.roamAlphaAPI.graph.name; @@ -67,8 +67,8 @@ const fetchOrCreateSpaceId = async ( }, body: JSON.stringify({ space: { url, name, platform }, - password: anon_password, - account_id: user_account_id, + password, + account_id, }), }); if (!response.ok) @@ -130,7 +130,7 @@ const fetchOrCreatePlatformAccount = async ({ }; export const getSupabaseContext = async (): Promise => { - if (CONTEXT_CACHE === null) { + if (_contextCache === null) { try { const accountLocalId = window.roamAlphaAPI.user.uid(); const spacePassword = getOrCreateSpacePassword(); @@ -142,7 +142,7 @@ export const getSupabaseContext = async (): Promise => { personEmail, }); const spaceId = await fetchOrCreateSpaceId(userId, spacePassword); - CONTEXT_CACHE = { + _contextCache = { platform: "Roam", spaceId, userId, @@ -153,31 +153,31 @@ export const getSupabaseContext = async (): Promise => { return null; } } - return CONTEXT_CACHE; + return _contextCache; }; -let LOGGED_IN_CLIENT: DGSupabaseClient | null = null; +let _loggedInClient: DGSupabaseClient | null = null; export const getLoggedInClient = async (): Promise => { - if (LOGGED_IN_CLIENT === null) { + if (_loggedInClient === null) { const context = await getSupabaseContext(); if (context === null) throw new Error("Could not create context"); - LOGGED_IN_CLIENT = createClient(); - const { error } = await LOGGED_IN_CLIENT.auth.signInWithPassword({ + _loggedInClient = createClient(); + const { error } = await _loggedInClient.auth.signInWithPassword({ email: spaceAnonUserEmail(context.platform, context.spaceId), password: context.spacePassword, }); if (error) { - LOGGED_IN_CLIENT = null; + _loggedInClient = null; throw new Error(`Authentication failed: ${error.message}`); } } else { // renew session - const { error } = await LOGGED_IN_CLIENT.auth.getSession(); + const { error } = await _loggedInClient.auth.getSession(); if (error) { - LOGGED_IN_CLIENT = null; + _loggedInClient = null; throw new Error(`Authentication expired: ${error.message}`); } } - return LOGGED_IN_CLIENT; + return _loggedInClient; }; diff --git a/apps/website/app/api/supabase/space/route.ts b/apps/website/app/api/supabase/space/route.ts index 1dad957c5..8585fd1ee 100644 --- a/apps/website/app/api/supabase/space/route.ts +++ b/apps/website/app/api/supabase/space/route.ts @@ -3,7 +3,6 @@ import { type PostgrestSingleResponse, PostgrestError, type User, - type SupabaseClient, } from "@supabase/supabase-js"; import { createClient } from "~/utils/supabase/server"; import { getOrCreateEntity, ItemValidator } from "~/utils/supabase/dbUtils";