Skip to content

Local emulator base#1233

Merged
BilalG1 merged 14 commits intodevfrom
local-emulator-b
Mar 10, 2026
Merged

Local emulator base#1233
BilalG1 merged 14 commits intodevfrom
local-emulator-b

Conversation

@BilalG1
Copy link
Collaborator

@BilalG1 BilalG1 commented Mar 9, 2026

Summary by CodeRabbit

  • New Features

    • Provision local-emulator projects from a local config file and return emulator credentials via a new internal endpoint.
    • Dashboard: "Open config file" flow to open local projects and refresh owned projects.
  • Changes

    • Branch config can prefer/read/write local files for emulator projects.
    • Environment config updates/resets are blocked for local-emulator projects.
    • Dashboard UI shows read-only notices and disables project creation in emulator mode.
    • Added DB mapping and a standard env flag to identify local-emulator projects.
  • Tests

    • New E2E tests covering provisioning and config restrictions.
  • Chores

    • Removed legacy emulator docs and compose; added CI workflow for local-emulator E2E runs.

@vercel
Copy link

vercel bot commented Mar 9, 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 Mar 10, 2026 9:22pm
stack-backend Ready Ready Preview, Comment Mar 10, 2026 9:22pm
stack-dashboard Ready Ready Preview, Comment Mar 10, 2026 9:22pm
stack-demo Ready Ready Preview, Comment Mar 10, 2026 9:22pm
stack-docs Ready Ready Preview, Comment Mar 10, 2026 9:22pm

@BilalG1 BilalG1 changed the title Local emulator b Local emulator Mar 9, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Adds a local-emulator feature: removes legacy Docker emulator artifacts, adds DB mapping for project↔file-path, provides a provisioning API to obtain emulator credentials and file-based branch config read/write, and adds guards to block environment-config changes for local-emulator projects.

Changes

Cohort / File(s) Summary
CI Workflows
\.github/workflows/docker-emulator-test.yaml, \.github/workflows/e2e-api-tests-local-emulator.yaml
Removed old Docker-emulator test workflow; added E2E workflow to run tests against a local emulator via docker-compose with background services, wait-on checks, retries, and log streaming.
Environment vars / env files
apps/backend/.env, apps/backend/.env.development, apps/dashboard/.env, apps/dashboard/.env.development
Introduce NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR (standardized emulator flag) and adjust related dev/debug flags.
Prisma: schema, migration, tests
apps/backend/prisma/schema.prisma, apps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/*
Add LocalEmulatorProject model and migration (absoluteFilePath ↔ projectId) with unique/index and FK cascade; add migration tests covering cascade and uniqueness.
Backend: seed, deps, utilities
apps/backend/prisma/seed.ts, apps/backend/package.json, apps/backend/src/lib/local-emulator.ts
Refactor seed to use local-emulator constants/flows; add jiti dependency; new local-emulator utility for feature flags, DB mapping, and file-based config read/write.
Backend: config logic & API guards
apps/backend/src/lib/config.tsx, apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx, .../reset-keys/route.tsx
Branch config read/write prefers local file when enabled; environment-config writes/resets are blocked for local-emulator projects (guard + error message) unless seed mode.
Backend: provisioning endpoint
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
New POST route to provision/return project credentials by absolute file path: validates env & path, ensures owner team/admin membership, upserts project & mapping, ensures API keys, and returns credentials + branch config string.
Dashboard: project creation & projects UI
apps/dashboard/src/app/.../new-project/page-client.tsx, .../projects/page-client.tsx, .../projects/page.tsx
Disable normal project creation in local-emulator mode; add "Open config file" dialog to provision projects by absolute file path via internal API, with validation and response handling.
Dashboard: UI/layout & utils
apps/dashboard/src/app/.../sidebar-layout.tsx, .../layout.tsx, apps/dashboard/src/lib/utils.tsx, apps/dashboard/src/app/.../emails/page-client.tsx, apps/dashboard/src/lib/env.tsx, apps/dashboard/src/lib/config-update.tsx
Remove legacy redirect helper and emulator conditional branches; always show switcher/user controls; make email server settings read-only in emulator mode; rename env keys and gate config-update flows for emulator mode.
E2E tests
apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts, .../config-local-emulator.test.ts
Add E2E tests for provisioning endpoint (path validation, idempotency, credentials) and config behavior (block env updates, allow branch updates) under local-emulator flag.
Docker: artifacts & docs removed
docker/emulator/docker.compose.yaml, docker/emulator/inbucket-nginx.conf, docker/README.md, docker/readme.md
Removed legacy docker-compose emulator definition, nginx proxy, and emulator-related README sections.
Build tooling
configs/tsdown/js-library.ts
Add tsup plugin to emit package.json with {"type":"module"} for ESM output directories.
Client internals
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
Add internal method refreshOwnedProjects() to refresh owned projects cache via existing session flow.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Dashboard Client
    participant API as Backend API\nPOST /internal/local-emulator/project
    participant DB as Database
    participant FS as File System
    participant Auth as Auth/Credentials

    Client->>API: POST { absolute_file_path }
    API->>API: assert NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR
    API->>API: validate absolute path & file existence
    API->>DB: ensure owner team exists & admin membership
    API->>FS: readConfigFromFile(absolute_file_path)
    FS-->>API: config object or error
    API->>DB: upsert Project & LocalEmulatorProject mapping
    API->>Auth: ensure/get API keys for project
    Auth-->>API: secret_server_key, super_secret_admin_key
    API->>FS: stringify branch config for response
    API-->>Client: 200 { project_id, secret_server_key, super_secret_admin_key, branch_config_override_string }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • N2D4

Poem

🐇 I sniff a path, I tap a key,
No Docker tide, just files and tree,
Projects hop from single line,
Secrets tucked where configs shine,
A joyful rabbit cheers, "All green!" 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is empty except for a boilerplate comment about reading CONTRIBUTING.md, providing no explanation of changes, rationale, implementation details, or testing approach. Add a comprehensive description explaining the local emulator feature, key components added (database schema, configuration system, API endpoints), and how to test the implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.11% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Local emulator base' is directly related to the PR's core purpose: implementing foundational local emulator infrastructure including database models, configuration handling, API endpoints, and environment flags.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch local-emulator-b

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
Contributor

greptile-apps bot commented Mar 9, 2026

Greptile Summary

This PR replaces the previous Docker-compose-based local emulator approach with a new path-based system where any local stack.config.ts file can be mapped to a dynamically-provisioned Stack project. It introduces a new LocalEmulatorProject DB table, a new /internal/local-emulator/project endpoint, file-backed branch config reads/writes via jiti, and corresponding dashboard UI changes (replacing the hardcoded emulator project redirect with an "Open config file" dialog).

Key changes:

  • New LocalEmulatorProject Prisma model mapping an absoluteFilePath (PK) to a projectId, with cascade delete from Project.
  • readConfigFromFile / writeConfigToFile in local-emulator.ts use jiti.evalModule to execute and serialize TypeScript config files directly.
  • Environment config overrides are blocked for local emulator projects (both at the API and dashboard levels), while branch config overrides are redirected to write back to the local file.
  • Dashboard page-client.tsx adds an "Open config file" dialog; layout.tsx removes the old hard-redirect to a fixed emulator project ID.
  • The old docker-emulator-test.yaml CI workflow and docker/emulator/ files are removed; a new e2e-api-tests-local-emulator.yaml workflow runs the full E2E suite against a locally-started stack.

Issues found:

  • The E2E test assertions on lines 62 and 85 checking that branch_config_override_string contains the file path will always fail because the test files do not exist on disk, so readConfigFromFile returns {}.
  • handleOpenConfigFile in page-client.tsx throws validation errors that are silently discarded when invoked as a bare async onClick; runAsynchronouslyWithAlert should be used instead.
  • getOrCreateLocalEmulatorProjectId has a TOCTOU race condition: two concurrent requests for the same path can create two separate Project rows, with the last writer winning the LocalEmulatorProject mapping.
  • config-update.tsx uses a native alert() call instead of the application's toast/dialog system.
  • readConfigFromFile executes arbitrary TypeScript on every branch config fetch (not just when explicitly opened), which is undocumented and worth noting.

Confidence Score: 1/5

  • Not safe to merge — the E2E tests have a failing assertion that will break CI, and validation errors in the dashboard dialog are silently swallowed with no user feedback.
  • Two critical issues must be fixed: (1) the E2E test assertion on lines 62 and 85 will always fail because non-existent file paths cause readConfigFromFile to return {}, breaking CI; (2) validation errors in handleOpenConfigFile are silently swallowed since it's called as a bare async onClick without runAsynchronouslyWithAlert, leaving users without feedback on invalid input. The TOCTOU race condition in project provisioning is a real logic bug but lower-severity for single-user local development. The undocumented arbitrary TypeScript execution is worth noting but acceptable for a development tool. These issues require fixes before merging.
  • apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts (failing assertions will block CI), apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx (silent error handling in async onClick), and apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx (TOCTOU race condition).

Sequence Diagram

sequenceDiagram
    participant Dashboard as Dashboard (browser)
    participant Backend as Backend API
    participant DB as PostgreSQL
    participant FS as Filesystem

    Note over Dashboard,FS: "Open config file" flow (local emulator mode)

    Dashboard->>Backend: POST /internal/local-emulator/project<br/>{absolute_file_path}
    Backend->>DB: SELECT projectId FROM LocalEmulatorProject<br/>WHERE absoluteFilePath = ?
    DB-->>Backend: existing row (or empty)
    Backend->>DB: UPSERT Project (id = existing or new UUID)
    Backend->>DB: UPSERT Tenancy
    Backend->>DB: INSERT LocalEmulatorProject ON CONFLICT DO UPDATE
    Backend->>DB: findFirst ApiKeySet (valid, non-revoked)
    alt no valid key set
        Backend->>DB: CREATE ApiKeySet (expires 2099)
    end
    Backend->>FS: readConfigFromFile(absoluteFilePath)
    FS-->>Backend: config object (or {} if file missing)
    Backend-->>Dashboard: {project_id, secret_server_key,<br/>super_secret_admin_key, branch_config_override_string}
    Dashboard->>Dashboard: router.push(/projects/{project_id})

    Note over Backend,FS: On every branch config fetch (getBranchConfigOverrideQuery)
    Backend->>DB: SELECT absoluteFilePath FROM LocalEmulatorProject<br/>WHERE projectId = ?
    DB-->>Backend: file path
    Backend->>FS: jiti.evalModule(fileContents) — executes TS
    FS-->>Backend: config object
Loading

Last reviewed commit: 2afacf0

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: 8

🤖 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-138: The GitHub Actions step "Start mock-oauth-server in
background" is waiting on the wrong URL (http://localhost:8102); update its
wait-on target to the mock OAuth server port (http://localhost:8114) or
reference the STACK_OAUTH_MOCK_URL environment variable so the readiness check
actually verifies the mock-oauth-server started (the step uses run: pnpm run
start:mock-oauth-server, tail: true, wait-for, etc., so change the wait-on value
accordingly).

In `@apps/backend/prisma/seed.ts`:
- Line 47: The seed currently gates creation of the invariant local-emulator
rows behind the localEmulatorEnabled flag (localEmulatorEnabled /
isLocalEmulatorEnabled), which results in the provisioning route being enabled
but missing LOCAL_EMULATOR_OWNER_TEAM_ID and LOCAL_EMULATOR_ADMIN_USER_ID if the
flag is flipped after seeding; remove that conditional and always ensure those
invariant rows are created/upserted during seed (or alternatively move the
upsert logic into the provisioning route), i.e., replace the guarded creation
code for the owner team and admin user with unconditional upsert logic that
creates or updates the rows with the constants LOCAL_EMULATOR_OWNER_TEAM_ID and
LOCAL_EMULATOR_ADMIN_USER_ID so they exist regardless of when
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR is set.

In `@apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx`:
- Around line 162-182: The response schema currently returns secret_server_key
and super_secret_admin_key to callers authenticated with
clientOrHigherAuthTypeSchema; update the handler in route.tsx so that when
auth.type matches clientOrHigherAuthTypeSchema it only returns project_id
(remove secret_server_key and super_secret_admin_key from that response), and
require a stricter auth level (e.g., admin or server-level) to return
secret_server_key and super_secret_admin_key (or move those to a separate
endpoint) — modify the request/response branching logic and the response
yupObject to enforce this separation and ensure only privileged auth values
receive the secret fields.
- Around line 58-105: getOrCreateLocalEmulatorProjectId can race under
concurrency because it reads the mapping, generates a UUID, then creates
project/tenancy before upserting LocalEmulatorProject; serialize same-path
claims by acquiring a Postgres advisory lock derived from absoluteFilePath at
the start of getOrCreateLocalEmulatorProjectId (use
globalPrismaClient.$executeRaw to call pg_advisory_xact_lock or
pg_advisory_lock) and hold it while re-checking the mapping, creating/upserting
the Project (project.upsert) and Tenancy (tenancy.upsert) and performing the
LocalEmulatorProject insert/ON CONFLICT; alternatively wrap the lock +
operations in a single transaction so the lock is released automatically,
ensuring only one caller provisions a project for a given absoluteFilePath and
preventing orphaned projects.

In `@apps/backend/src/lib/local-emulator.ts`:
- Around line 46-60: readConfigFromFile currently swallows file/parse failures
and returns {}, which the caller treats as a valid override and silently hides
errors; change readConfigFromFile to surface failures by removing the
catch-that-returns-empty behavior and either rethrow the caught Error or change
the return type to Promise<Record<string, unknown> | null> and return null on
failure so callers (e.g., the code that prefers local-emulator values) can
distinguish "no config" from an empty config; ensure you still log the error via
captureError but do not convert failures into {} and update callers to handle
the thrown error or null return accordingly; keep references to isValidConfig
and captureError in the flow.
- Line 11: Replace the repo-wide constant LOCAL_EMULATOR_ADMIN_PASSWORD with a
per-provisioned or environment-controlled secret: remove the fixed string and
instead export a function or value that either reads a secure env var (e.g.,
process.env.LOCAL_EMULATOR_ADMIN_PASSWORD) and falls back to generating a
cryptographically-random password at startup, and/or only enables the emulator
admin path when running locally (check host binding or NODE_ENV) so it cannot be
exposed in shared environments; update any imports that reference
LOCAL_EMULATOR_ADMIN_PASSWORD to use the new getter/value and ensure the
generated secret is logged or persisted only to local machine output or a
dev-only file.

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts`:
- Around line 47-85: The test's assertions expecting
branch_config_override_string to contain the temp file path are incorrect
because route.tsx returns JSON.stringify(readConfigFromFile(absolute_file_path))
and local-emulator.ts falls back to {} for missing files; update the test in
local-emulator-project.test.ts to create real temp config files at pathA/pathB
with known contents (e.g., write a minimal config object/string to the generated
/tmp/<uuid>/stack.config.ts) before calling niceBackendFetch, then assert that
JSON.parse(responseX.body.branch_config_override_string) equals or contains the
known config object/string; alternatively, if you prefer minimal change, remove
the toContain(pathA/pathB) assertions and instead assert that
branch_config_override_string parses to an object (e.g.,
expect(JSON.parse(...)).toEqual(expect.any(Object))). Ensure you reference
branch_config_override_string, niceBackendFetch, and
LOCAL_EMULATOR_PROJECT_ENDPOINT when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02624f88-7fe7-4028-adc7-528b602fbedf

📥 Commits

Reviewing files that changed from the base of the PR and between 57149bd and 2afacf0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • .github/workflows/docker-emulator-test.yaml
  • .github/workflows/e2e-api-tests-local-emulator.yaml
  • apps/backend/.env
  • apps/backend/.env.development
  • apps/backend/package.json
  • apps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/migration.sql
  • apps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/tests/cascades-on-project-delete.ts
  • apps/backend/prisma/migrations/20260226100000_add_local_emulator_project_mapping/tests/persists-and-enforces-uniqueness.ts
  • apps/backend/prisma/schema.prisma
  • apps/backend/prisma/seed.ts
  • apps/backend/src/app/api/latest/internal/config/override/[level]/reset-keys/route.tsx
  • apps/backend/src/app/api/latest/internal/config/override/[level]/route.tsx
  • apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
  • apps/backend/src/lib/config.tsx
  • apps/backend/src/lib/local-emulator.ts
  • apps/dashboard/.env
  • apps/dashboard/.env.development
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/layout.tsx
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page.tsx
  • apps/dashboard/src/app/(main)/(protected)/layout-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/dashboard/src/lib/config-update.tsx
  • apps/dashboard/src/lib/env.tsx
  • apps/dashboard/src/lib/utils.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/internal/config-local-emulator.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts
  • docker/README.md
  • docker/emulator/docker.compose.yaml
  • docker/emulator/inbucket-nginx.conf
  • docker/readme.md
💤 Files with no reviewable changes (7)
  • docker/README.md
  • docker/readme.md
  • docker/emulator/docker.compose.yaml
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/layout.tsx
  • docker/emulator/inbucket-nginx.conf
  • .github/workflows/docker-emulator-test.yaml
  • apps/dashboard/src/lib/utils.tsx

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

🤖 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/dashboard/src/app/`(main)/(protected)/projects/[projectId]/sidebar-layout.tsx:
- Line 612: The header is rendering ProjectSwitcher (and UserButton)
unconditionally which causes internal-only hooks (useUser with
projectIdMustMatch and user.useOwnedProjects) to run in local-emulator mode;
update the header in sidebar-layout.tsx to explicitly check the
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR flag and only render <ProjectSwitcher ...>
and <UserButton ...> when the flag is false, otherwise render a simplified
emulator-safe header/placeholder; alternatively make ProjectSwitcher and
UserButton emulator-safe by short-circuiting useUser and user.useOwnedProjects
when NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR so they never call internal-only hooks.
Ensure you reference the ProjectSwitcher and UserButton components and the
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR env var in your change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cd2ec3aa-1a43-4f0c-b5a2-e1a4368c3d97

📥 Commits

Reviewing files that changed from the base of the PR and between 2afacf0 and 6f957d3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@configs/tsdown/js-library.ts`:
- Line 82: The current ESM-dir check uses options.dir?.endsWith('/esm') or ===
'dist/esm', which fails on Windows paths; normalize the path string before
checking by replacing backslashes with forward slashes (or use path.normalize
and then convert to posix-style) and then test for suffixes like '/esm' or
'/dist/esm' against the normalized options.dir. Update the check around
options.dir in js-library.ts to normalize options.dir first and then perform the
endsWith/equals checks so absolute or Windows-style paths (e.g.,
'C:\\...\\dist\\esm' or 'dist\\esm') are detected correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4f8c595c-268b-45fa-ae9a-a621c83b8a95

📥 Commits

Reviewing files that changed from the base of the PR and between 6f957d3 and 9437893.

📒 Files selected for processing (1)
  • configs/tsdown/js-library.ts

@N2D4 N2D4 mentioned this pull request Mar 9, 2026
@BilalG1 BilalG1 changed the title Local emulator Local emulator base Mar 10, 2026
@BilalG1 BilalG1 requested a review from N2D4 March 10, 2026 16:20
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: 3

🧹 Nitpick comments (3)
apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts (2)

12-17: Consider cleaning up temp files after tests.

The tests create temp files in /tmp that are not cleaned up after test completion. While this is minor (OS typically cleans /tmp), adding cleanup improves test hygiene and prevents potential disk space issues in CI with many test runs.

💡 Example cleanup pattern
import { afterAll } from "vitest";

const tempFilesToCleanup: string[] = [];

async function createTempConfigFile(): Promise<string> {
  const filePath = `/tmp/${randomUUID()}/stack.config.ts`;
  await fs.mkdir(path.dirname(filePath), { recursive: true });
  await fs.writeFile(filePath, "export const config = {};\n", "utf-8");
  tempFilesToCleanup.push(path.dirname(filePath));
  return filePath;
}

afterAll(async () => {
  for (const dir of tempFilesToCleanup) {
    await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
  }
});

Also applies to: 71-89, 91-156

🤖 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`
around lines 12 - 17, The test creates temporary config files in
createTempConfigFile() but never removes them; add a module-scoped array (e.g.,
tempFilesToCleanup) to record created directories or file paths inside
createTempConfigFile(), and register an afterAll() hook that iterates the array
and removes each entry with fs.rm(..., { recursive: true, force: true }) (catch
errors to avoid failing teardown). Update all places that create temp artifacts
(the other blocks noted in the review) to push their paths into
tempFilesToCleanup so the centralized afterAll cleanup removes them after tests.

9-9: Consider importing LOCAL_EMULATOR_OWNER_TEAM_ID from a shared location.

The constant is duplicated from apps/backend/src/lib/local-emulator.ts. If the value changes in the source, this test would fail silently with a misleading assertion error.

However, importing directly from backend code into E2E tests may require additional build configuration. If that's not feasible, consider adding a comment noting the dependency:

-const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
+// Must match LOCAL_EMULATOR_OWNER_TEAM_ID in apps/backend/src/lib/local-emulator.ts
+const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
🤖 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 9, The test defines a duplicated constant LOCAL_EMULATOR_OWNER_TEAM_ID
that is already declared in the backend's local-emulator module; remove the
hard-coded value from the test and import LOCAL_EMULATOR_OWNER_TEAM_ID from the
shared source (or from the backend module that exports it) so the test uses the
single source of truth; if importing is not possible due to build constraints,
keep the constant but add a clear comment above LOCAL_EMULATOR_OWNER_TEAM_ID in
the test referencing the original symbol in local-emulator.ts and note that it
must be updated in both places, and consider moving the constant to a
shared/test-helpers module to resolve the duplication permanently.
apps/backend/src/lib/config.tsx (1)

269-275: Consider handling the case where isLocalEmulatorProject() returns true but filePath is null.

When isLocalEmulatorProject(options.projectId) returns true but getLocalEmulatorFilePath(options.projectId) returns null, the code silently falls through to the DB write path. This could happen if there's a TOCTOU window where the LocalEmulatorProject row is deleted between the two async calls, or if the mapping row exists without a valid absoluteFilePath.

While this may be intentional as a fallback, consider whether throwing an error would be more appropriate to surface unexpected state:

💡 Suggested alternative
   if (isLocalEmulatorEnabled() && await isLocalEmulatorProject(options.projectId)) {
     const filePath = await getLocalEmulatorFilePath(options.projectId);
-    if (filePath) {
-      await writeConfigToFile(filePath, newConfig);
-      return;
-    }
+    if (!filePath) {
+      throw new StackAssertionError("Local emulator project exists but has no file path", { projectId: options.projectId });
+    }
+    await writeConfigToFile(filePath, newConfig);
+    return;
   }
🤖 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 269 - 275, The code currently
treats a truthy isLocalEmulatorProject(options.projectId) but null filePath from
getLocalEmulatorFilePath(options.projectId) as a silent fallthrough; update the
isLocalEmulatorProject + getLocalEmulatorFilePath branch to explicitly handle
the null filePath case (instead of silently proceeding to DB writes) by throwing
a descriptive error or logging and returning, so callers don’t unknowingly write
to DB when an emulator mapping existed but no absolute path was found; modify
the block around isLocalEmulatorEnabled, isLocalEmulatorProject,
getLocalEmulatorFilePath, writeConfigToFile and newConfig to implement this
explicit error/early-return behavior.
🤖 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 190-194: The endpoint accepts arbitrary absolute paths and passes
them into readConfigFromFile which ultimately uses jiti.evalModule, allowing
remote code execution; restrict and validate paths and raise auth level. Update
the route handler that uses path.isAbsolute and path.resolve on
req.body.absolute_file_path to enforce a whitelist or base directory (e.g.,
join+normalize against a configured SAFE_CONFIG_DIR) and reject any path outside
that directory, and/or change the route's auth from clientOrHigherAuthTypeSchema
to a server/admin-only schema; additionally add safe file-content checks in
readConfigFromFile before calling jiti.evalModule (e.g., disallow executable JS,
require a JSON/YAML config or signature) and log/reject suspicious files.

In
`@apps/dashboard/src/app/`(main)/(protected)/(outside-dashboard)/projects/page-client.tsx:
- Around line 88-103: The code currently calls response.json() before checking
response.ok which throws a SyntaxError for non-JSON responses; change the logic
in page-client.tsx so you read the raw body first (e.g., response.text()),
inspect response.headers.get("content-type") or try JSON.parse on the text, and
only then branch on response.ok; if the response is non-JSON use the raw text as
the error message (or fall back to a generic message) so the error thrown from
this block (where responseBody is used) preserves actionable backend/proxy text
instead of hiding it behind a SyntaxError.
- Around line 195-223: The Dialog can still close via onOpenChange while
openingConfigFile is true; update the onOpenChange handler used by Dialog so it
guards against closing during the loading state: in the Dialog component that
uses onOpenChange, check the openingConfigFile flag and ignore attempts to set
open to false when openingConfigFile is true (i.e., only call
setOpenConfigFileDialog(false) if !openingConfigFile), and likewise ensure the
Cancel button and any other close paths respect openingConfigFile (keep existing
disabled prop), leaving handleOpenConfigFile unchanged.

---

Nitpick comments:
In `@apps/backend/src/lib/config.tsx`:
- Around line 269-275: The code currently treats a truthy
isLocalEmulatorProject(options.projectId) but null filePath from
getLocalEmulatorFilePath(options.projectId) as a silent fallthrough; update the
isLocalEmulatorProject + getLocalEmulatorFilePath branch to explicitly handle
the null filePath case (instead of silently proceeding to DB writes) by throwing
a descriptive error or logging and returning, so callers don’t unknowingly write
to DB when an emulator mapping existed but no absolute path was found; modify
the block around isLocalEmulatorEnabled, isLocalEmulatorProject,
getLocalEmulatorFilePath, writeConfigToFile and newConfig to implement this
explicit error/early-return behavior.

In
`@apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts`:
- Around line 12-17: The test creates temporary config files in
createTempConfigFile() but never removes them; add a module-scoped array (e.g.,
tempFilesToCleanup) to record created directories or file paths inside
createTempConfigFile(), and register an afterAll() hook that iterates the array
and removes each entry with fs.rm(..., { recursive: true, force: true }) (catch
errors to avoid failing teardown). Update all places that create temp artifacts
(the other blocks noted in the review) to push their paths into
tempFilesToCleanup so the centralized afterAll cleanup removes them after tests.
- Line 9: The test defines a duplicated constant LOCAL_EMULATOR_OWNER_TEAM_ID
that is already declared in the backend's local-emulator module; remove the
hard-coded value from the test and import LOCAL_EMULATOR_OWNER_TEAM_ID from the
shared source (or from the backend module that exports it) so the test uses the
single source of truth; if importing is not possible due to build constraints,
keep the constant but add a clear comment above LOCAL_EMULATOR_OWNER_TEAM_ID in
the test referencing the original symbol in local-emulator.ts and note that it
must be updated in both places, and consider moving the constant to a
shared/test-helpers module to resolve the duplication permanently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e917fc96-5fd1-4597-8ef8-00b78f9711a8

📥 Commits

Reviewing files that changed from the base of the PR and between 7ae90ae and ce230a7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • apps/backend/.env.development
  • apps/backend/package.json
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
  • apps/backend/src/lib/config.tsx
  • apps/backend/src/lib/local-emulator.ts
  • apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/projects/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/emails/page-client.tsx
  • apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/sidebar-layout.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/internal/local-emulator-project.test.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/backend/src/lib/local-emulator.ts
  • apps/backend/prisma/schema.prisma

@BilalG1 BilalG1 merged commit 66adb4e into dev Mar 10, 2026
35 checks passed
@BilalG1 BilalG1 deleted the local-emulator-b branch March 10, 2026 22:15
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