Skip to content

Speed up dummy-project seeding (preview create-project ~15s → ~1.3s)#1437

Open
BilalG1 wants to merge 3 commits into
devfrom
worktree-benchmark-preview-create
Open

Speed up dummy-project seeding (preview create-project ~15s → ~1.3s)#1437
BilalG1 wants to merge 3 commits into
devfrom
worktree-benchmark-preview-create

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented May 16, 2026

Summary

The internal preview/create-project endpoint was taking ~15s because seedDummyProject created its dummy users one at a time through the full usersCrudHandlers.adminCreate CRUD pipeline (one DB transaction + config render per user, ~86 users). This reworks the seeding path to use bulk inserts.

End-to-end, the endpoint's server-side handler time drops from ~15,100ms → ~1,300ms (~11× faster).

Seeding changes (seed-dummy-data.ts)

  • seedDummyUsers — bulk insert. Build every row (ProjectUser, ContactChannel, AuthMethod, ProjectUserOAuthAccount, OAuthAuthMethod, default permissions) up front with pre-generated UUIDs, then insert via one createMany per table inside a single transaction — replacing ~86 sequential adminCreate transactions. Named-user team memberships are bulk-inserted the same way (TeamMember + TeamMemberDirectPermission). Idempotency is preserved with a single up-front email lookup, so re-runs against an existing project still skip existing users.
  • Native randomUUID. The seed paths now use node:crypto's randomUUID() instead of stack-shared's generateUuid(). The browser-safe polyfill calls crypto.getRandomValues ~31× per UUID (once per template char, each with a fresh Uint8Array(1)); generating thousands of seed UUIDs made that ~800ms of pure CPU in the activity-event build alone.
  • seedBulkSignupsAndActivity. Skip the redundant back-date UPDATE for freshly-inserted users (createMany already writes correct createdAt/signedUpAt), and flush ClickHouse events in larger, parallel batches.
  • seedDummyProject. Run seedBulkSignupsAndActivity concurrently with the lighter remaining steps, and fold seedDummyTransactions into the emails/activity/replays Promise.all.
  • Removed the now-unused syncSeedUserOauthProviders helper.

The bulk path produces the same rows as the CRUD-handler path (verified row-count equality during development). Webhooks / soft-limit checks are intentionally not fired for seed data, consistent with the rest of the seed.

Also in this PR — preview-mode 404 fix (preview-project-redirect.tsx)

While testing the above, the dashboard 404'd right after a preview project was created. In preview mode the /projects page renders PreviewProjectRedirect, which POSTs /internal/preview/create-project and then router.push()es to /projects/<new-id> — but it never refreshed the client-side owned-projects cache, so the [projectId] route's useAdminApp() read a stale list, failed to find the just-created project, and called notFound().

Fixed by refreshing the owned-projects cache before navigating, matching what the normal create-project flow in page-client.tsx already does. (Pre-existing bug, not caused by the seeding change — but it surfaces the seeding path, so it's bundled here.)

Testing

pnpm typecheck and pnpm lint pass for both backend and dashboard. The preview endpoint was exercised repeatedly during development (HTTP 200, projects created and populated correctly).

Summary by CodeRabbit

  • Performance

    • Much faster bulk user and event seeding via larger, parallelized batches for quicker test-data generation.
  • Refactor

    • Redesigned dummy data seeding to be idempotent and bulk-oriented for more reliable, efficient setup.
  • Bug Fixes

    • Preview project creation now refreshes the local project list and validates client capabilities to prevent stale navigation or unclear errors.
    • Auto-login now runs only once to avoid duplicate sign-ins and related navigation issues.

Review Change Stack

Replace the per-user CRUD-handler calls in seedDummyUsers with bulk
createMany inserts, plus several smaller wins on the seeding path.
End-to-end the preview create-project endpoint drops from ~15s to ~1.3s.

- seedDummyUsers: build every row up front and insert via one createMany
  per table inside a single transaction, instead of ~86 sequential
  adminCreate transactions. Named-user team memberships are bulk-inserted
  the same way. Idempotency is preserved via a single up-front email
  lookup, so re-runs against an existing project still skip existing
  users.
- Use node:crypto randomUUID instead of stack-shared generateUuid in the
  seed paths. The browser-safe polyfill calls crypto.getRandomValues ~31x
  per UUID, which dominated CPU time when generating thousands of seed
  UUIDs (~800ms in the activity-event build alone).
- seedBulkSignupsAndActivity: skip the redundant back-date UPDATE for
  freshly-inserted users (createMany already writes correct timestamps),
  and flush ClickHouse events in larger, parallel batches.
- seedDummyProject: run seedBulkSignupsAndActivity concurrently with the
  lighter remaining steps, and fold seedDummyTransactions into the
  emails/activity/replays Promise.all.
- Remove the now-unused syncSeedUserOauthProviders helper.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 16, 2026

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

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment May 16, 2026 2:05am
stack-auth-mcp Ready Ready Preview, Comment May 16, 2026 2:05am
stack-auth-skills Ready Ready Preview, Comment May 16, 2026 2:05am
stack-backend Ready Ready Preview, Comment May 16, 2026 2:05am
stack-dashboard Ready Ready Preview, Comment May 16, 2026 2:05am
stack-demo Ready Ready Preview, Comment May 16, 2026 2:05am
stack-docs Ready Ready Preview, Comment May 16, 2026 2:05am
stack-preview-backend Ready Ready Preview, Comment May 16, 2026 2:05am
stack-preview-dashboard Ready Ready Preview, Comment May 16, 2026 2:05am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 16, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fc9c0627-3a97-47ee-ad8f-944cdfd781ea

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0393b and 891e596.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/layout-client.tsx

📝 Walkthrough

Walkthrough

Refactors backend seed scripts for deterministic bulk user creation and idempotent bulk inserts, increases ClickHouse batch sizes and concurrency, narrows backdating to existing users, overlaps bulk activity seeding with project setup, and adds runtime guards/refreshes in dashboard preview and layout auto-login.

Changes

Seed Data Refactoring & Performance Optimization

Layer / File(s) Summary
Type System & Import Updates
apps/backend/src/lib/seed-dummy-data.ts
Prisma imports updated to use tenancy-scoped TenancyPrismaClient. Added node:crypto imports (createHash, randomUUID).
User Seeding Bulk Refactor
apps/backend/src/lib/seed-dummy-data.ts
Deterministic two-phase generation of bulk user specs from a seeded PRNG, prefetch existing emails, generate UUIDs for new rows, and perform idempotent bulk createMany across ProjectUser, ContactChannel, OAuth methods/accounts, direct permissions, and team memberships. Removed sequential per-user OAuth sync helper.
Activity Event Batching & Backdating
apps/backend/src/lib/seed-dummy-data.ts
seedDummySessionActivityEvents and seedBulkSignupsAndActivity build 10,000-row ClickHouse batches and insert them concurrently via Promise.all. Introduced usersToBackdate to update timestamps only for pre-existing users. Switched $token-refresh refresh_token_id generation to randomUUID().
Concurrent Orchestration & Project Setup
apps/backend/src/lib/seed-dummy-data.ts
seedDummyProject uses randomUUID() for generated project IDs when not provided, starts seedBulkSignupsAndActivity as an overlapping promise after user seeding, includes seedDummyTransactions in the concurrency group, and awaits the bulk-signups promise before completion.

Preview Project Redirect Validation

Layer / File(s) Summary
Runtime Internals Validation & Cache Refresh
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx
Validate that appInternals includes sendRequest and refreshOwnedProjects; call appInternals.refreshOwnedProjects() after creating the preview project and before router.push to refresh the owned-projects cache.

Layout Client Auto-login Guard

Layer / File(s) Summary
useRef Guard for Auto-login
apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
Add autoLoginStarted ref and update the auto-login effect to return early when the ref is set or user exists; set the ref before starting the async auto-login.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • nams1570
  • aadesh18
  • N2D4

Poem

🐰 I seed in bulk, not one-by-one delight,
Tenancy Prisma hums through day and night,
ClickHouse batches leap in parallel flight,
Only old rows get backdated right,
A small refresh, then off to preview bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: the optimization of dummy-project seeding with concrete performance metrics (~15s → ~1.3s).
Description check ✅ Passed The description is comprehensive and covers all major changes: bulk seeding refactor, performance improvements, the cache-refresh fix, and testing verification.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-benchmark-preview-create

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/dashboard/src/app/(main)/(protected)/layout-client.tsx

Parsing error: error TS5012: Cannot read file '/tsconfig.json': ENOENT: no such file or directory, open '/tsconfig.json'.


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.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 16, 2026

Greptile Summary

This PR replaces per-user sequential adminCreate calls in seedDummyProject with batched createMany inserts, cutting the /preview/create-project server-side time from ~15 s to ~1.3 s. It also swaps generateUuid() (a browser-safe polyfill) for node:crypto's randomUUID(), parallelises ClickHouse event flushes, and folds seedBulkSignupsAndActivity into a concurrent execution window instead of running it after all other seeds.

  • seedDummyUsers now pre-builds all ProjectUser, ContactChannel, AuthMethod, ProjectUserOAuthAccount, OAuthAuthMethod, and ProjectUserDirectPermission rows in memory and inserts them in a single retryTransaction, replacing ~86 sequential transactions.
  • seedBulkSignupsAndActivity skips the redundant UPDATE for freshly-created rows (since createMany already writes the correct createdAt/signedUpAt) and flushes ClickHouse events in larger parallel batches (10 000-row vs 500-row sequential).
  • Concurrency model in seedDummyProject fires bulkSignupsPromise immediately after user seeding and awaits it only at the very end, overlapping it with lighter config and email seeds.

Confidence Score: 3/5

The bulk-insert path produces correct data under the happy path, but the floated bulkSignupsPromise can become an unhandled rejection if either intermediate Promise.all block throws, potentially leaving the database in a partially-seeded state.

The new concurrent execution model fires bulkSignupsPromise and then runs two separate await Promise.all(...) blocks before finally awaiting it. If either of those blocks throws, bulkSignupsPromise is never awaited: background database writes continue silently, and a subsequent rejection from that promise has no handler. On modern Node.js runtimes an unhandled rejection terminates the process.

apps/backend/src/lib/seed-dummy-data.ts — specifically the bulkSignupsPromise lifetime and the two Promise.all blocks that precede its final await.

Important Files Changed

Filename Overview
apps/backend/src/lib/seed-dummy-data.ts Bulk-insert rewrite of seedDummyUsers/seedBulkSignupsAndActivity for ~11× speedup; introduces a floated promise that can become an unhandled rejection if an intermediate Promise.all block throws.

Sequence Diagram

sequenceDiagram
    participant C as seedDummyProject
    participant PU as seedDummyUsers (bulk)
    participant BSA as seedBulkSignupsAndActivity
    participant PA1 as Promise.all #1 (config overrides)
    participant PA2 as Promise.all #2 (emails/txns/replays)
    participant CH as ClickHouse

    C->>PU: await (sequential — heavy Postgres)
    PU-->>C: userEmailToId Map

    C->>BSA: fire (Promise, not awaited yet)
    activate BSA

    C->>PA1: await Promise.all [overrideBranchConfig, overrideEnvConfig, ...]
    PA1-->>C: done (or throws ⚠️ — bulkSignupsPromise abandoned)

    C->>PA2: await Promise.all [seedDummyTransactions, seedDummyEmails, ...]
    PA2-->>C: done (or throws ⚠️ — bulkSignupsPromise abandoned)

    BSA->>BSA: createMany ProjectUsers
    BSA->>CH: Promise.all batch inserts (10 000-row batches, parallel)
    CH-->>BSA: ack
    BSA-->>C: resolves

    C->>C: await bulkSignupsPromise (line 2092)
Loading

Comments Outside Diff (1)

  1. apps/backend/src/lib/seed-dummy-data.ts, line 1978-1992 (link)

    P1 Unhandled rejection / stray background write if Promise.all throws

    bulkSignupsPromise is started at line 1978 and only awaited at line 2092, but there are two await Promise.all(...) calls between those points (lines 1983 and 2067). If either of those Promise.all blocks rejects (e.g. a Postgres error in overrideBranchConfigOverride or seedDummyTransactions), the function unwinds before reaching await bulkSignupsPromise. The promise is then abandoned: if it later resolves, the database is left in a partially-seeded state without the caller knowing; if it rejects, Node.js emits an unhandled-rejection warning (and in newer runtimes, terminates the process). A try/finally that aborts or awaits bulkSignupsPromise before re-throwing would prevent this.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/backend/src/lib/seed-dummy-data.ts
    Line: 1978-1992
    
    Comment:
    **Unhandled rejection / stray background write if `Promise.all` throws**
    
    `bulkSignupsPromise` is started at line 1978 and only awaited at line 2092, but there are two `await Promise.all(...)` calls between those points (lines 1983 and 2067). If either of those `Promise.all` blocks rejects (e.g. a Postgres error in `overrideBranchConfigOverride` or `seedDummyTransactions`), the function unwinds before reaching `await bulkSignupsPromise`. The promise is then abandoned: if it later resolves, the database is left in a partially-seeded state without the caller knowing; if it rejects, Node.js emits an unhandled-rejection warning (and in newer runtimes, terminates the process). A `try/finally` that aborts or awaits `bulkSignupsPromise` before re-throwing would prevent this.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
apps/backend/src/lib/seed-dummy-data.ts:1978-1992
**Unhandled rejection / stray background write if `Promise.all` throws**

`bulkSignupsPromise` is started at line 1978 and only awaited at line 2092, but there are two `await Promise.all(...)` calls between those points (lines 1983 and 2067). If either of those `Promise.all` blocks rejects (e.g. a Postgres error in `overrideBranchConfigOverride` or `seedDummyTransactions`), the function unwinds before reaching `await bulkSignupsPromise`. The promise is then abandoned: if it later resolves, the database is left in a partially-seeded state without the caller knowing; if it rejects, Node.js emits an unhandled-rejection warning (and in newer runtimes, terminates the process). A `try/finally` that aborts or awaits `bulkSignupsPromise` before re-throwing would prevent this.

Reviews (1): Last reviewed commit: "Speed up dummy-project seeding (preview ..." | Re-trigger Greptile

Copy link
Copy Markdown

@vercel vercel Bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestion:

seedBulkSignupsAndActivity function creates ProjectUser and ContactChannel rows for new users but does not create corresponding ProjectUserDirectPermission rows with default permissions, breaking expected permission behavior for bulk-seeded users.

Fix on Vercel

In preview mode the dashboard's /projects page renders PreviewProjectRedirect,
which POSTs /internal/preview/create-project and then router.push()es to
/projects/<new-id>. It never refreshed the client-side owned-projects cache,
so the [projectId] route's useAdminApp() read a stale list, failed to find the
just-created project, and called notFound() — showing a 404.

Refresh the owned-projects cache before navigating, matching what the normal
create-project flow in page-client.tsx already does.
Copy link
Copy Markdown
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.

🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx (1)

16-27: ⚡ Quick win

Avoid any/as for internals extraction; use a type guard instead.

Line 16 and Line 24-27 currently bypass the type system. Please narrow from unknown via a runtime type guard and return the narrowed value directly.

Proposed refactor
+type AppInternals = {
+  sendRequest: (path: string, options: RequestInit, type: string) => Promise<Response>;
+  refreshOwnedProjects: () => Promise<void>;
+};
+
+function isAppInternals(value: unknown): value is AppInternals {
+  return (
+    typeof value === "object" &&
+    value !== null &&
+    "sendRequest" in value &&
+    typeof (value as { sendRequest?: unknown }).sendRequest === "function" &&
+    "refreshOwnedProjects" in value &&
+    typeof (value as { refreshOwnedProjects?: unknown }).refreshOwnedProjects === "function"
+  );
+}
+
   const appInternals = useMemo(() => {
-    const internals = Reflect.get(app as any, stackAppInternalsSymbol);
-    if (
-      !internals ||
-      typeof internals.sendRequest !== "function" ||
-      typeof internals.refreshOwnedProjects !== "function"
-    ) {
+    const internals = Reflect.get(app, stackAppInternalsSymbol);
+    if (!isAppInternals(internals)) {
       throw new Error("The Stack client app cannot send internal requests.");
     }
-    return internals as {
-      sendRequest: (path: string, options: RequestInit, type: string) => Promise<Response>,
-      refreshOwnedProjects: () => Promise<void>,
-    };
+    return internals;
   }, [app]);

As per coding guidelines: "**/*.{ts,tsx}: Do NOT use as/any/type casts ... unless you specifically asked the user about it."

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

In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx
around lines 16 - 27, Replace the unchecked cast around Reflect.get(app as any,
stackAppInternalsSymbol) by treating the result as unknown and validating it
with a runtime type guard: implement a function isStackAppInternals(value:
unknown): value is { sendRequest: (path: string, options: RequestInit, type:
string) => Promise<Response>; refreshOwnedProjects: () => Promise<void>; } that
checks value is an object and both sendRequest and refreshOwnedProjects are
functions, then call Reflect.get(...) into a const internals: unknown, run the
guard and throw the same error if it returns false, and finally return the
narrowed internals (no use of any/as) so the return type is inferred from the
type predicate; reference symbols: stackAppInternalsSymbol, internals,
isStackAppInternals, sendRequest, refreshOwnedProjects.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx:
- Around line 16-27: Replace the unchecked cast around Reflect.get(app as any,
stackAppInternalsSymbol) by treating the result as unknown and validating it
with a runtime type guard: implement a function isStackAppInternals(value:
unknown): value is { sendRequest: (path: string, options: RequestInit, type:
string) => Promise<Response>; refreshOwnedProjects: () => Promise<void>; } that
checks value is an object and both sendRequest and refreshOwnedProjects are
functions, then call Reflect.get(...) into a const internals: unknown, run the
guard and throw the same error if it returns false, and finally return the
narrowed internals (no use of any/as) so the return type is inferred from the
type predicate; reference symbols: stackAppInternalsSymbol, internals,
isStackAppInternals, sendRequest, refreshOwnedProjects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 46e10b70-cafa-45a2-958a-9199f2c52675

📥 Commits

Reviewing files that changed from the base of the PR and between 8d400f5 and 2b0393b.

📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/preview-project-redirect.tsx

LayoutClient's auto-login effect had no run-once guard. React StrictMode
(on by default in Next dev) double-invokes effects, and both invocations
ran while `user` was still null — so in preview mode each generated a
fresh `preview-*@Preview.stack-auth.com` email and signed up a *separate*
preview user.

With two users created per load, the session settles on one while the
preview project may have been created for the other, so the project page
renders a 404 ("does not have access to it").

Guard the effect with a ref so the auto-login (and preview-user creation)
runs at most once per mount.
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.

2 participants