Skip to content

Commit 08d986d

Browse files
authored
[Backend][fix] - FK constraint on docker image starts (#1093)
# Foreign Key Constraint When deploying Stack Auth with Docker and changing `STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS` between container restarts, the seed script fails with: ```ts PrismaClientKnownRequestError: Invalid prisma.teamMemberDirectPermission.upsert() invocation: Foreign key constraint violated on the constraint: TeamMemberDirectPermission_tenancyId_projectUserId_teamId_fkey ``` This is a bug in the seed script's idempotency logic. The issue occurs in `apps/backend/prisma/seed.ts` (lines 296–388): - When admin credentials are provided, the script checks if the admin user already exists (line 297–303) - If the user exists, it skips the user creation block (line 305–306), which also skips creating the TeamMember record - However, the `grantTeamPermission()` call at line 382 is outside the if/else block and always runs - This tries to create a TeamMemberDirectPermission record, which has a foreign key constraint to TeamMember - If the TeamMember doesn't exist (e.g., user was created with `STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=false` previously, or the TeamMember was never created), the foreign key constraint fails. ## How could this happen? 1. Changed `INTERNAL_ACCESS` setting: First run with `STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=false` (user created, no TeamMember), then restarted with `=true` 2. Partial seed failure/interruption: A previous seed run created the user but failed before creating the TeamMember 3. Manual database modification: TeamMember was deleted but user still exists. The most likely scenario would be 1 here: ### Scenario 1: 1. First deployment: `STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=false` - User created ✓ - TeamMember **_NOT_** created(because `adminInternalAccess=false`) 2. Second deployment: `STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true` - User already exists → Skip creation - `grantTeamPermission()` called → tries to create TeamMemberDirectPermission - **_FAILS_** because TeamMember doesn't exist. ## Solution Add a `TeamMember` upsert before granting permissions when `adminInternalAccess` is true: ```ts if (adminInternalAccess) { await internalPrisma.teamMember.upsert({ where: { tenancyId_projectUserId_teamId: { tenancyId: internalTenancy.id, projectUserId: defaultUserId, teamId: internalTeamId, }, }, create: { tenancyId: internalTenancy.id, teamId: internalTeamId, projectUserId: defaultUserId, }, update: {}, }); } ``` This ensures the `TeamMember` record exists before `grantTeamPermission() is called, regardless of whether the user was just created or already existed. ## Impact - Existing deployments: No impact. If `TeamMember` already exists, the upsert does nothing. - New deployment: Works correctly. - Broken deployments: This fix will repair them on the next container restart. ## Testing Tested by building a local Docker image and running the reproduction script that: - Starts with `INTERNAL_ACCESS=false` - Restarts with `INTERNAL_ACCESS=true` - verifies no foreign key constraint error occurs. --- <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Made permission grants resilient to repeated seed runs by ensuring grants only apply when appropriate admin access is present. * Prevented duplicate team member entries during setup by making member creation idempotent, so repeated runs no longer create or alter existing records. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 3883b3b commit 08d986d

1 file changed

Lines changed: 29 additions & 15 deletions

File tree

apps/backend/prisma/seed.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -315,15 +315,8 @@ export async function seed() {
315315
}
316316
});
317317

318-
if (adminInternalAccess) {
319-
await internalPrisma.teamMember.create({
320-
data: {
321-
tenancyId: internalTenancy.id,
322-
teamId: internalTeamId,
323-
projectUserId: defaultUserId,
324-
},
325-
});
326-
}
318+
// Note: TeamMember creation is handled by the upsert below (after this if/else block)
319+
// to ensure idempotency when adminInternalAccess changes between runs
327320

328321
if (adminEmail && adminPassword) {
329322
await usersCrudHandlers.adminUpdate({
@@ -379,12 +372,33 @@ export async function seed() {
379372
}
380373
}
381374

382-
await grantTeamPermission(internalPrisma, {
383-
tenancy: internalTenancy,
384-
teamId: internalTeamId,
385-
userId: defaultUserId,
386-
permissionId: "team_admin",
387-
});
375+
// Create or ensure TeamMember exists before granting permissions.
376+
// Using upsert here (instead of create inside the else block above) ensures
377+
// idempotency when adminInternalAccess changes between seed runs.
378+
if (adminInternalAccess) {
379+
await internalPrisma.teamMember.upsert({
380+
where: {
381+
tenancyId_projectUserId_teamId: {
382+
tenancyId: internalTenancy.id,
383+
projectUserId: defaultUserId,
384+
teamId: internalTeamId,
385+
},
386+
},
387+
create: {
388+
tenancyId: internalTenancy.id,
389+
teamId: internalTeamId,
390+
projectUserId: defaultUserId,
391+
},
392+
update: {},
393+
});
394+
395+
await grantTeamPermission(internalPrisma, {
396+
tenancy: internalTenancy,
397+
teamId: internalTeamId,
398+
userId: defaultUserId,
399+
permissionId: "team_admin",
400+
});
401+
}
388402
}
389403

390404
if (emulatorEnabled) {

0 commit comments

Comments
 (0)