Skip to content

feat: multi-family isolation — scope all data by familyId - #7

Open
isaJrKai wants to merge 1 commit into
mainfrom
fix/multi-family-isolation
Open

feat: multi-family isolation — scope all data by familyId#7
isaJrKai wants to merge 1 commit into
mainfrom
fix/multi-family-isolation

Conversation

@isaJrKai

@isaJrKai isaJrKai commented Jul 1, 2026

Copy link
Copy Markdown
Owner

What this does

Adds familyId to all 9 financial models. Each new signup gets their own familyId. All queries and mutations are scoped by familyIdfamilies cannot see or modify each other's data.

This is Phase 1 of the "let families test the app independently" feature.

⚠️ MIGRATION REQUIRED — read before merging

Before merging, run the migration SQL against Neon:

  1. Open Neon console → SQL Editor
  2. Paste the contents of prisma/migrations/20260625010000_add_multi_family_isolation.sql
  3. Run

The migration:

  • Adds familyId column (nullable) to 9 tables
  • Backfills all existing data to the founder's familyId
  • Makes familyId NOT NULL
  • Drops the old FamilySettings singleton, creates per-family rows
  • Adds indexes

Safe to run while the app is live — adds nullable columns first, backfills, then makes NOT NULL.

Changes

1. Schema (prisma/schema.prisma)

Added familyId String + @@index([familyId]) to:

  • Child, ParentProfile, Account, Transaction
  • SpendingCategory, SpendingEntry, Investment
  • TokenLedgerEntry, Goal

FamilySettings changed from id: "singleton" to familyId @unique (per-family rows).

2. src/lib/auth.ts

  • performFounderSetup() — assigns familyId: "fam_" + crypto.randomUUID()
  • registerUserAsUSER() — assigns familyId: "fam_" + crypto.randomUUID()
  • getAuthUser() — returns familyId in the user object
  • AuthUser interface — includes familyId: string | null

3. src/lib/db-queries.ts

  • getFullState(familyId) — all 10 findMany calls scoped by familyId
  • All 16 mutation functions accept familyId:
    • Create functions: include familyId in the data block
    • Update/delete functions: verify ownership with findFirst({ where: { id, familyId } }) before operating
  • setFamilySettings(familyId, patch)upsert by familyId instead of singleton

4. API routes

  • GET /api/state — passes user.familyId to getFullState()
  • POST /api/mutations — passes user.familyId to all 16 mutation functions
  • GET /api/child/state — checks user.familyId exists

Files changed (7 files)

  • prisma/schema.prisma — +9 familyId columns, +9 indexes, FamilySettings restructure
  • prisma/migrations/20260625010000_add_multi_family_isolation.sql (new) — Neon migration
  • src/lib/auth.ts — familyId on signup/setup, getAuthUser returns familyId
  • src/lib/db-queries.ts — all queries/mutations scoped by familyId
  • src/app/api/state/route.ts — auth gate + familyId
  • src/app/api/mutations/route.ts — auth gate + familyId on all mutations
  • src/app/api/child/state/route.ts — familyId check

Verification

  • npx tsc --noEmit — clean
  • npx eslint — clean
  • npx prisma generate — clean

Vercel env vars

None needed.

After merging

  1. Each new signup gets their own isolated family
  2. Existing data belongs to the founder's family
  3. New users see an empty dashboard (no children, no data) — they create their own
  4. No user can see another user's data

Next phases

Summary by CodeRabbit

  • New Features

    • Added family-based data isolation across the app, so each family now sees its own children, accounts, transactions, goals, and settings.
    • Family settings are now stored per family instead of as a single shared record.
  • Bug Fixes

    • Strengthened sign-in checks to prevent access when a user isn’t linked to a family.
    • Updated state loading and updates to use the current family’s data, reducing cross-family data mixing.
  • Chores

    • Migrated existing data to the new family-scoped structure and added supporting database indexes for faster lookups.

Adds familyId to all 9 financial models (Child, ParentProfile, Account,
Transaction, SpendingCategory, SpendingEntry, Investment,
TokenLedgerEntry, Goal). Each new signup gets their own familyId.
All queries and mutations are scoped by familyId — families cannot
see or modify each other's data.

Changes:

1. Schema: add familyId column + index to 9 models. FamilySettings
   changes from singleton to per-family (keyed by familyId).
2. Migration: SQL to add columns, backfill existing data to founder's
   family, make NOT NULL, add indexes.
3. auth.ts: auto-generate familyId on signup + founder setup.
   getAuthUser() returns familyId.
4. db-queries.ts: getFullState(familyId) scopes all queries. All
   mutation functions accept familyId, include it in creates, and
   verify ownership on updates/deletes.
5. API routes: /api/state and /api/mutations pass familyId from
   getAuthUser() to all queries/mutations.

BEFORE MERGING: run the migration SQL on Neon.
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces multi-family data isolation across the application: a new familyId field is added to nine Prisma models with a migration that backfills existing data and enforces the column, AuthUser and login flows now carry familyId, and API routes and database-query functions enforce and propagate familyId scoping for all reads and writes.

Changes

Multi-family isolation

Layer / File(s) Summary
Schema and migration
prisma/schema.prisma, prisma/migrations/20260625010000_add_multi_family_isolation.sql
Adds familyId field and index to nine models (Child, ParentProfile, Account, Transaction, SpendingCategory, SpendingEntry, Investment, TokenLedgerEntry, Goal); migration backfills familyId from the founder user, enforces NOT NULL, converts FamilySettings from a singleton to per-family rows, and adds indexes.
Auth: familyId issuance
src/lib/auth.ts
AuthUser gains familyId: string | null; getAuthUser, registerUserAsUSER, performFounderSetup, attemptLogin, and completeLoginWith2FA generate, select, or return familyId.
Data-access layer
src/lib/db-queries.ts
getFullState and all mutation functions (addTransaction, addSpendingEntry, giveTokens, redeemTokens, investNow, createGoal, updateGoal, deleteGoal, contributeToGoal, setFamilySettings, profile photo/name setters, createChild, createParent) now require familyId and scope reads/writes and access checks by it.
API routes: auth guard and familyId propagation
src/app/api/state/route.ts, src/app/api/child/state/route.ts, src/app/api/mutations/route.ts
Routes call getAuthUser(), return 401 when familyId is missing, and pass familyId into getFullState and mutation calls.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
participant Client
participant APIRoute
participant AuthLib
participant DbQueries
Client->>APIRoute: Request (state/mutation)
APIRoute->>AuthLib: getAuthUser()
AuthLib-->>APIRoute: user with familyId or null
alt missing user or familyId
APIRoute-->>Client: 401 Not authenticated
else authenticated
APIRoute->>DbQueries: query/mutation(familyId, ...)
DbQueries-->>APIRoute: family-scoped result
APIRoute-->>Client: response with data
end
Loading

Related Issues: None referenced.

Related PRs: None referenced.

Suggested labels: database, security, backend

Suggested reviewers: isaJrKai

🐇 A rabbit hops from house to house,
Each family gets its own little spouse—
"familyId!" it squeaks with glee,
No more data leaked, you see,
Just tidy burrows, safe and free.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: scoping app data by familyId for multi-family isolation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/multi-family-isolation

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
prisma/schema.prisma (1)

161-184: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add the missing Goal.familyId index to the Prisma schema.

The migration creates "Goal_familyId_idx" and reads use where: { familyId }, but the Prisma model only declares @@index([ownerId]). This leaves schema drift and can cause future migrations to drop the production index.

Suggested fix
 model Goal {
   id            String   `@id` `@default`(cuid())
   familyId      String
@@
 
   @@index([ownerId])
+  @@index([familyId])
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/schema.prisma` around lines 161 - 184, The Goal Prisma model is
missing the familyId index that the migration already creates and that queries
rely on via where familyId. Update the Goal model in the Prisma schema to
declare an index for familyId alongside the existing ownerId index so the schema
matches the migration and prevents future drift. Use the Goal model definition
and its @@index block to locate the change.
src/lib/db-queries.ts (4)

178-221: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Scope child and account balance updates by familyId.

addTransaction writes familyId on the transaction row, but the balance mutations still target childId/accountId globally. A crafted payload can mutate another family’s child/account while recording the transaction under the caller’s family.

Proposed fix
+    const childUpdateWhere = { id: input.childId, familyId: input.familyId };
+
     if (input.type === "save" || input.type === "redeem") {
-      await tx.child.update({
-        where: { id: input.childId },
+      const updated = await tx.child.updateMany({
+        where: childUpdateWhere,
         data: { currentAmount: { increment: input.amount } },
       });
+      if (updated.count === 0) throw new Error("Child not found or access denied");
     } else if (input.type === "invest" || input.type === "withdraw") {
       // Conditional update — prevents going negative
       const updated = await tx.child.updateMany({
-        where: { id: input.childId, currentAmount: { gte: input.amount } },
+        where: { id: input.childId, familyId: input.familyId, currentAmount: { gte: input.amount } },
         data: { currentAmount: { decrement: input.amount } },
       });
       if (updated.count === 0) {
-        throw new Error("Insufficient balance for " + input.type);
+        throw new Error("Child not found, access denied, or insufficient balance for " + input.type);
       }
     }

     // Update linked account balance for save/withdraw
     if (input.accountId) {
       if (input.type === "save") {
-        await tx.account.updateMany({
-          where: { id: input.accountId, balance: { gte: input.amount } },
+        const updated = await tx.account.updateMany({
+          where: { id: input.accountId, familyId: input.familyId, balance: { gte: input.amount } },
           data: { balance: { decrement: input.amount } },
         });
+        if (updated.count === 0) throw new Error("Account not found, access denied, or insufficient balance");
       } else if (input.type === "withdraw") {
-        await tx.account.update({
-          where: { id: input.accountId },
+        const updated = await tx.account.updateMany({
+          where: { id: input.accountId, familyId: input.familyId },
           data: { balance: { increment: input.amount } },
         });
+        if (updated.count === 0) throw new Error("Account not found or access denied");
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/db-queries.ts` around lines 178 - 221, addTransaction currently
updates child and account balances using only childId/accountId, so scope every
balance mutation by familyId as well. Update the tx.child.update,
tx.child.updateMany, tx.account.updateMany, and tx.account.update calls in
db-queries.ts to include familyId in their where clauses, using the same
input.familyId already written to the transaction row. Keep the existing
insufficient-balance checks and transaction flow intact, but ensure only records
belonging to the caller’s family can be mutated.

372-395: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Use familyId in goal mutations.

updateGoal and contributeToGoal accept familyId but update by id only, so any authenticated family can modify another family’s goal if they know the ID.

Proposed fix
 export async function updateGoal(familyId: string, id: string, patch: Partial<{
@@
 }>) {
-  return db.goal.update({ where: { id }, data: patch });
+  const updated = await db.goal.updateMany({ where: { id, familyId }, data: patch });
+  if (updated.count === 0) throw new Error("Goal not found or access denied");
+  return db.goal.findFirstOrThrow({ where: { id, familyId } });
 }
@@
 export async function contributeToGoal(familyId: string, id: string, amount: number) {
   // Atomic increment — no race condition
-  return db.goal.update({
-    where: { id },
+  const updated = await db.goal.updateMany({
+    where: { id, familyId },
     data: { currentAmount: { increment: amount } },
   });
+  if (updated.count === 0) throw new Error("Goal not found or access denied");
+  return db.goal.findFirstOrThrow({ where: { id, familyId } });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/db-queries.ts` around lines 372 - 395, `updateGoal` and
`contributeToGoal` are missing the `familyId` ownership check, so they can
mutate a goal by `id` alone. Update the `db.goal.update` calls in these
functions to first verify the goal belongs to the given `familyId` (same pattern
as `deleteGoal` using `findFirst`), then perform the mutation using the matched
record’s `id` or a composite condition that includes `familyId`. Keep
`deleteGoal` as the reference for the access-control pattern.

255-321: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Enforce family ownership for token and investment child IDs.

These paths create family-scoped rows but still aggregate/update by global childId. Passing another family’s child ID can affect token balances, savings, or investments outside the caller’s family.

Proposed direction
-    const given = await tx.tokenLedgerEntry.aggregate({
-      where: { childId, type: "parent_give" },
+    const child = await tx.child.findFirst({ where: { id: childId, familyId } });
+    if (!child) throw new Error("Child not found or access denied");
+
+    const given = await tx.tokenLedgerEntry.aggregate({
+      where: { childId, familyId, type: "parent_give" },
       _sum: { tokens: true },
     });
     const redeemed = await tx.tokenLedgerEntry.aggregate({
-      where: { childId, type: "redeem" },
+      where: { childId, familyId, type: "redeem" },
       _sum: { tokens: true },
     });

Apply the same { id: childId, familyId } ownership check before giveTokens creates rows, and include familyId in investNow’s tx.child.updateMany predicate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/db-queries.ts` around lines 255 - 321, The token and investment flows
in giveTokens, redeemTokens, and investNow currently trust a global childId when
reading or updating child state. Add a family ownership guard so these
operations only affect rows where the child belongs to the provided familyId,
using the same { id: childId, familyId } predicate before creating
ledger/transaction rows and in investNow’s tx.child.updateMany filter. Also make
the balance aggregates in redeemTokens family-scoped so another family’s child
records cannot be counted or modified.

239-251: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Verify owner records before creating family-scoped rows.

addSpendingEntry and createGoal persist the caller’s familyId, but still trust ownerId from the payload. Validate that the referenced child/parent belongs to input.familyId before creating rows with cross-table ownership references.

Also applies to: 352-368

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/db-queries.ts` around lines 239 - 251, Validate the referenced owner
before inserting family-scoped records in addSpendingEntry and createGoal.
Before calling db.spendingEntry.create or db.goal.create, look up the
child/parent by input.ownerId and confirm it belongs to input.familyId, then use
the verified record to populate childId/parentId or reject the request if it
does not match. Keep the ownership check close to the create logic so the
familyId and ownerId cannot be combined across families.
src/app/api/child/state/route.ts (1)

10-17: 🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Apply familyId to the child-state reads too.

The new guard verifies user.familyId, but the route still reads the child, token ledger, and goals without that scope. Rows with the same childId/ownerId but a different familyId can leak into this response and affect token balance/progress.

Proposed fix
-  const child = await db.child.findUnique({ where: { id: childProfile.childId } });
+  const child = await db.child.findFirst({ where: { id: childProfile.childId, familyId: user.familyId } });
   if (!child) return NextResponse.json({ ok: false, error: "Child record not found" }, { status: 404 });
-  const tokenLedger = await db.tokenLedgerEntry.findMany({ where: { childId: child.id }, orderBy: { timestamp: "desc" }, take: 20 });
-  const goals = await db.goal.findMany({ where: { ownerId: child.id }, orderBy: { createdAt: "desc" } });
+  const tokenLedger = await db.tokenLedgerEntry.findMany({ where: { childId: child.id, familyId: user.familyId }, orderBy: { timestamp: "desc" }, take: 20 });
+  const goals = await db.goal.findMany({ where: { ownerId: child.id, familyId: user.familyId }, orderBy: { createdAt: "desc" } });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/api/child/state/route.ts` around lines 10 - 17, The child-state route
validates user.familyId but the subsequent reads in the route handler still
query childProfile, child, tokenLedgerEntry, and goal without scoping to that
family. Update the child-state lookup flow in route.ts so the child record and
all dependent reads are constrained by familyId through the existing
user.familyId guard, ensuring the response only includes rows belonging to the
current family. Use the route handler’s childProfile, child, tokenLedgerEntry,
and goals queries to apply the same family scope consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@prisma/migrations/20260625010000_add_multi_family_isolation.sql`:
- Around line 63-77: The FamilySettings backfill step is inserting per-family
rows before the schema has the required familyId column and unique constraint.
Update this migration to add FamilySettings.familyId and create the unique
key/index before the INSERT/DELETE block, then proceed with the backfill so
Prisma upserts using familyId match the database shape expected by
FamilySettings and its related upsert paths.

---

Outside diff comments:
In `@prisma/schema.prisma`:
- Around line 161-184: The Goal Prisma model is missing the familyId index that
the migration already creates and that queries rely on via where familyId.
Update the Goal model in the Prisma schema to declare an index for familyId
alongside the existing ownerId index so the schema matches the migration and
prevents future drift. Use the Goal model definition and its @@index block to
locate the change.

In `@src/app/api/child/state/route.ts`:
- Around line 10-17: The child-state route validates user.familyId but the
subsequent reads in the route handler still query childProfile, child,
tokenLedgerEntry, and goal without scoping to that family. Update the
child-state lookup flow in route.ts so the child record and all dependent reads
are constrained by familyId through the existing user.familyId guard, ensuring
the response only includes rows belonging to the current family. Use the route
handler’s childProfile, child, tokenLedgerEntry, and goals queries to apply the
same family scope consistently.

In `@src/lib/db-queries.ts`:
- Around line 178-221: addTransaction currently updates child and account
balances using only childId/accountId, so scope every balance mutation by
familyId as well. Update the tx.child.update, tx.child.updateMany,
tx.account.updateMany, and tx.account.update calls in db-queries.ts to include
familyId in their where clauses, using the same input.familyId already written
to the transaction row. Keep the existing insufficient-balance checks and
transaction flow intact, but ensure only records belonging to the caller’s
family can be mutated.
- Around line 372-395: `updateGoal` and `contributeToGoal` are missing the
`familyId` ownership check, so they can mutate a goal by `id` alone. Update the
`db.goal.update` calls in these functions to first verify the goal belongs to
the given `familyId` (same pattern as `deleteGoal` using `findFirst`), then
perform the mutation using the matched record’s `id` or a composite condition
that includes `familyId`. Keep `deleteGoal` as the reference for the
access-control pattern.
- Around line 255-321: The token and investment flows in giveTokens,
redeemTokens, and investNow currently trust a global childId when reading or
updating child state. Add a family ownership guard so these operations only
affect rows where the child belongs to the provided familyId, using the same {
id: childId, familyId } predicate before creating ledger/transaction rows and in
investNow’s tx.child.updateMany filter. Also make the balance aggregates in
redeemTokens family-scoped so another family’s child records cannot be counted
or modified.
- Around line 239-251: Validate the referenced owner before inserting
family-scoped records in addSpendingEntry and createGoal. Before calling
db.spendingEntry.create or db.goal.create, look up the child/parent by
input.ownerId and confirm it belongs to input.familyId, then use the verified
record to populate childId/parentId or reject the request if it does not match.
Keep the ownership check close to the create logic so the familyId and ownerId
cannot be combined across families.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bcf365c2-9d1e-4e91-851d-b008c75e0c9a

📥 Commits

Reviewing files that changed from the base of the PR and between 7053878 and 7ec10e3.

📒 Files selected for processing (7)
  • prisma/migrations/20260625010000_add_multi_family_isolation.sql
  • prisma/schema.prisma
  • src/app/api/child/state/route.ts
  • src/app/api/mutations/route.ts
  • src/app/api/state/route.ts
  • src/lib/auth.ts
  • src/lib/db-queries.ts

Comment on lines +63 to +77
-- Step 4: Drop old FamilySettings singleton, create per-family row
-- First, get the founder's familyId and create a FamilySettings row for it
INSERT INTO "FamilySettings" ("id", "familyId", "annualTheme", "monthlyQuote", "currency", "createdAt", "updatedAt")
SELECT
'fam_settings_' || "familyId",
"familyId",
COALESCE((SELECT "annualTheme" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "monthlyQuote" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "currency" FROM "FamilySettings" WHERE "id" = 'singleton'), 'UGX'),
NOW(),
NOW()
FROM "User" WHERE "platformRole" = 'FOUNDER' AND "familyId" IS NOT NULL;

-- Delete the old singleton row
DELETE FROM "FamilySettings" WHERE "id" = 'singleton';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Migrate the FamilySettings.familyId column and unique key before inserting per-family rows.

Line 65 writes "familyId", and downstream code upserts by where: { familyId }, but this migration never adds the column or creates the unique key. On the old singleton table this will fail, or the new app will not have the DB constraint Prisma expects.

Suggested fix
 -- Step 4: Drop old FamilySettings singleton, create per-family row
+ALTER TABLE "FamilySettings" ADD COLUMN "familyId" TEXT;
+
 -- First, get the founder's familyId and create a FamilySettings row for it
 INSERT INTO "FamilySettings" ("id", "familyId", "annualTheme", "monthlyQuote", "currency", "createdAt", "updatedAt")
@@
 -- Delete the old singleton row
 DELETE FROM "FamilySettings" WHERE "id" = 'singleton';
+
+ALTER TABLE "FamilySettings" ALTER COLUMN "familyId" SET NOT NULL;
+CREATE UNIQUE INDEX "FamilySettings_familyId_key" ON "FamilySettings"("familyId");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- Step 4: Drop old FamilySettings singleton, create per-family row
-- First, get the founder's familyId and create a FamilySettings row for it
INSERT INTO "FamilySettings" ("id", "familyId", "annualTheme", "monthlyQuote", "currency", "createdAt", "updatedAt")
SELECT
'fam_settings_' || "familyId",
"familyId",
COALESCE((SELECT "annualTheme" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "monthlyQuote" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "currency" FROM "FamilySettings" WHERE "id" = 'singleton'), 'UGX'),
NOW(),
NOW()
FROM "User" WHERE "platformRole" = 'FOUNDER' AND "familyId" IS NOT NULL;
-- Delete the old singleton row
DELETE FROM "FamilySettings" WHERE "id" = 'singleton';
-- Step 4: Drop old FamilySettings singleton, create per-family row
ALTER TABLE "FamilySettings" ADD COLUMN "familyId" TEXT;
-- First, get the founder's familyId and create a FamilySettings row for it
INSERT INTO "FamilySettings" ("id", "familyId", "annualTheme", "monthlyQuote", "currency", "createdAt", "updatedAt")
SELECT
'fam_settings_' || "familyId",
"familyId",
COALESCE((SELECT "annualTheme" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "monthlyQuote" FROM "FamilySettings" WHERE "id" = 'singleton'), ''),
COALESCE((SELECT "currency" FROM "FamilySettings" WHERE "id" = 'singleton'), 'UGX'),
NOW(),
NOW()
FROM "User" WHERE "platformRole" = 'FOUNDER' AND "familyId" IS NOT NULL;
-- Delete the old singleton row
DELETE FROM "FamilySettings" WHERE "id" = 'singleton';
ALTER TABLE "FamilySettings" ALTER COLUMN "familyId" SET NOT NULL;
CREATE UNIQUE INDEX "FamilySettings_familyId_key" ON "FamilySettings"("familyId");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@prisma/migrations/20260625010000_add_multi_family_isolation.sql` around lines
63 - 77, The FamilySettings backfill step is inserting per-family rows before
the schema has the required familyId column and unique constraint. Update this
migration to add FamilySettings.familyId and create the unique key/index before
the INSERT/DELETE block, then proceed with the backfill so Prisma upserts using
familyId match the database shape expected by FamilySettings and its related
upsert paths.

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.

1 participant