Skip to content

fix: project collection and db idx for environment#3973

Merged
chronark merged 17 commits intomainfrom
fix-collections-and-db-idx
Sep 16, 2025
Merged

fix: project collection and db idx for environment#3973
chronark merged 17 commits intomainfrom
fix-collections-and-db-idx

Conversation

@ogzhanolguncu
Copy link
Contributor

@ogzhanolguncu ogzhanolguncu commented Sep 15, 2025

What does this PR do?

This PR fixes project collections and the database index for environments. The old index was preventing us from adding new projects since they were treated as a unique workspaceId-slug constraint (e.g., ws_asdsad-production). When you tried to add a new project, that would fail due to this constraint.

Fixes # (issue)

If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

  • Project UI and project search should work

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

Summary by CodeRabbit

  • New Features

    • Author avatars and abbreviated commit SHAs across project and deployment views.
    • Per-project collections for scoped deployments, environments, and domains.
  • Improvements

    • Create Project dialog: slug & Git URL inputs, real-time validation, form reset on success.
    • Search inputs update immediately for faster feedback.
    • Timestamps rendered correctly; long branch names truncated.
    • Smooth layout transitions when deployment details open.
  • Behavior Changes

    • Environment slugs are unique per project.
    • “Live deployment” replaces the previous active-deployment field.
  • Removed

    • Legacy list endpoints and old global collection exports reorganized under a deploy namespace.

@vercel
Copy link

vercel bot commented Sep 15, 2025

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

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
dashboard Ignored Ignored Preview Sep 16, 2025 3:21pm
engineering Ignored Ignored Preview Sep 16, 2025 3:21pm

@changeset-bot
Copy link

changeset-bot bot commented Sep 15, 2025

⚠️ No Changeset found

Latest commit: 29e4fa0

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 15, 2025

📝 Walkthrough

Walkthrough

Reorganized TRPC under a new deploy namespace, moved collection APIs to per-project deploy/* modules with a CollectionManager, renamed active→live deployment identifiers across DB/Go/clients, added useDeployments hook and UI avatar/timestamp updates, and updated various client callsites to trpc.deploy.*.

Changes

Cohort / File(s) Summary
TRPC callsite migrations
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx, apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx, apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx, apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx, apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx
Updated client callsites to use trpc.deploy.* variants (search, rollback, buildLogs, environment list, getOpenApiDiff); internal logic unchanged.
Root router / namespace reorg
apps/dashboard/lib/trpc/routers/index.ts, apps/dashboard/lib/trpc/routers/deploy/*
Introduced top-level deploy namespace with nested project, environment, domain, and deployment routers; moved and re-exported endpoints under deploy.
Removed legacy deployment endpoints
apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts, apps/dashboard/lib/trpc/routers/deployment/listByProject.ts
Deleted listByEnvironment and listByProject TRPC procedures.
New deploy-scoped collections
apps/dashboard/lib/collections/deploy/deployments.ts, apps/dashboard/lib/collections/deploy/domains.ts, apps/dashboard/lib/collections/deploy/environments.ts, apps/dashboard/lib/collections/deploy/projects.ts
Added per-project deploy/* collection modules (deployments, domains, environments, projects). Deployments provide non-null dummy git metadata for UI; projects expose createProjectRequestSchema and onInsert create flow via trpcClient.deploy.project.create.
Global collection manager & collection surface change
apps/dashboard/lib/collections/index.ts
Introduced CollectionManager and collectionManager to create/cleanup per-project collections; global collection reduced to shared projects and ratelimit collections.
Create Project: UI & schema moves
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx, apps/dashboard/app/(app)/projects/_components/create-project/create-project.schema.ts, apps/dashboard/app/(app)/projects/_components/create-project/use-create-project.ts, apps/dashboard/lib/collections/projects.ts
Dialog now inserts into collection.projects (onInsert handles TRPC create); local schema and old hook removed; schema moved to collection module; duplicate-slug error surfaced via setError; toast success path removed from dialog.
Project create router input & payload changes
apps/dashboard/lib/trpc/routers/deploy/project/create.ts
create mutation now uses createProjectRequestSchema, includes liveDeploymentId: null, and sets createdAt/updatedAt in inserts.
useDeployments hook & UI consumption
apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts, .../deployments/components/table/deployments-list.tsx, .../deployments/page.tsx
Added useDeployments to centralize project/activeDeployment/deployments queries (joins, filters); DeploymentsList no longer accepts projectId prop and consumes the hook; page uses useProjectLayout for layout/collections.
Projects list, search, and filtering
apps/dashboard/app/(app)/projects/_components/list/index.tsx, apps/dashboard/components/list-search-input.tsx, apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx
Added ilike name filtering via useProjectsFilters; removed debounce from list search input; ProjectCard uses commitTimestamp? and TimestampInfo.
Avatar, timestamp, and small UI tweaks
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx, .../active-deployment-card/index.tsx, .../project-details-expandables/sections.tsx, apps/dashboard/app/(app)/projects/_components/list/projects-card.tsx
New reusable Avatar component; replaced img placeholders with Avatar; truncated branch display, shortened commit SHA to 7 chars, moved units outside numeric spans, and render timestamps when present.
Layout/provider changes
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx, apps/dashboard/app/(app)/projects/[projectId]/layout.tsx
ProjectLayoutContext now exposes collections (per-project) instead of projectId; layout initializes per-project collections via collectionManager.getProjectCollections(projectId) and sets default isDetailsOpen.
Client-side collection removal & migration
apps/dashboard/lib/collections/domains.ts, apps/dashboard/lib/collections/environments.ts, legacy deployments collection files
Removed legacy flat domains/environments collection modules; replaced with per-project deploy/* collection modules and updated exports.
TRPC handlers tightened with project inputs
apps/dashboard/lib/trpc/routers/deploy/domains/list.ts, apps/dashboard/lib/trpc/routers/deploy/project/list.ts, apps/dashboard/lib/trpc/routers/environment/list.ts
Added .input({ projectId: string }) to domain/environment list endpoints and tightened DB filters to include projectId; listProjects now projects liveDeploymentId.
DB and Go changes: active → live deployment
go/pkg/db/schema.sql, internal/db/src/schema/projects.ts, internal/db/src/schema/environments.ts, go/pkg/db/models_generated.go, go/pkg/db/queries/project_update_live_deployment_id.sql, generated Go files added/removed, go/apps/ctrl/services/deployment/deploy_workflow.go, go/apps/ctrl/services/routing/service.go`
Renamed active_deployment_idlive_deployment_id in schema and generated code; replaced generated UpdateProjectActiveDeploymentId with UpdateProjectLiveDeploymentId; updated Go services to use LiveDeploymentID; environments unique index moved to (project_id, slug).
Small refactors & cosmetic changes
multiple files under apps/dashboard/lib/collections/ratelimit/*, apps/dashboard/lib/trpc/routers/deploy/deployment/*, various UI small edits
Minor import path adjustments (relative → alias), formatting, and small UI class tweaks (badge color, spacing).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Browser UI
  participant Layout as useProjectLayout
  participant Hook as useDeployments
  participant Collections as Project Collections
  participant TRPC as trpcClient.deploy.*

  UI->>Layout: mount page -> request collections for project
  UI->>Hook: mount -> request deployments (uses collections + filters)
  Hook->>Collections: query project -> read liveDeploymentId
  Hook->>Collections: query activeDeployment by liveDeploymentId
  Hook->>Collections: query deployments (join environments, apply filters)
  Collections->>TRPC: fetch via trpcClient.deploy.deployment.list(...)
  TRPC-->>Collections: return deployments
  Collections-->>Hook: project, activeDeployment, deployments
  Hook-->>UI: provide { deployments, activeDeployment, activeDeploymentId }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

Bug, Needs Approval

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title Check ✅ Passed The title is concise and directly reflects the primary changes in the diff: fixes to the project collection surface and an environment DB index change. The changeset includes large collection refactoring (per-project deploy collections and collectionManager) and a schema/index migration moving environment uniqueness to project_id-slug, which matches the title scope. Because it names the two main areas impacted and is short and specific, it is appropriate for commit history and review scanning.
Description Check ✅ Passed The PR description explains the problem and the intended fix, includes a basic test instruction ("Project UI and project search should work"), and shows the author completed the checklist, so it is largely actionable for reviewers. The repository template's "Type of change" checkboxes are left unselected and there is no linked issue number, both of which the template expects to be filled when applicable. Overall the description is mostly complete and reviewable but would benefit from an explicit issue reference and a slightly more detailed test plan.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-collections-and-db-idx

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6081ff3 and 29e4fa0.

📒 Files selected for processing (3)
  • go/pkg/db/models_generated.go (1 hunks)
  • go/pkg/db/project_update_live_deployment_id.sql_generated.go (1 hunks)
  • go/pkg/db/querier_generated.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • go/pkg/db/project_update_live_deployment_id.sql_generated.go
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.
🧬 Code graph analysis (2)
go/pkg/db/models_generated.go (1)
go/pkg/db/project_update_active_deployment_id.sql_generated.go (1)
  • ActiveDeploymentID (19-23)
go/pkg/db/querier_generated.go (1)
go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)
  • UpdateProjectLiveDeploymentIdParams (19-23)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
🔇 Additional comments (2)
go/pkg/db/models_generated.go (1)

672-681: Verify no remaining active_deployment references after renaming to LiveDeploymentID
Sandbox run produced no output; run locally and confirm zero matches for ActiveDeploymentID, active_deployment_id, UpdateProjectActiveDeploymentId, project_update_active_deployment_id.sql, and project_update_active_deployment_id.sql_generated.go by running:
rg -nP -C2 '\bActiveDeploymentID\b|\bactive_deployment_id\b|\bUpdateProjectActiveDeploymentId\b' && fd -p 'project_update_active_deployment_id.sql' go/pkg/db/queries || true && fd -p 'project_update_active_deployment_id.sql_generated.go' go/pkg/db || true

go/pkg/db/querier_generated.go (1)

1677-1682: Approve — rename applied; no lingering artifacts detected.
UpdateProjectLiveDeploymentId exists in go/pkg/db/queries/project_update_live_deployment_id.sql, go/pkg/db/project_update_live_deployment_id.sql_generated.go, and go/pkg/db/querier_generated.go (lines 1677–1682). Call sites updated at go/apps/ctrl/services/routing/service.go:361 and go/apps/ctrl/services/deployment/deploy_workflow.go:385. No occurrences of UpdateProjectActiveDeploymentId or UpdateProjectActiveDeploymentIdParams found.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

},
(table) => ({
uniqueSlug: uniqueIndex("environments_workspace_id_slug_idx").on(table.workspaceId, table.slug),
uniqueSlug: uniqueIndex("environments_project_id_slug_idx").on(table.projectId, table.slug),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

and this

@github-actions
Copy link
Contributor

github-actions bot commented Sep 15, 2025

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

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

⚠️ Outside diff range comments (3)
apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1)

181-183: Bug: environment equality compares object references.

Compare ids or slugs; current check will almost always be false for identical envs from separate queries.

Apply:

-          isSameEnvironment: oldDeployment.environment === newDeployment.environment,
+          isSameEnvironment:
+            oldDeployment.environment?.id === newDeployment.environment?.id ||
+            oldDeployment.environment?.slug === newDeployment.environment?.slug,
apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx (1)

171-173: User-visible string bug: ${date} renders literally.

In JSX, use {date} not a literal ${date}.

Apply:

-                                      {environment?.slug} • ${date}
+                                      {environment?.slug} • {date}

Do this in both baseline and comparison selectors.

Also applies to: 249-251

apps/dashboard/lib/trpc/routers/deploy/project/create.ts (1)

66-80: Close the slug race: convert DB unique violations into 409 CONFLICT.
Pre‑check helps but TOCTOU still exists. If two requests pass the check concurrently, the insert must translate a unique‑key error into TRPCError(CONFLICT) for consistent UX.

Apply this diff to handle duplicate slugs gracefully:

@@
-      } catch (txErr) {
-        console.error({
+      } catch (txErr) {
+        // If the DB has a unique constraint on (workspaceId, slug) for projects,
+        // translate duplicate key errors into a 409.
+        if (txErr && typeof txErr === "object" && "code" in (txErr as any)) {
+          const code = (txErr as any).code;
+          // 23505 = unique_violation (Postgres)
+          if (code === "23505") {
+            throw new TRPCError({
+              code: "CONFLICT",
+              message: `A project with slug "${input.slug}" already exists in this workspace`,
+            });
+          }
+        }
+        console.error({
           message: "Transaction failed during project creation",
           userId,
           workspaceId,
           projectId,
           projectSlug: input.slug,
           error: txErr instanceof Error ? txErr.message : String(txErr),
           stack: txErr instanceof Error ? txErr.stack : undefined,
         });
 
         throw txErr; // Re-throw to be caught by outer catch
       }

Also applies to: 135-147

🧹 Nitpick comments (16)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (3)

15-21: Skip the WHERE when search is empty to avoid a no-op ILIKE.

Avoid ILIKE '%%' to reduce unnecessary filtering and keep the planner free to use order-only indexes.

-  const projects = useLiveQuery(
-    (q) =>
-      q
-        .from({ project: collection.projects })
-        .orderBy(({ project }) => project.updatedAt, "desc")
-        .where(({ project }) => ilike(project.name, `%${projectName}%`)),
-    [projectName],
-  );
+  const projects = useLiveQuery(
+    (q) => {
+      let base = q
+        .from({ project: collection.projects })
+        .orderBy(({ project }) => project.updatedAt, "desc");
+      if (projectName.trim() !== "") {
+        base = base.where(({ project }) => ilike(project.name, `%${projectName}%`));
+      }
+      return base;
+    },
+    [projectName],
+  );

20-20: Escape LIKE wildcards in user input.

Unescaped %, _, or \ in projectName will change match semantics.

-        .where(({ project }) => ilike(project.name, `%${projectName}%`)),
+        .where(({ project }) => ilike(project.name, `%${escapeLike(projectName)}%`)),

Add this helper near the top of the file:

function escapeLike(input: string) {
  // Escape backslash first, then % and _
  return input.replace(/([\\%_])/g, "\\$1");
}

20-20: Consider a trigram/CITEXT index for case-insensitive contains.

ILIKE '%…%' won't use a plain btree index. If this runs on Postgres and the dataset grows, add a GIN trigram index (or CITEXT + trigram) on projects.name to keep searches fast.

Example:

  • CREATE EXTENSION IF NOT EXISTS pg_trgm;
  • CREATE INDEX CONCURRENTLY IF NOT EXISTS projects_name_trgm_idx ON projects USING gin (name gin_trgm_ops);
internal/db/src/schema/environments.ts (2)

15-15: Fix stale comment (“within workspace” → “within project”)

The slug is now project‑scoped.

- slug: varchar("slug", { length: 256 }).notNull(), // URL-safe identifier within workspace
+ slug: varchar("slug", { length: 256 }).notNull(), // URL-safe identifier within project

21-23: Consider add’l index if querying by workspaceId

If envs are commonly listed by workspace, add an index on workspaceId (or composite workspaceId+projectId).

-import { mysqlTable, uniqueIndex, varchar } from "drizzle-orm/mysql-core";
+import { mysqlTable, uniqueIndex, varchar, index } from "drizzle-orm/mysql-core";
...
   (table) => ({
     uniqueSlug: uniqueIndex("environments_project_id_slug_idx").on(table.projectId, table.slug),
+    workspaceIdx: index("workspace_idx").on(table.workspaceId),
   }),
apps/dashboard/lib/collections/projects.ts (2)

39-40: Tiny nit: drop unnecessary async/await

Return the promise directly for parity with other collections.

-    queryFn: async () => {
-      return await trpcClient.deploy.project.list.query();
-    },
+    queryFn: () => trpcClient.deploy.project.list.query(),

45-49: Normalize empty gitRepositoryUrl to null (optional)

Prevents sending empty strings if the backend prefers null.

-      const createInput = createProjectRequestSchema.parse({
+      const createInput = createProjectRequestSchema.parse({
         name: changes.name,
         slug: changes.slug,
-        gitRepositoryUrl: changes.gitRepositoryUrl,
+        gitRepositoryUrl: changes.gitRepositoryUrl === "" ? null : changes.gitRepositoryUrl,
       });
apps/dashboard/lib/collections/domains.ts (1)

21-21: Namespace switch OK — domain.list is workspace‑scoped; no extra params required.

Server proc apps/dashboard/lib/trpc/routers/deploy/domains/list.ts uses requireWorkspace and filters by ctx.workspace.id, so trpcClient.deploy.domain.list.query() does not need workspaceId/projectId inputs. Optional: scope the cache key in apps/dashboard/lib/collections/domains.ts (currently queryKey: ["domains"]) to include workspaceId (e.g. ["domains", workspaceId]) to avoid cross‑workspace cache collisions.

apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx (1)

10-11: Temporary endpoint name (list_dummy)—track and gate.

If this is mock-only, gate behind a flag or TODO with an issue reference, and plan the rename to list to minimize churn.

Proposed TODO:

-  const { data } = trpc.deploy.environment.list_dummy.useQuery({ projectId });
+  // TODO(unkey#xxxx): replace list_dummy with list once backend is wired to DB
+  const { data } = trpc.deploy.environment.list_dummy.useQuery({ projectId });
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (1)

62-71: Validate slug when auto‑updating it from name.
setValue doesn’t validate by default; isValid may lag. Trigger validation to keep the submit button state accurate.

-    setValue("slug", slug);
+    setValue("slug", slug, { shouldValidate: true });
apps/dashboard/lib/trpc/routers/index.ts (1)

320-321: Rename list_dummy to something stable before publicizing.
list_dummy suggests placeholder; consider listVariables or getEnvs for consistency.

apps/dashboard/components/list-search-input.tsx (5)

21-26: Remove dead prop from the public type.

debounceTime is no longer used or destructured; keep the API clean to avoid confusion.

Apply this diff:

 type Props<T extends BaseFilter = BaseFilter> = {
   useFiltersHook: () => FilterHook<T>;
   placeholder?: string;
-  debounceTime?: number;
   className?: string;
 };

39-39: Delete leftover debounce plumbing.

No timeout is scheduled anymore; the ref and clearTimeouts add noise and maintenance overhead.

Apply this diff:

-  const debounceRef = useRef<NodeJS.Timeout>();
+  // no debounce; update on each keystroke

-  // Cleanup debounce on unmount
-  useEffect(() => {
-    return () => {
-      if (debounceRef.current) {
-        clearTimeout(debounceRef.current);
-      }
-    };
-  }, []);
+  // no debounce cleanup needed

   const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const value = e.target.value;
     setSearchText(value);
-
-    // Clear existing debounce
-    if (debounceRef.current) {
-      clearTimeout(debounceRef.current);
-    }
-
     updateQuery(value);
   };

   const handleClear = () => {
     setSearchText("");
-
-    // Clear debounce
-    if (debounceRef.current) {
-      clearTimeout(debounceRef.current);
-    }
-
     // Immediately update filters
     updateQuery("");
   };

   const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
     if (e.key === "Escape") {
       handleClear();
       inputRef.current?.blur();
     }

     if (e.key === "Enter") {
-      // Clear debounce and immediately update
-      if (debounceRef.current) {
-        clearTimeout(debounceRef.current);
-      }
       updateQuery(searchText);
     }
   };

Also applies to: 60-68, 94-98, 105-109, 121-125


69-88: Preserve a stable filter id for the query chip.

Creating a new random id on every keystroke can cause unnecessary re-renders or chip remounts. Reuse the existing id if present.

Apply this diff:

-  const updateQuery = (value: string) => {
-    // Remove existing filters for query field
-    const filtersWithoutCurrent = filters.filter((f) => f.field !== "query");
-
-    if (value.trim()) {
-      // Add new filter
-      updateFilters([
-        ...filtersWithoutCurrent,
-        {
-          field: "query",
-          id: crypto.randomUUID(),
-          operator: "contains",
-          value: value.trim(),
-        } as T,
-      ]);
-    } else {
-      // Just remove query filters if empty
-      updateFilters(filtersWithoutCurrent);
-    }
-  };
+  const updateQuery = (value: string) => {
+    const trimmed = value.trim();
+    const existing = filters.find((f) => f.field === "query");
+    const id = existing?.id ?? crypto.randomUUID();
+    const filtersWithoutCurrent = filters.filter((f) => f.field !== "query");
+
+    if (trimmed) {
+      updateFilters([
+        ...filtersWithoutCurrent,
+        {
+          field: "query",
+          id,
+          operator: "contains",
+          value: trimmed,
+        } as T,
+      ]);
+    } else {
+      updateFilters(filtersWithoutCurrent);
+    }
+  };

153-153: Honor className in the non-loading state too.

It’s applied in the loading skeleton wrapper but ignored in the normal render.

Apply this diff:

-  return (
-    <div className="flex items-center gap-2 w-full flex-1 md:w-80 h-8">
+  return (
+    <div className={cn("flex items-center gap-2 w-full flex-1 md:w-80 h-8", className)}>

114-127: Avoid redundant Enter-triggered updates.

Since updates happen on each keystroke, pressing Enter can still fire another state update (and potential network call). Guard for no-op.

Apply this diff:

   if (e.key === "Enter") {
-      updateQuery(searchText);
+      const current = filters.find((f) => f.field === "query");
+      if ((current?.value ?? "") !== searchText.trim()) {
+        updateQuery(searchText);
+      }
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49a3c48 and 273851b.

📒 Files selected for processing (23)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (5 hunks)
  • apps/dashboard/app/(app)/projects/_components/create-project/create-project.schema.ts (0 hunks)
  • apps/dashboard/app/(app)/projects/_components/create-project/use-create-project.ts (0 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (1 hunks)
  • apps/dashboard/components/list-search-input.tsx (1 hunks)
  • apps/dashboard/lib/collections/deployments.ts (1 hunks)
  • apps/dashboard/lib/collections/domains.ts (1 hunks)
  • apps/dashboard/lib/collections/environments.ts (1 hunks)
  • apps/dashboard/lib/collections/projects.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts (3 hunks)
  • apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts (0 hunks)
  • apps/dashboard/lib/trpc/routers/deployment/listByProject.ts (0 hunks)
  • apps/dashboard/lib/trpc/routers/index.ts (2 hunks)
  • go/pkg/db/schema.sql (1 hunks)
  • internal/db/src/schema/environments.ts (1 hunks)
💤 Files with no reviewable changes (4)
  • apps/dashboard/lib/trpc/routers/deployment/listByEnvironment.ts
  • apps/dashboard/app/(app)/projects/_components/create-project/use-create-project.ts
  • apps/dashboard/lib/trpc/routers/deployment/listByProject.ts
  • apps/dashboard/app/(app)/projects/_components/create-project/create-project.schema.ts
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/api/keys/query-overview-timeseries/index.ts:1-2
Timestamp: 2025-08-22T12:50:07.024Z
Learning: The team at Unkey is comfortable accepting TRPC schema imports from app route folders in server-side TRPC routers as technical debt, since they're planning to migrate away from TRPC to calling their v2 API directly, making these imports temporary and not worth refactoring.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/workspace/onboarding.ts:1-1
Timestamp: 2025-08-22T12:45:07.187Z
Learning: The team at Unkey is planning to move from TRPC to calling their v2 API directly, making current TRPC schema organization temporary. They're comfortable with keeping server input schemas imported from app route folders as technical debt since this code will be replaced.
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/lib/collections/deployments.ts
  • apps/dashboard/lib/collections/environments.ts
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx
  • apps/dashboard/lib/collections/domains.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts
  • apps/dashboard/lib/trpc/routers/index.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts
📚 Learning: 2025-07-25T19:11:00.208Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts:110-147
Timestamp: 2025-07-25T19:11:00.208Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts, the user mcstepp prefers to keep mock data fallbacks in POC/demonstration code for simplicity, even if it wouldn't be production-ready. This aligns with the PR being work-in-progress for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/lib/collections/deployments.ts
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx
  • apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts
  • apps/dashboard/lib/trpc/routers/index.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts
📚 Learning: 2025-08-31T19:30:38.171Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3853
File: apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts:75-87
Timestamp: 2025-08-31T19:30:38.171Z
Learning: In apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts, the getEnvs procedure currently uses mock data (VARIABLES) and ignores the projectId input parameter. This is intentional temporary behavior - the user ogzhanolguncu indicated they plan to hook this up to the database later.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx
  • apps/dashboard/lib/collections/environments.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts
📚 Learning: 2025-06-02T11:09:58.791Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3292
File: apps/dashboard/lib/trpc/routers/key/create.ts:11-14
Timestamp: 2025-06-02T11:09:58.791Z
Learning: In the unkey codebase, TypeScript and the env() function implementation already provide sufficient validation for environment variables, so additional runtime error handling for missing env vars is not needed.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx
📚 Learning: 2024-12-03T14:23:07.189Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2143
File: apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx:37-49
Timestamp: 2024-12-03T14:23:07.189Z
Learning: In `apps/dashboard/app/(app)/logs/components/log-details/resizable-panel.tsx`, the resize handler is already debounced.

Applied to files:

  • apps/dashboard/components/list-search-input.tsx
📚 Learning: 2025-08-22T12:50:07.024Z
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/api/keys/query-overview-timeseries/index.ts:1-2
Timestamp: 2025-08-22T12:50:07.024Z
Learning: The team at Unkey is comfortable accepting TRPC schema imports from app route folders in server-side TRPC routers as technical debt, since they're planning to migrate away from TRPC to calling their v2 API directly, making these imports temporary and not worth refactoring.

Applied to files:

  • apps/dashboard/lib/trpc/routers/index.ts
🧬 Code graph analysis (12)
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx (1)
apps/dashboard/lib/trpc/server.ts (1)
  • trpc (7-14)
apps/dashboard/app/(app)/projects/[projectId]/details/env-variables-section/hooks/use-env-var-manager.tsx (1)
apps/dashboard/lib/trpc/server.ts (1)
  • trpc (7-14)
apps/dashboard/lib/collections/deployments.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/collections/environments.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx (1)
apps/dashboard/lib/trpc/server.ts (1)
  • trpc (7-14)
apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx (1)
apps/dashboard/lib/trpc/server.ts (1)
  • trpc (7-14)
apps/dashboard/lib/collections/domains.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/trpc/routers/deploy/project/create.ts (1)
apps/dashboard/lib/collections/projects.ts (1)
  • createProjectRequestSchema (16-28)
apps/dashboard/lib/trpc/routers/index.ts (9)
apps/dashboard/lib/trpc/routers/deploy/project/list.ts (1)
  • listProjects (5-30)
apps/dashboard/lib/trpc/routers/deploy/project/create.ts (1)
  • createProject (8-177)
apps/dashboard/lib/trpc/routers/deploy/envs/list.ts (1)
  • getEnvs (68-80)
apps/dashboard/lib/trpc/routers/environment/list.ts (1)
  • listEnvironments (5-24)
apps/dashboard/lib/trpc/routers/deploy/domains/list.ts (1)
  • listDomains (5-28)
apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (1)
  • listDeployments (5-37)
apps/dashboard/lib/trpc/routers/deploy/deployment/llm-search/index.ts (1)
  • searchDeployments (13-20)
apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1)
  • getOpenApiDiff (7-196)
apps/dashboard/lib/trpc/routers/deploy/deployment/buildLogs.ts (1)
  • getDeploymentBuildLogs (217-230)
apps/dashboard/lib/collections/projects.ts (2)
internal/db/src/schema/projects.ts (1)
  • projects (8-31)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (2)
apps/dashboard/app/(app)/projects/_components/hooks/use-projects-filters.ts (1)
  • useProjectsFilters (17-103)
apps/dashboard/lib/collections/index.ts (1)
  • collection (17-24)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (2)
apps/dashboard/lib/collections/projects.ts (2)
  • CreateProjectRequestSchema (31-31)
  • createProjectRequestSchema (16-28)
apps/dashboard/lib/collections/index.ts (2)
  • collection (17-24)
  • reset (27-34)
🔇 Additional comments (19)
apps/dashboard/app/(app)/projects/_components/list/index.tsx (2)

2-2: LGTM: correct imports for ilike/useLiveQuery.


13-14: Confirmed: filter field key is "query".
projectsFilterFieldConfig defines a "query" field and projectsListFilterFieldNames exports it (apps/dashboard/app/(app)/projects/_components/projects-filters.schema.ts), so the code will correctly read the query filter.

internal/db/src/schema/environments.ts (1)

22-22: Index scope change LGTM

unique(projectId, slug) aligns with the PR goal and generated SQL.

Please confirm a matching migration exists (drop old workspace_id index, create new project_id index).

apps/dashboard/lib/collections/deployments.ts (1)

45-45: Deployments: confirm trpc route and include env/project filters in call & queryKey

Search returned no matches; verify that trpcClient.deploy.deployment.list exists and whether it accepts { environmentId, projectId }. If it does, pass those params to queryFn and include them in the queryKey for correct caching (e.g. queryFn: () => trpcClient.deploy.deployment.list.query({ environmentId, projectId }), queryKey: ['deployments', projectId, environmentId]). Location: apps/dashboard/lib/collections/deployments.ts — current line: queryFn: () => trpcClient.deploy.deployment.list.query().

apps/dashboard/lib/collections/projects.ts (1)

16-28: Server imports shared schema; confirm gitRepositoryUrl normalization

createProjectRequestSchema is imported by the server create route (apps/dashboard/lib/trpc/routers/deploy/project/create.ts) and used by the client form (apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx). No code was found that converts an empty gitRepositoryUrl to null (apps/dashboard/lib/collections/projects.ts uses createProjectRequestSchema.parse but contains no explicit ''→null normalization). If the server normalizes empty string → null, mirror that client-side before submit; otherwise no change required.

apps/dashboard/lib/trpc/routers/deploy/deployment/getById.ts (1)

2-2: Alias import switch looks good.

Consistent with the deploy namespace reorg.

apps/dashboard/lib/trpc/routers/deploy/deployment/getOpenApiDiff.ts (1)

3-3: Alias import switch LGTM.

apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (1)

2-2: Alias import switch LGTM.

apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-search/index.tsx (1)

9-10: Mutation path update looks correct.

No behavior change; UI callbacks intact.

apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx (1)

53-55: Ensure router export exists and guard query when id is empty.

Add { enabled: Boolean(deploymentId) } to the useQuery to avoid calls with falsy ids and confirm deploy.deployment.buildLogs is exported on the server router — search returned no matches in apps/dashboard/lib/trpc/routers.

-  const { data: logsData, isLoading } = trpc.deploy.deployment.buildLogs.useQuery({
-    deploymentId,
-  });
+  const { data: logsData, isLoading } = trpc.deploy.deployment.buildLogs.useQuery(
+    { deploymentId },
+    { enabled: Boolean(deploymentId) },
+  );

Location: apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/hooks/use-deployment-logs.tsx:53-55. Check server routers under apps/dashboard/lib/trpc/routers for the buildLogs export.

apps/dashboard/lib/trpc/routers/deploy/deployment/index.ts (1)

1-10: Router surface changed — removed procedures aren’t used. No occurrences of listByEnvironment or listByProject were found; the deploy root is mounted at apps/dashboard/lib/trpc/routers/index.ts:314 and the deployment router exposes list (listDeployments), search, and getOpenApiDiff.

apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx (1)

50-58: Namespace update reads right. Hook path aligns with deploy router. Ran a repo-wide regex search for trpc.deployment.(getOpenApiDiff|list|search|buildLogs); no matches found — manual verification recommended for aliased/indirect references.

apps/dashboard/lib/trpc/routers/deploy/project/create.ts (3)

2-12: Input schema swap looks correct and aligns with shared collection schema.
Using createProjectRequestSchema keeps client/server validation in sync.


102-111: No DB migration needed — environments already enforce (projectId, slug) uniqueness. Schema defines uniqueIndex("environments_project_id_slug_idx").on(table.projectId, table.slug) in internal/db/src/schema/environments.ts.


63-79: No change required — schema uses numeric timestamps (bigint, mode: "number").

createdAt/updatedAt are defined in internal/db/src/schema/util/lifecycle_dates.ts as bigint(..., { mode: "number" }) with $defaultFn/$onUpdateFn using Date.now(), so assigning Date.now() (ms number) in create.ts is correct. Also applies to the other mentioned lines (149–155).

Likely an incorrect or invalid review comment.

apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (1)

100-103: Button state ties to isSubmitting—now correct once insert is awaited.
After the await fix, disabled/loading behavior will reflect the real request lifecycle.

apps/dashboard/lib/trpc/routers/index.ts (2)

314-332: Deploy namespace wiring looks good and consistent.
project/environment/domain/deployment nested routes are clear; good consolidation.


314-332: Sweep callers for old trpc paths and migrate to trpc.deploy

Ensure all usages of trpc.(project|deployment|environment|domain).* are updated to trpc.deploy.* to avoid runtime breaks. Automated scan in this environment skipped files — run these locally and fix any matches:

# search dotted and bracketed usages
rg -nP -S -uu -C2 '\btrpc\.(project|deployment|environment|domain)\.' || true
rg -nP -S -uu -C2 "trpc\[['\"](?:project|deployment|environment|domain)['\"]\]" || true

# verify new path and any env var callers
rg -nP -S -uu -C2 '\btrpc\.deploy\b' || true
rg -nP -S -uu -C2 '\benvironmentVariables\b' || true
apps/dashboard/components/list-search-input.tsx (1)

31-36: Ensure ListSearchInput is client-only (hooks require client runtime)

Automated search found no importers; confirm this component is only imported from Client Components — either add "use client" at the top of apps/dashboard/components/list-search-input.tsx or ensure every importing file has "use client" in its first lines.

@ogzhanolguncu
Copy link
Contributor Author

@perkinsjr @chronark @mcstepp Folks can I get a quick review for this? I wanna start fresh tomorrow

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (1)

36-47: Awaiting persistence looks good; confirms previous concern is addressed

Switching to await tx.isPersisted.promise prevents premature modal close and ensures errors bubble to the catch block. Please confirm that DuplicateKeyError is raised through isPersisted.promise in your collection layer.

#!/bin/bash
# Verify where DuplicateKeyError is thrown/rethrown so the catch will see it.
rg -n -C2 'DuplicateKeyError|duplicate key|unique constraint' --ignore-case
🧹 Nitpick comments (2)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (2)

41-45: Trim DB-managed fields from insert payload

Avoid sending server-managed fields (activeDeploymentId, updatedAt, id) from the client to reduce coupling and future breakage.

-      const tx = collection.projects.insert({
-        name: values.name,
-        slug: values.slug,
-        gitRepositoryUrl: values.gitRepositoryUrl || null,
-        activeDeploymentId: null,
-        updatedAt: null,
-        id: "will-be-replace-by-server",
-      });
+      const tx = collection.projects.insert({
+        name: values.name,
+        slug: values.slug,
+        gitRepositoryUrl: values.gitRepositoryUrl || null,
+      });

23-27: Don’t overwrite a hand-edited slug; validate slug when auto-updating

Only auto-generate the slug while the slug field hasn’t been touched, and ensure setValue triggers validation so isValid updates immediately.

-    setError,
-    reset,
-    formState: { errors, isSubmitting, isValid },
+    setError,
+    reset,
+    formState: { errors, isSubmitting, isValid, dirtyFields },
-  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
+  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
     const name = e.target.value;
     const slug = name
       .toLowerCase()
       .replace(/[^a-z0-9\s-]/g, "")
       .replace(/\s+/g, "-")
       .replace(/-+/g, "-")
       .replace(/^-|-$/g, "");
-    setValue("slug", slug);
+    if (!dirtyFields?.slug) {
+      setValue("slug", slug, { shouldDirty: true, shouldTouch: true, shouldValidate: true });
+    }
   };

Also applies to: 63-72

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 273851b and d8fa102.

📒 Files selected for processing (1)
  • apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: imeyer
PR: unkeyed/unkey#3755
File: .github/actions/setup-node/action.yaml:0-0
Timestamp: 2025-08-08T15:10:46.436Z
Learning: Repo: unkeyed/unkey — Preference: If imeyer comments “issue” on a PR thread, automatically open a thorough GitHub issue (Summary, Impact, Where, Observed vs Expected, Acceptance Criteria, Validation Plan, Out of Scope, References), include backlinks to the PR and comment, and assign to imeyer.
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/workspace/onboarding.ts:1-1
Timestamp: 2025-08-22T12:45:07.187Z
Learning: The team at Unkey is planning to move from TRPC to calling their v2 API directly, making current TRPC schema organization temporary. They're comfortable with keeping server input schemas imported from app route folders as technical debt since this code will be replaced.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/api/keys/query-overview-timeseries/index.ts:1-2
Timestamp: 2025-08-22T12:50:07.024Z
Learning: The team at Unkey is comfortable accepting TRPC schema imports from app route folders in server-side TRPC routers as technical debt, since they're planning to migrate away from TRPC to calling their v2 API directly, making these imports temporary and not worth refactoring.
🧬 Code graph analysis (1)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (2)
apps/dashboard/lib/collections/projects.ts (2)
  • CreateProjectRequestSchema (31-31)
  • createProjectRequestSchema (16-28)
apps/dashboard/lib/collections/index.ts (2)
  • collection (17-24)
  • reset (27-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Build / Build
🔇 Additional comments (2)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (2)

102-103: Good submit gating

disabled={isSubmitting || !isValid} and loading={isSubmitting} correctly prevent double submissions and reflect async state.


10-10: Confirm DuplicateKeyError constructor identity to avoid instanceof false negatives

You import DuplicateKeyError from "@tanstack/react-db" and use instanceof checks — confirm that errors thrown by collection code come from that same constructor (no duplicate package/module copies) so instanceof will work at runtime.

Locations:

  • Imports: apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx:10; apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx:6; apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx:5
  • throw site: apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx:74
  • instanceof checks: apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx:51; apps/dashboard/app/(app)/ratelimits/_components/create-namespace-button.tsx:53; apps/dashboard/app/(app)/ratelimits/[namespaceId]/_components/identifier-dialog.tsx:88

Copy link
Collaborator

mcstepp commented Sep 15, 2025

I would, but I'm already resolving merge conflict hell from one of my open PRs that I'm trying to get merged soon too, and I fear this would add to it.

@ogzhanolguncu
Copy link
Contributor Author

I would, but I'm already resolving merge conflict hell from one of my open PRs that I'm trying to get merged soon too, and I fear this would add to it.

@mcstepp I agree lets first merge yours

Copy link
Collaborator

@chronark chronark left a comment

Choose a reason for hiding this comment

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

I'm confused, you asked about the nested trpc routes here, the feedback was that we don't need to nest them. and then you do exactly that in this PR?

I don't necessarily care about the nesting as much, but why are you asking if you disregard it silently afterwards?

@ogzhanolguncu
Copy link
Contributor Author

ogzhanolguncu commented Sep 16, 2025

I'm confused, you asked about the nested trpc routes here, the feedback was that we don't need to nest them. and then you do exactly that in this PR?

I don't necessarily care about the nesting as much, but why are you asking if you disregard it silently afterwards?

I think we had a misunderstanding. I don't mind keeping everything flat for actual router like /[slug]/environments etc... , but having stuff originated from deploy makes much more sense to me and keeps it clean. What's the point of having environments like this trpc.environment put yourself in the shoes of contributor or a new team member and tell me which one feels easier to reason about trpc.environment vs trpc.deploy.env | deployments | details

@chronark
Copy link
Collaborator

I migrated the schema in planetsacle

@ogzhanolguncu
Copy link
Contributor Author

I migrated the schema in planetsacle

:ack:

@ogzhanolguncu
Copy link
Contributor Author

@chronark I'm sending one more minor fix then i'll let you folks merge 🫡

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
go/apps/ctrl/services/deployment/deploy_workflow.go (1)

169-171: Compile-time bug: invalid range over integer

for i := range 300 is not valid Go and will not compile. Use a classic for loop.

-		for i := range 300 {
+		for i := 0; i < 300; i++ {
🧹 Nitpick comments (1)
go/apps/ctrl/services/deployment/deploy_workflow.go (1)

384-393: Avoid leaking/overwriting outer err; reuse completionTime; add success log

Inside the hydra step, assigning to the outer-scoped err is brittle and hard to read. Prefer a locally scoped variable. Also, reuse completionTime to keep timestamps consistent within the step and consider logging success.

-		// TODO: This section will be removed in the future in favor of "Promote to Production"
-		err = db.Query.UpdateProjectActiveDeploymentId(stepCtx, w.db.RW(), db.UpdateProjectActiveDeploymentIdParams{
-			ID:                 req.ProjectID,
-			ActiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID},
-			UpdatedAt:          sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()},
-		})
-		if err != nil {
-			return nil, fmt.Errorf("failed to update project %s active deployment ID to %s: %w", req.ProjectID, req.DeploymentID, err)
-		}
+		// TODO: This will be removed in favor of an explicit "Promote to Production" flow
+		if projUpdateErr := db.Query.UpdateProjectActiveDeploymentId(
+			stepCtx,
+			w.db.RW(),
+			db.UpdateProjectActiveDeploymentIdParams{
+				ID:                 req.ProjectID,
+				ActiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID},
+				UpdatedAt:          sql.NullInt64{Valid: true, Int64: completionTime},
+			},
+		); projUpdateErr != nil {
+			return nil, fmt.Errorf("failed to update project %s active deployment ID to %s: %w", req.ProjectID, req.DeploymentID, projUpdateErr)
+		}
+		w.logger.Info("project active deployment updated", "project_id", req.ProjectID, "deployment_id", req.DeploymentID)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5391b75 and 2f8691d.

📒 Files selected for processing (1)
  • go/apps/ctrl/services/deployment/deploy_workflow.go (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/api/keys/query-overview-timeseries/index.ts:1-2
Timestamp: 2025-08-22T12:50:07.024Z
Learning: The team at Unkey is comfortable accepting TRPC schema imports from app route folders in server-side TRPC routers as technical debt, since they're planning to migrate away from TRPC to calling their v2 API directly, making these imports temporary and not worth refactoring.
Learnt from: perkinsjr
PR: unkeyed/unkey#3775
File: apps/dashboard/lib/trpc/routers/workspace/onboarding.ts:1-1
Timestamp: 2025-08-22T12:45:07.187Z
Learning: The team at Unkey is planning to move from TRPC to calling their v2 API directly, making current TRPC schema organization temporary. They're comfortable with keeping server input schemas imported from app route folders as technical debt since this code will be replaced.
🧬 Code graph analysis (1)
go/apps/ctrl/services/deployment/deploy_workflow.go (2)
go/pkg/db/project_update_active_deployment_id.sql_generated.go (1)
  • UpdateProjectActiveDeploymentIdParams (19-23)
go/pkg/db/types/null_string.go (1)
  • NullString (10-10)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test

Copy link
Collaborator

sure

Copy link
Collaborator

can you do me a favour
can you rename the db column projects.activeDeploymentId to liveDeploymentId?

that captures the meaning better

Copy link
Collaborator

might as well do it here since we do db changes

Copy link
Contributor Author

@chronark we are good to go. Could you please double check my last commit to see if we are missing anything?

Copy link
Collaborator

on it

Copy link
Collaborator

Copy link
Contributor Author

lemme fix it real quick

Copy link
Contributor Author

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx (1)

22-29: Avoid mutating query data; use a copy and the already-captured projectId.

  • Use the projectId variable (already typed) in the where-clause.
  • Don’t call .sort on deployments.data directly; copy before sorting to avoid mutating cached data.

Apply:

-      .from({ deployment: collections.deployments })
-      .where(({ deployment }) => eq(deployment.projectId, params?.projectId))
+      .from({ deployment: collections.deployments })
+      .where(({ deployment }) => eq(deployment.projectId, projectId))

And below:

-  const sortedDeployments = deployments.data.sort(
+  const sortedDeployments = [...deployments.data].sort(
     (a, b) => b.deployment.createdAt - a.deployment.createdAt,
   );
♻️ Duplicate comments (2)
go/apps/ctrl/services/deployment/deploy_workflow.go (1)

384-393: Unconditional “promote to live” risks flipping production on every ready deploy. Gate or remove now.

This auto-writes projects.live_deployment_id for every deployment; preview/staging or concurrent deploys can hijack “live.”

Apply one of:

  • Remove auto-promotion until an explicit “Promote to Production” flow exists.
  • Or gate by environment (needs env in DeployRequest).

Minimal safe fix (remove for now):

-        // TODO: This section will be removed in the future in favor of "Promote to Production"
-        err = db.Query.UpdateProjectLiveDeploymentId(stepCtx, w.db.RW(), db.UpdateProjectLiveDeploymentIdParams{
-            ID:               req.ProjectID,
-            LiveDeploymentID: sql.NullString{Valid: true, String: req.DeploymentID},
-            UpdatedAt:        sql.NullInt64{Valid: true, Int64: time.Now().UnixMilli()},
-        })
-        if err != nil {
-            return nil, fmt.Errorf("failed to update project %s active deployment ID to %s: %w", req.ProjectID, req.DeploymentID, err)
-        }
+        // Intentionally not auto-promoting to live; promotion is an explicit action.

If you prefer gating, extend DeployRequest with EnvironmentID and only promote when env == production.

apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (1)

46-49: Resolved: persistence is awaited before closing the modal.

Awaiting tx.isPersisted.promise addresses the prior “insert not awaited” bug; UI no longer closes early and errors are catchable.

🧹 Nitpick comments (30)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx (4)

67-69: Type-safety nit: avoid passing setState directly.

Depending on prop typing, onSuggestionChange={(t) => setTitle(t)} is safer than handing setTitle through (avoids contravariant callback issues).

-      initialTitle={title ?? ""}
-      onSuggestionChange={setTitle}
+      initialTitle={title}
+      onSuggestionChange={(t) => setTitle(t)}

75-78: Brittle style toggle based on string equality.

Checking title !== "Last 12 hours" couples styling to wording. Base it on underlying filters instead.

Add this near where timeValues is defined:

const isDefaultRange =
  timeValues?.since === "12h" && !timeValues?.startTime && !timeValues?.endTime;

Then update the class toggle:

-            title !== "Last 12 hours" ? "bg-gray-4" : "",
+            isDefaultRange ? "" : "bg-gray-4",

79-83: A11y/text accuracy: label mentions “logs” in a deployments view.

Update copy to reflect deployments and avoid confusion. Also, only keep the keyboard-hint title if 'T' is actually bound here.

-          aria-label="Filter logs by time"
+          aria-label="Filter deployments by time"
@@
-          title="Press 'T' to toggle filters"
+          title="Press 'T' to toggle date/time filters"

If there is no 'T' hotkey for this control, drop the title attribute entirely.


82-86: Remove the disabled/loading phase.

Once the title is derived from filters, keeping the button disabled and showing “Loading...” is unnecessary and harms UX.

-          disabled={!title}
+          // Keep interactive; label is always derived from filters
+          disabled={false}
@@
-          <span className="text-gray-12 font-medium text-[13px]">{title ?? "Loading..."}</span>
+          <span className="text-gray-12 font-medium text-[13px]">{title}</span>
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx (1)

10-17: Guard for undefined data and use stable IDs.

useLiveQuery may return undefined before hydrate; also prefer environment.id over array index for option id. Consider sorting for stable UX.

-  const environments = useLiveQuery((q) => q.from({ environment: collections.environments }));
+  const environments = useLiveQuery((q) => q.from({ environment: collections.environments }));
+  const envs = (environments.data ?? []).slice().sort((a, b) => a.slug.localeCompare(b.slug));
...
-      options={environments.data.map((environment, i) => ({
-        id: i,
-        slug: environment.slug,
-      }))}
+      options={envs.map((environment) => ({
+        id: environment.id,
+        slug: environment.slug,
+      }))}
internal/db/src/schema/projects.ts (1)

21-21: Index liveDeploymentId for lookups.

If you ever resolve project from a deployment quickly, add a non-unique index on liveDeploymentId.

   (table) => ({
     workspaceIdx: index("workspace_idx").on(table.workspaceId),
     workspaceSlugIdx: uniqueIndex("workspace_slug_idx").on(table.workspaceId, table.slug),
+    liveDeploymentIdx: index("projects_live_deployment_id_idx").on(table.liveDeploymentId),
   }),
apps/dashboard/lib/trpc/routers/environment/list.ts (1)

18-21: Add deterministic ordering and ensure supporting index.

Sort by slug for stable UI; consider an index on (project_id) or (workspace_id, project_id) to match this predicate.

-      return await db.query.environments.findMany({
+      return await db.query.environments.findMany({
         where: and(
           eq(environments.workspaceId, ctx.workspace.id),
           eq(environments.projectId, input.projectId),
         ),
+        orderBy: (t, { asc }) => [asc(t.slug)],
         columns: {
           id: true,
           projectId: true,
           slug: true,
         },
       });

To back the query, add in internal/db/src/schema/environments.ts:

   (table) => ({
     uniqueSlug: uniqueIndex("environments_project_id_slug_idx").on(table.projectId, table.slug),
+    projectIdx: index("environments_project_id_idx").on(table.projectId),
+    workspaceProjectIdx: index("environments_workspace_project_id_idx").on(table.workspaceId, table.projectId),
   }),
apps/dashboard/lib/trpc/routers/deploy/domains/list.ts (1)

14-16: Add ordering by domain for stable UI; ensure index on projectId.

Sorting improves UX; an index on domains.project_id (and/or workspace_id, project_id) will support the filter.

-        where: (table, { eq, and }) =>
-          and(eq(table.workspaceId, ctx.workspace.id), eq(table.projectId, input.projectId)),
+        where: (table, { eq, and }) =>
+          and(eq(table.workspaceId, ctx.workspace.id), eq(table.projectId, input.projectId)),
+        orderBy: (t, { asc }) => [asc(t.domain)],

Schema suggestion (internal DB):

// domains table indexes
+domainsProjectIdx: index("domains_project_id_idx").on(table.projectId),
+domainsWorkspaceProjectIdx: index("domains_workspace_project_id_idx").on(table.workspaceId, table.projectId),
apps/dashboard/lib/trpc/routers/deploy/project/create.ts (4)

39-47: Normalize slug to lowercase server-side.

Prevents case-related duplicates regardless of client behavior.

-      const existingProject = await db.query.projects.findFirst({
+      const slugNormalized = input.slug.trim().toLowerCase();
+      const existingProject = await db.query.projects.findFirst({
         where: (table, { eq, and }) =>
-          and(eq(table.workspaceId, workspaceId), eq(table.slug, input.slug)),
+          and(eq(table.workspaceId, workspaceId), eq(table.slug, slugNormalized)),

66-79: Use normalized slug and trimmed repo URL in insert.

Apply the same normalization during insert for consistency.

-          await tx.insert(schema.projects).values({
+          await tx.insert(schema.projects).values({
             id: projectId,
             workspaceId,
             name: input.name,
-            slug: input.slug,
+            slug: slugNormalized,
             liveDeploymentId: null,
-            gitRepositoryUrl: input.gitRepositoryUrl || null,
+            gitRepositoryUrl: input.gitRepositoryUrl?.trim() || null,
             defaultBranch: "main",
             deleteProtection: false,
             createdAt: now,
             updatedAt: now,
           });

87-95: Reflect normalized slug in audit log.

Keep logs consistent with stored values.

-            description: `Created project "${input.name}" with slug "${input.slug}"`,
+            description: `Created project "${input.name}" with slug "${slugNormalized}"`,

135-147: Map duplicate-key to CONFLICT instead of 500.

On rare races, insert may still hit unique constraint. Translate duplicate errors to TRPC CONFLICT.

-      } catch (txErr) {
+      } catch (txErr) {
         console.error({
           message: "Transaction failed during project creation",
           userId,
           workspaceId,
           projectId,
           projectSlug: input.slug,
           error: txErr instanceof Error ? txErr.message : String(txErr),
           stack: txErr instanceof Error ? txErr.stack : undefined,
         });
-
-        throw txErr; // Re-throw to be caught by outer catch
+        if (txErr instanceof Error && /duplicate|dup key|unique/i.test(txErr.message)) {
+          throw new TRPCError({
+            code: "CONFLICT",
+            message: `A project with slug "${slugNormalized}" already exists in this workspace`,
+          });
+        }
+        throw txErr; // Re-throw to be caught by outer catch
apps/dashboard/lib/trpc/routers/deploy/project/list.ts (1)

10-21: Add deterministic ordering (updatedAt DESC) to avoid flicker/non‑stable pages.

Limit without order can shuffle results between requests.

Example (Drizzle):

       .findMany({
         where: (table, { eq }) => eq(table.workspaceId, ctx.workspace.id),
+        orderBy: (table, { desc }) => [desc(table.updatedAt)],
         columns: {
apps/dashboard/lib/collections/deploy/deployments.ts (2)

12-23: Don’t hardcode git metadata as non‑nullable — add a presence flag.

Returning dummy values server‑side + non‑nullable client schema risks showing fake data as real.

Apply:

   // Git information
-  // TEMP: Git fields as non-nullable for UI development with mock data
-  // TODO: Convert to nullable (.nullable()) when real git integration is added
-  // In production, deployments may not have git metadata if triggered manually
-  gitCommitSha: z.string(),
-  gitBranch: z.string(),
-  gitCommitMessage: z.string(),
-  gitCommitAuthorName: z.string(),
-  gitCommitAuthorEmail: z.string(),
-  gitCommitAuthorUsername: z.string(),
-  gitCommitAuthorAvatarUrl: z.string(),
-  gitCommitTimestamp: z.number().int(),
+  // In production, deployments may not have git metadata (manual triggers, etc.)
+  hasGitMetadata: z.boolean().default(false),
+  gitCommitSha: z.string().nullable(),
+  gitBranch: z.string().nullable(),
+  gitCommitMessage: z.string().nullable(),
+  gitCommitAuthorName: z.string().nullable(),
+  gitCommitAuthorEmail: z.string().nullable(),
+  gitCommitAuthorUsername: z.string().nullable(),
+  gitCommitAuthorAvatarUrl: z.string().nullable(),
+  gitCommitTimestamp: z.number().int().nullable(),

Follow‑up: set hasGitMetadata server‑side when any git field is non‑null.


41-55: Stabilize cache identity and reduce refetch churn.

Include a static prefix and consider a small staleTime.

-      queryKey: [projectId, "deployments"],
+      queryKey: ["project", projectId, "deployments"],
       retry: 3,
       queryFn: () => trpcClient.deploy.deployment.list.query({ projectId }),
       getKey: (item) => item.id,
       id: `${projectId}-deployments`,
+      staleTime: 30_000,
apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (1)

13-33: Order results by createdAt DESC and consider pagination.

Current limit: 500 without order can yield non‑deterministic pages and heavy payloads for large projects.

-      const deployments = await db.query.deployments.findMany({
+      const deployments = await db.query.deployments.findMany({
         where: (table, { eq, and }) =>
           and(eq(table.workspaceId, ctx.workspace.id), eq(table.projectId, input.projectId)),
+        orderBy: (table, { desc }) => [desc(table.createdAt)],
         columns: {

If needed soon, add cursor/offset inputs.

apps/dashboard/app/(app)/projects/[projectId]/layout.tsx (1)

30-39: Memoize per‑project collections to avoid re‑creating on each render.

Prevents unnecessary re-renders and re-subscriptions downstream.

-import { useState } from "react";
+import { useMemo, useState } from "react";
...
-  const collections = collectionManager.getProjectCollections(projectId);
+  const collections = useMemo(
+    () => collectionManager.getProjectCollections(projectId),
+    [projectId],
+  );

Optional: also memoize the provider value.

go/apps/ctrl/services/deployment/deploy_workflow.go (1)

106-116: Nit: step label says “version” while updating a deployment.

For consistency in logs/telemetry, rename to “update-deployment-building”.

- _, err = hydra.Step(ctx, "update-version-building", func(stepCtx context.Context) (*struct{}, error) {
+ _, err = hydra.Step(ctx, "update-deployment-building", func(stepCtx context.Context) (*struct{}, error) {

Same for “update-version-deploying” below.

apps/dashboard/lib/collections/ratelimit/overrides.ts (1)

21-24: Remove noisy console.info in data fetch.

This will spam consoles in production.

-      console.info("DB fetching ratelimitOverrides");
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx (3)

108-109: Clamp long commit messages to avoid overflow.

-            <div className="text-gray-9 text-xs">{deployment.gitCommitMessage}</div>
+            <div className="text-gray-9 text-xs truncate max-w-[38ch]">
+              {deployment.gitCommitMessage}
+            </div>

121-124: Improve avatar alt text for a11y.

-              <Avatar src={deployment.gitCommitAuthorAvatarUrl} alt="Author" />
+              <Avatar
+                src={deployment.gitCommitAuthorAvatarUrl}
+                alt={deployment.gitCommitAuthorName || "Commit author"}
+              />

145-151: Field shape check: confirm branch/sha fields; add safe fallbacks.

If the canonical Deployment shape uses source.branch/source.gitSha (per recent components), verify these top‑level fields exist; otherwise swap. Also add a fallback when missing.

-                <span className="text-grayA-9 text-xs truncate max-w-32">
-                  {deployment.gitBranch}
+                <span className="text-grayA-9 text-xs truncate max-w-32">
+                  {deployment.gitBranch ?? deployment.source?.branch ?? "unknown"}
                 </span>
@@
-                <span className="text-grayA-9 text-xs">
-                  {(deployment.gitCommitSha ?? "").slice(0, 7)}
+                <span className="text-grayA-9 text-xs">
+                  {(deployment.gitCommitSha ?? deployment.source?.gitSha ?? "").slice(0, 7) || "—"}
                 </span>
apps/dashboard/lib/collections/deploy/domains.ts (1)

1-31: Solid per‑project domains collection; consider runtime validation of responses.

Add Zod validation in queryFn to catch server/shape drift early in clients.

-      queryFn: () => trpcClient.deploy.domain.list.query({ projectId }),
+      queryFn: async () => {
+        const data = await trpcClient.deploy.domain.list.query({ projectId });
+        return z.array(schema).parse(data);
+      },
go/apps/ctrl/services/routing/service.go (1)

361-365: Rename complete; fix log message and keep status checks consistent.

  • Great switch to UpdateProjectLiveDeploymentId.
  • Update the nearby error log to say “live deployment ID”.
  • Prefer the enum constant for VM status everywhere for consistency.
-    s.logger.ErrorContext(ctx, "failed to update project active deployment ID",
+    s.logger.ErrorContext(ctx, "failed to update project live deployment ID",

Outside this hunk, also align VM status check:

-  for _, vm := range vms {
-    if vm.Status == "running" {
+  for _, vm := range vms {
+    if vm.Status == partitiondb.VmsStatusRunning {
       gatewayConfig.Vms = append(gatewayConfig.Vms, &partitionv1.VM{
         Id: vm.ID,
       })
     }
   }
apps/dashboard/lib/collections/deploy/environments.ts (1)

1-31: Per‑project environments collection — LGTM; optionally validate payloads.

Mirror the domains collection suggestion to parse responses with Zod.

-      queryFn: () => trpcClient.deploy.environment.list.query({ projectId }),
+      queryFn: async () => {
+        const data = await trpcClient.deploy.environment.list.query({ projectId });
+        return z.array(schema).parse(data);
+      },
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (3)

10-10: Handle TRPC conflict in UI and drop dead DuplicateKeyError path.

Conflicts are surfaced by TRPC (err.data.code === "CONFLICT"), not DuplicateKeyError. Set the slug field error on that condition; remove the unused import/branch.

Apply this diff:

-import { DuplicateKeyError } from "@tanstack/react-db";
-    } catch (error) {
-      if (error instanceof DuplicateKeyError) {
-        setError("slug", {
-          type: "custom",
-          message: "Project with this slug already exists",
-        });
-      } else {
-        console.error("Form submission error:", error);
-        // The collection's onInsert will handle showing error toasts
-      }
-    }
+    } catch (error: any) {
+      if (error?.data?.code === "CONFLICT") {
+        setError("slug", {
+          type: "custom",
+          message: "Project with this slug already exists",
+        });
+        return;
+      }
+      console.error("Form submission error:", error);
+      // The collection's onInsert already surfaces a toast
+    }

Also applies to: 50-59


38-45: Send only request-shaped fields on insert.

onInsert only uses name/slug/gitRepositoryUrl; extra fields (liveDeploymentId, updatedAt, placeholder id) are unused noise.

Apply this diff:

-      const tx = collection.projects.insert({
-        name: values.name,
-        slug: values.slug,
-        gitRepositoryUrl: values.gitRepositoryUrl || null,
-        liveDeploymentId: null,
-        updatedAt: null,
-        id: "will-be-replace-by-server",
-      });
+      const tx = collection.projects.insert({
+        name: values.name,
+        slug: values.slug,
+        gitRepositoryUrl: values.gitRepositoryUrl || null,
+      });

63-72: Re-validate slug when auto-updating from name.

Without flags, setValue won’t trigger validation; users may not see updated error state.

Apply this diff:

-    setValue("slug", slug);
+    setValue("slug", slug, { shouldDirty: true, shouldValidate: true });
apps/dashboard/lib/collections/deploy/projects.ts (1)

43-49: Don’t assume mutations[0] exists.

Accessing transaction.mutations[0] can throw if the array is empty or reordered by future changes. Guard or pick by intent (e.g., find the insert mutation) to make this resilient.

Apply this defensive tweak:

-      const { changes } = transaction.mutations[0];
+      const first = transaction.mutations[0];
+      if (!first) return;
+      const { changes } = first;
apps/dashboard/lib/collections/index.ts (1)

17-22: Optional: rename projects in ProjectCollections to avoid confusion.

Since this is the global/shared collection, a name like sharedProjects is clearer.

Apply this diff (non-breaking if you update callsites):

-type ProjectCollections = {
-  environments: ReturnType<typeof createEnvironmentsCollection>;
-  domains: ReturnType<typeof createDomainsCollection>;
-  deployments: ReturnType<typeof createDeploymentsCollection>;
-  projects: typeof projects;
-};
+type ProjectCollections = {
+  environments: ReturnType<typeof createEnvironmentsCollection>;
+  domains: ReturnType<typeof createDomainsCollection>;
+  deployments: ReturnType<typeof createDeploymentsCollection>;
+  sharedProjects: typeof projects;
+};
@@
-      this.projectCollections.set(projectId, {
+      this.projectCollections.set(projectId, {
         environments: createEnvironmentsCollection(projectId),
         domains: createDomainsCollection(projectId),
         deployments: createDeploymentsCollection(projectId),
-        projects,
+        sharedProjects: projects,
       });

Also applies to: 32-37

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f8691d and 6081ff3.

📒 Files selected for processing (36)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx (5 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx (4 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/layout.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/[projectId]/page.tsx (2 hunks)
  • apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (5 hunks)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx (2 hunks)
  • apps/dashboard/lib/collections/deploy/deployments.ts (1 hunks)
  • apps/dashboard/lib/collections/deploy/domains.ts (1 hunks)
  • apps/dashboard/lib/collections/deploy/environments.ts (1 hunks)
  • apps/dashboard/lib/collections/deploy/projects.ts (1 hunks)
  • apps/dashboard/lib/collections/domains.ts (0 hunks)
  • apps/dashboard/lib/collections/environments.ts (0 hunks)
  • apps/dashboard/lib/collections/index.ts (1 hunks)
  • apps/dashboard/lib/collections/ratelimit/namespaces.ts (3 hunks)
  • apps/dashboard/lib/collections/ratelimit/overrides.ts (2 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/domains/list.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts (4 hunks)
  • apps/dashboard/lib/trpc/routers/deploy/project/list.ts (1 hunks)
  • apps/dashboard/lib/trpc/routers/environment/list.ts (1 hunks)
  • go/apps/ctrl/services/deployment/deploy_workflow.go (1 hunks)
  • go/apps/ctrl/services/routing/service.go (1 hunks)
  • go/pkg/db/models_generated.go (2 hunks)
  • go/pkg/db/project_update_active_deployment_id.sql_generated.go (0 hunks)
  • go/pkg/db/project_update_live_deployment_id.sql_generated.go (1 hunks)
  • go/pkg/db/querier_generated.go (2 hunks)
  • go/pkg/db/queries/project_update_active_deployment_id.sql (0 hunks)
  • go/pkg/db/queries/project_update_live_deployment_id.sql (1 hunks)
  • go/pkg/db/schema.sql (2 hunks)
  • internal/db/src/schema/projects.ts (1 hunks)
💤 Files with no reviewable changes (4)
  • go/pkg/db/queries/project_update_active_deployment_id.sql
  • apps/dashboard/lib/collections/environments.ts
  • go/pkg/db/project_update_active_deployment_id.sql_generated.go
  • apps/dashboard/lib/collections/domains.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/dashboard/app/(app)/projects/_components/list/index.tsx
  • go/pkg/db/schema.sql
  • apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/page.tsx
  • apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-deployments.ts
🧰 Additional context used
🧠 Learnings (9)
📓 Common learnings
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.
📚 Learning: 2025-09-12T08:01:20.752Z
Learnt from: Flo4604
PR: unkeyed/unkey#3944
File: go/pkg/db/acme_challenge_update_verified_with_expiry.sql_generated.go:31-39
Timestamp: 2025-09-12T08:01:20.752Z
Learning: Do not review or suggest changes to files with sql_generated.go suffix or other files marked as auto-generated (containing "Code generated by" comments), as these are generated by tools like sqlc and changes would be overwritten on regeneration.

Applied to files:

  • go/pkg/db/project_update_live_deployment_id.sql_generated.go
📚 Learning: 2025-08-31T19:30:38.171Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3853
File: apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts:75-87
Timestamp: 2025-08-31T19:30:38.171Z
Learning: In apps/dashboard/lib/trpc/routers/deploy/project/envs/list.ts, the getEnvs procedure currently uses mock data (VARIABLES) and ignores the projectId input parameter. This is intentional temporary behavior - the user ogzhanolguncu indicated they plan to hook this up to the database later.

Applied to files:

  • apps/dashboard/lib/collections/deploy/environments.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/list.ts
  • apps/dashboard/lib/trpc/routers/environment/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts
📚 Learning: 2025-07-25T19:09:43.284Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/list.ts:11-11
Timestamp: 2025-07-25T19:09:43.284Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/list.ts, the listDeployments procedure intentionally queries the versions table rather than a deployments table. The user mcstepp indicated that renaming the table would require a database migration, which was deferred for the current PR focused on UI features.

Applied to files:

  • apps/dashboard/lib/collections/deploy/deployments.ts
  • apps/dashboard/lib/trpc/routers/deploy/domains/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts
  • internal/db/src/schema/projects.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts
📚 Learning: 2025-07-25T19:11:00.208Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts:110-147
Timestamp: 2025-07-25T19:11:00.208Z
Learning: In apps/dashboard/lib/trpc/routers/deployment/getOpenApiDiff.ts, the user mcstepp prefers to keep mock data fallbacks in POC/demonstration code for simplicity, even if it wouldn't be production-ready. This aligns with the PR being work-in-progress for demonstration purposes.

Applied to files:

  • apps/dashboard/lib/trpc/routers/deploy/project/list.ts
  • apps/dashboard/lib/trpc/routers/deploy/project/create.ts
📚 Learning: 2025-08-25T12:56:59.310Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3834
File: apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts:59-66
Timestamp: 2025-08-25T12:56:59.310Z
Learning: In the ratelimit namespace query system (apps/dashboard/lib/trpc/routers/ratelimit/query-namespaces/index.ts), the nameQuery filter is designed as an array for future extensibility to support multiple filters, but currently only the first filter (index 0) is processed. This is intentional future-proofing.

Applied to files:

  • apps/dashboard/lib/collections/ratelimit/namespaces.ts
📚 Learning: 2025-07-28T20:38:53.244Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx:322-341
Timestamp: 2025-07-28T20:38:53.244Z
Learning: In apps/dashboard/app/(app)/projects/[projectId]/diff/[...compare]/components/client.tsx, mcstepp prefers to keep hardcoded endpoint logic in the getDiffType function during POC phases for demonstrating diff functionality, rather than implementing a generic diff algorithm. This follows the pattern of keeping simplified implementations for demonstration purposes.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx
📚 Learning: 2025-08-18T10:28:47.391Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#3797
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/control-cloud/index.tsx:1-4
Timestamp: 2025-08-18T10:28:47.391Z
Learning: In Next.js App Router, components that use React hooks don't need their own "use client" directive if they are rendered within a client component that already has the directive. The client boundary propagates to child components.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx
📚 Learning: 2025-09-12T17:57:12.128Z
Learnt from: mcstepp
PR: unkeyed/unkey#3952
File: apps/dashboard/app/(app)/projects/[projectId]/deployments/components/rollback-dialog.tsx:111-116
Timestamp: 2025-09-12T17:57:12.128Z
Learning: In the Deployment type used across the dashboard deployment components, the source field is required and will always be present, so optional chaining (source?.branch) is not needed when accessing source.branch or source.gitSha.

Applied to files:

  • apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx
🧬 Code graph analysis (24)
apps/dashboard/lib/collections/deploy/domains.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)
go/pkg/db/types/null_string.go (1)
  • NullString (10-10)
apps/dashboard/app/(app)/projects/[projectId]/layout.tsx (2)
apps/dashboard/lib/collections/index.ts (1)
  • collectionManager (69-69)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • ProjectLayoutContext (11-11)
apps/dashboard/lib/collections/ratelimit/overrides.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/collections/deploy/environments.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/collections/deploy/projects.ts (3)
go/pkg/db/models_generated.go (1)
  • Project (671-682)
internal/db/src/schema/projects.ts (1)
  • projects (8-31)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/collections/deploy/deployments.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/trpc/routers/deploy/domains/list.ts (1)
apps/dashboard/lib/trpc/trpc.ts (4)
  • t (8-8)
  • requireUser (10-21)
  • requireWorkspace (23-36)
  • withRatelimit (122-138)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-filters/components/environment-filter.tsx (2)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • useProjectLayout (13-19)
internal/db/src/schema/environments.ts (1)
  • environments (8-24)
apps/dashboard/lib/trpc/routers/environment/list.ts (3)
apps/dashboard/lib/trpc/trpc.ts (3)
  • t (8-8)
  • requireUser (10-21)
  • requireWorkspace (23-36)
apps/dashboard/lib/db.ts (1)
  • db (5-26)
internal/db/src/schema/environments.ts (1)
  • environments (8-24)
go/apps/ctrl/services/routing/service.go (1)
go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)
  • UpdateProjectLiveDeploymentIdParams (19-23)
apps/dashboard/lib/collections/ratelimit/namespaces.ts (1)
apps/dashboard/lib/collections/client.ts (1)
  • trpcClient (12-25)
apps/dashboard/lib/trpc/routers/deploy/deployment/list.ts (2)
apps/dashboard/lib/trpc/trpc.ts (3)
  • t (8-8)
  • requireUser (10-21)
  • requireWorkspace (23-36)
internal/db/src/schema/deployments.ts (1)
  • deployments (9-52)
apps/dashboard/lib/trpc/routers/deploy/project/create.ts (1)
apps/dashboard/lib/collections/deploy/projects.ts (1)
  • createProjectRequestSchema (16-28)
apps/dashboard/app/(app)/projects/[projectId]/page.tsx (3)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • useProjectLayout (13-19)
internal/db/src/schema/domains.ts (1)
  • domains (4-20)
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx (1)
  • ActiveDeploymentCard (71-272)
apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx (1)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • useProjectLayout (13-19)
go/pkg/db/querier_generated.go (2)
go/pkg/partition/db/database.go (1)
  • DBTX (10-10)
go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)
  • UpdateProjectLiveDeploymentIdParams (19-23)
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx (3)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • useProjectLayout (13-19)
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/git-avatar.tsx (1)
  • Avatar (10-29)
apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/info-chip.tsx (1)
  • InfoChip (8-13)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
apps/dashboard/lib/collections/index.ts (1)
  • collectionManager (69-69)
apps/dashboard/app/(app)/projects/[projectId]/deployments/components/controls/components/deployment-list-datetime/index.tsx (1)
apps/dashboard/app/(app)/projects/[projectId]/deployments/hooks/use-filters.ts (1)
  • useFilters (33-127)
apps/dashboard/app/(app)/projects/_components/create-project/create-project-dialog.tsx (2)
apps/dashboard/lib/collections/deploy/projects.ts (2)
  • CreateProjectRequestSchema (31-31)
  • createProjectRequestSchema (16-28)
apps/dashboard/lib/collections/index.ts (2)
  • collection (72-76)
  • reset (78-86)
go/apps/ctrl/services/deployment/deploy_workflow.go (2)
go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)
  • UpdateProjectLiveDeploymentIdParams (19-23)
go/pkg/db/types/null_string.go (1)
  • NullString (10-10)
apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx (2)
apps/dashboard/app/(app)/projects/[projectId]/layout-provider.tsx (1)
  • useProjectLayout (13-19)
apps/dashboard/lib/collections/index.ts (1)
  • collection (72-76)
apps/dashboard/lib/collections/index.ts (4)
apps/dashboard/lib/collections/deploy/environments.ts (1)
  • createEnvironmentsCollection (15-30)
apps/dashboard/lib/collections/deploy/domains.ts (1)
  • createDomainsCollection (16-31)
apps/dashboard/lib/collections/deploy/deployments.ts (1)
  • createDeploymentsCollection (41-56)
apps/dashboard/lib/collections/deploy/projects.ts (1)
  • projects (33-98)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (22)
apps/dashboard/lib/trpc/routers/environment/list.ts (1)

28-36: Good error normalization.

Re-throwing TRPCError and mapping others to INTERNAL_SERVER_ERROR is solid.

apps/dashboard/lib/trpc/routers/deploy/project/create.ts (1)

102-111: LGTM: default environments created atomically.

Creating production/preview within the same transaction is correct and matches the new (project_id, slug) uniqueness.

go/pkg/db/querier_generated.go (1)

1677-1683: API rename acknowledged — sweep callsites

Interface renamed to UpdateProjectLiveDeploymentId in go/pkg/db/querier_generated.go (lines 1677–1683). Confirm all callsites migrated from UpdateProjectActiveDeploymentId to UpdateProjectLiveDeploymentId; e.g. run: rg -nP '\b(UpdateProjectActiveDeploymentId|UpdateProjectLiveDeploymentId)\b' .

go/pkg/db/models_generated.go (2)

3-3: Inconsistent sqlc versions in generated files — align to a single sqlc version (recommended: v1.29.0)

Most generated files are stamped v1.29.0, but the following have older headers — regenerate these with sqlc v1.29.0 (do not edit generated files manually):

  • v1.27.0

    • go/pkg/db/models_generated.go
    • go/pkg/db/project_update_live_deployment_id.sql_generated.go
    • go/pkg/db/querier_generated.go
  • v1.28.0

    • go/pkg/kv/stores/mysql/db.go
    • go/pkg/kv/stores/mysql/delete.sql.go
    • go/pkg/kv/stores/mysql/delete_expired.sql.go
    • go/pkg/kv/stores/mysql/get.sql.go
    • go/pkg/kv/stores/mysql/list_by_workspace.sql.go
    • go/pkg/kv/stores/mysql/models.go
    • go/pkg/kv/stores/mysql/set.sql.go

Regenerate these files with sqlc v1.29.0 or standardize the generator in CI to ensure reproducible builds.

⛔ Skipped due to learnings
Learnt from: Flo4604
PR: unkeyed/unkey#3944
File: go/pkg/db/acme_challenge_update_verified_with_expiry.sql_generated.go:31-39
Timestamp: 2025-09-12T08:01:20.752Z
Learning: Do not review or suggest changes to files with sql_generated.go suffix or other files marked as auto-generated (containing "Code generated by" comments), as these are generated by tools like sqlc and changes would be overwritten on regeneration.
Learnt from: chronark
PR: unkeyed/unkey#3420
File: go/pkg/hydra/store/gorm/gorm.go:486-498
Timestamp: 2025-07-02T11:51:58.572Z
Learning: The Hydra package (go/pkg/hydra) is planned to be migrated from GORM to sqlc for database operations, which explains why raw SQL queries are acceptable in the current implementation.

672-682: LiveDeploymentID rename validated — confirm migration applied

Backend models, generated queries, and schema now use live_deployment_id; no occurrences of ActiveDeploymentID/active_deployment_id found. Remaining hits are a frontend sentinel and an unrelated count alias.

  • go/pkg/db/models_generated.go — Project.LiveDeploymentID (db:"live_deployment_id").
  • go/pkg/db/schema.sql — contains live_deployment_id.
  • go/pkg/db/project_update_live_deployment_id.sql & go/pkg/db/project_update_live_deployment_id.sql_generated.go — update query for live_deployment_id.
  • go/apps/ctrl/services/deployment/deploy_workflow.go:387 and go/apps/ctrl/services/routing/service.go:363 — set LiveDeploymentID.
  • internal/db/src/schema/projects.ts:21 — liveDeploymentId.
  • Frontend sentinel: apps/dashboard/.../deployment-list-table-action.popover.constants.tsx:56 — "current_active_deployment_id" (UI-only).
  • Unrelated: go/apps/metald/sqlc/queries.sql & go/apps/metald/internal/database/queries.sql.go — active_deployments used as a count alias.

Confirm the DB migration that renames/applies this column was executed in all target environments (or include the migration if missing).

go/pkg/db/queries/project_update_live_deployment_id.sql (1)

1-4: LGTM — confirm timestamp semantics.

projects.updated_at is BIGINT (go/pkg/db/schema.sql) and UpdateProjectLiveDeploymentId expects UpdatedAt sql.NullInt64 (go/pkg/db/project_update_live_deployment_id.sql_generated.go). Repo lifecycle defaults use Date.now() (milliseconds) (internal/db/src/schema/util/lifecycle_dates.ts). Ensure callers pass epoch milliseconds (not NOW()) when calling this query.

apps/dashboard/lib/collections/ratelimit/namespaces.ts (3)

6-6: Import path change to shared client looks good.


30-33: Namespace create call formatting change is no‑op — fine to keep.


62-64: Namespace delete call formatting change is no‑op — fine to keep.

apps/dashboard/app/(app)/projects/[projectId]/page.tsx (2)

41-48: liveDeploymentId swap looks correct.

ActiveDeploymentCard will now read from the per‑project deployments collection; good alignment.


14-17: Resolved — domains collection is project‑scoped.
createDomainsCollection(projectId) is used when populating projectCollections (apps/dashboard/lib/collections/index.ts) and createDomainsCollection throws if projectId is missing (apps/dashboard/lib/collections/deploy/domains.ts).

apps/dashboard/lib/collections/ratelimit/overrides.ts (2)

6-6: Import path change to shared client is correct.


59-61: Delete mutation formatting change is a no‑op — fine.

apps/dashboard/app/(app)/projects/[projectId]/details/project-details-expandables/index.tsx (1)

23-31: Join source swap + liveDeploymentId — looks good; verify cross-collection join compatibility.

Using global collection.projects with per-project collections.deployments should share the same React‑DB client; confirm they’re produced by the same queryClient to avoid join-time cache mismatches.

apps/dashboard/app/(app)/projects/[projectId]/diff/page.tsx (1)

8-12: Confirm layout wrapper usage.

This page assumes useProjectLayout() context. Ensure it’s always rendered under ProjectLayoutWrapper.

apps/dashboard/app/(app)/projects/[projectId]/details/active-deployment-card/index.tsx (1)

72-77: Collection source switch — good.

Querying collections.deployments by deploymentId matches the new per‑project pattern.

go/pkg/db/project_update_live_deployment_id.sql_generated.go (1)

1-34: Auto-generated file — no review.

apps/dashboard/lib/collections/deploy/projects.ts (2)

33-41: Projects list wiring looks good.

Solid query keying and list fetch via trpcClient.deploy.project.list.query(). getKey: item.id is appropriate.


52-95: Good UX on error mapping and toast lifecycle.

toast.promise(mutation, …); await mutation; provides user feedback and keeps the transaction lifecycle correct.

Please confirm the created project appears in the list without a manual reload (collection should reconcile automatically). If it doesn’t, we can add an invalidate/refetch hook after success.

apps/dashboard/lib/collections/index.ts (3)

24-41: Per‑project collection cache is clean and cohesive.

Nice encapsulation of env/domains/deployments keyed by projectId; returning the shared projects collection is pragmatic.


56-66: Cleanup order and concurrency look fine.

Project collections are cleaned first, then global collections; Promise.all prevents leaks.


1-87: Verify active→live deployment rename is complete — manual re-check required

ripgrep in the verification run skipped files; results are inconclusive. From the repo root run (locally) to confirm no stale references remain:

# Search code for leftover fields/keys
git ls-files -z | xargs -0 rg -nP -S '\bactiveDeploymentId\b|active_deployment_id|active[_-]?deployment' || true

# Search DB migrations / index names for environment uniqueness by project
git ls-files -z | xargs -0 rg -nP -S -i '(environment.*unique.*project_id)|(unique.*project_id.*environment)' || true

# Fallback (if rg isn't available or to double-check)
git grep -nE 'activeDeploymentId|active_deployment_id|active[_-]?deployment' || true
git grep -niE 'environment.*unique.*project_id|unique.*project_id.*environment' || true

If any matches are found, update usages/migrations to the new "live" naming and ensure DB/index names and any serialized keys are migrated consistently.

@chronark
Copy link
Collaborator

just waiting on green CI here

@chronark chronark merged commit 132f477 into main Sep 16, 2025
18 checks passed
@chronark chronark deleted the fix-collections-and-db-idx branch September 16, 2025 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants