Skip to content

Conversation

@sidneyswift
Copy link
Contributor

Summary

Adds a new API endpoint for creating workspaces with proper authentication and organization linking.

Changes

  • POST /api/workspaces - Create workspace with Bearer token or API key auth
  • Supports organization_id parameter to link workspace to an org
  • Validates organization access before linking
  • Creates account, account_info, and owner link in one transaction

Files Added

  • app/api/workspaces/route.ts - API route
  • lib/workspaces/createWorkspaceInDb.ts - Database creation logic
  • lib/workspaces/createWorkspacePostHandler.ts - Request handler
  • lib/workspaces/validateCreateWorkspaceBody.ts - Auth + validation
  • lib/supabase/account_workspace_ids/insertAccountWorkspaceId.ts - Owner link helper

- Create insertAccountWorkspaceId helper for owner-workspace linking
- Create createWorkspaceInDb with account, info, owner link, and org link
- Add validateCreateWorkspaceBody with API key and Bearer token auth
- Add organization access validation for workspace linking
- Expose POST /api/workspaces route with CORS support
@vercel
Copy link

vercel bot commented Jan 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
recoup-api Ready Ready Preview Jan 19, 2026 5:13pm

@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment on lines +43 to +45
if (organizationId) {
await addArtistToOrganization(account.id, organizationId);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Organization linking failures are silently ignored, causing the API to return success (201) even when the workspace fails to link to the requested organization.

View Details
📝 Patch Details
diff --git a/lib/artists/__tests__/createArtistInDb.test.ts b/lib/artists/__tests__/createArtistInDb.test.ts
index e979fbb..9a02b15 100644
--- a/lib/artists/__tests__/createArtistInDb.test.ts
+++ b/lib/artists/__tests__/createArtistInDb.test.ts
@@ -133,4 +133,17 @@ describe("createArtistInDb", () => {
 
     expect(result).toBeNull();
   });
+
+  it("returns null when linking artist to organization fails", async () => {
+    mockInsertAccount.mockResolvedValue(mockAccount);
+    mockInsertAccountInfo.mockResolvedValue(mockAccountInfo);
+    mockSelectAccountWithSocials.mockResolvedValue(mockFullAccount);
+    mockInsertAccountArtistId.mockResolvedValue({ id: "rel-123" });
+    mockAddArtistToOrganization.mockResolvedValue(null);
+
+    const result = await createArtistInDb("Test Artist", "owner-456", "org-789");
+
+    expect(mockAddArtistToOrganization).toHaveBeenCalledWith("artist-123", "org-789");
+    expect(result).toBeNull();
+  });
 });
diff --git a/lib/artists/createArtistInDb.ts b/lib/artists/createArtistInDb.ts
index e1eeceb..3990e08 100644
--- a/lib/artists/createArtistInDb.ts
+++ b/lib/artists/createArtistInDb.ts
@@ -44,7 +44,8 @@ export async function createArtistInDb(
 
     // Step 5: Link to organization if provided
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const organizationLinkId = await addArtistToOrganization(account.id, organizationId);
+      if (!organizationLinkId) return null;
     }
 
     return {
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..8a81851 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -41,7 +41,8 @@ export async function createWorkspaceInDb(
     if (!linkId) return null;
 
     if (organizationId) {
-      await addArtistToOrganization(account.id, organizationId);
+      const organizationLinkId = await addArtistToOrganization(account.id, organizationId);
+      if (!organizationLinkId) return null;
     }
 
     return {

Analysis

Organization linking failures silently ignored in workspace and artist creation

What fails: createWorkspaceInDb() and createArtistInDb() return the created workspace/artist object and allow the API to return 201 (success) even when addArtistToOrganization() fails to link the workspace/artist to the requested organization.

How to reproduce:

  1. Create a workspace with an organizationId parameter: POST /api/workspaces with body {"name":"Test","organization_id":"org-123"}
  2. The Supabase upsert in addArtistToOrganization() fails (database constraint violation, timeout, permission error, etc.)
  3. The function returns null to indicate failure
  4. The calling code ignores this return value and continues execution
  5. The handler receives the workspace object and returns 201 Created with the workspace data

Result: API returns HTTP 201 with the workspace object, indicating success. However, the workspace is NOT linked to the requested organization.

Expected: When organization linking is explicitly requested and fails, the entire workspace/artist creation should fail. The API should return a 500 error and the workspace should not be created (or should be rolled back). This is consistent with how other critical operations are handled:

  • If insertAccountInfo() returns null → function returns null → API returns 500
  • If insertAccountWorkspaceId() returns null → function returns null → API returns 500
  • If addArtistToOrganization() returns null → function should return null → API should return 500

The fix validates the return value from addArtistToOrganization() and propagates the failure, consistent with the error-handling pattern established in the handler code (addArtistToOrgHandler.ts checks the return value and returns 500 if it fails).

Files fixed:

  • lib/workspaces/createWorkspaceInDb.ts (lines 43-45)
  • lib/artists/createArtistInDb.ts (lines 43-47)
  • lib/artists/__tests__/createArtistInDb.test.ts (added test case for organization linking failure)

Comment on lines +31 to +42
try {
const account = await insertAccount({ name });

const accountInfo = await insertAccountInfo({ account_id: account.id });
if (!accountInfo) return null;

const workspace = await selectAccountWithSocials(account.id);
if (!workspace) return null;

const linkId = await insertAccountWorkspaceId(accountId, account.id);
if (!linkId) return null;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If insertAccountInfo fails, an orphaned account record is left in the database while the API returns a 500 error to the user.

View Details
📝 Patch Details
diff --git a/lib/supabase/accounts/deleteAccount.ts b/lib/supabase/accounts/deleteAccount.ts
new file mode 100644
index 0000000..28379eb
--- /dev/null
+++ b/lib/supabase/accounts/deleteAccount.ts
@@ -0,0 +1,18 @@
+import supabase from "../serverClient";
+
+/**
+ * Deletes an account by its ID
+ *
+ * @param accountId - The ID of the account to delete
+ * @returns true if the delete was successful, false otherwise
+ */
+export async function deleteAccount(accountId: string): Promise<boolean> {
+  const { error } = await supabase.from("accounts").delete().eq("id", accountId);
+
+  if (error) {
+    console.error("[ERROR] deleteAccount:", error);
+    return false;
+  }
+
+  return true;
+}
diff --git a/lib/workspaces/createWorkspaceInDb.ts b/lib/workspaces/createWorkspaceInDb.ts
index d7684c5..21b98e9 100644
--- a/lib/workspaces/createWorkspaceInDb.ts
+++ b/lib/workspaces/createWorkspaceInDb.ts
@@ -1,4 +1,5 @@
 import { insertAccount } from "@/lib/supabase/accounts/insertAccount";
+import { deleteAccount } from "@/lib/supabase/accounts/deleteAccount";
 import { insertAccountInfo } from "@/lib/supabase/account_info/insertAccountInfo";
 import {
   selectAccountWithSocials,
@@ -32,13 +33,25 @@ export async function createWorkspaceInDb(
     const account = await insertAccount({ name });
 
     const accountInfo = await insertAccountInfo({ account_id: account.id });
-    if (!accountInfo) return null;
+    if (!accountInfo) {
+      // Clean up the orphaned account record if account_info creation fails
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const workspace = await selectAccountWithSocials(account.id);
-    if (!workspace) return null;
+    if (!workspace) {
+      // Clean up if we can't retrieve the workspace
+      await deleteAccount(account.id);
+      return null;
+    }
 
     const linkId = await insertAccountWorkspaceId(accountId, account.id);
-    if (!linkId) return null;
+    if (!linkId) {
+      // Clean up if workspace link creation fails
+      await deleteAccount(account.id);
+      return null;
+    }
 
     if (organizationId) {
       await addArtistToOrganization(account.id, organizationId);

Analysis

Orphaned account records left in database when workspace creation partially fails

What fails: createWorkspaceInDb() can leave an orphaned account record in the database if any operation after insertAccount() fails, while the API returns a 500 error to the user. The account is created and committed to the database, but subsequent operations (particularly insertAccountInfo()) may fail due to database errors, network issues, or permission problems. When these operations fail, the account record remains in the database despite the overall workspace creation being reported as failed.

How to reproduce:

// Call createWorkspaceInDb with conditions that cause insertAccountInfo to fail
// Example: Database temporarily unavailable after account insertion
// This will insert the account but then return null when insertAccountInfo fails
const workspace = await createWorkspaceInDb("Test Workspace", "owner-account-id");
// Result: Returns null (500 error to user)
// Side effect: Account record exists in database with no corresponding account_info record

Result: Orphaned account record exists in the database. The account was successfully inserted but account_info and subsequent operations failed, leaving partial state in the database.

Expected: Either all operations succeed and a complete workspace is created, or none persist any state. The operation should be atomic from the user's perspective.

Fix implemented: Added cleanup logic to delete the orphaned account record when any subsequent operation fails. Created deleteAccount() function and updated createWorkspaceInDb() to call it on failure paths, ensuring no orphaned records are left in the database.

Technical background: Supabase does not support client-side transactions directly (unlike traditional ORMs). Operations are executed sequentially, so partial failures can occur. The fix ensures data consistency by cleaning up partial state before returning null to the caller.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants