Skip to content

Initial app setup with databases, auth and testing#2

Open
nealhorner wants to merge 28 commits into
mainfrom
dev
Open

Initial app setup with databases, auth and testing#2
nealhorner wants to merge 28 commits into
mainfrom
dev

Conversation

@nealhorner

@nealhorner nealhorner commented May 3, 2026

Copy link
Copy Markdown
Owner

Initial app setup with databases, auth, and testing.

Summary by CodeRabbit

  • New Features

    • Full auth integration (email + OAuth + admin signup), first-time setup API and UI, and desktop mode selection (local vs remote) with local sidecar and remote login window.
  • Documentation

    • Added CONTRIBUTING guide; README updated with server env quick reference and E2E instructions.
  • Tests

    • Playwright E2E suites for web and desktop; new dev E2E scripts and CI job rework.
  • Chores

    • Env schema, DB schema migrations, package scripts, and tooling/config updates.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds Better Auth integration, migrates application auth from a custom session model to Better Auth (schema, adapters, runtime), separates Postgres/SQLite env vars, implements desktop local/remote mode with Tauri-sidecar management, and introduces comprehensive Playwright E2E suites plus supporting dev scripts and docs.

Changes

Better Auth, Desktop Modes, DB split, and E2E

Layer / File(s) Summary
Env / Manifest / Types
.env.schema, .gitignore, .vscode/settings.json, apps/web/src/app.d.ts, package.json
Adds POSTGRES_DATABASE_URL, SQLITE_DATABASE_URL, BETTER_AUTH_SECRET, keeps .env.schema whitelisted, updates gitignore and editor words, adds env type reference for web, and adds global scripts/dev deps.
Data Shape / Prisma migrations
packages/prisma/prisma/postgres/schema.prisma, packages/prisma/prisma/postgres/migrations/*, packages/prisma/seed.ts, packages/prisma/prisma.config.ts, packages/prisma/prisma.config.sqlite.ts
Replaces legacy User/Session models with Better Auth models (user, session, account, verification) and introduces AppUser (int) linked by authUserId; adds setupCompleteAt and authSettings; updates seed/config to prefer POSTGRES_DATABASE_URL and adds a SQLite config file.
Core Server Implementation
apps/server/src/lib/betterAuth.ts, apps/server/src/lib/db.ts, apps/server/src/lib/initial-setup.ts, apps/server/src/lib/setup-config.ts, apps/server/src/env.ts, apps/server/src/lib/platform.ts
Creates configured better-auth instance (providers/plugins/trusted origins), switches DB connection preference to POSTGRES_DATABASE_URL, implements first-time setup orchestration, instance config loader, local-only mode detection, and cryptographic desktop request verification.
Session & Routes Wiring
apps/server/src/middleware/session.ts, apps/server/src/routes/v1/auth.ts, apps/server/src/routes/v1/setup.ts, apps/server/src/routes/v1/index.ts, apps/server/src/hono-env.d.ts, apps/server/src/index.ts
Replaces manual session handling with auth.api calls (getSession/signIn/signOut), updates dev-login/logout flows, adds /setup routes (/status, /complete), exposes /public-config, includes isTenantAdmin in context, and routes /api/auth/* to Better Auth handler.
Desktop Tauri Core
apps/desktop/src-tauri/src/lib.rs, apps/desktop/src-tauri/capabilities/default.json, apps/desktop/src-tauri/Cargo.toml
Adds persisted DesktopConfig/AppMode, Tauri commands (get_desktop_config, set_desktop_config, generate_local_display_name, open_remote_login_window), sidecar lifecycle (start/stop/wait), remote_auth window capability, and rand dependency.
Desktop API client & Local sidecar
apps/desktop/src/lib/api.ts, packages/desktop-local-api/src/db.ts, packages/desktop-local-api/src/index.ts
Centralizes Tauri invocations via tauriInvoke (with E2E hook), implements mode-aware apiBaseUrl/apiFetch, prefers SQLITE_DATABASE_URL, and passes SQLITE_DATABASE_URL/PORT to sidecar process.
Desktop UI / Routes
apps/desktop/src/lib/desktopConfig.ts, apps/desktop/src/routes/+layout.svelte, apps/desktop/src/routes/+page.svelte, apps/desktop/src/routes/connect/+page.svelte
Adds desktop config types; layout loads config and guards remote sessions; onboarding page enables choosing local vs remote and persists config; connect page performs remote sign-in and can open remote login webview.
Web client auth & UX
apps/web/src/lib/auth-client.ts, apps/web/src/routes/login/+page.svelte, apps/web/src/routes/setup/+page.svelte, apps/web/src/routes/welcome/+page.svelte, apps/web/src/routes/+layout.svelte, apps/web/src/lib/shell.ts
Adds Better Auth client for web, converts login to multi-provider flows (email/password + social/OIDC), implements setup page driven by /api/v1/setup, updates layout guards and tenant label, and narrows home-path logic.
E2E infra & Dev scripts
apps/desktop/e2e/*, apps/server/e2e/*, apps/desktop/playwright.config.mjs, apps/server/playwright.config.mjs, scripts/dev-e2e.mjs, scripts/dev-e2e-desktop.mjs, apps/desktop/turbo.json, apps/desktop/vite.config.js
Adds Playwright configs and desktop E2E bridge, global-setup scripts that reset Prisma using POSTGRES_DATABASE_URL, dev webserver scripts with health checks and port conflict handling, Turbo e2e task and Vite test exclusions, and package script updates to run E2E.
Tests & Test adapters
apps/server/src/__tests__/*, apps/server/src/__tests__/integration/*, apps/server/vitest.config.ts, packages/desktop-local-api/src/__tests__/*, apps/desktop/e2e/*, apps/server/e2e/*
Updates unit/integration tests to use Better Auth mocks (auth.api.*), adds cookie header helper, adjusts integration expectations, sets test env to use POSTGRES_DATABASE_URL, and updates SQLite test setup to use SQLITE_DATABASE_URL.
CI, Docs & Hooks
.github/workflows/ci.yml, CONTRIBUTING.md, README.md, .husky/pre-commit, .prettierignore, turbo.json
Adds varlock action to CI, splits integration/E2E job, documents contribution/E2E/setup steps, updates README references to POSTGRES_DATABASE_URL and server env quick reference, updates pre-commit, and ignores Tauri build output for Prettier.

Sequence Diagrams

sequenceDiagram
    participant Desktop UI
    participant Tauri Runtime
    participant Node Sidecar
    participant Server API
    participant Database

    Desktop UI->>Tauri Runtime: start_local_api()
    Tauri Runtime->>Tauri Runtime: Read DesktopConfig
    alt mode != "local"
        Tauri Runtime->>Desktop UI: error/deny
    else mode == "local"
        Tauri Runtime->>Node Sidecar: spawn process with PORT + SQLITE_DATABASE_URL
        Node Sidecar->>Node Sidecar: wait for TCP ready
        Tauri Runtime->>Tauri Runtime: cache base_url
        Desktop UI->>Server API: GET /api/v1/me (same-origin)
        Server API->>Database: resolve session/appUser via Better Auth
        Server API->>Desktop UI: user + isTenantAdmin
    end
Loading
sequenceDiagram
    participant Web UI
    participant Server API
    participant Better Auth
    participant Database

    Web UI->>Server API: GET /api/v1/setup/status
    Server API->>Database: check tenant.setupCompleteAt
    Server API->>Web UI: { needsSetup, prefill, setupTokenConfigured }
    Web UI->>Server API: POST /api/v1/setup/complete (authSettings, admin creds)
    Server API->>Better Auth: auth.api.signUpEmail(admin)
    Better Auth->>Database: create auth user
    Server API->>Database: create tenant, AppUser, org/project/board/ticket
    Server API->>Web UI: 201 { ok: true }
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • nealhorner/trackr#1: Related — both PRs modify the same server auth and database code (e.g., apps/server/src/env.ts, middleware/session.ts, db.ts and Prisma schema), with the main PR evolving Phase 1’s session/DB setup into a Better Auth–backed integration.

Poem

🐇 I hopped through schemas, secrets, and ports,

Switched sessions to Better Auth of all sorts,
Desktop chose local or remote with cheer,
E2E checks the flows we hold dear,
A carrot for tests — all green from here.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

@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: 13

🧹 Nitpick comments (16)
apps/server/src/lib/setup-config.ts (1)

27-36: ⚡ Quick win

Consider wrapping file parsing in try-catch for clearer error messages.

Both JSON.parse() and instanceConfigSchema.parse() can throw. If the config file is malformed or fails validation, the error won't indicate which file caused the issue. Adding a try-catch with a descriptive error message would help operators debug configuration problems.

Suggested improvement
 export function loadInstanceConfigFile(): InstanceConfigFile | null {
   const p = process.env.TRACKR_CONFIG_PATH;
   if (!p) return null;
   if (!existsSync(p)) {
     return null;
   }
-  const raw = readFileSync(p, "utf8");
-  const j = JSON.parse(raw) as unknown;
-  return instanceConfigSchema.parse(j);
+  try {
+    const raw = readFileSync(p, "utf8");
+    const j = JSON.parse(raw) as unknown;
+    return instanceConfigSchema.parse(j);
+  } catch (err) {
+    throw new Error(`Failed to load config from ${p}: ${err instanceof Error ? err.message : err}`);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/lib/setup-config.ts` around lines 27 - 36, Wrap the JSON
parsing and schema validation inside loadInstanceConfigFile in a try-catch to
surface a clear, contextual error that includes the TRACKR_CONFIG_PATH;
specifically catch errors from JSON.parse(raw) and instanceConfigSchema.parse(j)
and rethrow (or throw a new Error) with a message like "Failed to
load/parse/validate config at <path>: <original error message>" so operators
know which file failed (refer to process.env.TRACKR_CONFIG_PATH, JSON.parse, and
instanceConfigSchema.parse to locate the code).
apps/desktop/e2e/global-setup.ts (1)

19-20: ⚡ Quick win

Fragile CWD-based repo-root resolution

path.resolve(process.cwd(), "..", "..") relies on process.cwd() being apps/desktop at the time Playwright's global setup runs. Turbo sets this correctly when invoking the workspace script, but running npx playwright test directly from the repo root (or any other directory) resolves two levels above the repo root, causing execSync to fail with a non-existent path.

Use the setup file's own location instead:

♻️ Proposed fix
 import { execSync } from "node:child_process";
 import path from "node:path";
+import { fileURLToPath } from "node:url";

+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);

 export default async function globalSetup(): Promise<void> {
   ...
-  const repoRoot = path.resolve(process.cwd(), "..", "..");
+  // __dirname = apps/desktop/e2e → ../../.. = repo root
+  const repoRoot = path.resolve(__dirname, "../../..");
   const prismaDir = path.join(repoRoot, "packages", "prisma");

If the file is compiled to CJS (verify with the db.ts check above), __dirname is already available and no shim is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/e2e/global-setup.ts` around lines 19 - 20, The repoRoot
calculation is fragile because it uses path.resolve(process.cwd(), "..", "..")
which breaks when tests are run from other CWDs; update repoRoot to derive from
the setup file location instead (use __dirname if the compiled bundle is CJS,
otherwise compute from import.meta.url via fileURLToPath and path.dirname), and
then set prismaDir = path.join(repoRoot, "packages", "prisma") as before; change
the code around the repoRoot and prismaDir definitions (replace usage of
process.cwd() and reference path.resolve(process.cwd(), "..", "..")) so the path
is computed from the file's directory rather than the current working directory.
apps/server/src/__tests__/auth-routes.test.ts (1)

59-87: 💤 Low value

Consider adding a test case for when the app user is not found.

The test covers the success path where findFirstAppUser returns a user with authUserId. Consider adding coverage for the scenario where no matching app user exists (returns null), to verify the expected error response.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/__tests__/auth-routes.test.ts` around lines 59 - 87, Add a
new unit test in apps/server/src/__tests__/auth-routes.test.ts that mocks
findFirstAppUser to return null and issues a POST to the /api/v1/auth/dev-login
route (same request shape as the existing test); assert the response status and
body represent the expected error (e.g., 404 or 400 and an error message), and
verify that signInEmail is not called. Use the same test harness/makeApp()
setup, and reference findFirstAppUser and signInEmail in the assertions to
ensure the negative path is covered.
apps/server/e2e/global-setup.ts (1)

18-32: 💤 Low value

Consider adding error handling for the execSync call.

If prisma migrate reset fails (e.g., connection timeout, invalid credentials), the error propagates and halts the test run, which is acceptable. However, a clearer error message could help debugging CI failures.

♻️ Optional: wrap with try-catch for clearer error context
+  try {
     execSync(
       "npx prisma migrate reset --force --skip-seed --config prisma.config.ts",
       {
         cwd: prismaDir,
         stdio: "inherit",
         env: {
           ...process.env,
           POSTGRES_DATABASE_URL: dbUrl,
         },
       },
     );
+  } catch (err) {
+    console.error("[playwright global-setup] prisma migrate reset failed");
+    throw err;
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/e2e/global-setup.ts` around lines 18 - 32, Wrap the execSync call
that runs "npx prisma migrate reset ..." in a try-catch inside
apps/server/e2e/global-setup.ts so failures provide clearer context; catch the
error around the execSync invocation (the block that uses prismaDir and env with
POSTGRES_DATABASE_URL/dbUrl), log a descriptive message including prismaDir and
the caught error (e.g., via console.error or existing logger), and then rethrow
or call process.exit(1) to fail fast with the improved message.
apps/desktop/src/routes/+layout.svelte (1)

45-48: 💤 Low value

Consider caching config to avoid redundant reads on every navigation.

afterNavigate calls getDesktopConfig() on every navigation, which could be inefficient if it reads from disk/storage each time. Consider only reloading when necessary (e.g., after settings changes) or caching the config.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/routes/`+layout.svelte around lines 45 - 48, afterNavigate
currently calls getDesktopConfig() on every navigation causing redundant reads;
change this by caching the result (e.g., store the config in the existing cfg
variable or a Svelte writable store) and only call getDesktopConfig() if cfg is
unset or explicitly invalidated. Update the afterNavigate handler to use the
cached cfg, add a clear/invalidateConfig function or listen for settings-change
events to force a reload, and keep guardRemoteSession() as-is but ensure it uses
the cached cfg when needed so navigation no longer triggers disk/storage reads
each time.
apps/server/src/lib/initial-setup.ts (2)

81-85: 💤 Low value

Minor: batch column creation with createMany.

The sequential prisma.boardColumn.create calls in a loop could be replaced with a single prisma.boardColumn.createMany for slightly better performance during setup.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/lib/initial-setup.ts` around lines 81 - 85, Replace the
sequential prisma.boardColumn.create loop in initial-setup.ts with a single
prisma.boardColumn.createMany call: map the existing columns array to include
the boardId for each item (e.g., columns.map(c => ({ boardId: board.id, ...c
}))), call await prisma.boardColumn.createMany({ data: mappedColumns }) and
adjust any code that relied on created records (createMany returns a count, not
created rows); keep using the same symbols (columns, board.id,
prisma.boardColumn.create -> createMany) and await the promise for proper setup
sequencing.

22-106: ⚖️ Poor tradeoff

Consider wrapping seed operations in a transaction.

After the appUser is created (line 59), failures in subsequent operations (org, project, board, columns, member, ticket) will leave the database in an inconsistent state. The tenant won't be rolled back, but the seeded defaults may be partially created.

For first-time setup this is unlikely to cause user-facing issues since a retry would fail on duplicate email. However, wrapping lines 69-105 in a prisma.$transaction() would ensure atomicity of the default data seeding.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/lib/initial-setup.ts` around lines 22 - 106, The seed steps
after creating appUser (admin) in runFirstTimeSetup are not atomic; wrap the
subsequent org/project/board/columns/projectMember/ticket creation and the final
tenant.update in a single Prisma transaction so partial seeds can't be left
behind. Replace the multiple prisma.* calls (organization.create,
project.create, board.create, boardColumn.create loop, projectMember.create,
boardColumn.findFirstOrThrow, ticket.create, tenant.update) with a
prisma.$transaction callback and use the transaction client (e.g.,
tx.organization.create, tx.project.create, etc.) inside it so all creations and
the tenant update succeed or roll back together.
apps/web/src/routes/setup/+page.svelte (1)

36-41: 💤 Low value

Simplify the auth settings prefill loop.

The current implementation uses type casting and non-null assertions. A cleaner approach would use Object.assign with validated values.

💡 Simplified prefill
     if (p?.authSettings) {
-      for (const k of Object.keys(auth) as (keyof AuthFlags)[]) {
-        if (typeof p.authSettings![k] === "boolean")
-          (auth as Record<string, boolean>)[k] = p.authSettings![k] as boolean;
-      }
+      Object.assign(auth, {
+        password: p.authSettings.password ?? auth.password,
+        google: p.authSettings.google ?? auth.google,
+        apple: p.authSettings.apple ?? auth.apple,
+        github: p.authSettings.github ?? auth.github,
+        okta: p.authSettings.okta ?? auth.okta,
+      });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/setup/`+page.svelte around lines 36 - 41, The loop that
copies p.authSettings into auth (checking p?.authSettings and iterating keys)
can be simplified: validate p.authSettings exists, then build an object
containing only keys whose values are boolean and merge it into auth via
Object.assign. Update the code that references p.authSettings, auth, and
AuthFlags to avoid non-null assertions and casts by filtering
Object.entries(p.authSettings) for typeof value === "boolean" and then
Object.assign(auth, filteredObject).
apps/desktop/e2e/onboarding.spec.ts (1)

29-41: 💤 Low value

Consider extracting duplicate credentials to a constant.

The admin credentials are duplicated between completeSetupIfNeeded (lines 34-35) and the test (lines 55-56). While acceptable for test readability, extracting to a constant would reduce drift risk.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/e2e/onboarding.spec.ts` around lines 29 - 41, Duplicate admin
credentials are hard-coded in the test fetch body and in completeSetupIfNeeded;
extract them into shared constants (e.g., ADMIN_EMAIL, ADMIN_PASSWORD,
TENANT_NAME or a single SETUP_CREDENTIALS object) and reference those constants
from both the POST payload in the fetch to /api/v1/setup/complete and from the
completeSetupIfNeeded helper so both places use the same source of truth and
avoid drift.
apps/desktop/src/routes/+page.svelte (1)

35-52: 💤 Low value

Consider validating the remote URL before saving.

The chooseRemote function trims the URL but doesn't validate its format before persisting. While <input type="url"> provides browser-level validation, it's bypassed if called programmatically. Consider adding a basic URL validation check.

💡 Optional validation
   async function chooseRemote() {
     err = "";
     busy = true;
     try {
+      const trimmed = remoteUrl.trim();
+      try {
+        new URL(trimmed);
+      } catch {
+        err = "Invalid URL format";
+        return;
+      }
       await setDesktopConfig({
         version: 1,
         mode: "remote",
-        remoteBaseUrl: remoteUrl.trim(),
+        remoteBaseUrl: trimmed,
         localDisplayName: null,
       });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/routes/`+page.svelte around lines 35 - 52, The chooseRemote
function currently trims remoteUrl but does not validate it before calling
setDesktopConfig; add a validation step using a robust check (e.g., attempt new
URL(trimmed) or a strict regex) against the trimmed remoteUrl and if it fails
set err to a helpful message and return early (do not call setDesktopConfig or
goto), otherwise proceed to call setDesktopConfig with remoteBaseUrl set to the
trimmed value; update references in this flow (chooseRemote, remoteUrl,
setDesktopConfig, getDesktopConfig) accordingly so invalid URLs are rejected
before persisting.
apps/server/src/middleware/session.ts (1)

10-24: 💤 Low value

Consider adding error handling around the Better Auth API call.

If auth.api.getSession throws an unexpected error (e.g., network issue to auth service, malformed headers), this middleware will propagate the exception and fail the request. For resilience, consider wrapping the call in a try-catch so that transient auth failures gracefully fall back to an unauthenticated state rather than causing 500 errors.

♻️ Proposed defensive handling
 export const loadSession = createMiddleware(async (c, next) => {
-  const s = await auth.api.getSession({ headers: c.req.raw.headers });
-  if (!s?.user?.id) {
+  let s: Awaited<ReturnType<typeof auth.api.getSession>> | null = null;
+  try {
+    s = await auth.api.getSession({ headers: c.req.raw.headers });
+  } catch {
+    // Treat auth failures as unauthenticated
+  }
+  if (!s?.user?.id) {
     c.set("authUser", null);
     return next();
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/middleware/session.ts` around lines 10 - 24, Wrap the call to
auth.api.getSession inside a try-catch in the loadSession middleware so
transient or unexpected errors from auth.api.getSession do not bubble up; on
catch, log the error (or use c.log/processLogger) and call c.set("authUser",
null) then return next() to treat the request as unauthenticated; keep the
existing logic for prisma.appUser.findFirst unchanged so only failures from
auth.api.getSession are handled defensively.
apps/desktop/src/lib/api.ts (1)

70-81: 💤 Low value

Redundant config fetch in apiFetch.

apiFetch calls getDesktopConfig() on line 74, then apiBaseUrl() on line 75, but apiBaseUrl() also calls getDesktopConfig() internally. While caching prevents duplicate Tauri invocations after the first call, this adds unnecessary await overhead and is misleading.

Consider refactoring to fetch config once and pass it to a variant of apiBaseUrl.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/lib/api.ts` around lines 70 - 81, apiFetch currently calls
getDesktopConfig() and then apiBaseUrl(), but apiBaseUrl() calls
getDesktopConfig() again causing redundant awaits; refactor by fetching the
config once in apiFetch and pass it into a new or overloaded helper (e.g., add
apiBaseUrlFromConfig(config: DesktopConfig) or change apiBaseUrl to accept an
optional config parameter) so apiFetch uses the single config instance to
compute base URL and set credentials; update apiFetch to call
apiBaseUrlFromConfig(config) (or apiBaseUrl(config)) and remove the second
getDesktopConfig() call, and adapt any callers of the modified apiBaseUrl
signature accordingly.
apps/web/src/routes/login/+page.svelte (2)

62-66: ⚡ Quick win

Error response parsing may lose structured error information.

The server may return a JSON error body (e.g., { "error": "...", "message": "..." }), but the code reads the raw text. This could display raw JSON to users or miss the actual error message.

♻️ Proposed improvement for error parsing
       if (!res.ok) {
-        const t = (await res.text().catch(() => "")) || "Sign in failed";
-        err = t;
+        try {
+          const body = await res.json();
+          err = body.message ?? body.error ?? "Sign in failed";
+        } catch {
+          err = "Sign in failed";
+        }
         return;
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/login/`+page.svelte around lines 62 - 66, Replace the raw
text-only error handling for the fetch response (the res / t / err block) with
logic that first attempts to parse res.json() and extract a user-friendly
message from common fields like message or error, falling back to res.text() and
then a default "Sign in failed"; set err to that extracted string so structured
JSON errors are shown instead of raw JSON. Ensure any JSON parse errors are
caught and ignored so the fallback to text/default still occurs.

30-45: 💤 Low value

No loading indicator during initial config fetch.

The onMount fetch happens asynchronously while the form is already rendered with default auth values. This could cause a brief flash of the password form before OAuth buttons appear (or vice versa), and the redirect to /setup happens after the page is visible.

Consider adding a loading state to avoid layout shifts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/routes/login/`+page.svelte around lines 30 - 45, The page
renders with default auth values while onMount fetches /api/v1/public-config,
causing a flash and delayed redirect; introduce a loading boolean (e.g.,
loading) initialized true, set it before/after the fetch inside onMount around
the fetch("/api/v1/public-config") call, and only render the form/UI when
loading is false (or render a spinner when true); if j.data.setupComplete is
false call goto("/setup") before clearing loading (so redirect occurs
immediately) and still update setupComplete, auth, and authAvailable after the
fetch. Ensure you update references to setupComplete/auth/authAvailable
rendering logic to wait for loading to be false.
apps/desktop/src-tauri/src/lib.rs (2)

125-135: ⚡ Quick win

Silent failure when stopping sidecar if lock is poisoned.

The if let Ok(...) pattern silently ignores lock poisoning. If a previous thread panicked while holding the lock, stop_sidecar won't kill the child process, potentially leaving orphaned processes.

Consider using .lock().unwrap_or_else(|e| e.into_inner()) to recover from poisoned locks, or at least log a warning.

♻️ Proposed fix to handle poisoned locks
 fn stop_sidecar(state: &State<SidecarState>) {
-    if let Ok(mut c) = state.child.lock() {
+    let mut c = state.child.lock().unwrap_or_else(|e| e.into_inner());
-        if let Some(ref mut ch) = *c {
-            let _ = ch.kill();
-        }
-        *c = None;
+    if let Some(ref mut ch) = *c {
+        let _ = ch.kill();
     }
-    if let Ok(mut u) = state.base_url.lock() {
-        *u = None;
-    }
+    *c = None;
+    let mut u = state.base_url.lock().unwrap_or_else(|e| e.into_inner());
+    *u = None;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src-tauri/src/lib.rs` around lines 125 - 135, The stop_sidecar
function currently ignores poisoned mutexes by using if let Ok(...) which can
skip killing the child process; change the two lock acquisitions on state.child
and state.base_url to recover from poisoned locks (e.g. use
.lock().unwrap_or_else(|e| e.into_inner()) or equivalent) so you still obtain
the mutex guard, kill the child (call ch.kill()) and clear the Option, and add a
warning log when a PoisonError is encountered to aid debugging; reference the
stop_sidecar function and the state.child and state.base_url mutexes when making
the change.

233-249: 💤 Low value

Closing existing window may race with building the new one.

Line 237-238 closes an existing remote_auth window, then immediately builds a new one with the same label. If the close is asynchronous, there could be a brief conflict. Consider using set_url on an existing window instead of close + recreate, or await the close completion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src-tauri/src/lib.rs` around lines 233 - 249, The current
open_remote_login_window closes an existing window returned by
app.get_webview_window("remote_auth") then immediately recreates it, which can
race; instead, detect the existing window
(app.get_webview_window("remote_auth")) and if present call its set_url or
navigate method to point to the new login URL (reusing the window) or, if you
must close and recreate, await/confirm the completion of w.close() before
calling WebviewWindowBuilder::new; update open_remote_login_window to use
w.set_url(WebviewUrl::External(login)) when w exists or await the close future
prior to building a new WebviewWindowBuilder to avoid label conflict.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src-tauri/src/lib.rs`:
- Around line 212-231: start_sidecar currently spawns the Node sidecar and
returns base_url immediately, which can cause “connection refused” errors; after
spawning the child in start_sidecar (and after setting *state.child and before
setting/returning *state.base_url), poll the sidecar until it accepts
connections by repeatedly attempting a simple TCP connect to 127.0.0.1:port or a
minimal HTTP GET to base_url with a short timeout/backoff (e.g., try ~50 times
with sleep 100ms), and only set *state.base_url and return Ok(base_url) once a
probe succeeds; on timeout/failed readiness, kill the spawned child process,
clear the child state, and return an Err describing the readiness failure.
Ensure you reference start_sidecar, SidecarState (state.child/state.base_url),
the local port variable, and add the needed blocking probe utility (or enable
reqwest blocking) if you choose HTTP probing.
- Around line 99-110: The current generate_local_display_name function
deterministically derives an index from SystemTime, causing collisions and
limited uniqueness; replace the time-based selection with a proper random source
(e.g., use the rand crate or UUIDs) to pick adjective and noun or append a short
random suffix. Update generate_local_display_name to use rand::thread_rng() (or
uuid::Uuid::new_v4()) to select indices into ADJ and NOUN (refer to ADJ and NOUN
arrays) or to generate a unique hex/short-id, and add the chosen dependency to
Cargo.toml; remove the duration_since fallback logic since it will no longer be
needed. Ensure the function remains annotated with #[tauri::command] and still
returns a String like "local-<adj>-<noun>-<rand>" or
"local-<adj>-<noun>-<shortid>".

In `@apps/desktop/src/routes/`+layout.svelte:
- Around line 29-38: The guardRemoteSession function should handle network/fetch
errors when calling fetch(`${base}/api/v1/me`): wrap the await fetch call in a
try/catch (or handle rejection) so network failures don't cause unhandled
promise rejections; on error, log or silently handle the failure and navigate to
the connection flow (use goto("/connect")) or otherwise return early;
specifically update guardRemoteSession to catch errors from fetch for
cfg.remoteBaseUrl and use $page.url.pathname and goto to keep the same control
flow.

In `@apps/desktop/src/routes/connect/`+page.svelte:
- Around line 22-50: The submit function currently uses try/finally without a
catch so thrown network/CORS errors from the first fetch or the me fetch get
swallowed and err is never set; add a catch block around the existing
try/finally in submit to catch any thrown Error (or non-Response exceptions) and
set err to a helpful message (e.g., error.message or a fallback like "Network
error during sign in"), then return (or otherwise stop further processing)
before the finally that resets busy; update references to the two fetch calls
(the first fetch to `${base}/api/auth/sign-in/email` and the second fetch to
`${base}/api/v1/me`) to rely on this catch for unexpected exceptions so the UI
shows feedback instead of silently re-enabling the form.

In `@apps/server/playwright.config.mjs`:
- Around line 17-42: Playwright may start the "server-shell" project before the
API on port 3000 is ready because webServer.url only checks 5173; update the
readiness so both services are confirmed before tests run: either change
webServer to use Playwright's multi-server support (provide an array of
webServer entries/URLs checking both http://127.0.0.1:5173 and
http://127.0.0.1:3000), or modify the dev:e2e command (the script invoked by
webServer.command) to block until both ports are listening or expose a combined
readiness endpoint and point webServer.url to that; ensure reuseExistingServer
logic (reuseExistingServer) still applies when adding the second readiness
check.

In `@apps/server/src/env.ts`:
- Around line 44-52: The isLocalOnlyMode logic includes a leading null from
toOrigin(process.env.BETTER_AUTH_URL) but filters nulls from
BETTER_AUTH_TRUSTED_ORIGINS, causing wrong false results; fix by building
configuredOrigins with both sources mapped through toOrigin and then filtering
out nulls consistently (e.g., .filter((o): o is string => Boolean(o))), and
change the return to explicitly guard empty lists: return
configuredOrigins.length > 0 && configuredOrigins.every((origin) =>
isLocalOrigin(origin)); update references in isLocalOnlyMode, toOrigin usage,
and ensure this preserves the betterAuth.ts fallback-secret path behavior when
only BETTER_AUTH_TRUSTED_ORIGINS is set.

In `@apps/server/src/lib/betterAuth.ts`:
- Around line 19-23: The code currently leaves secret undefined when
BETTER_AUTH_SECRET is missing or too short; update the validation around the
constant secret (and the existing localOnlyMode/isLocalOnlyMode check) to
enforce a fail-fast behavior: if !secret || secret.length < 32 and localOnlyMode
is false, throw an Error (or call process.exit with a clear processLogger.error)
describing the invalid BETTER_AUTH_SECRET so the app won't start insecurely;
retain the current relaxed behavior only when localOnlyMode is true and log a
warning in that case.

In `@apps/server/src/routes/v1/setup.ts`:
- Around line 111-118: The catch block for runFirstTimeSetup currently returns a
400 client error; change it to return a 500 server error for unexpected
failures: capture the thrown error in the catch (e) and return jsonError(c,
"server_error", "Could not complete setup. Email may already be in use.", 500,
getRequestId(c)); optionally log the caught error via your logger before
returning. Ensure you update the catch that wraps runFirstTimeSetup/first-time
setup logic (the catch using jsonError and getRequestId) so genuine server
issues return HTTP 500 instead of 400.

In `@apps/web/src/routes/`+layout.svelte:
- Around line 16-17: The reactive statement uses get(page) so isBare doesn't
re-evaluate on navigation; replace the get(page) usage with the auto-subscribed
$page to ensure reactivity (update the line that computes isBare to reference
$page.url.pathname) and keep the bare array as-is; change any other references
that rely on get(page) in this component to use $page so route changes update
isBare and dependent UI correctly.
- Around line 39-42: The fetch for public config in +layout.svelte (the const pc
/ const d block) lacks error handling and can throw on network failures; wrap
the await fetch(...).json() in a try/catch, check response.ok before parsing,
and fall back to a safe default (e.g., { data: { setupComplete: false } }) on
error; mirror the pattern used in loadTenantName(), log the error (or use
processLogger) and ensure downstream code reads from the safe default so
navigation won't break.
- Around line 47-51: The early return "if (path === \"/login\" || path ===
\"/setup\") return;" makes the later block that checks "if (path === \"/setup\"
&& d.setupComplete) { await goto(\"/\"); return; }" unreachable; fix by either
removing the redundant early-return or moving the setup-completion redirect (the
check using path, d.setupComplete and goto) above the early-return so the
redirect can run when setupComplete is true, ensuring only one of those branches
remains and the unreachable code is eliminated.

In `@apps/web/src/routes/login/`+page.svelte:
- Around line 16-17: The file exposes hardcoded dev credentials in the variables
email and password; update the initialization of email and password (the let
email and let password bindings) so they only default to development values when
running in dev mode (e.g. check import.meta.env.DEV or NODE_ENV ===
"development"), otherwise initialize them to empty strings (or remove defaults
entirely) to ensure production bundles do not contain the credentials; change
the logic where email/password are assigned to conditionally set the dev values
only when the dev flag is true.

In `@apps/web/src/routes/welcome/`+page.svelte:
- Around line 8-18: The onMount block containing the fetch("/api/v1/me") call
can throw network exceptions that prevent redirect; wrap the async logic in a
try/catch around the fetch/json parsing and set isAdmin only on success, and in
the catch call await goto("/login") (and return) so any thrown error
(network/CORS/etc.) triggers the same redirect path as a non-ok response;
reference the onMount anonymous async function, the fetch("/api/v1/me") call,
the json parsing into j, the isAdmin assignment, and the goto("/login") call
when making this change.

---

Nitpick comments:
In `@apps/desktop/e2e/global-setup.ts`:
- Around line 19-20: The repoRoot calculation is fragile because it uses
path.resolve(process.cwd(), "..", "..") which breaks when tests are run from
other CWDs; update repoRoot to derive from the setup file location instead (use
__dirname if the compiled bundle is CJS, otherwise compute from import.meta.url
via fileURLToPath and path.dirname), and then set prismaDir =
path.join(repoRoot, "packages", "prisma") as before; change the code around the
repoRoot and prismaDir definitions (replace usage of process.cwd() and reference
path.resolve(process.cwd(), "..", "..")) so the path is computed from the file's
directory rather than the current working directory.

In `@apps/desktop/e2e/onboarding.spec.ts`:
- Around line 29-41: Duplicate admin credentials are hard-coded in the test
fetch body and in completeSetupIfNeeded; extract them into shared constants
(e.g., ADMIN_EMAIL, ADMIN_PASSWORD, TENANT_NAME or a single SETUP_CREDENTIALS
object) and reference those constants from both the POST payload in the fetch to
/api/v1/setup/complete and from the completeSetupIfNeeded helper so both places
use the same source of truth and avoid drift.

In `@apps/desktop/src-tauri/src/lib.rs`:
- Around line 125-135: The stop_sidecar function currently ignores poisoned
mutexes by using if let Ok(...) which can skip killing the child process; change
the two lock acquisitions on state.child and state.base_url to recover from
poisoned locks (e.g. use .lock().unwrap_or_else(|e| e.into_inner()) or
equivalent) so you still obtain the mutex guard, kill the child (call ch.kill())
and clear the Option, and add a warning log when a PoisonError is encountered to
aid debugging; reference the stop_sidecar function and the state.child and
state.base_url mutexes when making the change.
- Around line 233-249: The current open_remote_login_window closes an existing
window returned by app.get_webview_window("remote_auth") then immediately
recreates it, which can race; instead, detect the existing window
(app.get_webview_window("remote_auth")) and if present call its set_url or
navigate method to point to the new login URL (reusing the window) or, if you
must close and recreate, await/confirm the completion of w.close() before
calling WebviewWindowBuilder::new; update open_remote_login_window to use
w.set_url(WebviewUrl::External(login)) when w exists or await the close future
prior to building a new WebviewWindowBuilder to avoid label conflict.

In `@apps/desktop/src/lib/api.ts`:
- Around line 70-81: apiFetch currently calls getDesktopConfig() and then
apiBaseUrl(), but apiBaseUrl() calls getDesktopConfig() again causing redundant
awaits; refactor by fetching the config once in apiFetch and pass it into a new
or overloaded helper (e.g., add apiBaseUrlFromConfig(config: DesktopConfig) or
change apiBaseUrl to accept an optional config parameter) so apiFetch uses the
single config instance to compute base URL and set credentials; update apiFetch
to call apiBaseUrlFromConfig(config) (or apiBaseUrl(config)) and remove the
second getDesktopConfig() call, and adapt any callers of the modified apiBaseUrl
signature accordingly.

In `@apps/desktop/src/routes/`+layout.svelte:
- Around line 45-48: afterNavigate currently calls getDesktopConfig() on every
navigation causing redundant reads; change this by caching the result (e.g.,
store the config in the existing cfg variable or a Svelte writable store) and
only call getDesktopConfig() if cfg is unset or explicitly invalidated. Update
the afterNavigate handler to use the cached cfg, add a clear/invalidateConfig
function or listen for settings-change events to force a reload, and keep
guardRemoteSession() as-is but ensure it uses the cached cfg when needed so
navigation no longer triggers disk/storage reads each time.

In `@apps/desktop/src/routes/`+page.svelte:
- Around line 35-52: The chooseRemote function currently trims remoteUrl but
does not validate it before calling setDesktopConfig; add a validation step
using a robust check (e.g., attempt new URL(trimmed) or a strict regex) against
the trimmed remoteUrl and if it fails set err to a helpful message and return
early (do not call setDesktopConfig or goto), otherwise proceed to call
setDesktopConfig with remoteBaseUrl set to the trimmed value; update references
in this flow (chooseRemote, remoteUrl, setDesktopConfig, getDesktopConfig)
accordingly so invalid URLs are rejected before persisting.

In `@apps/server/e2e/global-setup.ts`:
- Around line 18-32: Wrap the execSync call that runs "npx prisma migrate reset
..." in a try-catch inside apps/server/e2e/global-setup.ts so failures provide
clearer context; catch the error around the execSync invocation (the block that
uses prismaDir and env with POSTGRES_DATABASE_URL/dbUrl), log a descriptive
message including prismaDir and the caught error (e.g., via console.error or
existing logger), and then rethrow or call process.exit(1) to fail fast with the
improved message.

In `@apps/server/src/__tests__/auth-routes.test.ts`:
- Around line 59-87: Add a new unit test in
apps/server/src/__tests__/auth-routes.test.ts that mocks findFirstAppUser to
return null and issues a POST to the /api/v1/auth/dev-login route (same request
shape as the existing test); assert the response status and body represent the
expected error (e.g., 404 or 400 and an error message), and verify that
signInEmail is not called. Use the same test harness/makeApp() setup, and
reference findFirstAppUser and signInEmail in the assertions to ensure the
negative path is covered.

In `@apps/server/src/lib/initial-setup.ts`:
- Around line 81-85: Replace the sequential prisma.boardColumn.create loop in
initial-setup.ts with a single prisma.boardColumn.createMany call: map the
existing columns array to include the boardId for each item (e.g., columns.map(c
=> ({ boardId: board.id, ...c }))), call await prisma.boardColumn.createMany({
data: mappedColumns }) and adjust any code that relied on created records
(createMany returns a count, not created rows); keep using the same symbols
(columns, board.id, prisma.boardColumn.create -> createMany) and await the
promise for proper setup sequencing.
- Around line 22-106: The seed steps after creating appUser (admin) in
runFirstTimeSetup are not atomic; wrap the subsequent
org/project/board/columns/projectMember/ticket creation and the final
tenant.update in a single Prisma transaction so partial seeds can't be left
behind. Replace the multiple prisma.* calls (organization.create,
project.create, board.create, boardColumn.create loop, projectMember.create,
boardColumn.findFirstOrThrow, ticket.create, tenant.update) with a
prisma.$transaction callback and use the transaction client (e.g.,
tx.organization.create, tx.project.create, etc.) inside it so all creations and
the tenant update succeed or roll back together.

In `@apps/server/src/lib/setup-config.ts`:
- Around line 27-36: Wrap the JSON parsing and schema validation inside
loadInstanceConfigFile in a try-catch to surface a clear, contextual error that
includes the TRACKR_CONFIG_PATH; specifically catch errors from JSON.parse(raw)
and instanceConfigSchema.parse(j) and rethrow (or throw a new Error) with a
message like "Failed to load/parse/validate config at <path>: <original error
message>" so operators know which file failed (refer to
process.env.TRACKR_CONFIG_PATH, JSON.parse, and instanceConfigSchema.parse to
locate the code).

In `@apps/server/src/middleware/session.ts`:
- Around line 10-24: Wrap the call to auth.api.getSession inside a try-catch in
the loadSession middleware so transient or unexpected errors from
auth.api.getSession do not bubble up; on catch, log the error (or use
c.log/processLogger) and call c.set("authUser", null) then return next() to
treat the request as unauthenticated; keep the existing logic for
prisma.appUser.findFirst unchanged so only failures from auth.api.getSession are
handled defensively.

In `@apps/web/src/routes/login/`+page.svelte:
- Around line 62-66: Replace the raw text-only error handling for the fetch
response (the res / t / err block) with logic that first attempts to parse
res.json() and extract a user-friendly message from common fields like message
or error, falling back to res.text() and then a default "Sign in failed"; set
err to that extracted string so structured JSON errors are shown instead of raw
JSON. Ensure any JSON parse errors are caught and ignored so the fallback to
text/default still occurs.
- Around line 30-45: The page renders with default auth values while onMount
fetches /api/v1/public-config, causing a flash and delayed redirect; introduce a
loading boolean (e.g., loading) initialized true, set it before/after the fetch
inside onMount around the fetch("/api/v1/public-config") call, and only render
the form/UI when loading is false (or render a spinner when true); if
j.data.setupComplete is false call goto("/setup") before clearing loading (so
redirect occurs immediately) and still update setupComplete, auth, and
authAvailable after the fetch. Ensure you update references to
setupComplete/auth/authAvailable rendering logic to wait for loading to be
false.

In `@apps/web/src/routes/setup/`+page.svelte:
- Around line 36-41: The loop that copies p.authSettings into auth (checking
p?.authSettings and iterating keys) can be simplified: validate p.authSettings
exists, then build an object containing only keys whose values are boolean and
merge it into auth via Object.assign. Update the code that references
p.authSettings, auth, and AuthFlags to avoid non-null assertions and casts by
filtering Object.entries(p.authSettings) for typeof value === "boolean" and then
Object.assign(auth, filteredObject).
🪄 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

Run ID: fcdc6d13-7663-4e14-9e23-232c295fb1fa

📥 Commits

Reviewing files that changed from the base of the PR and between 6e36632 and eb529b8.

⛔ Files ignored due to path filters (2)
  • apps/desktop/src-tauri/icons/icon.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (62)
  • .env.schema
  • .github/workflows/ci.yml
  • .gitignore
  • .vscode/settings.json
  • CONTRIBUTING.md
  • README.md
  • apps/desktop/e2e/global-setup.ts
  • apps/desktop/e2e/onboarding.spec.ts
  • apps/desktop/e2e/tauri-e2e-bridge.ts
  • apps/desktop/package.json
  • apps/desktop/playwright.config.mjs
  • apps/desktop/src-tauri/capabilities/default.json
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/lib/api.ts
  • apps/desktop/src/lib/desktopConfig.ts
  • apps/desktop/src/routes/+layout.svelte
  • apps/desktop/src/routes/+page.svelte
  • apps/desktop/src/routes/connect/+page.svelte
  • apps/desktop/turbo.json
  • apps/desktop/vite.config.js
  • apps/server/e2e/global-setup.ts
  • apps/server/e2e/onboarding.spec.ts
  • apps/server/package.json
  • apps/server/playwright.config.mjs
  • apps/server/src/__tests__/auth-routes.test.ts
  • apps/server/src/__tests__/integration/dev-flow.integration.test.ts
  • apps/server/src/env.ts
  • apps/server/src/hono-env.d.ts
  • apps/server/src/index.ts
  • apps/server/src/lib/betterAuth.ts
  • apps/server/src/lib/db.ts
  • apps/server/src/lib/initial-setup.ts
  • apps/server/src/lib/platform.ts
  • apps/server/src/lib/setup-config.ts
  • apps/server/src/middleware/session.ts
  • apps/server/src/routes/v1/auth.ts
  • apps/server/src/routes/v1/index.ts
  • apps/server/src/routes/v1/setup.ts
  • apps/server/src/scripts/seed-postgres.ts
  • apps/server/vitest.config.ts
  • apps/web/package.json
  • apps/web/src/lib/auth-client.ts
  • apps/web/src/lib/shell.ts
  • apps/web/src/routes/+layout.svelte
  • apps/web/src/routes/login/+page.svelte
  • apps/web/src/routes/projects/+page.svelte
  • apps/web/src/routes/setup/+page.svelte
  • apps/web/src/routes/welcome/+page.svelte
  • package.json
  • packages/desktop-local-api/src/__tests__/local-api.integration.test.ts
  • packages/desktop-local-api/src/db.ts
  • packages/desktop-local-api/src/index.ts
  • packages/prisma/package.json
  • packages/prisma/prisma.config.sqlite.ts
  • packages/prisma/prisma.config.ts
  • packages/prisma/prisma/postgres/migrations/20250426120000_better_auth_and_app_user/migration.sql
  • packages/prisma/prisma/postgres/migrations/20260502160558/migration.sql
  • packages/prisma/prisma/postgres/migrations/migration_lock.toml
  • packages/prisma/prisma/postgres/schema.prisma
  • packages/prisma/seed.ts
  • scripts/dev-e2e-desktop.mjs
  • scripts/dev-e2e.mjs

Comment thread apps/desktop/src-tauri/src/lib.rs
Comment thread apps/desktop/src-tauri/src/lib.rs
Comment thread apps/desktop/src/routes/+layout.svelte
Comment thread apps/desktop/src/routes/connect/+page.svelte
Comment thread apps/server/playwright.config.mjs Outdated
Comment thread apps/web/src/routes/+layout.svelte Outdated
Comment thread apps/web/src/routes/+layout.svelte Outdated
Comment thread apps/web/src/routes/+layout.svelte Outdated
Comment thread apps/web/src/routes/login/+page.svelte Outdated
Comment thread apps/web/src/routes/welcome/+page.svelte

@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: 2

🧹 Nitpick comments (1)
packages/prisma/seed.ts (1)

4-6: Silent fallback to localhost in seed script — consider adding a guard

The code silently falls back to localhost:5432/trackr when POSTGRES_DATABASE_URL is unset. There is no intermediate fallback (the AI summary incorrectly mentions DATABASE_URL), and the file contains no dotenv import.

This is compounded by Prisma v7's removal of automatic environment variable loading. If the seed is run without POSTGRES_DATABASE_URL explicitly set in the shell environment, it will silently target the hardcoded localhost address. This works fine for local development but creates a risk of accidental database misconfiguration in CI or staging if env vars are not properly supplied.

Consider adding an explicit guard to fail fast or warn when the variable is absent, especially in production contexts:

Optional robustness improvements

Fail fast in production:

const connectionString =
  process.env.POSTGRES_DATABASE_URL ??
  (process.env.NODE_ENV === "production"
    ? (() => { throw new Error("POSTGRES_DATABASE_URL must be set in production"); })()
    : "postgresql://postgres:postgres@localhost:5432/trackr");

Or log a warning:

const connectionString =
  process.env.POSTGRES_DATABASE_URL ??
  // eslint-disable-next-line no-console
  (console.warn("⚠️  POSTGRES_DATABASE_URL not set, using local default"), "postgresql://postgres:postgres@localhost:5432/trackr");

Note: The same pattern exists in prisma.config.ts (lines 10–12) and affects Prisma CLI commands as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/prisma/seed.ts` around lines 4 - 6, The seed script currently
assigns connectionString to a hardcoded localhost URL when POSTGRES_DATABASE_URL
is missing; change this to fail-fast or warn: at the top of
packages/prisma/seed.ts (and mirror in prisma.config.ts) explicitly load
environment variables if desired (e.g., require('dotenv').config()) and then
check process.env.POSTGRES_DATABASE_URL — if it's undefined and NODE_ENV !==
'development' throw an error and exit (or at minimum log a clear warning and
refuse to run), otherwise allow a local-only fallback only when NODE_ENV ===
'development'; update the connectionString assignment to use the validated
POSTGRES_DATABASE_URL and remove the silent hardcoded default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/env.ts`:
- Around line 9-18: toOrigin currently returns the ASCII string "null" for
opaque origins (e.g. new URL("tauri://localhost").origin === "null"), which then
survives .filter(Boolean) and breaks isLocalOnlyMode; update the toOrigin
function to detect opaque origins by checking the parsed URL's origin and
returning actual null when origin === "null" (i.e., after constructing new
URL(value) if origin === "null" return null, otherwise return origin), so
downstream checks in isLocalOrigin and isLocalOnlyMode behave correctly.

In `@apps/server/src/lib/betterAuth.ts`:
- Around line 6-17: Update the top comment around the createRequire/nodeRequire
usage to specify the correct minimal Node versions for requiring ESM: "Node ≥
22.12.0 or ≥ 20.19.0" instead of "Node 22+", and keep the note about better-auth
not using top-level await; then add an engines entry to apps/server/package.json
to enforce the minimum Node engine (e.g., "node": ">=20.19.0 || >=22.12.0") so
runtime installs fail fast; touch the lines referencing createRequire,
nodeRequire and the betterAuth/prisma/social-providers/plugins imports to locate
the change.

---

Nitpick comments:
In `@packages/prisma/seed.ts`:
- Around line 4-6: The seed script currently assigns connectionString to a
hardcoded localhost URL when POSTGRES_DATABASE_URL is missing; change this to
fail-fast or warn: at the top of packages/prisma/seed.ts (and mirror in
prisma.config.ts) explicitly load environment variables if desired (e.g.,
require('dotenv').config()) and then check process.env.POSTGRES_DATABASE_URL —
if it's undefined and NODE_ENV !== 'development' throw an error and exit (or at
minimum log a clear warning and refuse to run), otherwise allow a local-only
fallback only when NODE_ENV === 'development'; update the connectionString
assignment to use the validated POSTGRES_DATABASE_URL and remove the silent
hardcoded default.
🪄 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

Run ID: 4112ea35-e5fc-47e3-bd36-0f6ba9882582

📥 Commits

Reviewing files that changed from the base of the PR and between eb529b8 and c611677.

⛔ Files ignored due to path filters (1)
  • apps/desktop/src-tauri/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • .env.schema
  • apps/desktop/src-tauri/Cargo.toml
  • apps/desktop/src-tauri/src/lib.rs
  • apps/desktop/src/routes/+layout.svelte
  • apps/desktop/src/routes/connect/+page.svelte
  • apps/server/playwright.config.mjs
  • apps/server/src/env.ts
  • apps/server/src/lib/betterAuth.ts
  • apps/server/vitest.config.ts
  • packages/prisma/seed.ts
✅ Files skipped from review due to trivial changes (3)
  • apps/desktop/src-tauri/Cargo.toml
  • apps/server/playwright.config.mjs
  • apps/server/vitest.config.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/desktop/src/routes/+layout.svelte

Comment thread apps/server/src/env.ts
Comment on lines +9 to +18
function toOrigin(value?: string): string | null {
if (!value) {
return null;
}
try {
return new URL(value).origin;
} catch {
return null;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm that Node.js URL parser returns the string "null" for tauri:// origins.
node -e "
  const u = new URL('tauri://localhost');
  console.log('origin:', JSON.stringify(u.origin));
  console.log('is truthy:', Boolean(u.origin));
"

Repository: nealhorner/trackr

Length of output: 92


🏁 Script executed:

cat -n apps/server/src/env.ts

Repository: nealhorner/trackr

Length of output: 1903


toOrigin("tauri://localhost") returns the truthy string "null", breaking isLocalOnlyMode detection for tauri-only setups.

Per the WHATWG URL Standard, non-standard-scheme URLs produce an opaque origin serialized as the ASCII string "null". The Node.js URL parser confirms: new URL("tauri://localhost").origin === "null" (a truthy string, not JavaScript null). This string passes the .filter(Boolean) at line 48, but then isLocalOrigin("null") returns false because:

  • Line 21's hardcoded checks for "tauri://localhost" don't match the string "null"
  • Parsing new URL("null") throws (no valid scheme), returning false from the catch block

Concrete impact: if BETTER_AUTH_TRUSTED_ORIGINS=tauri://localhost, configuredOrigins becomes ["null"], every(isLocalOrigin) returns false, and isLocalOnlyMode() returns false despite being a local-only setup. This causes betterAuth.ts to fail at startup due to missing BETTER_AUTH_SECRET.

https://tauri.localhost is unaffected because https: is a special scheme with a valid origin.

🛠️ Proposed fix
 function toOrigin(value?: string): string | null {
-  if (!value) {
-    return null;
-  }
+  if (!value) return null;
+  const trimmed = value.trim();
+  // tauri:// is a non-standard scheme; the WHATWG URL parser produces the
+  // opaque-origin string "null" for it.  Preserve the raw value so that
+  // isLocalOrigin() can match it with its hardcoded check.
+  if (trimmed === "tauri://localhost") return "tauri://localhost";
   try {
-    return new URL(value).origin;
+    const origin = new URL(trimmed).origin;
+    // Guard against other non-standard schemes that also produce the opaque
+    // origin string "null" — treat them as unparseable and drop them.
+    return origin !== "null" ? origin : null;
   } catch {
     return null;
   }
 }
📝 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
function toOrigin(value?: string): string | null {
if (!value) {
return null;
}
try {
return new URL(value).origin;
} catch {
return null;
}
}
function toOrigin(value?: string): string | null {
if (!value) return null;
const trimmed = value.trim();
// tauri:// is a non-standard scheme; the WHATWG URL parser produces the
// opaque-origin string "null" for it. Preserve the raw value so that
// isLocalOrigin() can match it with its hardcoded check.
if (trimmed === "tauri://localhost") return "tauri://localhost";
try {
const origin = new URL(trimmed).origin;
// Guard against other non-standard schemes that also produce the opaque
// origin string "null" — treat them as unparseable and drop them.
return origin !== "null" ? origin : null;
} catch {
return null;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/env.ts` around lines 9 - 18, toOrigin currently returns the
ASCII string "null" for opaque origins (e.g. new URL("tauri://localhost").origin
=== "null"), which then survives .filter(Boolean) and breaks isLocalOnlyMode;
update the toOrigin function to detect opaque origins by checking the parsed
URL's origin and returning actual null when origin === "null" (i.e., after
constructing new URL(value) if origin === "null" return null, otherwise return
origin), so downstream checks in isLocalOrigin and isLocalOnlyMode behave
correctly.

Comment on lines +6 to +17
const nodeRequire = createRequire(__filename);
// better-auth is published as ESM; Node 22+ can still load it from CJS via `createRequire`
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
const { betterAuth } = nodeRequire("better-auth") as any;
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
const { prismaAdapter } = nodeRequire("better-auth/adapters/prisma") as any;
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
const { google, github, apple } = nodeRequire(
"better-auth/social-providers",
) as any;
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-explicit-any
const { genericOAuth, okta } = nodeRequire("better-auth/plugins") as any;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. Check declared engines constraint (if any).
fd -i 'package.json' apps/server --exec grep -H '"engines"' {}

# 2. Check the Node.js version in use (.nvmrc / .node-version / Volta).
fd -g '.nvmrc' -g '.node-version' --exec cat {}
cat package.json 2>/dev/null | grep -A5 '"volta"' || true

# 3. Check if better-auth or its sub-paths use top-level await.
fd -e mjs -e js . -p 'node_modules/better-auth' --exec grep -l 'await ' {} | head -20

Repository: nealhorner/trackr

Length of output: 274


🏁 Script executed:

# Find and check package.json files
find . -maxdepth 3 -name package.json -type f | head -20

Repository: nealhorner/trackr

Length of output: 293


🏁 Script executed:

# Check for .nvmrc and .node-version files
find . -maxdepth 2 \( -name '.nvmrc' -o -name '.node-version' \) -type f

Repository: nealhorner/trackr

Length of output: 43


🏁 Script executed:

# Check apps/server/package.json for engines constraint if it exists
cat apps/server/package.json 2>/dev/null | head -50

Repository: nealhorner/trackr

Length of output: 801


🌐 Web query:

Node.js require ESM unflagged version 22.12.0 20.19.0 experimental flag

💡 Result:

In Node.js, the ability to use require to load native ES modules (ESM) without throwing ERR_REQUIRE_ESM was previously behind the --experimental-require-module flag. - Node.js 22.12.0 (released 2024-12-03): Unflagged, now enabled by default (flag defaults to true). Still experimental; emits a warning on first use unless from node_modules. Disable with --no-experimental-require-module. Supports synchronous ESM only (no top-level await, throws ERR_REQUIRE_ASYNC_MODULE if present). Check with process.features.require_module. - Node.js 20.19.0 (released 2025-03-13): Backported and unflagged, enabled by default. No warning unless --trace-require-module used. Same limitations and disable option. Prior versions require the explicit --experimental-require-module flag. By 2026, marked stable across LTS lines including these versions.

Citations:


🌐 Web query:

better-auth 1.6.9 package ESM top-level await

💡 Result:

better-auth 1.6.9 does not indicate usage of top-level await; instead it is published as an ES module package ("type": "module") and its async behavior is implemented via regular async functions/Promises (e.g., awaitable context and provider config). If you are encountering a top-level-await-related error, it’s likely coming from your build/tooling (CJS output or transforming ESM/TLA) rather than from better-auth 1.6.9 itself. What the evidence shows: 1) better-auth 1.6.9 packages are ESM (“type”: “module”). The v1.6.9 release comparison shows both the better-auth and @better-auth/core package.json files with "type": "module". 2) The project uses async resolution patterns instead of requiring top-level await. For example, Better Auth has logic that awaits a promise-based context in middleware (const authContext = await ctx) rather than using module-level top-level await. 3) The Better Auth changelog for recent versions includes ESM/CJS compatibility fixes for a dependency (samlify), but it does not mention top-level await as the underlying issue. Practical guidance: - Ensure your consumer build outputs ESM (not CJS) if your code or dependencies use top-level await. - If you run a bundler/transpiler that converts ESM to CJS (or otherwise rewrites module execution), top-level await may fail—this is a common root cause seen in ESM/CJS + tooling scenarios. If you paste the exact error message and your runtime/build setup (Node version, framework, bundler, whether you transpile node_modules), I can pinpoint whether it’s your tooling rather than better-auth itself.

Citations:


require(ESM) requires Node.js ≥ 22.12.0 (or ≥ 20.19.0) — the comment "Node 22+" understates this.

require(ESM) is unflagged from Node.js v20.19.0+ and v22.12.0+. On v22.0.0–v22.11.x the --experimental-require-module flag is still required, and on any v20 prior to v20.19.0 it is unavailable entirely — without it, require() of an ES module throws ERR_REQUIRE_ESM.

Consider:

  1. Tightening the comment to "Node ≥ 22.12.0 or ≥ 20.19.0".
  2. Adding an engines constraint in apps/server/package.json to enforce the minimum version (currently missing).

Note: better-auth@1.6.9 does not use top-level await, so that concern does not apply here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/lib/betterAuth.ts` around lines 6 - 17, Update the top
comment around the createRequire/nodeRequire usage to specify the correct
minimal Node versions for requiring ESM: "Node ≥ 22.12.0 or ≥ 20.19.0" instead
of "Node 22+", and keep the note about better-auth not using top-level await;
then add an engines entry to apps/server/package.json to enforce the minimum
Node engine (e.g., "node": ">=20.19.0 || >=22.12.0") so runtime installs fail
fast; touch the lines referencing createRequire, nodeRequire and the
betterAuth/prisma/social-providers/plugins imports to locate the change.

nealhorner added 5 commits May 3, 2026 12:14
… error handling and ensure valid data structure
…te configuration; adjust login page to conditionally set credentials based on environment
…ettierignore and .vitest-cache to .gitignore, and enhance build/test scripts for better modularity and clarity

@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: 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 @.github/workflows/ci.yml:
- Line 111: The CI job integration_and_e2e is currently blocked by build_desktop
via the needs: [quality, build_web_stack, build_desktop] entry; remove
build_desktop from the needs array (so it becomes needs: [quality,
build_web_stack]) in the integration_and_e2e job definition to avoid
desktop-only failures gating web/server integration and Playwright tests. Locate
the integration_and_e2e job in the workflow and update its needs list
accordingly.

In @.husky/pre-commit:
- Line 1: The pre-commit hook runs "npm run format:fix" but doesn't restage
files, so commits may contain unformatted versions; update the .husky/pre-commit
hook to restage changes after formatting (e.g., run the formatter then call git
add to stage modified files) or switch to using lint-staged so only staged files
are formatted and automatically re-staged; ensure the hook either stages
formatted changes (git add) or uses lint-staged invocation instead of only
running "npm run format:fix".
- Line 3: The pre-commit hook currently runs the full monorepo build via the
command "npm run build:all", which forces local Rust/Tauri toolchain installs;
update the hook to skip the desktop build by replacing "npm run build:all" with
a fast build script that excludes desktop artifacts (for example "npm run
build:web" or a project-specific "npm run build:fast" that omits Tauri/Rust), or
wrap the call to only run targeted packages; ensure the updated line in
.husky/pre-commit no longer invokes "npm run build:all" so web/server-only
contributors can commit without the desktop toolchain.
🪄 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

Run ID: 3ab0b162-b45f-4c02-acd6-b54f38c30a6e

📥 Commits

Reviewing files that changed from the base of the PR and between c684c83 and bd1d472.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • .gitignore
  • .husky/pre-commit
  • .prettierignore
  • package.json
  • turbo.json
✅ Files skipped from review due to trivial changes (2)
  • .prettierignore
  • .gitignore

Comment thread .github/workflows/ci.yml
run: npm run build

integration_and_e2e:
needs: [quality, build_web_stack, build_desktop]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't gate web/server tests on the desktop build.

Line 111 makes integration_and_e2e wait for build_desktop, but this job only runs server integration tests and Playwright with --filter=!@trackr/desktop. A desktop-only failure now prevents unrelated web/server validation from running.

Suggested fix
-    needs: [quality, build_web_stack, build_desktop]
+    needs: [quality, build_web_stack]
📝 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
needs: [quality, build_web_stack, build_desktop]
needs: [quality, build_web_stack]
🤖 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 @.github/workflows/ci.yml at line 111, The CI job integration_and_e2e is
currently blocked by build_desktop via the needs: [quality, build_web_stack,
build_desktop] entry; remove build_desktop from the needs array (so it becomes
needs: [quality, build_web_stack]) in the integration_and_e2e job definition to
avoid desktop-only failures gating web/server integration and Playwright tests.
Locate the integration_and_e2e job in the workflow and update its needs list
accordingly.

Comment thread .husky/pre-commit
@@ -1,4 +1,4 @@
npm run format:fix

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't auto-fix in pre-commit unless you restage.

Line 1 writes formatting changes to the working tree, but this hook never adds them back to the index. The hook can pass on formatted files while the commit still contains the pre-format version.

Suggested fix
-npm run format:fix
+npm run format:check
📝 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
npm run format:fix
npm run format:check
🤖 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 @.husky/pre-commit at line 1, The pre-commit hook runs "npm run format:fix"
but doesn't restage files, so commits may contain unformatted versions; update
the .husky/pre-commit hook to restage changes after formatting (e.g., run the
formatter then call git add to stage modified files) or switch to using
lint-staged so only staged files are formatted and automatically re-staged;
ensure the hook either stages formatted changes (git add) or uses lint-staged
invocation instead of only running "npm run format:fix".

Comment thread .husky/pre-commit
npm run test
npm run build -- --filter=!@trackr/desktop
npm run format No newline at end of file
npm run build: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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep the desktop build out of the pre-commit fast path.

Line 3 runs the full monorepo build, so every commit now depends on the Rust/Tauri desktop toolchain being installed locally. That will block web/server-only contributors from committing at all.

Suggested fix
-npm run build:all
+npx turbo build --filter=@trackr/web --filter=@trackr/server
📝 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
npm run build:all
npx turbo build --filter=@trackr/web --filter=@trackr/server
🤖 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 @.husky/pre-commit at line 3, The pre-commit hook currently runs the full
monorepo build via the command "npm run build:all", which forces local
Rust/Tauri toolchain installs; update the hook to skip the desktop build by
replacing "npm run build:all" with a fast build script that excludes desktop
artifacts (for example "npm run build:web" or a project-specific "npm run
build:fast" that omits Tauri/Rust), or wrap the call to only run targeted
packages; ensure the updated line in .husky/pre-commit no longer invokes "npm
run build:all" so web/server-only contributors can commit without the desktop
toolchain.

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