Skip to content

feat: Add SpiceDB authorization sync for project lifecycle#1916

Merged
omar-inkeep merged 3 commits intomainfrom
feat/spicedb-project-sync
Feb 12, 2026
Merged

feat: Add SpiceDB authorization sync for project lifecycle#1916
omar-inkeep merged 3 commits intomainfrom
feat/spicedb-project-sync

Conversation

@amikofalvy
Copy link
Collaborator

Summary

  • Wrap project create (POST and PUT upsert) in two-phase commit: DB transactions + SpiceDB sync so authorization relationships are created atomically with database records
  • On project delete, clean up SpiceDB relationships with graceful fallback (log warning on failure, don't block deletion)
  • Add SpiceDB error detection (gRPC error characteristics + message matching) with specific error responses
  • Mock SpiceDB sync functions in integration tests since they test the API/DB layer only
  • Remove flawed empty-ID project test

Split from #1836 — this PR contains only the SpiceDB/authz synchronization work.

Test plan

  • Integration tests updated with SpiceDB mocks
  • Pre-commit hooks pass (lint, test, format)
  • Manual testing of project create/delete with SpiceDB running
  • Verify SpiceDB rollback behavior when sync fails

Made with Cursor

Wrap project create operations in two-phase commit with SpiceDB sync so
authorization relationships are created atomically with DB records. On delete,
clean up SpiceDB relationships with graceful fallback. Mock SpiceDB functions
in integration tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel
Copy link

vercel bot commented Feb 11, 2026

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

Project Deployment Actions Updated (UTC)
agents-api Ready Ready Preview, Comment Feb 12, 2026 9:04pm
agents-manage-ui Ready Ready Preview, Comment Feb 12, 2026 9:04pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
agents-docs Skipped Skipped Feb 12, 2026 9:04pm

Request Review

@changeset-bot
Copy link

changeset-bot bot commented Feb 11, 2026

🦋 Changeset detected

Latest commit: 8c2eeb6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@inkeep/agents-api Patch
@inkeep/agents-manage-ui Patch
@inkeep/agents-cli Patch
@inkeep/agents-core Patch
@inkeep/agents-manage-mcp Patch
@inkeep/agents-mcp Patch
@inkeep/agents-sdk Patch
@inkeep/agents-work-apps Patch
@inkeep/ai-sdk-provider Patch
@inkeep/create-agents Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

6 Key Findings | Risk: Medium

🟠⚠️ Major (3) 🟠⚠️

🟠 1) projectFull.ts:132-189 "Two-phase commit" naming overstates consistency guarantees

Issue: The code comments claim "two-phase commit" where "If SpiceDB sync fails, both DB transactions will rollback automatically." This is architecturally misleading. Calling an external gRPC service (SpiceDB) within a database transaction does not make the external call part of that transaction. The pattern provides fail-fast semantics (SpiceDB failure before commit prevents DB commit), but not true atomicity.

Why: If SpiceDB sync succeeds but a subsequent operation fails (or the DB commit itself fails for any reason), the SpiceDB relationships remain — they cannot be rolled back. This could leave orphaned authorization state. Additionally, there's no compensation mechanism to clean up SpiceDB if something goes wrong after the syncProjectToSpiceDb call returns successfully.

Fix: Consider either:

  1. Rename the pattern in comments to clarify actual guarantees (e.g., "fail-fast consistency: SpiceDB failure prevents DB commit")
  2. Move SpiceDB sync after the transaction commits, with explicit rollback logic (delete the project) if SpiceDB fails
  3. Accept eventual consistency with a background reconciliation job

Refs:


🟠 2) projectFull.ts:204-206 gRPC error detection heuristic may misclassify errors

Issue: The check error?.metadata !== undefined && typeof error?.code === 'number' is intended to detect gRPC errors from SpiceDB, but this heuristic is fragile. Many JavaScript errors have numeric code properties, and various libraries attach metadata properties. If a non-SpiceDB error matches this pattern, users receive the misleading message about SpiceDB authorization failure.

Why: False positives would show "Failed to set up project authorization" when the actual error is unrelated to SpiceDB. False negatives would let SpiceDB errors slip through to generic error handling, potentially exposing internal error details.

Fix: Consider more specific detection:

// Check for gRPC status code range (0-16) and required properties
const isGrpcError = (
  error?.metadata !== undefined &&
  typeof error?.code === 'number' &&
  error?.code >= 0 &&
  error?.code <= 16 &&
  typeof error?.details === 'string'
);

Or wrap syncProjectToSpiceDb in its own try-catch to distinguish SpiceDB errors explicitly.

Refs:


🟠 3) projectFull.ts vs projects.ts Inconsistent SpiceDB error handling between sibling routes

Issue: This PR introduces a stricter consistency model (rollback on SpiceDB failure) for projectFull.ts, while the sibling projects.ts route uses graceful fallback (log warning, continue). Both endpoints handle project creation but with different failure semantics.

Why: A customer using POST /manage/tenants/{id}/projects gets different behavior than one using POST /manage/tenants/{id}/project-full. With the former, SpiceDB failures are logged but creation succeeds. With the latter, SpiceDB failures roll back the entire operation. This behavioral divergence creates confusing API semantics.

Fix: Either:

  1. Align projectFull.ts to use the same graceful fallback as projects.ts
  2. Update projects.ts to use the new stricter pattern
  3. If divergence is intentional, document why the two routes have different failure semantics

Refs:

// Findings posted as inline comments:

  • 🟠 Major: projectFull.ts:473 Variable shadowing — isCreate redeclared

🟡 Minor (2) 🟡

🟡 1) projectFull.test.ts:9-19 No test coverage for SpiceDB failure scenarios

Issue: The integration tests mock all SpiceDB functions to return success. The critical error handling logic (gRPC error detection, transaction rollback) has no test coverage.

Why: If the rollback-on-failure logic has bugs (e.g., SpiceDB call executes outside transaction scope), it won't be caught by tests. The PR's core consistency guarantee is untested.

Fix: Add at least one test that configures the mock to throw and verifies DB rollback:

it('should rollback DB when SpiceDB sync fails during create', async () => {
  vi.mocked(syncProjectToSpiceDb).mockRejectedValueOnce(
    Object.assign(new Error('SpiceDB connection failed'), { metadata: {}, code: 14 })
  );
  // ... verify 500 response and project NOT in DB
});

Refs:


🟡 2) projectFull.ts:176-184 External service call inside open transaction extends lock duration

Issue: The SpiceDB gRPC call occurs while two database transactions are held open. SpiceDB involves network I/O with variable latency (DNS, TLS, potential retries). During this time, both databases hold locks, increasing contention under load.

Why: If SpiceDB is slow or times out, both databases are blocked. This is the "pessimistic lock held across async boundary" anti-pattern that can cause cascading contention.

Fix: Consider moving SpiceDB sync after the transactions commit. Pattern:

const project = await runDbClient.transaction(...);
await syncProjectToSpiceDb(...); // After commit
return project;

If SpiceDB then fails, implement explicit rollback logic (delete the just-created project).

Refs:

// Findings posted as inline comments:

  • 🟡 Minor: projectFull.ts:178 Silent skip when userId is falsy
  • 🟡 Minor: projectFull.ts:582-593 Standardize error key in log

💭 Consider (2) 💭

💭 1) projectFull.ts:179-183 No timeout or retry for SpiceDB calls
Issue: SpiceDB gRPC calls lack explicit timeout configuration and retry logic for transient failures.
Why: Could hang indefinitely or fail on brief network blips.
Fix: Configure gRPC deadline (5-10s) and consider 2-3 retries with exponential backoff for creates.

💭 2) sync.ts SpiceDB sync for API key/system users may create orphaned relationships
Issue: When userId is apikey:... or system, the sync creates relationships for non-existent SpiceDB users.
Why: Could pollute SpiceDB with invalid relationships.
Fix: Add explicit handling: skip SpiceDB sync entirely for non-user principals, or only create the project-to-org link.


💡 APPROVE WITH SUGGESTIONS

Summary: This PR adds valuable SpiceDB authorization sync for project lifecycle, addressing a real gap in the authorization system. The core implementation is solid. The main concerns are: (1) the "two-phase commit" naming overstates the consistency guarantees — clarifying the actual semantics would help future maintainers, (2) the gRPC error detection heuristic could be more robust, and (3) the behavioral divergence from the sibling projects.ts route should be intentional and documented. None of these are blocking, but addressing them would strengthen the implementation.

Discarded (5)
Location Issue Reason Discarded
projectFull.ts:574-593 Asymmetric consistency (DELETE graceful, CREATE strict) Intentional design choice documented in code comments
client.ts:21-33 SpiceDB client lacks health monitoring Pre-existing — not introduced by this PR
projectFull.ts:207-215 Missing correlation ID in logs Valid but very minor — logger may already include request context
projectFull.test.ts Test mock completely bypasses SpiceDB Expected behavior for API/DB layer tests; documented in PR
projects.test.ts Sibling test doesn't mock SpiceDB Pre-existing — consequence of existing graceful fallback pattern
Reviewers (7)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-architecture 5 2 0 0 0 0 3
pr-review-security-iam 4 0 1 0 1 0 2
pr-review-sre 6 0 1 0 0 0 5
pr-review-errors 5 1 0 0 1 0 3
pr-review-standards 3 1 0 0 1 0 1
pr-review-tests 3 1 0 0 0 0 2
pr-review-consistency 4 1 0 0 0 0 3
Total 30 6 2 0 3 0 19

}

// Handle SpiceDB sync failures for creates - transactions already rolled back
const isCreate = c.get('isProjectCreate') ?? false;
Copy link
Contributor

Choose a reason for hiding this comment

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

🟠 MAJOR Variable shadowing: isCreate redeclared in catch block

Issue: The variable isCreate is already declared at line 405 and is redeclared identically here in the catch block.

Why: While functionally harmless (same value), this duplication is unnecessary and could confuse maintainers if the logic diverges in the future.

Fix: (1-click apply)

Suggested change
const isCreate = c.get('isProjectCreate') ?? false;
// Handle SpiceDB sync failures for creates - transactions already rolled back
if (c.get('isProjectCreate')) {

Refs:


// Phase 2: Sync to SpiceDB (still within transaction scope)
// If this fails, both transactions will rollback automatically
if (userId) {
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR Silent skip when userId is falsy — no logging

Issue: When userId is not set, the SpiceDB sync is silently skipped without any log entry.

Why: If a project is created in a code path where userId is undefined (e.g., certain test scenarios or future code changes), the project will exist but have no authorization relationships. The silent skip makes this difficult to debug.

Fix: Add explicit logging:

if (userId) {
  await syncProjectToSpiceDb({
    tenantId,
    projectId: validatedProjectData.id,
    creatorUserId: userId,
  });
} else {
  logger.warn(
    { tenantId, projectId: validatedProjectData.id },
    'Skipping SpiceDB sync: no userId available - project may have no explicit owner'
  );
}

Refs:

  • Same pattern at line 440 in PUT handler

Comment on lines 582 to 593
} catch (spiceDbError) {
// Log but don't fail - the project data is already deleted
// This could leave orphaned auth relationships, but won't affect functionality
logger.warn(
{
spiceDbError,
tenantId,
projectId,
},
'Failed to remove project from SpiceDB - orphaned auth relationships may remain'
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 MINOR Standardize error key and add remediation guidance

Issue: The SpiceDB error is logged as spiceDbError instead of the conventional error key, which may cause inconsistent log parsing.

Why: Log aggregation tools that key on error may not detect this failure. Operations teams also need guidance on remediation.

Fix:

Suggested change
} catch (spiceDbError) {
// Log but don't fail - the project data is already deleted
// This could leave orphaned auth relationships, but won't affect functionality
logger.warn(
{
spiceDbError,
tenantId,
projectId,
},
'Failed to remove project from SpiceDB - orphaned auth relationships may remain'
);
}
} catch (spiceDbError) {
// Log but don't fail - the project data is already deleted
// This could leave orphaned auth relationships, but won't affect functionality
logger.warn(
{
error: spiceDbError,
tenantId,
projectId,
},
'Failed to remove project from SpiceDB - orphaned auth relationships may remain. Manual cleanup may be required if this persists.'
);
}

Refs:

@github-actions github-actions bot deleted a comment from claude bot Feb 11, 2026
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(1) Total Issues | Risk: High

🔴❗ Critical (1) ❗🔴

Inline Comments:

  • 🔴 Critical: projectFull.ts:415-431 Duplicate createProjectMetadataAndBranch call — merge artifact causing authorization gap

🟡 Minor (0) 🟡

No new Minor findings in this delta review.

💭 Consider (3) 💭

💭 1) projectFull.ts:542-547, 565-571, 588-593 Promise.allSettled results not aggregated for observability

Issue: The scheduled trigger reconciliation uses Promise.allSettled but doesn't examine or log the aggregate results. Individual failures are logged via .catch() handlers, but there's no summary of how many operations succeeded/failed.

Why: In high-failure scenarios, operators would need to correlate multiple log entries to understand the scope of the problem.

Fix: Consider logging an aggregate summary:

const results = await Promise.allSettled(workflowOperations);
const failures = results.filter(r => r.status === 'rejected');
if (failures.length > 0) {
  logger.warn(
    { tenantId, projectId, agentId, failedCount: failures.length, totalCount: results.length },
    'Some scheduled trigger workflow operations failed during reconciliation'
  );
}

💭 2) projectFull.ts:606-611 Outer try-catch lacks agent context in error log

Issue: The catch block logs err, tenantId, and projectId but not which agent was being processed when the failure occurred.

Why: Debugging multi-agent project updates is difficult when failures occur at the agent iteration level.

Fix: Add agent context: agentIds: Object.keys(validatedProjectData.agents || {})

💭 3) projectFull.ts:503-599 Trigger reconciliation pattern diverges from agentFull.ts sibling

Issue: This code uses Promise.all with .map() and Promise.allSettled, while agentFull.ts uses sequential for...of loops with try/catch per operation.

Why: The divergence creates maintenance burden and inconsistent error semantics.

Fix: Consider aligning patterns or documenting the intentional divergence (parallel execution for performance).

🕐 Pending Recommendations (3)

Prior feedback from automated review — still unresolved:


🚫 REQUEST CHANGES

Summary: The merge from main introduced a critical bug: duplicate createProjectMetadataAndBranch calls in the PUT handler that cause project creation to fail with an authorization gap (project exists in DB but has no SpiceDB permissions). This must be fixed before merge. The duplicate block at lines 415-431 should be removed entirely — the transactional path at lines 447-488 is the correct implementation. Additionally, the 3 prior review items remain unaddressed.

Discarded (2)
Location Issue Reason Discarded
client.ts:21-33 SpiceDB client lacks timeout/retry configuration Pre-existing — not introduced by this PR
projectFull.ts:141-196 External gRPC call inside transaction extends lock duration Already raised in prior review (Major #3)
Reviewers (5)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 1 0 0 0 1 0 0
pr-review-security-iam 1 0 0 0 1 0 0
pr-review-sre 4 0 0 0 1 0 2
pr-review-errors 2 0 2 0 0 0 0
pr-review-consistency 2 0 1 0 1 0 0
Total 10 0 3 0 1 3 2

Note: All 5 reviewers independently identified the duplicate createProjectMetadataAndBranch call as a critical issue — merged into single inline comment.

@github-actions github-actions bot deleted a comment from claude bot Feb 12, 2026
Copy link
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

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

PR Review Summary

(0) Total Issues | Risk: Low

This is a delta review following the previous REQUEST_CHANGES review. The commit 8c2eeb63 ("address comments") correctly addresses all prior feedback.

✅ Prior Feedback Verified as Addressed

Prior Issue Status
🔴 Critical: Duplicate createProjectMetadataAndBranch call causing authorization gap ✅ Fixed — duplicate block removed, single call inside transaction
🟠 Major: Variable shadowing — isCreate redeclared in catch block ✅ Fixed — now declared once at line 404
🟠 Major: "Two-phase commit" naming overstated consistency guarantees ✅ Fixed — comments now accurately describe fail-fast semantics
🟡 Minor: Silent skip when userId is falsy — no logging ✅ Fixed — logger.warn added at lines 192-196 and 470-471
🟡 Minor: SpiceDB error logged as spiceDbError instead of error ✅ Fixed — standardized to error at line 726

Security Assessment

The userId skip behavior (when falsy) was verified as correct:

  • system and apikey:... principals bypass SpiceDB permission checks via requirePermission.ts
  • Skipping SpiceDB sync for these principals is semantically correct
  • The added warning log provides appropriate observability

✅ APPROVE

Summary: All critical and major issues from the prior review have been correctly addressed. The delta is clean — no new issues introduced. The SpiceDB authorization sync for project lifecycle is ready for merge. 🎉

Reviewers (2)
Reviewer Returned Main Findings Consider While You're Here Inline Comments Pending Recs Discarded
pr-review-standards 0 0 0 0 0 0 0
pr-review-security-iam 0 0 0 0 0 0 0
Total 0 0 0 0 0 0 0

Note: Both reviewers verified all prior feedback was correctly addressed; no new findings.

@github-actions github-actions bot deleted a comment from claude bot Feb 12, 2026
@omar-inkeep omar-inkeep merged commit 6922f83 into main Feb 12, 2026
11 checks passed
@omar-inkeep omar-inkeep deleted the feat/spicedb-project-sync branch February 12, 2026 21:14
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.

3 participants