Skip to content

feat: db change and new workspace slug#3849

Merged
chronark merged 19 commits intomainfrom
workspace-slug-in-db
Aug 31, 2025
Merged

feat: db change and new workspace slug#3849
chronark merged 19 commits intomainfrom
workspace-slug-in-db

Conversation

@MichaelUnkey
Copy link
Collaborator

@MichaelUnkey MichaelUnkey commented Aug 25, 2025

What does this PR do?

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?

  • Column in DB is what we want
  • New workspace functions as expected
  • No errors

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

    • Workspaces now have a unique URL slug. During creation, you’ll enter a slug (3–64 chars; lowercase letters, numbers, hyphens; no leading/trailing hyphens).
    • The form now has two required fields: name and slug, with live validation and automatic slug suggestions from the name (editable).
    • Workspace URL preview shows app.unkey.com/.
  • Chores

    • Backend and database updated to store and enforce unique workspace slugs.

@changeset-bot
Copy link

changeset-bot bot commented Aug 25, 2025

⚠️ No Changeset found

Latest commit: af74624

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

@vercel
Copy link

vercel bot commented Aug 25, 2025

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

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Aug 31, 2025 9:30am
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Aug 31, 2025 9:30am

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 25, 2025

📝 Walkthrough

Walkthrough

Introduces a workspace slug across UI, API, and database layers. Adds slug field to DB schemas and migrations with a unique constraint, updates createWorkspace input/return, modifies the onboarding form to collect/validate slug, and seeds test/local data with slug values.

Changes

Cohort / File(s) Summary of changes
DB schema and migrations
internal/db/src/schema/workspaces.ts, go/pkg/db/schema.sql, internal/db/drizzle/0000_dazzling_colonel_america.sql, internal/db/drizzle/meta/0000_snapshot.json, internal/db/drizzle/meta/_journal.json
Add workspaces.slug varchar(64) with UNIQUE constraint; update Drizzle and Go SQL schemas; snapshot and journal metadata updated accordingly.
Dashboard workspace creation flow
apps/dashboard/lib/trpc/routers/workspace/create.ts, apps/dashboard/app/new/hooks/use-workspace-step.tsx
createWorkspace now requires { name, slug } with stricter validation; returns slug. Onboarding form adds slug field, validation, auto-generation rules, submission includes slug; requiredFieldCount updated to 2.
Test/local data seeding
apps/api/src/pkg/testutil/harness.ts, tools/local/src/db.ts
Seeded workspaces now include slug values (e.g., "unkey-workspace", "user-workspace").

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant D as Dashboard (Form)
  participant A as createWorkspace (tRPC)
  participant DB as Database

  U->>D: Enter name and slug
  D->>A: mutate({ name, slug })
  A->>DB: INSERT INTO workspaces (name, slug, ...)
  alt slug unique
    DB-->>A: OK (workspace id, slug)
    A-->>D: Success
    D-->>U: Workspace created
  else slug conflict
    DB-->>A: UNIQUE constraint error
    A-->>D: Error (slug taken)
    D-->>U: Show validation error
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • imeyer
  • mcstepp
  • ogzhanolguncu
  • perkinsjr
  • Flo4604

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch workspace-slug-in-db

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

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

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@MichaelUnkey MichaelUnkey marked this pull request as ready for review August 25, 2025 18:53
@github-actions
Copy link
Contributor

github-actions bot commented Aug 25, 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: 11

Caution

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

⚠️ Outside diff range comments (3)
tools/local/src/db.ts (1)

28-33: Make seeding idempotent: set slug on duplicate as well.

If the workspaces row already exists (created before this migration), onDuplicateKeyUpdate won’t populate slug, leaving it NULL. Seed should converge state by updating slug too.

Apply this diff:

       .onDuplicateKeyUpdate({ set: { createdAtM: Date.now() } });
+      .onDuplicateKeyUpdate({
+        set: { createdAtM: Date.now(), slug: "unkey-workspace" },
+      });
apps/dashboard/lib/trpc/routers/workspace/create.ts (2)

49-53: Pre-check slug uniqueness before creating the tenant to avoid orphaned external records.

If the DB insert fails on the unique slug, we’ve already created a tenant via authProvider.createTenant. Do a quick existence check first to fail fast. Keep the DB unique index as the source of truth to guard against races.

Apply this diff:

-    const orgId = await authProvider.createTenant({
-      name: input.name,
-      userId,
-    });
+    // Fail fast if slug is already taken (DB unique index remains the final arbiter)
+    const existingSlug = await db.query.workspaces.findFirst({
+      where: (w, { eq }) => eq(w.slug, input.slug),
+    });
+    if (existingSlug) {
+      throw new TRPCError({ code: "CONFLICT", message: "Workspace slug is already in use" });
+    }
+
+    const orgId = await authProvider.createTenant({
+      name: input.name,
+      userId,
+    });

