Skip to content

Phase B: session resolves and carries the active organization - #49

Merged
Bonobo791 merged 7 commits into
mainfrom
mt-b-session
Aug 2, 2026
Merged

Phase B: session resolves and carries the active organization#49
Bonobo791 merged 7 commits into
mainfrom
mt-b-session

Conversation

@Bonobo791

@Bonobo791 Bonobo791 commented Aug 2, 2026

Copy link
Copy Markdown
Owner

User description

Behavior

Second phase of the multi-tenancy plan. Production already has migration 0012 (Phase A gate cleared), so the tenant code can now ship.

  • org.ts (new): resolveActiveOrg — session's active_org_id when the membership still exists, else the user's OLDEST membership (deterministic); returns null only for zero memberships (a data bug). listOrgMemberships feeds the nav switcher (Phase D). asOrgRole narrows raw role strings, throwing on unknown values.
  • session.ts: SessionUser gains orgId/orgName/orgRole; plan is now the ACTIVE ORGANIZATION's plan (the users.plan select is removed — the LEGACY comment from Phase A is now literally true). getSessionUser resolves the tenant on every resolution: fallback is logged loudly (console.info) AND repairs the session row; zero memberships throws (console.error + throw) — never signs out, never improvises access. createSession accepts an optional activeOrgId (signup passes the personal org in Phase C).
  • (app)/+layout.server.ts: exposes orgs for the (Phase D) nav switcher; unused until then.

No route behavior changes yet — ownedChannel still gates on channels.userId until Phase C.

Verification

  • npm run check / build / test green (308 tests; 4 new session tests)
  • New tests: active-org resolution onto the user (incl. org-sourced plan), NULL active org → oldest membership, vanished active-org membership → fallback + session-row repair (re-queried and asserted), zero memberships → throws (not null); tombstone regression guard unchanged
  • Mutation check performed: removing the session-repair UPDATE makes the fallback test fail; restored and green
  • Fixtures updated to the Phase A backfill shape (personal org + owner membership per seeded user) — assertions never weakened
  • codacy-analysis: 0 issues on all touched files

Manual check still owed (needs a browser + Google account)

Plan B4 asks for a dev sign-in smoke test (dashboard renders with no console errors using the org-carrying session). Not executable headlessly — flagging honestly; the deployed preview + a dev sign-in covers it.

Ref: Moderaty Multi-Tenancy Plan, Phase B (steps B1–B4).


CodeAnt-AI Description

Resolve the signed-in user’s active organization and keep organization data consistent

What Changed

  • Signed-in users now receive their active organization, role, name, and organization plan in session data.
  • Sessions use the selected organization when membership is valid; otherwise they choose the oldest membership deterministically and repair the saved session choice.
  • New accounts receive a personal organization and owner membership during signup, and the app layout provides the user’s organization list for navigation.
  • Account deletion now removes the user’s personal organization, memberships, and created invites while preserving unrelated organizations.
  • Missing organization memberships fail clearly instead of silently treating the account as signed out.

Impact

✅ Consistent organization and plan context across signed-in pages
✅ Reliable recovery when an organization membership is removed
✅ Complete removal of personal tenancy data on account deletion

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@cla-bot cla-bot Bot added the cla-signed label Aug 2, 2026
@netlify

netlify Bot commented Aug 2, 2026

Copy link
Copy Markdown

Deploy Preview for moderaty ready!

Name Link
🔨 Latest commit 1f7582b
🔍 Latest deploy log https://app.netlify.com/projects/moderaty/deploys/6a6fd0af99c2d100083534cf
😎 Deploy Preview https://deploy-preview-49--moderaty.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 90
Accessibility: 97
Best Practices: 100
SEO: 100
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@codeant-ai

codeant-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed 1f7582b Aug 02, 2026 · 23:20 23:22
✅ Incremental review completed a43eda6 Aug 02, 2026 · 23:08 23:10
✅ Incremental review completed 9f565ce Aug 02, 2026 · 22:53 22:55
✅ Reviewed your PR fede1c1 Aug 02, 2026 · 22:42 22:44

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added organization-aware sessions displaying the active organization’s name, plan, and role.
    • Added active organization selection with automatic fallback to a valid membership.
    • Added organization membership data for switching and team navigation.
    • New accounts now receive a personal organization with owner access.
  • Bug Fixes
    • Improved recovery from stale or invalid organization selections.
    • Added clear session errors when no organization membership is available.
  • Tests
    • Added coverage for organization selection, fallback behavior, membership ordering, and session handling.

Walkthrough

The PR adds organization tenancy utilities, resolves active memberships in sessions, creates personal organizations during signup, and exposes memberships through the app layout. It also adds tests for fallback, repair, deterministic ordering, missing memberships, and transactional signup behavior.

Changes

Organization tenancy

