Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRemoves legacy Docker emulator artifacts and composer; adds local-emulator support across backend, database, dashboard, E2E tests, and CI; introduces LocalEmulatorProject model, seed/API endpoints, runtime guards for env overrides, and dashboard UI/flow changes for read-only emulator mode. Changes
Sequence DiagramsequenceDiagram
actor User as Dashboard User
participant Dashboard as Dashboard Client
participant API as Backend API
participant Prisma as Prisma Client
participant DB as PostgreSQL
User->>Dashboard: submit absolute config file path
Dashboard->>API: POST /internal/local-emulator/project { absolute_file_path }
API->>API: validate local-emulator enabled & path
API->>Prisma: derive/check projectId & upsert LocalEmulatorProject
Prisma->>DB: SELECT/INSERT Project + LocalEmulatorProject
DB-->>Prisma: project row / confirmation
Prisma-->>API: project and mapping result
API->>Prisma: find/create credentials keys for project
Prisma->>DB: SELECT/INSERT keys
DB-->>Prisma: keys
API-->>Dashboard: return project_id, secret_server_key, super_secret_admin_key, branch_config_override_string
Dashboard->>User: navigate to project using returned credentials
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Pull request overview
This pull request refactors and enhances the local emulator functionality by renaming environment variables, removing old Docker emulator infrastructure, and introducing a new path-based project mapping system. The changes enable developers to work with local config files mapped to emulator projects in the Stack dashboard.
Changes:
- Renamed
NEXT_PUBLIC_STACK_EMULATOR_ENABLEDandNEXT_PUBLIC_STACK_EMULATOR_PROJECT_IDto a singleNEXT_PUBLIC_STACK_IS_LOCAL_EMULATORflag - Removed docker-compose based emulator setup and associated documentation
- Introduced database-backed local emulator project mapping via absolute file paths
- Added UI flow for opening config files in local emulator mode with read-only environment config restrictions
- Added comprehensive E2E tests and CI workflow for local emulator mode
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/backend/src/lib/local-emulator.ts | New library defining constants and utilities for local emulator functionality |
| apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx | New endpoint for creating/retrieving local emulator projects by file path |
| apps/backend/src/lib/config.tsx | Modified to fetch branch config overrides from LocalEmulatorProject table and block environment config updates |
| apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx | Added local emulator environment config blocking logic |
| apps/backend/src/app/api/latest/internal/config/override/[level]/reset-keys/route.tsx | Added local emulator environment config blocking logic |
| apps/backend/prisma/schema.prisma | Added LocalEmulatorProject model with file path to project ID mapping |
| apps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/migration.sql | Database migration creating LocalEmulatorProject table |
| apps/backend/prisma/seed.ts | Updated to use new local emulator constants and simplified emulator project seeding |
| apps/dashboard/src/lib/env.tsx | Renamed emulator environment variables |
| apps/dashboard/src/lib/utils.tsx | Removed redirectToProjectIfEmulator function |
| apps/dashboard/src/lib/config-update.tsx | Added local emulator check to block environment config updates |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx | Removed conditional UI elements based on emulator mode |
| apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx | Added local emulator read-only UI with alerts |
| apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx | Added config file opening dialog for local emulator mode |
| apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx | Added local emulator blocking message for project creation |
| apps/dashboard/src/app/(main)/(protected)/layout-client.tsx | Updated to use renamed environment variable |
| apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts | New E2E tests for local emulator project endpoint |
| apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts | New E2E tests for config restrictions in local emulator |
| .github/workflows/e2e-api-tests-local-emulator.yaml | New CI workflow for local emulator E2E tests |
| docker/readme.md | Removed (deleted old emulator documentation) |
| docker/emulator/docker.compose.yaml | Removed (deleted docker-compose emulator setup) |
| .github/workflows/docker-emulator-test.yaml | Removed (deleted old emulator workflow) |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Outdated
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (10)
.github/workflows/e2e-api-tests-local-emulator.yaml (4)
113-113: Redundant&operators in background service commands.Similar to the Docker Compose step, the
&at the end of eachruncommand is redundant when usingbackground-action. The action already handles backgrounding the process.🧹 Suggested cleanup for all background services
- run: pnpm run start:backend --log-order=stream & + run: pnpm run start:backend --log-order=streamApply the same pattern to lines 123, 133, 143, and 153.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/e2e-api-tests-local-emulator.yaml at line 113, Remove the redundant trailing ampersand (&) from backgrounded commands that use the background-action; specifically update each run step that currently uses the string "pnpm run start:backend --log-order=stream &" (and the other similar run commands on the same pattern at the other service steps) to omit the trailing "&" so the command becomes "pnpm run start:backend --log-order=stream" — apply the same change for the analogous run entries referenced in the workflow.
160-161: Consider documenting the sleep duration.The 10-second sleep before running tests appears to be a timing workaround. If this is required for services to stabilize after startup, a comment explaining the reason would be helpful.
📝 Suggested documentation
+ # Allow background services to fully initialize before running tests - name: Wait 10 seconds run: sleep 10🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/e2e-api-tests-local-emulator.yaml around lines 160 - 161, Add a short inline comment above the "Wait 10 seconds" workflow step explaining why the 10s sleep is necessary (e.g., to allow the emulator/service to initialize before tests) and what conditions it is covering, and if applicable replace or augment the step name or comment to reference a link or note on a more robust readiness check (so maintainers know this is a timing workaround and where to find alternatives).
166-172: Consider documenting the repeated test runs.Running tests 3 times on
main/devbranches is an unusual pattern. If this is for flakiness detection or stability verification, adding a comment would help future maintainers understand the intent.📝 Suggested documentation
+ # Run tests multiple times on main/dev to detect flaky tests - name: Run tests again (attempt 1) if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' run: pnpm test run🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/e2e-api-tests-local-emulator.yaml around lines 166 - 172, Add a short comment above the repeated test steps to document why tests are run multiple times on main/dev (e.g., flakiness mitigation or stability checks) and clarify intent; update the step names "Run tests again (attempt 1)" and "Run tests again (attempt 2)" (or add inline comments) to state the reason (e.g., "retry for flaky test detection" or "stability verification") so future maintainers understand the pattern.
46-46: Redundant background operator&in command.The
&at the end of the docker compose command is redundant sincebackground-actionalready runs the command in the background. The command will work, but the trailing&is unnecessary.🧹 Suggested cleanup
- run: docker compose -f docker/dependencies/docker.compose.yaml up --pull always -d & + run: docker compose -f docker/dependencies/docker.compose.yaml up --pull always -d🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.github/workflows/e2e-api-tests-local-emulator.yaml at line 46, Remove the redundant background operator by editing the GitHub Actions step that currently uses the command string "docker compose -f docker/dependencies/docker.compose.yaml up --pull always -d &" and drop the trailing "&"; keep the rest of the run command identical so the background-action semantics handle detaching without the extra ampersand.apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx (1)
149-183: Add email configuration UI state to Playground for documentation and testing.The Playground was updated in this PR but does not showcase the new local-emulator read-only state pattern introduced in the email configuration section. Update the Playground to document this state (read-only copy, conditional buttons, informational alert) to keep design behavior testable and referenceable alongside other dashboard UI patterns.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx around lines 149 - 183, Add a Playground variant that demonstrates the new email-config read-only emulator state by rendering the same header and controls using the isLocalEmulator flag and emailConfig.isShared check: display the Typography copy for the emulator ("Email server settings are read-only in the local emulator") when isLocalEmulator is true, hide the TestSendingDialog trigger when emailConfig.isShared || isLocalEmulator, hide the EditEmailServerDialog trigger when isLocalEmulator is true, and render the Alert with AlertDescription when isLocalEmulator is true so the Playground shows the read-only copy, conditional buttons, and informational alert; reference the existing components/props used in the page (isLocalEmulator, emailConfig.isShared, TestSendingDialog, EditEmailServerDialog, Alert, AlertDescription, Typography, Button) so the Playground mirrors the real UI behavior.apps/dashboard/.env.development (1)
12-12: Consider documenting the reason for disabling the debugger.The change from
truetofalseforNEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERRORappears unrelated to the local emulator feature. If intentional, a comment explaining the rationale would help future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/.env.development` at line 12, The environment variable NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR was changed from true to false without explanation; either revert it if that was accidental or add a concise comment above NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR in the .env.development indicating the reason for disabling the debugger (e.g., to avoid noisy logs during local emulator runs or to prevent exposing debug UI), and include any conditions when it should be re-enabled to guide future maintainers.apps/dashboard/src/lib/config-update.tsx (1)
272-275: Consider using a toast instead ofalert()for better UX.While
alert()works, it blocks the UI and is inconsistent with the modern toast-based feedback used elsewhere in the dashboard. A toast would provide a non-blocking notification.💡 Suggested improvement using toast
+import { toast } from "@/components/ui/use-toast"; + // In the callback: if (isLocalEmulator) { - alert("These settings are read-only in the local emulator. Update them in your production deployment instead."); + toast({ + title: "Read-only in local emulator", + description: "Update these settings in your production deployment instead.", + variant: "destructive", + }); return false; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/lib/config-update.tsx` around lines 272 - 275, Replace the blocking alert() call in the isLocalEmulator branch with the dashboard's non-blocking toast notification so UX stays consistent: instead of alert("These settings are read-only..."), call the existing toast/notification utility used elsewhere in the app (e.g., toast or notify) with the same message and an appropriate level (info/warning), keep the return false behavior, and add/import the toast helper if not already present so the change is local to the isLocalEmulator conditional.apps/backend/src/lib/local-emulator.ts (1)
6-7: Consider adding a comment explaining the intentional hardcoded password.
LOCAL_EMULATOR_ADMIN_PASSWORDis hardcoded, which is intentional for local development. A brief comment would help future maintainers understand this is deliberate and not a security oversight.📝 Suggested documentation
export const LOCAL_EMULATOR_ADMIN_EMAIL = "local-emulator@stack-auth.com"; +// Intentionally hardcoded for local development only. This password is used +// to seed the local emulator admin user and is not used in production. export const LOCAL_EMULATOR_ADMIN_PASSWORD = "LocalEmulatorPassword";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/lib/local-emulator.ts` around lines 6 - 7, Add a brief inline comment above LOCAL_EMULATOR_ADMIN_PASSWORD (and optionally above LOCAL_EMULATOR_ADMIN_EMAIL) stating that the hardcoded password is intentional for local development/emulation only, not used in production, and include guidance to change or override it via environment variables or secrets for any shared/dev environments; reference the constants LOCAL_EMULATOR_ADMIN_PASSWORD and LOCAL_EMULATOR_ADMIN_EMAIL so maintainers understand the intent and where to look.apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts (1)
21-26: Same partialprojectKeyspattern as in local-emulator-project.test.ts.This sets only
projectIdandsuperSecretAdminKey. Consider creating a shared helper that consistently sets all required keys from the provisioned response, includingsecretServerKeywhich is also returned by the endpoint.♻️ More complete projectKeys
backendContext.set({ projectKeys: { projectId: response.body.project_id, + secretServerKey: response.body.secret_server_key, superSecretAdminKey: response.body.super_secret_admin_key, }, });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts` around lines 21 - 26, The test is only setting partial projectKeys via backendContext.set({ projectKeys: { projectId, superSecretAdminKey } }) and misses other keys (e.g., secretServerKey) returned by the provision endpoint; add a shared helper (e.g., setProjectKeysFromResponse(response) or populateProjectKeys) that extracts all required keys from the provision response (project_id, super_secret_admin_key, secret_server_key, etc.) and calls backendContext.set({ projectKeys: { ... } }), then replace the inline object in this test and local-emulator-project.test.ts with that helper to ensure consistency.apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts (1)
7-7: Consider importingLOCAL_EMULATOR_OWNER_TEAM_IDfrom the source module.This constant is duplicated from
apps/backend/src/lib/local-emulator.ts. If the value changes in the source, this test will silently use a stale value and may pass incorrectly or fail misleadingly.♻️ Suggested import
-const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428"; +import { LOCAL_EMULATOR_OWNER_TEAM_ID } from "@/lib/local-emulator";Note: You may need to adjust the import path based on how the E2E test environment resolves backend modules.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts` at line 7, Replace the duplicated hardcoded LOCAL_EMULATOR_OWNER_TEAM_ID in the test with a single import of that constant from the source module that defines it (the local-emulator module) so the test always uses the canonical value; remove the const declaration in the test, add an import for LOCAL_EMULATOR_OWNER_TEAM_ID, and if necessary adjust the test's module resolution/import path so the test can import from the local-emulator module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/e2e-api-tests-local-emulator.yaml:
- Around line 130-158: The mock-oauth-server background action's wait-on URL is
incorrect—update the "Start mock-oauth-server in background" action to wait on
http://localhost:8114 (instead of http://localhost:8102) so the
background-action verifies the mock-oauth-server is ready; leave the "Start
run-email-queue in background" and "Start run-cron-jobs in background" steps
unchanged since they should continue waiting on the backend URL
(http://localhost:8102); alternatively, if you prefer configurable ports, pass
PORT or similar env into the mock-oauth-server action (e.g., set env PORT=8102)
and keep wait-on aligned with that env.
In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/page-client.tsx:
- Around line 89-105: The current code masks JSON parse failures by using
response.json().catch(() => null) and then treating responseBody as if it were
valid; instead, call response.json() inside a try/catch and if parsing throws,
rethrow a descriptive error (including the original parse error message and
context like the request/endpoint) so parse failures fail loudly; keep the
subsequent response.ok checks that inspect responseBody, but remove the
.catch(() => null) and ensure any JSON parse exception is propagated as a new
Error before the response.ok handling (referencing response.json() and
responseBody in your changes).
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Line 133: The current assignment to isLocalEmulator uses
getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true" which silently
treats any unexpected value as false; change it to explicitly validate the env
var value: read the raw string via
getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR"), then if it equals "true"
set isLocalEmulator = true, else if it equals "false" set isLocalEmulator =
false, otherwise throw a clear Error (or console.error + throw) mentioning
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR is invalid so the app fails fast; update the
code around isLocalEmulator in page-client.tsx to perform this validation.
In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts`:
- Around line 87-92: The backendContext.set call is only setting projectId and
superSecretAdminKey, causing niceBackendFetch to send undefined headers when
called with accessType "admin"; update the backendContext.set invocations (the
ones around the local-emulator-project.test file where projectKeys is set) to
include all four keys returned in the response: publishableClientKey,
secretServerKey, superSecretAdminKey, and projectId so that niceBackendFetch
(which reads these headers) has all required values for accessType "admin".
---
Nitpick comments:
In @.github/workflows/e2e-api-tests-local-emulator.yaml:
- Line 113: Remove the redundant trailing ampersand (&) from backgrounded
commands that use the background-action; specifically update each run step that
currently uses the string "pnpm run start:backend --log-order=stream &" (and the
other similar run commands on the same pattern at the other service steps) to
omit the trailing "&" so the command becomes "pnpm run start:backend
--log-order=stream" — apply the same change for the analogous run entries
referenced in the workflow.
- Around line 160-161: Add a short inline comment above the "Wait 10 seconds"
workflow step explaining why the 10s sleep is necessary (e.g., to allow the
emulator/service to initialize before tests) and what conditions it is covering,
and if applicable replace or augment the step name or comment to reference a
link or note on a more robust readiness check (so maintainers know this is a
timing workaround and where to find alternatives).
- Around line 166-172: Add a short comment above the repeated test steps to
document why tests are run multiple times on main/dev (e.g., flakiness
mitigation or stability checks) and clarify intent; update the step names "Run
tests again (attempt 1)" and "Run tests again (attempt 2)" (or add inline
comments) to state the reason (e.g., "retry for flaky test detection" or
"stability verification") so future maintainers understand the pattern.
- Line 46: Remove the redundant background operator by editing the GitHub
Actions step that currently uses the command string "docker compose -f
docker/dependencies/docker.compose.yaml up --pull always -d &" and drop the
trailing "&"; keep the rest of the run command identical so the
background-action semantics handle detaching without the extra ampersand.
In `@apps/backend/src/lib/local-emulator.ts`:
- Around line 6-7: Add a brief inline comment above
LOCAL_EMULATOR_ADMIN_PASSWORD (and optionally above LOCAL_EMULATOR_ADMIN_EMAIL)
stating that the hardcoded password is intentional for local
development/emulation only, not used in production, and include guidance to
change or override it via environment variables or secrets for any shared/dev
environments; reference the constants LOCAL_EMULATOR_ADMIN_PASSWORD and
LOCAL_EMULATOR_ADMIN_EMAIL so maintainers understand the intent and where to
look.
In `@apps/dashboard/.env.development`:
- Line 12: The environment variable
NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR was changed from true to false
without explanation; either revert it if that was accidental or add a concise
comment above NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR in the
.env.development indicating the reason for disabling the debugger (e.g., to
avoid noisy logs during local emulator runs or to prevent exposing debug UI),
and include any conditions when it should be re-enabled to guide future
maintainers.
In
`@apps/dashboard/src/app/`(main)/(protected)/projects/[projectId]/emails/page-client.tsx:
- Around line 149-183: Add a Playground variant that demonstrates the new
email-config read-only emulator state by rendering the same header and controls
using the isLocalEmulator flag and emailConfig.isShared check: display the
Typography copy for the emulator ("Email server settings are read-only in the
local emulator") when isLocalEmulator is true, hide the TestSendingDialog
trigger when emailConfig.isShared || isLocalEmulator, hide the
EditEmailServerDialog trigger when isLocalEmulator is true, and render the Alert
with AlertDescription when isLocalEmulator is true so the Playground shows the
read-only copy, conditional buttons, and informational alert; reference the
existing components/props used in the page (isLocalEmulator,
emailConfig.isShared, TestSendingDialog, EditEmailServerDialog, Alert,
AlertDescription, Typography, Button) so the Playground mirrors the real UI
behavior.
In `@apps/dashboard/src/lib/config-update.tsx`:
- Around line 272-275: Replace the blocking alert() call in the isLocalEmulator
branch with the dashboard's non-blocking toast notification so UX stays
consistent: instead of alert("These settings are read-only..."), call the
existing toast/notification utility used elsewhere in the app (e.g., toast or
notify) with the same message and an appropriate level (info/warning), keep the
return false behavior, and add/import the toast helper if not already present so
the change is local to the isLocalEmulator conditional.
In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts`:
- Around line 21-26: The test is only setting partial projectKeys via
backendContext.set({ projectKeys: { projectId, superSecretAdminKey } }) and
misses other keys (e.g., secretServerKey) returned by the provision endpoint;
add a shared helper (e.g., setProjectKeysFromResponse(response) or
populateProjectKeys) that extracts all required keys from the provision response
(project_id, super_secret_admin_key, secret_server_key, etc.) and calls
backendContext.set({ projectKeys: { ... } }), then replace the inline object in
this test and local-emulator-project.test.ts with that helper to ensure
consistency.
In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts`:
- Line 7: Replace the duplicated hardcoded LOCAL_EMULATOR_OWNER_TEAM_ID in the
test with a single import of that constant from the source module that defines
it (the local-emulator module) so the test always uses the canonical value;
remove the const declaration in the test, add an import for
LOCAL_EMULATOR_OWNER_TEAM_ID, and if necessary adjust the test's module
resolution/import path so the test can import from the local-emulator module.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (32)
.github/workflows/docker-emulator-test.yaml.github/workflows/e2e-api-tests-local-emulator.yamlapps/backend/.envapps/backend/.env.developmentapps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/migration.sqlapps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/tests/cascades-on-project-delete.tsapps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/tests/persists-and-enforces-uniqueness.tsapps/backend/prisma/schema.prismaapps/backend/prisma/seed.tsapps/backend/src/app/api/latest/internal/config/override/[level]/reset-keys/route.tsxapps/backend/src/app/api/latest/internal/config/override/[level]/route.tsxapps/backend/src/app/api/latest/internal/local-emulator/project/route.tsxapps/backend/src/lib/config.tsxapps/backend/src/lib/local-emulator.tsapps/dashboard/.envapps/dashboard/.env.developmentapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/layout.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page.tsxapps/dashboard/src/app/(main)/(protected)/layout-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsxapps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsxapps/dashboard/src/lib/config-update.tsxapps/dashboard/src/lib/env.tsxapps/dashboard/src/lib/utils.tsxapps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.tsapps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.tsdocker/README.mddocker/emulator/docker.compose.yamldocker/emulator/inbucket-nginx.confdocker/readme.md
💤 Files with no reviewable changes (7)
- apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/layout.tsx
- apps/dashboard/src/lib/utils.tsx
- docker/readme.md
- docker/emulator/inbucket-nginx.conf
- docker/README.md
- docker/emulator/docker.compose.yaml
- .github/workflows/docker-emulator-test.yaml
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
Show resolved
Hide resolved
apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts
Show resolved
Hide resolved
Greptile SummaryIntroduces local emulator mode gated by Key Changes:
Issues Found:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant Dashboard
participant Internal API
participant LocalEmulatorEndpoint
participant Database
participant SeedScript
Note over SeedScript,Database: Setup Phase (when NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true)
SeedScript->>Database: Create LOCAL_EMULATOR_OWNER_TEAM
SeedScript->>Database: Create LOCAL_EMULATOR_ADMIN_USER
SeedScript->>Database: Add user to team
Note over Dashboard,Database: Project Provisioning Flow
Dashboard->>Internal API: POST /internal/local-emulator/project<br/>{absolute_file_path}
Internal API->>LocalEmulatorEndpoint: Validate path is absolute
LocalEmulatorEndpoint->>Database: Check for existing LocalEmulatorProject
alt Project exists
LocalEmulatorEndpoint->>LocalEmulatorEndpoint: Reuse existing project_id
else New project
LocalEmulatorEndpoint->>LocalEmulatorEndpoint: Generate deterministic UUID from path hash
end
LocalEmulatorEndpoint->>Database: Upsert Project
LocalEmulatorEndpoint->>Database: Upsert Tenancy
LocalEmulatorEndpoint->>Database: Upsert LocalEmulatorProject mapping
LocalEmulatorEndpoint->>Database: Get or create API key set
LocalEmulatorEndpoint-->>Dashboard: Return {project_id, keys, branch_override}
Note over Dashboard,Database: Config Restriction Flow
Dashboard->>Internal API: PUT/PATCH /config/override/environment
Internal API->>Database: Check isLocalEmulatorProject(projectId)
alt Is local emulator project
Internal API-->>Dashboard: 400 "cannot be changed in local emulator"
else Normal project
Internal API->>Database: Update environment config
Internal API-->>Dashboard: 200 OK
end
Last reviewed commit: ca89fd8 |
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Outdated
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Outdated
Show resolved
Hide resolved
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/dashboard/src/lib/config-update.tsx (1)
266-285: Consider extracting the local emulator check to a shared constant or utility.The pattern
getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"is duplicated on lines 47 and 266. Extracting it to a module-level constant or a dedicated hook (e.g.,useIsLocalEmulator) would improve maintainability and reduce the risk of inconsistency.♻️ Example extraction
+const IS_LOCAL_EMULATOR = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"; + export function ConfigUpdateDialogProvider({ children }: { children: React.ReactNode }) { // ... const showPushableDialog = useCallback(async (...) => { // ... - const isLocalEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"; + const isLocalEmulator = IS_LOCAL_EMULATOR; // ... }, []); // ... } export function useUpdateConfig() { const { showPushableDialog } = useConfigUpdateDialog(); - const isLocalEmulator = getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true"; + const isLocalEmulator = IS_LOCAL_EMULATOR; // ... - }, [isLocalEmulator, showPushableDialog]); + }, [showPushableDialog]); // isLocalEmulator is now a stable module-level constant }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/lib/config-update.tsx` around lines 266 - 285, Extract the duplicated environment check getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true" into a shared utility (either a module-level constant or a hook like useIsLocalEmulator) and replace the inline checks in this file (the isLocalEmulator local variable inside the useCallback) and the other occurrence referenced in the review with an import/use of that shared symbol; update this file to call the new utility (e.g., const isLocalEmulator = useIsLocalEmulator() or import { IS_LOCAL_EMULATOR } from ...) and remove the direct getPublicEnvVar call, keeping existing behavior in the useCallback that shows an alert and prevents updates when true, and ensure showPushableDialog and project.updateConfig paths remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/dashboard/src/lib/config-update.tsx`:
- Around line 266-285: Extract the duplicated environment check
getPublicEnvVar("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR") === "true" into a shared
utility (either a module-level constant or a hook like useIsLocalEmulator) and
replace the inline checks in this file (the isLocalEmulator local variable
inside the useCallback) and the other occurrence referenced in the review with
an import/use of that shared symbol; update this file to call the new utility
(e.g., const isLocalEmulator = useIsLocalEmulator() or import {
IS_LOCAL_EMULATOR } from ...) and remove the direct getPublicEnvVar call,
keeping existing behavior in the useCallback that shows an alert and prevents
updates when true, and ensure showPushableDialog and project.updateConfig paths
remain unchanged.
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx (1)
177-187: Wrap async handler inrunAsynchronouslyWithAlertfor proper error handling.While the Button component handles loading states, errors thrown in
handleOpenConfigFile(lines 55, 62, 69, 89, 98, 100, 109) should be shown to users via alerts rather than potentially being swallowed. The coding guidelines specify usingrunAsynchronouslyWithAlertfor error handling in async handlers.♻️ Proposed fix
+import { runAsynchronouslyWithAlert } from "@stackframe/stack-shared/dist/utils/promises";<Button onClick={async () => { if (isLocalEmulator) { setOpenConfigFileDialog(true); return; } router.push("/new-project"); return await wait(2000); }} >{isLocalEmulator ? "Open config file" : "Create Project"} </Button>And for line 219:
- <Button onClick={handleOpenConfigFile} loading={openingConfigFile}> + <Button onClick={() => runAsynchronouslyWithAlert(handleOpenConfigFile())} loading={openingConfigFile}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/page-client.tsx around lines 177 - 187, The inline async onClick handler for the Button should be wrapped with runAsynchronouslyWithAlert to surface any thrown errors to the user; replace the current async () => { ... } with () => runAsynchronouslyWithAlert(async () => { if (isLocalEmulator) { setOpenConfigFileDialog(true); return; } router.push("/new-project"); await wait(2000); }); so the same logic (isLocalEmulator check, setOpenConfigFileDialog, router.push, wait) runs inside runAsynchronouslyWithAlert and errors from this handler (and related functions like handleOpenConfigFile) are shown via alerts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx`:
- Around line 98-105: The ON CONFLICT clause in the
globalPrismaClient.$executeRaw insert into "LocalEmulatorProject" currently does
DO UPDATE SET "projectId" = EXCLUDED."projectId", which can overwrite an
existing mapping when projectId is newly generated; change the conflict behavior
to preserve the original mapping (either use DO NOTHING on conflict or only
update "updatedAt") so existing absoluteFilePath -> projectId mappings are not
replaced; update the INSERT statement targeting "LocalEmulatorProject"
(references: absoluteFilePath, projectId, globalPrismaClient.$executeRaw) to use
the safer ON CONFLICT behavior.
- Around line 58-108: The current getOrCreateLocalEmulatorProjectId has a race
where two concurrent callers can generate different projectIds and create
orphaned Project/Tenancy rows; fix by making the operation atomic: wrap the
logic in a single Prisma transaction (Prisma.$transaction) with a serializable
isolation level and perform an INSERT ... ON CONFLICT ... RETURNING "projectId"
(using globalPrismaClient.$executeRaw/ $queryRaw inside the transaction) to
obtain a canonical projectId for the given "absoluteFilePath", then call
project.upsert and tenancy.upsert using that returned projectId (keep references
to getOrCreateLocalEmulatorProjectId, LocalEmulatorProject, project.upsert,
tenancy.upsert and the $executeRaw/$queryRaw usage).
---
Nitpick comments:
In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/page-client.tsx:
- Around line 177-187: The inline async onClick handler for the Button should be
wrapped with runAsynchronouslyWithAlert to surface any thrown errors to the
user; replace the current async () => { ... } with () =>
runAsynchronouslyWithAlert(async () => { if (isLocalEmulator) {
setOpenConfigFileDialog(true); return; } router.push("/new-project"); await
wait(2000); }); so the same logic (isLocalEmulator check,
setOpenConfigFileDialog, router.push, wait) runs inside
runAsynchronouslyWithAlert and errors from this handler (and related functions
like handleOpenConfigFile) are shown via alerts.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsxapps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
Show resolved
Hide resolved
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
apps/backend/src/lib/config.tsx (1)
1079-1108: Consider adding test coverage for the allowing cases.The test verifies that writes are blocked when all guard conditions are met, which is good. However, it would strengthen confidence to also test that writes are allowed when:
STACK_SEED_MODEis"true"(seeding should bypass the guard)- The project is not a local emulator project
- Local emulator mode is disabled
This would ensure the guard doesn't accidentally block legitimate writes.
💡 Example additional test case for seed mode bypass
import.meta.vitest?.test('setEnvironmentConfigOverride allows writes in seed mode', async ({ expect }) => { const vi = import.meta.vitest?.vi; if (!vi) { throw new StackAssertionError("Vitest context is required for in-source tests."); } const envUtils = await import("@stackframe/stack-shared/dist/utils/env"); const localEmulator = await import("./local-emulator"); const getEnvVariableSpy = vi.spyOn(envUtils, "getEnvVariable").mockImplementation((name: string, defaultValue?: string) => { if (name === "STACK_SEED_MODE") { return "true"; // Seed mode enabled } return defaultValue ?? "test-value"; }); const isLocalEmulatorEnabledSpy = vi.spyOn(localEmulator, "isLocalEmulatorEnabled").mockReturnValue(true); const isLocalEmulatorProjectSpy = vi.spyOn(localEmulator, "isLocalEmulatorProject").mockResolvedValue(true); try { // Should not throw - mock the DB call if needed or use a different assertion // This is a conceptual example; actual implementation depends on DB mocking strategy } finally { isLocalEmulatorProjectSpy.mockRestore(); isLocalEmulatorEnabledSpy.mockRestore(); getEnvVariableSpy.mockRestore(); } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/backend/src/lib/config.tsx` around lines 1079 - 1108, Add positive test coverage confirming setEnvironmentConfigOverride does not throw when the local-emulator guard should be bypassed: create three new tests that mock env.utils.getEnvVariable and ./local-emulator helpers and assert success (or non-throw) for (1) STACK_SEED_MODE === "true", (2) isLocalEmulatorProject returns false, and (3) isLocalEmulatorEnabled returns false; restore mocks in finally blocks and reuse the same LOCAL_EMULATOR_ENV_CONFIG_BLOCKED_MESSAGE and setEnvironmentConfigOverride symbol names so readers can locate the original failing test and the guard logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@apps/backend/src/lib/config.tsx`:
- Around line 1079-1108: Add positive test coverage confirming
setEnvironmentConfigOverride does not throw when the local-emulator guard should
be bypassed: create three new tests that mock env.utils.getEnvVariable and
./local-emulator helpers and assert success (or non-throw) for (1)
STACK_SEED_MODE === "true", (2) isLocalEmulatorProject returns false, and (3)
isLocalEmulatorEnabled returns false; restore mocks in finally blocks and reuse
the same LOCAL_EMULATOR_ENV_CONFIG_BLOCKED_MESSAGE and
setEnvironmentConfigOverride symbol names so readers can locate the original
failing test and the guard logic.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsxapps/backend/src/lib/config.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx
|
closing in favor of #1233 |
Note
Medium Risk
Medium risk: introduces a new Prisma migration/table plus a new internal endpoint that provisions projects and long-lived API keys, and changes config override write paths to reject updates for local-emulator projects.
Overview
Adds a
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATORmode that provisions local-emulator projects from an absolute config-file path and threads that behavior through backend, dashboard, and CI.Backend adds the
LocalEmulatorProjectmapping table and a hidden/internal/local-emulator/projectendpoint that upserts a project/tenancy, reuses or creates anApiKeySet, and returns credentials plus a placeholder branch override; the seed script now ensures the local-emulator owner team/user exist.Environment-level config overrides become read-only for local-emulator projects (API routes and
setEnvironmentConfigOverrideblock writes/resets), and the dashboard updates UX accordingly (disable project creation, add “Open config file” flow, auto-login, and hide env/email config editing in emulator mode). CI replaces the old Docker emulator check with a full local-emulator E2E workflow, and removes legacy docker emulator compose/docs.Written by Cursor Bugbot for commit c398730. This will update automatically on new commits. Configure here.
Summary by CodeRabbit
New Features
Bug Fixes
Chores