74-109: Map duplicate-key errors to a 409/CONFLICT and preserve generic fallback.

Current catch rethrows a generic 500, which masks user-facing feedback on slug conflicts. Detect the duplicate-key error and return a proper TRPCError with code CONFLICT.

Apply this diff:

-      })
-      .catch((_err) => {
-        throw new TRPCError({
-          code: "INTERNAL_SERVER_ERROR",
-          message:
-            "We are unable to create the workspace. Please try again or contact support@unkey.dev",
-        });
-      });
+      })
+      .catch((err) => {
+        const code = (err as any)?.code as string | undefined;
+        const msg = (err as Error)?.message ?? "";
+        // MySQL duplicate key
+        if (code === "ER_DUP_ENTRY" || /workspaces_slug_unique|Duplicate entry/.test(msg)) {
+          throw new TRPCError({
+            code: "CONFLICT",
+            message: "Workspace slug is already in use",
+          });
+        }
+        throw new TRPCError({
+          code: "INTERNAL_SERVER_ERROR",
+          message:
+            "We are unable to create the workspace. Please try again or contact support@unkey.dev",
+        });
+      });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 84aefd8 and 0560ccc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (8)
  • apps/api/src/pkg/testutil/harness.ts (2 hunks)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
  • apps/dashboard/lib/trpc/routers/workspace/create.ts (2 hunks)
  • internal/db/drizzle/0001_legal_kat_farrell.sql (1 hunks)
  • internal/db/drizzle/meta/0001_snapshot.json (1 hunks)
  • internal/db/drizzle/meta/_journal.json (1 hunks)
  • internal/db/src/schema/workspaces.ts (1 hunks)
  • tools/local/src/db.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • tools/local/src/db.ts
  • internal/db/src/schema/workspaces.ts
  • apps/api/src/pkg/testutil/harness.ts
  • apps/dashboard/lib/trpc/routers/workspace/create.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • tools/local/src/db.ts
  • internal/db/src/schema/workspaces.ts
  • apps/api/src/pkg/testutil/harness.ts
  • apps/dashboard/lib/trpc/routers/workspace/create.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • tools/local/src/db.ts
  • internal/db/src/schema/workspaces.ts
  • apps/api/src/pkg/testutil/harness.ts
  • apps/dashboard/lib/trpc/routers/workspace/create.ts
🧠 Learnings (4)
📚 Learning: 2025-04-08T09:34:24.576Z
Learnt from: ogzhanolguncu
PR: unkeyed/unkey#2872
File: apps/dashboard/lib/trpc/routers/ratelimit/createNamespace.ts:36-39
Timestamp: 2025-04-08T09:34:24.576Z
Learning: In the Unkey dashboard, when making database queries involving workspaces, use `ctx.workspace.id` directly instead of fetching the workspace separately for better performance and security.

Applied to files:

  • tools/local/src/db.ts
📚 Learning: 2025-08-21T12:37:40.966Z
Learnt from: Flo4604
PR: unkeyed/unkey#3821
File: apps/dashboard/lib/trpc/routers/key/updateRootKeyPermissions.ts:74-74
Timestamp: 2025-08-21T12:37:40.966Z
Learning: Root keys in Unkey have two workspace fields: `workspaceId` (always set to env().UNKEY_WORKSPACE_ID for the Unkey workspace that owns the key) and `forWorkspaceId` (set to ctx.workspace.id for the user's workspace that the key is for). When querying root keys, the system filters by forWorkspaceId to get keys for the current user's workspace, but the returned rootKey.workspaceId is always the Unkey workspace ID.

Applied to files:

  • tools/local/src/db.ts
  • apps/api/src/pkg/testutil/harness.ts
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (7-39)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-114)
⏰ 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 Go API Local / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test Packages / Test
🔇 Additional comments (3)
internal/db/drizzle/meta/_journal.json (1)

12-18: Journal entry looks consistent with the migration.

Idx increments, tag matches the SQL filename, and timestamp ordering is coherent. No action needed.

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

58-59: OK once normalization is in place.

With the Zod .transform((s) => s.toLowerCase()), storing slug: input.slug preserves canonical lowercase.

apps/api/src/pkg/testutil/harness.ts (1)

257-277: Good: seed data now includes slugs matching the new schema.

Adding slug: "unkey-workspace" and slug: "user-workspace" keeps Workspace literals compliant and exercises the new unique index during tests. No issues.

Comment on lines +1243 to +1251
"workspaces_org_id_unique": {
"name": "workspaces_org_id_unique",
"columns": ["org_id"]
},
"workspaces_slug_unique": {
"name": "workspaces_slug_unique",
"columns": ["slug"]
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Heads-up: unique index on nullable slug allows multiple NULLs in MySQL.

workspaces_slug_unique won’t prevent multiple rows with slug = NULL. That’s acceptable for a staged rollout, but once backfilled, consider enforcing NOT NULL to ensure uniqueness semantics match expectations.

🤖 Prompt for AI Agents
internal/db/drizzle/meta/0001_snapshot.json lines 1243-1251: the unique index
workspaces_slug_unique is defined on a nullable slug, which in MySQL allows
multiple NULLs; to enforce true uniqueness after rollout, backfill all NULL
slugs (or assign unique placeholder values and resolve duplicates), verify no
duplicate non-NULL slugs remain, then create a migration that alters the slug
column to NOT NULL and (re)applies the unique constraint so the DB enforces
uniqueness; include queries to detect duplicates and a safe backfill/update step
before altering the column.

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

♻️ Duplicate comments (3)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)

172-186: Avoid hardcoded domain in prefix; read from env/config

Replace the literal app.unkey.com/ with an env-driven value to support non-prod environments and white-labeling.

Example:

-              prefix="app.unkey.com/"
+              prefix={`${process.env.NEXT_PUBLIC_UNKEY_DASHBOARD_APP_HOST ?? "app.unkey.com"}/`}

Per guidelines, prefer env name like UNKEY_DASHBOARD_APP_HOST (exposed via NEXT_PUBLIC_).


105-108: Unify mutation style (await mutateAsync or use mutate callbacks)

You call mutateAsync but don’t await it. Prefer one style to avoid unhandled promise behavior and make control flow explicit.

Apply one of:

-    createWorkspace.mutateAsync({
+    await createWorkspace.mutateAsync({
       name: data.workspaceName,
       slug: data.workspaceUrl.toLowerCase(),
     });

or

-    createWorkspace.mutateAsync({
+    createWorkspace.mutate({
       name: data.workspaceName,
       slug: data.workspaceUrl.toLowerCase(),
     });

(You already have onSuccess/onError callbacks, so mutate is a clean fit.)

internal/db/drizzle/meta/0001_snapshot.json (1)

1196-1202: workspaces.slug is nullable; uniqueness won’t prevent multiple NULLs

For rollout this is fine, but plan to backfill and then set NOT NULL to have true uniqueness semantics.

Follow-up migration:

  • backfill slug for all existing workspaces,
  • ALTER TABLE workspaces MODIFY slug varchar(64) NOT NULL;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0560ccc and b255d98.

📒 Files selected for processing (5)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
  • internal/db/drizzle/0001_workable_wildside.sql (1 hunks)
  • internal/db/drizzle/meta/0001_snapshot.json (1 hunks)
  • internal/db/drizzle/meta/_journal.json (1 hunks)
  • internal/db/src/schema/workspaces.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • internal/db/src/schema/workspaces.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • internal/db/src/schema/workspaces.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • internal/db/src/schema/workspaces.ts
🧠 Learnings (2)
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (7-39)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-114)
⏰ 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 API / API Test Local
  • GitHub Check: Build / Build
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (6)
internal/db/src/schema/workspaces.ts (1)

24-26: Plan NOT NULL and lowercase invariants for slug

  • Currently in internal/db/src/schema/workspaces.ts the slug column is defined as
    slug: varchar("slug", { length: 64 }).unique(),
    which leaves it nullable by default—meaning MySQL will allow multiple rows with NULL slugs. That’s acceptable for a phased rollout, but we need to backfill existing records and then lock down the schema to prevent future NULL values.
  • We should also enforce lowercase at the API boundary now, since the database won’t normalize case for us.

Proposed phased plan (apply after data backfill completes):

--- a/internal/db/src/schema/workspaces.ts
+++ b/internal/db/src/schema/workspaces.ts
@@ export const workspaces = mysqlTable("workspaces", {
-  slug: varchar("slug", { length: 64 }).unique(),
+  slug: varchar("slug", { length: 64 }).notNull().unique(),

I can draft the backfill script and the ALTER TABLE migration when you’re ready.

apps/dashboard/app/new/hooks/use-workspace-step.tsx (3)

31-33: Nice: explicit “manually edited” guard for slug generation

Good UX improvement to prevent overwriting user input. LGTM.


158-167: Auto-generate slug only when appropriate — looks correct

Good checks for empty, not-dirty, and not manually edited before generating. This avoids clobbering user-entered slugs.


193-193: requiredFieldCount updated to 2 — correct

Matches required fields (name + URL handle). LGTM.

internal/db/drizzle/meta/0001_snapshot.json (2)

1247-1250: Unique constraint present — good

workspaces_slug_unique is in the snapshot and matches the ORM definition.


250-256: Incorrect constraint migration assumption

The unique_slug_per_workspace_idx constraint is created in the initial migration and remains in place after dropping the name constraint, so no additional migration is needed.

• In internal/db/drizzle/0000_broken_blockbuster.sql, the permissions table is created with both

CONSTRAINT `unique_name_per_workspace_idx` UNIQUE(`workspace_id`,`name`),
CONSTRAINT `unique_slug_per_workspace_idx` UNIQUE(`workspace_id`,`slug`)

(lines 47–48)
• In internal/db/drizzle/0001_workable_wildside.sql, only the name index is dropped:

ALTER TABLE `permissions` DROP INDEX `unique_name_per_workspace_idx`;

(line 1)

Because the slug uniqueness constraint already exists from the 0000 migration and is not removed, no “ADD CONSTRAINT unique_slug_per_workspace_idx” step is required.

Likely an incorrect or invalid review comment.

Comment on lines +2 to +3
ALTER TABLE `workspaces` ADD `slug` varchar(64);
ALTER TABLE `workspaces` ADD CONSTRAINT `workspaces_slug_unique` UNIQUE(`slug`);--> statement-breakpoint
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

🛠️ Refactor suggestion

Workspaces slug DDL looks good; plan backfill + NOT NULL

Adding slug varchar(64) with a unique constraint is correct for a non-breaking rollout. Follow up with:

  • backfill slugs for existing rows (slugify(name), ensure uniqueness via suffixing),
  • set NOT NULL on slug.

I can draft a backfill script plus ALTER migration that safely handles collisions (e.g., appending “-2”, “-3”, …) and adds a unique index on LOWER(slug) via a generated column if you want case-insensitive guarantees.

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 (1)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)

72-106: Map unique-constraint violations to CONFLICT and surface a helpful message.

Right now, all DB errors become INTERNAL_SERVER_ERROR. Detect duplicate slug (e.g., Postgres code 23505, constraint workspaces_slug_unique) and throw CONFLICT with a precise message so the UI can display “This workspace URL is already taken.”

Example (outside the selected range; adapt to your DB client):

try {
  await db.transaction(async (tx) => { /* ... */ });
} catch (err: any) {
  // Postgres
  if (err?.code === "23505" && String(err?.constraint || "").includes("workspaces_slug_unique")) {
    throw new TRPCError({ code: "CONFLICT", message: "Workspace URL is already in use" });
  }
  throw new TRPCError({
    code: "INTERNAL_SERVER_ERROR",
    message:
      "We are unable to create the workspace. Please try again or contact support@unkey.dev",
  });
}

If you want, I can wire this up for Drizzle/pg and add a small e2e test. Do you want me to open a follow-up?

♻️ Duplicate comments (2)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)

15-19: Normalize, trim, and bound slug input; align length constraints with DB (64).

Add .trim() to both fields and enforce .min(3).max(64) on slug. Consider canonicalizing to lowercase at the API boundary so clients can be lax while storage stays canonical. This prevents whitespace-only names, overlong slugs, and mixed-case drift.

Apply:

-      name: z.string().min(3).max(50),
-      slug: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
-        message: "Slug must be lowercase alphanumeric, words separated by single hyphens",
-      }),
+      name: z.string().trim().min(3).max(50),
+      slug: z
+        .string()
+        .trim()
+        .min(3)
+        .max(64)
+        .transform((s) => s.toLowerCase())
+        .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, {
+          message:
+            "Slug must be lowercase alphanumeric, words separated by single hyphens",
+        }),
apps/dashboard/app/new/hooks/use-workspace-step.tsx (1)

105-108: Use a single mutation style: either await mutateAsync or use mutate.

You’re calling mutateAsync without awaiting. Prefer await mutateAsync(...) (and let onError handle UI) or switch to mutate(...). This avoids dangling promises and keeps error handling predictable.

-    createWorkspace.mutateAsync({
+    await createWorkspace.mutateAsync({
       name: data.workspaceName,
       slug: data.workspaceUrl.toLowerCase(),
     });
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between b255d98 and 506e25a.

📒 Files selected for processing (2)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
  • apps/dashboard/lib/trpc/routers/workspace/create.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/lib/trpc/routers/workspace/create.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/lib/trpc/routers/workspace/create.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • apps/dashboard/lib/trpc/routers/workspace/create.ts
🧠 Learnings (4)
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/lib/trpc/routers/workspace/create.ts
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-05-16T16:16:02.286Z
Learnt from: mcstepp
PR: unkeyed/unkey#3258
File: apps/dashboard/components/dashboard/feedback-component.tsx:28-35
Timestamp: 2025-05-16T16:16:02.286Z
Learning: When validating string inputs in forms using Zod, it's best practice to use `.trim()` before length checks to prevent submissions consisting only of whitespace characters, particularly for feedback forms where meaningful content is expected.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-07-15T14:25:05.608Z
Learnt from: chronark
PR: unkeyed/unkey#3560
File: go/apps/api/routes/v2_keys_create_key/handler.go:353-466
Timestamp: 2025-07-15T14:25:05.608Z
Learning: In the Unkey codebase, input validation for API endpoints is handled at the OpenAPI schema layer, which validates request fields like permission slugs (pattern: "^[a-zA-Z0-9_]+$", length: 1-100 characters) before requests reach the handler code. This validation occurs during the zen.BindBody call in handlers.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (7-39)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-112)
⏰ 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 API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (4)

158-167: Nice: respectful auto-generation logic.

Only generating a slug when empty/not-dirty/not-manually-edited avoids clobbering user intent. Good UX.


193-193: Required field count = 2 is correct.

Matches the schema (name + URL). Keeps the step gating accurate.


214-222: Slugify helper aligns with server rules.

Lowercasing, stripping invalid chars, collapsing and trimming hyphens are correct and consistent with the proposed regex. LGTM.


19-26: Align client-side workspace URL validation with server-side rules

The current client regex /^(?!-)[a-z0-9-]+(?<!-)$/ still permits consecutive hyphens (e.g. foo--bar), whereas our backend enforces single hyphens only via ^[a-z0-9]+(?:-[a-z0-9]+)*$. Please verify the server-side workspaceUrl validation pattern and ensure the client matches it exactly to avoid any drift.

• File: apps/dashboard/app/new/hooks/use-workspace-step.tsx
Lines: 19–26

Suggested diff:

-    .regex(
-      /^(?!-)[a-z0-9-]+(?<!-)$/,
-      "Use lowercase letters, numbers, and hyphens (no leading/trailing hyphens).",
-    ),
+    .regex(
+      /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
+      "Use lowercase letters and numbers, separated by single hyphens.",
+    ),

@chronark
Copy link
Collaborator

overall looks good, can you create a preview branch in pscale with this?
we need to migrate the schema before deploying this code

Copy link
Member

Mike doesn’t have access to Planetscale to create branches. @chronark

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

♻️ Duplicate comments (3)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (3)

33-33: Let auto-generation resume when the slug input is cleared

If the user deletes the slug, slugManuallyEdited stays true and blocks auto-generate on name blur. Reset the flag when empty.

-              onChange={(evt) => {
-                // Mark slug as manually edited when user changes it
-                if (evt.currentTarget.value) {
-                  setSlugManuallyEdited(true);
-                }
-              }}
+              onChange={(evt) => {
+                const v = evt.currentTarget.value;
+                // Mark manually edited if non-empty; reset when cleared
+                setSlugManuallyEdited(v.length > 0);
+              }}

Also applies to: 181-186


106-109: Use a single mutation style (callbacks) to avoid unhandled promise rejections

You’re calling mutateAsync without awaiting/catching. Either await with try/catch or use mutate and rely on onSuccess/onError (already defined).

-    createWorkspace.mutateAsync({
+    createWorkspace.mutate({
       name: data.workspaceName,
       slug: data.slug.toLowerCase(),
     });

173-187: Rename label to “slug” and externalize the URL prefix

  • Label still says “Workspace URL handle”; switch to “Workspace slug” per backend/DB naming.
  • Avoid hardcoding app.unkey.com/; derive from env to work across environments.
-              label="Workspace URL handle"
+              label="Workspace slug"
@@
-              prefix="app.unkey.com/"
+              prefix={`${process.env.NEXT_PUBLIC_UNKEY_DASHBOARD_APP_HOST ?? "app.unkey.com"}/`}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 1b4f0c5 and c87e538.

📒 Files selected for processing (1)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧠 Learnings (4)
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-05-16T16:16:02.286Z
Learnt from: mcstepp
PR: unkeyed/unkey#3258
File: apps/dashboard/components/dashboard/feedback-component.tsx:28-35
Timestamp: 2025-05-16T16:16:02.286Z
Learning: When validating string inputs in forms using Zod, it's best practice to use `.trim()` before length checks to prevent submissions consisting only of whitespace characters, particularly for feedback forms where meaningful content is expected.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-07-15T14:25:05.608Z
Learnt from: chronark
PR: unkeyed/unkey#3560
File: go/apps/api/routes/v2_keys_create_key/handler.go:353-466
Timestamp: 2025-07-15T14:25:05.608Z
Learning: In the Unkey codebase, input validation for API endpoints is handled at the OpenAPI schema layer, which validates request fields like permission slugs (pattern: "^[a-zA-Z0-9_]+$", length: 1-100 characters) before requests reach the handler code. This validation occurs during the zen.BindBody call in handlers.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (6-38)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-112)
⏰ 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 Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (3)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (3)

159-168: Good guard: don’t overwrite a user-typed slug

Only generating when empty, not dirty, and not manually edited is the right UX.


195-195: requiredFieldCount = 2 is correct

Matches the two required fields (name, slug).


215-223: Slugify matches the schema (lowercase, sanitize, collapse, trim)

Implementation aligns with the allowed charset and hyphen rules. Keeping it inline is fine given prior preference during POC phases.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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: 2

♻️ Duplicate comments (3)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (3)

106-109: Don’t call mutateAsync without await; use mutate since you rely on callbacks.

Keeps a single style and avoids dangling promises.

-    createWorkspace.mutateAsync({
+    createWorkspace.mutate({
       name: data.workspaceName,
       slug: data.slug.toLowerCase(),
     });

173-180: Externalize the URL prefix, rename label to “Workspace slug”, and disable after create.

Improves clarity and env portability; prevents post-create edits.

-              label="Workspace URL handle"
+              label="Workspace slug"
               required
               error={form.formState.errors.slug?.message}
-              prefix="app.unkey.com/"
+              prefix={`${process.env.NEXT_PUBLIC_UNKEY_DASHBOARD_APP_HOST ?? "app.unkey.com"}/`}
               maxLength={64}
+              disabled={isLoading || workspaceCreated}

215-223: Slugify looks correct; consider sharing a single schema/util to avoid drift.

Optionally extract a shared workspaceSlugSchema/slugify in a common package and reuse on client/server.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c87e538 and f1191b8.

📒 Files selected for processing (1)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧠 Learnings (3)
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-05-16T16:16:02.286Z
Learnt from: mcstepp
PR: unkeyed/unkey#3258
File: apps/dashboard/components/dashboard/feedback-component.tsx:28-35
Timestamp: 2025-05-16T16:16:02.286Z
Learning: When validating string inputs in forms using Zod, it's best practice to use `.trim()` before length checks to prevent submissions consisting only of whitespace characters, particularly for feedback forms where meaningful content is expected.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (6-38)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-112)
⏰ 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: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (2)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)

159-168: Auto-fill respects user intent — LGTM.

Only generates when slug is empty, not dirty, and not manually edited. Good.


194-194: Required fields count updated — LGTM.

Matches the two required inputs.

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

♻️ Duplicate comments (2)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)

173-185: Rename label to “Workspace slug”, externalize prefix, and disable during submit.
Keeps terminology consistent; avoids hardcoded host; prevents edits while loading.

   <FormInput
     {...form.register("slug")}
     placeholder="enter-a-handle"
-    label="Workspace URL handle"
+    label="Workspace slug"
     required
     error={form.formState.errors.slug?.message}
-    prefix="app.unkey.com/"
+    prefix={`${process.env.NEXT_PUBLIC_UNKEY_DASHBOARD_APP_HOST ?? "app.unkey.com"}/`}
     maxLength={64}
+    disabled={isLoading || workspaceCreated}
     onChange={(evt) => {
       const v = evt.currentTarget.value;
       setSlugManuallyEdited(v.length > 0);
     }}
   />

106-109: Avoid unawaited mutateAsync; use a single mutation style.
You’re using onSuccess/onError callbacks; prefer mutate(...) for consistency.

Apply:

-    createWorkspace.mutateAsync({
+    createWorkspace.mutate({
       name: data.workspaceName,
       slug: data.slug.toLowerCase(),
     });

Alternative: await mutateAsync in try/catch and remove the callbacks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f1191b8 and 64122d8.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/dashboard/app/new/hooks/use-workspace-step.tsx (4 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧠 Learnings (3)
📚 Learning: 2025-07-28T19:42:37.047Z
Learnt from: mcstepp
PR: unkeyed/unkey#3662
File: apps/dashboard/app/(app)/projects/page.tsx:74-81
Timestamp: 2025-07-28T19:42:37.047Z
Learning: In apps/dashboard/app/(app)/projects/page.tsx, the user mcstepp prefers to keep placeholder functions like generateSlug inline during POC/demonstration phases rather than extracting them to utility modules, with plans to refactor later when the feature matures beyond the proof-of-concept stage.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2024-10-04T17:27:09.821Z
Learnt from: chronark
PR: unkeyed/unkey#2146
File: apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx:74-75
Timestamp: 2024-10-04T17:27:09.821Z
Learning: In `apps/dashboard/app/(app)/apis/[apiId]/settings/default-prefix.tsx`, the hidden `<input>` elements for `workspaceId` and `keyAuthId` work correctly without being registered with React Hook Form.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
📚 Learning: 2025-05-16T16:16:02.286Z
Learnt from: mcstepp
PR: unkeyed/unkey#3258
File: apps/dashboard/components/dashboard/feedback-component.tsx:28-35
Timestamp: 2025-05-16T16:16:02.286Z
Learning: When validating string inputs in forms using Zod, it's best practice to use `.trim()` before length checks to prevent submissions consisting only of whitespace characters, particularly for feedback forms where meaningful content is expected.

Applied to files:

  • apps/dashboard/app/new/hooks/use-workspace-step.tsx
🧬 Code graph analysis (1)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (2)
apps/dashboard/app/new/components/onboarding-wizard.tsx (1)
  • OnboardingStep (6-38)
apps/dashboard/lib/trpc/routers/workspace/create.ts (1)
  • createWorkspace (11-112)
⏰ 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). (6)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Test API / API Test Local
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (5)
apps/dashboard/app/new/hooks/use-workspace-step.tsx (5)

19-27: LGTM: solid slug schema (trim/min/max + single-hyphen regex).
Matches the intended constraints and avoids lookbehind pitfalls.


33-33: Manual-edit flag: good UX guard.
The separate flag plus dirty check avoids accidental overwrites.


192-192: requiredFieldCount update looks correct.
Matches the two required fields in the schema.


213-221: Slugify is consistent with the schema; keeping it inline is fine.
Aligns with the shared rules; inline helper matches prior preference for POCs.


19-27: The root directory listing is needed to locate where the server-side slug validation lives. Please run the above to show top‐level folders so we can search for “slug” or Zod schemas on the backend.

@chronark chronark requested a review from imeyer as a code owner August 31, 2025 09:27
@chronark chronark merged commit d378e22 into main Aug 31, 2025
17 of 18 checks passed
@chronark chronark deleted the workspace-slug-in-db branch August 31, 2025 09:31
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 (1)
internal/db/drizzle/0000_dazzling_colonel_america.sql (1)

178-199: Operational sequencing: run migration before app deploy.

Follow chronark’s note: create a pscale preview branch, apply migration, validate, then open a deploy request before merging.

♻️ Duplicate comments (1)
internal/db/src/schema/workspaces.ts (1)

22-24: Good addition; align future invariant to non-null + lowercase.

After backfilling slugs, consider making it non-null. Also ensure API normalizes to lowercase and validates allowed characters.

Apply after backfill:

-  slug: varchar("slug", { length: 64 }).unique(),
+  slug: varchar("slug", { length: 64 }).notNull().unique(),
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 14a6e61 and af74624.

📒 Files selected for processing (5)
  • go/pkg/db/schema.sql (2 hunks)
  • internal/db/drizzle/0000_dazzling_colonel_america.sql (3 hunks)
  • internal/db/drizzle/meta/0000_snapshot.json (3 hunks)
  • internal/db/drizzle/meta/_journal.json (1 hunks)
  • internal/db/src/schema/workspaces.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{js,jsx,ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{js,jsx,ts,tsx}: Use Biome for formatting and linting in TypeScript/JavaScript projects
Prefer named exports over default exports in TypeScript/JavaScript, except for Next.js pages

Files:

  • internal/db/src/schema/workspaces.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Follow strict TypeScript configuration
Use Zod for runtime validation in TypeScript projects

Files:

  • internal/db/src/schema/workspaces.ts
**/*.{env,js,ts,go}

📄 CodeRabbit inference engine (CLAUDE.md)

All environment variables must follow the format: UNKEY_<SERVICE_NAME>_VARNAME

Files:

  • internal/db/src/schema/workspaces.ts
⏰ 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 API / API Test Local
  • GitHub Check: Test Packages / Test
  • GitHub Check: Build / Build
  • GitHub Check: Test Go API Local / Test
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (4)
go/pkg/db/schema.sql (2)

182-183: Workspace slug column and unique constraint look correct (global uniqueness).

Length 64 matches the TS schema; UNIQUE(slug) creates the needed lookup index.

Also applies to: 197-199


197-199: Confirm intent: global slug uniqueness vs. per-org.

UNIQUE(slug) enforces global slugs across all orgs. If slugs were intended to be namespaced per org, switch to UNIQUE(org_id, slug) instead.

internal/db/drizzle/meta/0000_snapshot.json (1)

1130-1136: Snapshot matches DB: workspaces.slug varchar(64) nullable.

Looks consistent with Go and TS schemas.

internal/db/drizzle/0000_dazzling_colonel_america.sql (1)

182-199: Migration adds slug and unique constraint correctly.

Matches snapshot and app schema; additive and safe with nullable column.

Comment on lines +182 to 183
`slug` varchar(64),
`partition_id` varchar(256),
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Plan a follow-up to make slug required and canonicalized.

Slug is nullable; if every workspace should have a URL slug, backfill existing rows, then add NOT NULL and enforce lowercase at the API boundary.

I can draft the migration plan if helpful.

🤖 Prompt for AI Agents
In go/pkg/db/schema.sql around lines 182-183, `slug` is currently nullable; plan
and implement a migration to make it required and canonicalized: first write a
backfill script that generates slugs for existing rows (e.g., derive from
workspace name or id), normalizes to lowercase and URL-safe format, and ensures
uniqueness (add suffixes if necessary); run and verify the backfill; then add a
schema migration to ALTER TABLE to set `slug` NOT NULL and create a unique index
on `slug`; finally enforce lowercase/canonicalization at the API boundary for
all new/updated workspaces (validate and transform input to lowercase and
URL-safe form before inserting/updating).

Comment on lines +182 to 183
`slug` varchar(64),
`partition_id` varchar(256),
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Case handling and URL safety.

If slugs appear in URLs, enforce lowercase and character set in the API layer; DB collation might already be case-insensitive, but explicit normalization avoids surprises.

Comment on lines +8 to 10
"when": 1756630515466,
"tag": "0000_dazzling_colonel_america",
"breakpoints": true
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Journal tag updated—ensure migration is wired into deploy process.

Confirm a PlanetScale preview branch is created and the migration is applied before deploying code that writes/reads slug.

🤖 Prompt for AI Agents
In internal/db/drizzle/meta/_journal.json around lines 8 to 10, the journal tag
was updated to "0000_dazzling_colonel_america" but the deployment pipeline may
not run the corresponding migration; ensure the migration is wired into the
deploy process by (1) creating a PlanetScale preview branch for this change and
applying the migration there, (2) updating CI/CD pipeline steps to create/use
that preview branch and run the migration step before any code that reads/writes
the slug is deployed, (3) adding a verification step in CI that checks the
migration ran successfully (e.g., using a migration status command or simple
schema check), and (4) updating the deploy docs/README with the new
preview-branch + migration steps so future deploys follow the same flow.

Comment on lines 1243 to 1250
"workspaces_org_id_unique": {
"name": "workspaces_org_id_unique",
"columns": ["org_id"]
},
"workspaces_slug_unique": {
"name": "workspaces_slug_unique",
"columns": ["slug"]
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Unique constraint captured; consider eventual NOT NULL.

Keep as-is now; after backfill, flip slug to notNull in schema + follow-up migration.

🤖 Prompt for AI Agents
internal/db/drizzle/meta/0000_snapshot.json lines 1243-1250: the snapshot
records a unique index on "slug" but the schema still allows NULL; after
performing the planned backfill you should update the schema to mark the slug
column as notNull and add a follow-up migration that alters the column to NOT
NULL (or recreates it appropriately), ensure the backfill has completed for all
rows before running the migration, include a migration down/rollback step if
appropriate, and add/adjust tests to validate the new NOT NULL constraint.

Comment on lines +22 to +24
// slug is used for the workspace URL
slug: varchar("slug", { length: 64 }).unique(),

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify runtime validation at the API boundary.

Ensure createWorkspace Zod schema enforces: lowercase, [a-z0-9-], no leading/trailing hyphen, max 64.

Run to confirm:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 --type tsx --type ts '\bslug\b' apps/dashboard/lib/trpc/routers/workspace/create.ts apps/dashboard/app/new/hooks/use-workspace-step.tsx
rg -nP -C2 --type ts 'z\.object\([^)]*slug[^)]*\)' apps/dashboard/lib/trpc/routers/workspace/create.ts
rg -nP -C2 --type ts '(toLowerCase|slugify)' apps/dashboard/lib/trpc/routers/workspace/create.ts apps/dashboard/app/new/hooks/use-workspace-step.tsx

Length of output: 2026


Add slug validation in createWorkspace schema
In apps/dashboard/lib/trpc/routers/workspace/create.ts the .input() Zod object doesn’t validate slug at runtime. Enforce lowercase alphanumerics and hyphens only (no leading/trailing hyphen) and max 64 chars, for example:

slug: z
  .string()
  .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "invalid slug")
  .max(64),
🤖 Prompt for AI Agents
internal/db/src/schema/workspaces.ts around lines 22 to 24: the workspace slug
must be validated at runtime in the createWorkspace input schema; update the Zod
input in apps/dashboard/lib/trpc/routers/workspace/create.ts to validate slug as
a string matching lowercase alphanumerics and single hyphen separators (no
leading/trailing hyphen) and max 64 chars by using
.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "invalid slug").max(64) so runtime
inputs cannot bypass the DB constraint.

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