Layer / File(s) Summary
Organization resolution utilities
src/lib/server/org.ts, src/lib/server/org.test.ts
Adds typed organization roles and contexts. Resolves active memberships with deterministic fallback. Lists normalized memberships. Reuses or creates personal organizations with owner memberships.
Organization-aware session context
src/lib/server/session.ts, src/lib/server/session.test.ts
Persists active organization IDs and returns organization-derived session data. Repairs stale organization references. Tests cover active, fallback, repaired, and missing memberships.
Signup and layout integration
src/routes/consent/+page.server.ts, src/routes/consent/consent.test.ts, src/routes/(app)/+layout.server.ts
Creates the personal organization and owner membership in the signup transaction. Links the first session to the organization. Loads memberships for authenticated app layouts.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ConsentFlow
  participant ensurePersonalOrg
  participant SignupDatabase
  participant createSession
  ConsentFlow->>ensurePersonalOrg: user identity
  ensurePersonalOrg->>SignupDatabase: reuse or create organization and owner membership
  SignupDatabase-->>ensurePersonalOrg: personal organization ID
  ConsentFlow->>createSession: user ID and organization ID
  createSession->>SignupDatabase: persist organization-linked session
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes the main change: session handling now resolves and carries the active organization.
Description check ✅ Passed The description directly explains the organization resolution, session changes, signup behavior, tests, and remaining manual verification.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mt-b-session

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

@codeant-ai

codeant-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 1f7582b1
Scan Time: 2026-08-02 23:26:35 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues

View Full Results

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Aug 2, 2026
@codeant-ai

codeant-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Sequence Diagram

This PR resolves the session user against the active organization, falls back to the oldest membership when needed, and repairs the session. The application layout also loads the user's organization memberships for future switching.

sequenceDiagram
    participant Browser
    participant App
    participant Session
    participant Organization
    participant Database

    Browser->>App: Request application page
    App->>Session: Resolve session user
    Session->>Organization: Resolve active organization
    Organization->>Database: Load organization memberships
    Database-->>Organization: Membership and organization data

    alt Active membership exists
        Organization-->>Session: Active organization context
    else Active membership is missing
        Organization-->>Session: Oldest organization context
        Session->>Database: Repair session organization
    end

    Session-->>App: User with organization and plan
    App->>Organization: Load organization list
    Organization-->>App: User organization memberships
    App-->>Browser: Render application page
Loading

Generated by CodeAnt AI

@codacy-production

codacy-production Bot commented Aug 2, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 28 complexity · 1 duplication

Metric Results
Complexity 28 (≤ 100 complexity)
Duplication 1 (≤ 1 duplication)

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR successfully implements Phase B of the multi-tenancy plan, introducing organization-level session context. The implementation is thorough and well-tested with 308 passing tests including comprehensive coverage of edge cases (zero memberships, vanished org memberships, fallback logic, and session repair).

The code quality is solid:

  • Proper error handling for data bugs (zero memberships throws rather than improvising access)
  • Deterministic fallback logic using oldest membership
  • Session repair mechanism for vanished org memberships
  • Clean separation of concerns with the new org.ts module
  • No security vulnerabilities identified

All changes align with the stated goals and maintain backward compatibility while preparing for future phases. The test coverage validates both happy paths and edge cases including mutation testing verification.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Resolve active organization during session hydration (multi-tenancy Phase B)

✨ Enhancement 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Add org tenancy helpers to resolve the active org and list memberships deterministically.
• Hydrate sessions with org id/name/role and use the active org’s plan (not legacy users.plan).
• Add session tests for fallback/repair behavior and expose org list in (app) layout load.
Diagram

graph TD
  A["Browser request"] --> B["(app) +layout.server.ts"] --> C["locals.user"]
  C --> D["session.getSessionUser"] --> E["org.resolveActiveOrg"] --> F[("DB: organizations + memberships")]
  D --> G[("DB: sessions")]
  B --> H["org.listOrgMemberships"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Push selection to SQL (ORDER BY + LIMIT)
  • ➕ Avoids loading/sorting all memberships in application code
  • ➕ Keeps deterministic fallback in the database (single query with explicit ordering)
  • ➖ Slightly trickier SQL (especially to prioritize a specific active_org_id)
  • ➖ Less straightforward to unit test without mirroring the ordering logic carefully
2. Enforce active-org validity via DB constraints/triggers
  • ➕ Prevents (or auto-repairs) invalid active_org_id at write time
  • ➕ Reduces need for runtime fallback/repair logic on every session hydration
  • ➖ Harder with memberships as a join/composite key; may require triggers
  • ➖ Adds migration/operational complexity and couples behavior tightly to SQLite specifics

Recommendation: Current approach is reasonable for Phase B: it is explicit, deterministic, and well-tested, and the runtime cost is acceptable given small per-user membership counts. If membership counts grow or hydration becomes a bottleneck, consider the SQL ordering approach to reduce data transfer and TS-side sorting; DB-level enforcement is likely overkill for now.

Files changed (4) +178 / -10

Enhancement (3) +112 / -8
org.tsAdd org tenancy helpers (role narrowing, active org resolution, membership listing) +84/-0

Add org tenancy helpers (role narrowing, active org resolution, membership listing)

• Introduces OrgContext and OrgRole, including a strict asOrgRole() that throws on unknown roles. Adds resolveActiveOrg() to pick the session active org when valid, otherwise deterministically fall back to the user’s oldest membership. Adds listOrgMemberships() ordered by membership age for the future org switcher.

src/lib/server/org.ts

session.tsHydrate sessions with active-organization context and repair invalid active_org_id +24/-6

Hydrate sessions with active-organization context and repair invalid active_org_id

• Extends SessionUser with orgId/orgName/orgRole and redefines plan as the active organization’s plan (removing the users.plan select). Updates createSession() to accept and persist an optional activeOrgId. Updates getSessionUser() to resolve the active org on every request, throw on zero memberships, and repair the session row when the stored active_org_id is no longer valid.

src/lib/server/session.ts

+layout.server.tsExpose user org membership list from (app) layout server load +4/-2

Expose user org membership list from (app) layout server load

• Makes the layout load async and returns both locals.user and the user’s org membership list via listOrgMemberships(). This data is intended for a future nav org/team switcher.

src/routes/(app)/+layout.server.ts

Tests (1) +66 / -2
session.test.tsAdd session tenancy resolution tests and update fixtures for Phase A backfill shape +66/-2

Add session tenancy resolution tests and update fixtures for Phase A backfill shape

• Extends the test DB setup to include organizations/memberships and seeds a personal org + owner membership per user. Adds tests covering: org-derived plan hydration, NULL active org fallback to oldest membership, invalid active org repair via session UPDATE, and zero-membership users throwing instead of being treated as signed-out.

src/lib/server/session.test.ts

@codeant-ai

codeant-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Latest suggestions up to commit fede1c1
CategorySuggestion                                                                                                                                    SeverityGenerated at (UTC)
Api mismatch
New accounts without a created organization receive unusable sessions and fail every authenticated request

The new invariant makes every session resolution throw when a user has no
membership, but the new-account consent transaction still creates only the user,
consent, and session. A newly registered user therefore receives a valid cookie and
is immediately rejected with a 500 on the next request. Create the personal
organization and owner membership in the same signup transaction before creating the
session, or do not enforce this invariant until that migration is complete.

src/lib/server/session.ts [110-113]

Why it matters? 🤔
  • ❌ New Google signups receive unusable authenticated sessions.
  • ❌ Dashboard requests fail after consent acceptance.
  • ❌ The consent transaction creates no organization membership.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/session.ts
**Line:** 110:113
**Comment:**
	*Api Mismatch: The new invariant makes every session resolution throw when a user has no membership, but the new-account consent transaction still creates only the user, consent, and session. A newly registered user therefore receives a valid cookie and is immediately rejected with a 500 on the next request. Create the personal organization and owner membership in the same signup transaction before creating the session, or do not enforce this invariant until that migration is complete.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Critical2026-08-02 22:44

@codacy-production codacy-production 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.

Pull Request Overview

The PR successfully implements the functional requirements for Phase B, making sessions organization-aware and handling fallback logic. However, the implementation is currently not up to standards due to a critical runtime risk in the sorting logic and performance inefficiencies.

A primary concern is the use of localeCompare on what are likely Date objects in the organization sorting logic; this will cause a runtime crash. Additionally, the application currently performs redundant database queries by fetching user memberships twice on every page load—once during session resolution and again in the layout server load. These findings, combined with inconsistent field naming and logic duplication in org.ts, should be addressed before merging to ensure stability and maintainability.

About this PR

  • While the session tests exercise the organization logic indirectly, the new 'asOrgRole' helper and the 'listOrgMemberships' sorting logic lack direct unit tests. Given the risk of runtime errors in the current sorting implementation, explicit test coverage is required.
  • There is a systemic inefficiency where organization membership data is fetched multiple times during a single request lifecycle. Since 'resolveActiveOrg' already retrieves this data to determine the fallback organization, the full list should be persisted to 'event.locals' to avoid subsequent queries in '+layout.server.ts'.

Test suggestions

  • Resolve session with a valid active organization ID
  • Fall back to oldest membership when active_org_id is null
  • Fall back to oldest membership and repair DB when active_org_id membership no longer exists
  • Fail with an error when a user has no organization memberships
  • List all organization memberships for a user sorted by creation date
  • Verify role narrowing (asOrgRole) throws on invalid input
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. List all organization memberships for a user sorted by creation date
2. Verify role narrowing (asOrgRole) throws on invalid input

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread src/lib/server/org.ts Outdated
.where(eq(memberships.userId, userId))
.all();
if (rows.length === 0) return null;
const sorted = [...rows].sort((a, b) => a.membershipCreatedAt.localeCompare(b.membershipCreatedAt));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 HIGH RISK

JavaScript sorting with localeCompare is less efficient than SQL ORDER BY and will crash at runtime if 'createdAt' is a Date object (standard for Drizzle) rather than a string. Update 'src/lib/server/org.ts' to use 'orderBy' in the Drizzle queries and remove unnecessary array spreading.

export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) throw redirect(302, '/login');
return { user: locals.user };
return { user: locals.user, orgs: await listOrgMemberships(locals.user.id) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: This layout load triggers a redundant database query. Since 'resolveActiveOrg' already fetches the full membership list during session resolution, refactor the resolution logic to carry the membership list in 'locals.user' so it can be reused here without a second network roundtrip.

Comment thread src/lib/server/org.ts Outdated
Comment on lines +75 to +80
const rows = await db
.select({ orgId: organizations.id, name: organizations.name, role: memberships.role, createdAt: memberships.createdAt })
.from(memberships)
.innerJoin(organizations, eq(memberships.orgId, organizations.id))
.where(eq(memberships.userId, userId))
.all();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: The logic for querying user memberships is duplicated between 'listOrgMemberships' and 'resolveActiveOrg', and uses inconsistent aliases (e.g., 'name' vs 'orgName'). Extract the common database query into a private helper function to ensure consistent field naming and reduce maintenance overhead.

@qodo-code-review

qodo-code-review Bot commented Aug 2, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 73 rules

Grey Divider


Remediation recommended

1. Nondeterministic membership ordering ✓ Resolved 🐞 Bug ≡ Correctness
Description
resolveActiveOrg() and listOrgMemberships() sort solely by memberships.createdAt, so if two
memberships share the same created_at string, the “oldest membership” (and org list order) can vary
because the SQL query has no ORDER BY and the JS sort has no tie-breaker. This can cause the active
org (and therefore orgRole/plan) to flip unpredictably for the same user/session when timestamps
collide.
Code

src/lib/server/org.ts[R65-66]

+	const sorted = [...rows].sort((a, b) => a.membershipCreatedAt.localeCompare(b.membershipCreatedAt));
+	const chosen = sorted.find((r) => r.orgId === activeOrgId) ?? sorted[0];
Relevance

●●● Strong

Team accepted deterministic tie-breakers for same-timestamp ordering (createdAt + id) in PR #46.

PR-#46

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
Both functions fetch memberships without SQL ordering and then apply a JS sort that only compares
the timestamp string, so ties are not deterministically ordered. The schema confirms createdAt is a
non-unique TEXT field, making same-timestamp ties plausible.

src/lib/server/org.ts[48-70]
src/lib/server/org.ts[73-84]
src/lib/server/db/schema.ts[55-67]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`resolveActiveOrg()` and `listOrgMemberships()` promise an “oldest membership first” ordering, but currently derive it by sorting only on `createdAt`. If multiple memberships have identical timestamps (possible with batched inserts or same-transaction inserts), the chosen “oldest” row can differ across runs because the DB result order is undefined without `ORDER BY`.

### Issue Context
- `memberships.createdAt` is a TEXT ISO timestamp and is not unique.
- Both org resolution and the org switcher list depend on this ordering.

### Fix Focus Areas
- src/lib/server/org.ts[48-84]
- src/lib/server/db/schema.ts[55-67]

### Suggested fix
- Prefer pushing ordering into SQL: add an `orderBy(memberships.createdAt, organizations.id)` (or `memberships.orgId`) so ties are deterministically broken.
- If keeping JS sorting, add a secondary key:
 - first compare `createdAt`
 - if equal, compare `orgId` (and/or `orgName`) to ensure a total order.
- Apply the same deterministic ordering to both `resolveActiveOrg()` and `listOrgMemberships()` so the UI list and session resolution agree.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Ambiguous fellBack semantics ✓ Resolved 🐞 Bug ⚙ Maintainability
Description
resolveActiveOrg() reports fellBack: chosen.orgId !== activeOrgId, which makes fellBack true
whenever activeOrgId is null even though no explicit org choice became invalid. The current
getSessionUser() caller guards the null case, but the flag’s meaning is ambiguous and can mislead
future callers/telemetry.
Code

src/lib/server/org.ts[R67-70]

+	return {
+		org: { orgId: chosen.orgId, orgName: chosen.orgName, orgRole: asOrgRole(chosen.role), plan: chosen.plan },
+		fellBack: chosen.orgId !== activeOrgId
+	};
Relevance

●● Moderate

No clear historical evidence on boolean flag semantics like fellBack for null inputs.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
activeOrgId is nullable, and the current boolean expression will always differ from a string orgId
when activeOrgId is null. session.ts demonstrates the current caller already adds its own
activeOrgId !== null guard to interpret the flag safely.

src/lib/server/org.ts[48-70]
src/lib/server/session.ts[106-117]
src/lib/server/db/schema.ts[31-38]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`fellBack` currently means “chosen org differs from the provided activeOrgId”, which evaluates to `true` when `activeOrgId` is `null`. If the intent is specifically “the session had an explicit active org that is no longer valid”, then `fellBack` should only be true when `activeOrgId` was non-null and didn’t match any membership.

### Issue Context
`getSessionUser()` currently avoids repairing/logging when `activeOrgId` is null, so there’s no immediate bug, but the exported API is easy to misuse later.

### Fix Focus Areas
- src/lib/server/org.ts[48-70]
- src/lib/server/session.ts[106-118]

### Suggested fix
- Decide/encode the contract:
 - If `fellBack` is meant to mean “explicit org invalidated”, change to:
   - `fellBack: activeOrgId !== null && chosen.orgId !== activeOrgId`
 - Otherwise, rename the field to something like `usedDefaultOrg` / `usedFallbackOrg` and document that `null` implies fallback.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Unconditional org list load 🐞 Bug ➹ Performance
Description
The (app) layout now always calls listOrgMemberships() when its server load runs, adding an extra
memberships+organizations join query even before the org switcher UI is used. This increases DB
work/latency on authenticated layout loads and could be deferred or made conditional if it’s not yet
needed.
Code

src/routes/(app)/+layout.server.ts[R26-28]

+export const load: LayoutServerLoad = async ({ locals }) => {
	if (!locals.user) throw redirect(302, '/login');
-	return { user: locals.user };
+	return { user: locals.user, orgs: await listOrgMemberships(locals.user.id) };
Relevance

●● Moderate

No direct precedent; past reviews focus on data minimization/authorization, not eager layout DB
query performance.

PR-#25

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The app layout’s load function unconditionally awaits listOrgMemberships(), which itself performs a
DB join query and sorts the results in JS.

src/routes/(app)/+layout.server.ts[19-29]
src/lib/server/org.ts[73-84]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`+layout.server.ts` now eagerly fetches `orgs` via `listOrgMemberships()` for every authenticated layout load execution, even though the data is described as “unused until Phase D”.

### Issue Context
`listOrgMemberships()` performs a join + in-memory sort and returns the full org list to the client.

### Fix Focus Areas
- src/routes/(app)/+layout.server.ts[19-29]
- src/lib/server/org.ts[73-84]

### Suggested fix
- If the switcher isn’t rendered yet, consider gating the fetch behind a feature flag or only fetching when the component is enabled.
- Alternative: expose a dedicated endpoint for org list and fetch lazily when the user opens the switcher.
- If keeping eager load, consider caching/memoizing org list per-request or selecting with explicit ORDER BY in SQL to avoid extra JS work.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/lib/server/org.ts Outdated
Comment thread src/lib/server/org.ts
Comment on lines +26 to +28
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) throw redirect(302, '/login');
return { user: locals.user };
return { user: locals.user, orgs: await listOrgMemberships(locals.user.id) };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Unconditional org list load 🐞 Bug ➹ Performance

The (app) layout now always calls listOrgMemberships() when its server load runs, adding an extra
memberships+organizations join query even before the org switcher UI is used. This increases DB
work/latency on authenticated layout loads and could be deferred or made conditional if it’s not yet
needed.
Agent Prompt
### Issue description
`+layout.server.ts` now eagerly fetches `orgs` via `listOrgMemberships()` for every authenticated layout load execution, even though the data is described as “unused until Phase D”.

### Issue Context
`listOrgMemberships()` performs a join + in-memory sort and returns the full org list to the client.

### Fix Focus Areas
- src/routes/(app)/+layout.server.ts[19-29]
- src/lib/server/org.ts[73-84]

### Suggested fix
- If the switcher isn’t rendered yet, consider gating the fetch behind a feature flag or only fetching when the component is enabled.
- Alternative: expose a dedicated endpoint for org list and fetch lazily when the user opens the switcher.
- If keeping eager load, consider caching/memoizing org list per-request or selecting with explicit ORDER BY in SQL to avoid extra JS work.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 3

🤖 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 `@src/lib/server/org.ts`:
- Around line 48-84: Extract the shared memberships/organizations select, join,
and user filter into a reusable helper, then have both resolveActiveOrg and
listOrgMemberships call it. Replace localeCompare-based createdAt sorting with
deterministic ISO-string ordering using the query’s
orderBy(memberships.createdAt), or an explicit < and > comparator if sorting in
memory; preserve oldest-membership ordering and existing mapping and fallback
behavior.

In `@src/lib/server/session.ts`:
- Around line 114-124: In getSessionUser, merge the activeOrgId repair and
expiry renewal updates into one sessions UPDATE by building a single update
payload from the resolved fallback and renewal conditions, then executing one
db.update(sessions).set(...).where(eq(sessions.id, token)) call when either
change is needed. Preserve the existing logging and returned expiry/renewed
values.

In `@src/routes/`(app)/+layout.server.ts:
- Around line 21-29: Update session resolution so
getSessionUser/resolveActiveOrg returns the fetched membership list and
hooks.server.ts exposes it on locals alongside locals.user. Then change the
layout load to return that locals membership data instead of calling
listOrgMemberships, removing the duplicate query while preserving the existing
unauthenticated redirect and response shape.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2c3d22e9-45bd-4057-96b3-ad57d6a872b7

📥 Commits

Reviewing files that changed from the base of the PR and between 9034aa0 and fede1c1.

📒 Files selected for processing (4)
  • src/lib/server/org.ts
  • src/lib/server/session.test.ts
  • src/lib/server/session.ts
  • src/routes/(app)/+layout.server.ts

Comment thread src/lib/server/org.ts Outdated
Comment thread src/lib/server/session.ts
Comment on lines +114 to +124
if (resolved.fellBack && row.session.activeOrgId !== null) {
console.info(`session for user ${row.user.id}: active org ${row.session.activeOrgId} no longer valid, falling back to ${resolved.org.orgId}`);
await db.update(sessions).set({ activeOrgId: resolved.org.orgId }).where(eq(sessions.id, token));
}
const user = { ...row.user, plan: resolved.org.plan, orgId: resolved.org.orgId, orgName: resolved.org.orgName, orgRole: resolved.org.orgRole };
if (expiresMs - Date.now() < RENEW_BELOW_MS) {
const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString();
await db.update(sessions).set({ expiresAt }).where(eq(sessions.id, token));
return { user: row.user, expiresAt, renewed: true };
return { user, expiresAt, renewed: true };
}
return { user: row.user, expiresAt: row.session.expiresAt, renewed: false };
return { user, expiresAt: row.session.expiresAt, renewed: false };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Merge the repair and renewal writes into one UPDATE.

Lines 116 and 121 can both execute against the same sessions row in a single getSessionUser call (repaired active org and renewed expiry). Combine them into one db.update(sessions).set({...}) call to save a round trip on this hot path.

⚡ Proposed merge
-	if (resolved.fellBack && row.session.activeOrgId !== null) {
-		console.info(`session for user ${row.user.id}: active org ${row.session.activeOrgId} no longer valid, falling back to ${resolved.org.orgId}`);
-		await db.update(sessions).set({ activeOrgId: resolved.org.orgId }).where(eq(sessions.id, token));
-	}
+	const updates: Partial<typeof sessions.$inferInsert> = {};
+	if (resolved.fellBack && row.session.activeOrgId !== null) {
+		console.info(`session for user ${row.user.id}: active org ${row.session.activeOrgId} no longer valid, falling back to ${resolved.org.orgId}`);
+		updates.activeOrgId = resolved.org.orgId;
+	}
 	const user = { ...row.user, plan: resolved.org.plan, orgId: resolved.org.orgId, orgName: resolved.org.orgName, orgRole: resolved.org.orgRole };
-	if (expiresMs - Date.now() < RENEW_BELOW_MS) {
-		const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString();
-		await db.update(sessions).set({ expiresAt }).where(eq(sessions.id, token));
-		return { user, expiresAt, renewed: true };
-	}
+	if (expiresMs - Date.now() < RENEW_BELOW_MS) {
+		const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString();
+		updates.expiresAt = expiresAt;
+		if (Object.keys(updates).length > 0) await db.update(sessions).set(updates).where(eq(sessions.id, token));
+		return { user, expiresAt, renewed: true };
+	}
+	if (Object.keys(updates).length > 0) await db.update(sessions).set(updates).where(eq(sessions.id, token));
 	return { user, expiresAt: row.session.expiresAt, renewed: false };
📝 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
if (resolved.fellBack && row.session.activeOrgId !== null) {
console.info(`session for user ${row.user.id}: active org ${row.session.activeOrgId} no longer valid, falling back to ${resolved.org.orgId}`);
await db.update(sessions).set({ activeOrgId: resolved.org.orgId }).where(eq(sessions.id, token));
}
const user = { ...row.user, plan: resolved.org.plan, orgId: resolved.org.orgId, orgName: resolved.org.orgName, orgRole: resolved.org.orgRole };
if (expiresMs - Date.now() < RENEW_BELOW_MS) {
const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString();
await db.update(sessions).set({ expiresAt }).where(eq(sessions.id, token));
return { user: row.user, expiresAt, renewed: true };
return { user, expiresAt, renewed: true };
}
return { user: row.user, expiresAt: row.session.expiresAt, renewed: false };
return { user, expiresAt: row.session.expiresAt, renewed: false };
if (resolved.fellBack && row.session.activeOrgId !== null) {
console.info(`session for user ${row.user.id}: active org ${row.session.activeOrgId} no longer valid, falling back to ${resolved.org.orgId}`);
updates.activeOrgId = resolved.org.orgId;
}
const user = { ...row.user, plan: resolved.org.plan, orgId: resolved.org.orgId, orgName: resolved.org.orgName, orgRole: resolved.org.orgRole };
if (expiresMs - Date.now() < RENEW_BELOW_MS) {
const expiresAt = new Date(Date.now() + SESSION_TTL_MS).toISOString();
updates.expiresAt = expiresAt;
if (Object.keys(updates).length > 0) await db.update(sessions).set(updates).where(eq(sessions.id, token));
return { user, expiresAt, renewed: true };
}
if (Object.keys(updates).length > 0) await db.update(sessions).set(updates).where(eq(sessions.id, token));
return { user, expiresAt: row.session.expiresAt, renewed: false };
🤖 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/server/session.ts` around lines 114 - 124, In getSessionUser, merge
the activeOrgId repair and expiry renewal updates into one sessions UPDATE by
building a single update payload from the resolved fallback and renewal
conditions, then executing one
db.update(sessions).set(...).where(eq(sessions.id, token)) call when either
change is needed. Preserve the existing logging and returned expiry/renewed
values.

Comment on lines +21 to 29
import { listOrgMemberships } from '$lib/server/org';

import type { LayoutServerLoad } from './$types';

// Everything under (app) requires a signed-in user.
export const load: LayoutServerLoad = ({ locals }) => {
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) throw redirect(302, '/login');
return { user: locals.user };
return { user: locals.user, orgs: await listOrgMemberships(locals.user.id) };
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect how locals.user is populated and whether membership rows are already available.
fd hooks.server.ts src
rg -n -B3 -A15 'getSessionUser\(' src/hooks.server.ts 2>/dev/null

Repository: Bonobo791/Moderaty

Length of output: 832


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- session symbols ---'
ast-grep outline src/lib/server/session.ts --view expanded
printf '%s\n' '--- session implementation ---'
sed -n '1,260p' src/lib/server/session.ts
printf '%s\n' '--- organization symbols ---'
ast-grep outline src/lib/server/org.ts --view expanded
printf '%s\n' '--- organization implementation ---'
sed -n '1,260p' src/lib/server/org.ts
printf '%s\n' '--- locals declarations and org-list consumers ---'
rg -n -A8 -B4 'App\\.Locals|interface Locals|listOrgMemberships|orgs' src

Repository: Bonobo791/Moderaty

Length of output: 18304


🏁 Script executed:

#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re

session = Path("src/lib/server/session.ts").read_text()
org = Path("src/lib/server/org.ts").read_text()
hooks = Path("src/hooks.server.ts").read_text()
layout = Path("src/routes/(app)/+layout.server.ts").read_text()

checks = {
    "session calls resolveActiveOrg": bool(re.search(r"resolveActiveOrg\(row\.user\.id,\s*row\.session\.activeOrgId\)", session)),
    "resolveActiveOrg selects all membership rows": bool(re.search(r"\.where\(eq\(memberships\.userId,\s*userId\)\)\s*\.all\(\)", org[org.index("export async function resolveActiveOrg"):org.index("/** Every org")])),
    "listOrgMemberships performs a second membership query": bool(re.search(r"export async function listOrgMemberships[\s\S]*?\.where\(eq\(memberships\.userId,\s*userId\)\)\s*\.all\(\)", org)),
    "hooks exposes only resolution.user": bool(re.search(r"event\.locals\.user\s*=\s*resolution\?\.user\s*\?\?\s*null", hooks)),
    "layout calls listOrgMemberships": "listOrgMemberships(locals.user.id)" in layout,
}
for name, result in checks.items():
    print(f"{'PASS' if result else 'FAIL'}: {name}")
if not all(checks.values()):
    raise SystemExit(1)
PY

Repository: Bonobo791/Moderaty

Length of output: 383


Reuse the membership rows from session resolution

resolveActiveOrg already fetches all membership and organization rows, but getSessionUser and hooks.server.ts expose only the active organization through locals.user. Return the membership list from session resolution and expose it on locals, then remove the second listOrgMemberships query from this layout.

🤖 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/routes/`(app)/+layout.server.ts around lines 21 - 29, Update session
resolution so getSessionUser/resolveActiveOrg returns the fetched membership
list and hooks.server.ts exposes it on locals alongside locals.user. Then change
the layout load to return that locals membership data instead of calling
listOrgMemberships, removing the duplicate query while preserving the existing
unauthenticated redirect and response shape.

Qodo + Codacy (valid): "oldest membership" sorted only by created_at in JS
over rows with no SQL ORDER BY — a timestamp tie (batched inserts) made the
active org (and orgRole/plan) depend on undefined row order. Both queries
now ORDER BY created_at, org_id in SQL, so session resolution and the nav
list share one total order. Mutation-checked: DESC ordering fails the
tie-break test; restored green.

Qodo (valid, minor): fellBack was true whenever activeOrgId was null, though
no explicit choice was invalidated. It now means exactly "an explicit active
org lost its membership" — the null case is the ordinary fresh-login path.

Note: the tie-break test's pre-fix behavior was order-dependent by
definition (undefined row order happened to pass); the fellBack test failed
cleanly pre-fix (expected true to be false). Both are regression locks now.
@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Aug 2, 2026
@Bonobo791

Copy link
Copy Markdown
Owner Author

Review-finding triage (executor, 9f565ce)

All 6 inline findings reviewed (Amazon Q's summary had no findings; CodeRabbit is still processing — its findings get the same treatment when they land).

✅ VALID — fixed in 9f565ce

Qodo (Bug) + Codacy (HIGH): nondeterministic "oldest membership" on created_at ties. Correct: both resolveActiveOrg and listOrgMemberships JS-sorted by createdAt alone over rows fetched with no SQL ORDER BY, so a timestamp tie (batched inserts — e.g. invite acceptance in the same millisecond) made the active org, and with it orgRole/plan, depend on undefined row order. Both queries now use ORDER BY created_at, org_id in SQL — one total order shared by session resolution and the nav list, which also covers Codacy's orderBy suggestion. Honest caveat: the tie-break test's pre-fix outcome was by definition order-dependent (undefined order happened to pass), so it can't prove red-before-green; it now locks the contract, and a DESC-ordering mutation fails it deterministically. (Codacy's crash claim — createdAt arriving as a Date object — is false: the column is TEXT, drizzle returns strings.)

Qodo (Optional): fellBack true when activeOrgId is null. Correct — the flag meant "chosen ≠ provided", which made every fresh login a "fallback". It now means exactly "an explicit active org lost its membership"; the null case is the ordinary path. This test did fail cleanly pre-fix (expected true to be false).

❌ NOT ADOPTED — answered

Codacy (MEDIUM) + Qodo (Optional): listOrgMemberships in the layout load is a redundant/eager query. True but by design: the approved plan's step B4 ships the eager load now precisely so Phase D is UI-only, and it is a cheap indexed join once per authenticated request. Carrying the membership list inside SessionUser or gating behind a flag deviates from the approved plan for a micro-optimization — happy to revisit as a plan amendment if the maintainer wants it.

Codacy (MEDIUM): extract a shared membership-query helper (name vs orgName alias mismatch). The two queries are plan-specified verbatim with different projections (resolution needs plan+timestamps, the list doesn't). Extracting a helper now adds indirection without removing a real bug; the alias difference is intentional (each function's return shape).

310 tests green, npm run check/build clean, codacy-analysis 0 issues on touched files.

@Bonobo791

Copy link
Copy Markdown
Owner Author

Review-finding triage round 2 (executor, a43eda6)

CodeRabbit's three inline findings (posted against fede1c1) and CodeAnt's critical suggestion, re-checked against current code:

  • VALID — CodeAnt Critical (session.ts: new signups get unusable sessions). The consent transaction created user + consent + session but no org/membership, and Phase B makes getSessionUser throw on zero memberships — every new signup would 500 on its first authenticated request once main auto-deploys. Fixed: the new-account transaction now calls ensurePersonalOrg (creates the personal org named after the user + owner membership, matching the 0012 backfill; idempotent so the concurrent same-sub signup race finds the winner's org) and starts the session with activeOrgId set. Reproducing test added first (consent.test.ts: signup yields org + owner membership + session resolves into it), watched red, then green; rollback test extended to prove org/membership writes roll back with the transaction. Mutation-checked (owner→member flip fails the test).
  • PARTIALLY VALID — CodeRabbit org.ts:84 (dedup + localeCompare). The localeCompare claim was stale — round 1 (9f565ce) already moved ordering into SQL .orderBy(createdAt, orgId); nothing locale-sensitive remains. The dedup half was valid: after that fix both functions ran identical select/join/where/orderBy. Fixed: shared private fetchMembershipRows now backs both resolveActiveOrg and listOrgMemberships (project rule: no copy-paste).
  • VALID (trivial) — CodeRabbit session.ts:124 (merge repair + renewal UPDATEs). Fixed: both writes now build one updates payload and issue a single UPDATE when they coincide. No observable behavior change (pure round-trip savings), so no new failing test is possible; existing repair/renewal tests cover behavior.
  • NOT VALID — CodeRabbit layout.server.ts:29 (reuse membership rows from session resolution). Same finding as round 1, same verdict: locals.user deliberately carries only the ACTIVE org — putting the full membership list on locals would bloat every request's hydration for a nav-switcher-only concern. The layout's listOrgMemberships call is one indexed query per page load; that is the right seam.

Gates: 311 tests green (310 + 1 new), npm run check 0 errors, npm run build clean, codacy 0 issues on changed files. Amazon Q approved; SonarCloud gate passed.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Aug 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 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 `@src/routes/consent/`+page.server.ts:
- Around line 96-101: Shorten the contiguous transaction comment surrounding the
same-sub sign-up flow in the server consent handler to fewer than 500
characters. Preserve only the essential transactional guarantees and
retry/idempotency behavior, and move any remaining durable design rationale to
project documentation if needed.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ce15a27b-be88-46ab-8e31-ddeaa443aa07

📥 Commits

Reviewing files that changed from the base of the PR and between 9f565ce and a43eda6.

📒 Files selected for processing (4)
  • src/lib/server/org.ts
  • src/lib/server/session.ts
  • src/routes/consent/+page.server.ts
  • src/routes/consent/consent.test.ts

Comment on lines +96 to +101
// same-sub sign-up; ensurePersonalOrg makes that race idempotent
// too. Every user needs a personal org — session resolution fails
// loudly on zero memberships. User, personal org, consent record,
// and first session commit as ONE unit — a session-write failure
// rolls everything back and the parked cookie lets the same
// submission retry cleanly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep the transaction comment below 500 characters.

The contiguous comment at Lines 89-101 exceeds the allowed length. Shorten it or move durable design rationale to project documentation.

As per coding guidelines, "Do not store comment text longer than 500 characters."

🤖 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/routes/consent/`+page.server.ts around lines 96 - 101, Shorten the
contiguous transaction comment surrounding the same-sub sign-up flow in the
server consent handler to fewer than 500 characters. Preserve only the essential
transactional guarantees and retry/idempotency behavior, and move any remaining
durable design rationale to project documentation if needed.

Source: Coding guidelines

@Bonobo791

Copy link
Copy Markdown
Owner Author

Phase A/B database-engineering review (executor, 1f7582b)

Full pass over Phase A (migration 0012 + schema) and Phase B using the sqlite-engineering checklists (field-failures pre-flight, migrations, schema-design, runtimes, indexing). Findings and verdicts:

FIXED — account deletion left tenancy behind (PII). deleteUserRecords predates orgs: users are tombstoned, not deleted, so ON DELETE CASCADE never fires. After deletion, the personal org survived with name = the user's real display name (PII the deletion policy promises to wipe), plus dangling memberships and invites.created_by rows. Fix: the same transaction now deletes invites + memberships for the personal org(s), the personal org(s) themselves, invites created by the user, and any remaining membership rows — explicit child-to-parent deletes, no FK reliance. Tests: seeded org/membership/invite per fixture; erasure test and survivor-isolation test went red before the fix, green after; mutation-checked (dropping the org delete → 2 failures). 311 tests green, check/build clean, codacy 0 issues.

VERIFIED CLEAN:

  • Migration journal intact: 13 entries 0000–0012, no gaps/rewrites; the 0012 edit (composite-key guard) changed no DDL, and dev/prod backfill results are byte-identical (memberships was empty at apply time).
  • Dev Turso: PRAGMA foreign_keys = ON; backfill complete (2 orgs, 2 memberships, 0 channels missing org_id).
  • Query plan for the new Phase B membership lookup: SEARCH m USING INDEX sqlite_autoindex_memberships_1 (user_id=?) + SEARCH o USING INDEX ... (id=?); temp B-tree for ORDER BY is fine at per-user membership cardinality.
  • Drizzle migrator is timestamp-based (created_at < folderMillis) — the 0012 hash divergence could never block future migrations.
  • hooks.server.ts converts the zero-membership throw into a loud 500 (no silent sign-out), as designed.

RECONCILED — dev __drizzle_migrations hash for 0012 updated to match the file on disk (audit-trail hygiene only; the migrator never reads it). Prod note for the maintainer: if moderaty (prod) applied 0012 before the guard fix landed, its recorded hash will differ from the file too — same one-line UPDATE reconciles it; if prod applied after, it already matches.

ADVISORY (not fixed, needs maintainer decision): memberships.role / invites.role / organizations.plan have no CHECK constraints and the new tables are non-STRICT (matches repo legacy style; asOrgRole validates at read time). Hardening requires table rebuilds — recommend deferring to the contract-phase migration.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Aug 2, 2026
@sonarqubecloud

sonarqubecloud Bot commented Aug 2, 2026

Copy link
Copy Markdown

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

Labels

cla-signed size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant