Skip to content

client sdk local emulator#1247

Open
BilalG1 wants to merge 2 commits intodevfrom
client-sdk-local-emulator-support
Open

client sdk local emulator#1247
BilalG1 wants to merge 2 commits intodevfrom
client-sdk-local-emulator-support

Conversation

@BilalG1
Copy link
Copy Markdown
Collaborator

@BilalG1 BilalG1 commented Mar 13, 2026

Summary by CodeRabbit

  • New Features

    • Local emulator support with automatic project credential fetching and initialization
    • Emulator credential override mechanism for client, server, and admin flows
    • Option to specify a local emulator config file path
    • API endpoints extended to accept and return publishable client keys for emulator usage
  • Chores

    • Updated initialization and request-preparation flows to respect emulator mode and credential overrides

@vercel
Copy link
Copy Markdown

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds publishableClientKey support to the local emulator API and wiring, introduces three emulator credential constants, enables runtime emulator credential overrides across client/server/admin interfaces, and adds emulator detection + async credential fetching into app initialization flows.

Changes

Cohort / File(s) Summary
Backend Emulator Endpoint
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
POST route schema extended to accept/return publishable_client_key; credential creation/validation updated to require and return publishableClientKey.
Backend Constants & Entrypoint
apps/backend/src/lib/local-emulator.ts, docker/server/entrypoint.sh
Added three exported emulator key constants; Docker entrypoint now conditionally uses fixed placeholder keys when running the local emulator.
Shared Interface Overrides
packages/stack-shared/src/interface/client-interface.ts, packages/stack-shared/src/interface/server-interface.ts, packages/stack-shared/src/interface/admin-interface.ts
Added protected override fields and _updateEmulatorCredentials implementations; request header construction now prefers override values when present.
Template Common Utilities
packages/template/src/lib/stack-app/apps/implementations/common.ts
New emulator helpers: constants, local config path resolver, and fetchEmulatorProjectCredentials; default/key resolution and getBaseUrl now accept { isEmulator }.
App Implementations
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts, .../server-app-impl.ts, .../admin-app-impl.ts
Detects emulator via config path, uses emulator-aware defaults (including internal publishable key), lazily fetches credentials and calls interface _updateEmulatorCredentials, and awaits initialization in request preparation.
Client App Options
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
Added optional localEmulatorConfigFilePath?: string to constructor options to specify emulator config location.

Sequence Diagram

sequenceDiagram
    participant AppInit as App Initialization
    participant Detect as Emulator Detection
    participant FS as Config File
    participant EmulatorAPI as Local Emulator API
    participant Interface as SDK Interface
    participant Request as API Request

    AppInit->>Detect: check getLocalEmulatorConfigFilePath(...)
    Detect->>FS: resolve config path
    alt config found (emulator)
        Detect->>EmulatorAPI: fetchEmulatorProjectCredentials(configPath)
        EmulatorAPI-->>Detect: return { projectId, publishableClientKey, secretServerKey, superSecretAdminKey }
        Detect->>Interface: iface._updateEmulatorCredentials({...})
        Interface-->>Interface: store override values
    else no config (normal)
        Detect-->>AppInit: proceed with defaults
    end
    AppInit->>Request: prepareRequest()
    Request->>Interface: request credentials (projectId, keys)
    Interface-->>Request: return overrides if present, else defaults
    Request->>EmulatorAPI: perform API call with headers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hopped to fetch keys in a cosy little den,

publishable, secret — now flowing again,
Client, server, admin all snug in a row,
Overrides whisper where emulator winds blow,
Hop on, little keys — off to testing we go! 🎋✨

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only a boilerplate CONTRIBUTING.md reminder with no implementation details, change summary, objectives, or notes about the changeset. Add a comprehensive description including: summary of changes, motivation/rationale, how emulator support works, credential management flow, and testing considerations. Reference the commit message which mentions prepareRequest callbacks and _emulatorInitPromise.
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.
Title check ❓ Inconclusive The title 'client sdk local emulator' is vague and generic, using non-descriptive terms that don't clearly convey the specific nature of changes (e.g., adding emulator support, credential management, etc.). Provide a more descriptive title that clearly indicates the main change, such as 'Add local emulator support to client, admin, and server SDKs' or 'Implement emulator credential initialization for SDK interfaces'.

✏️ 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 client-sdk-local-emulator-support
📝 Coding Plan
  • Generate coding plan for human review comments

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 Mar 13, 2026

Greptile Summary

This PR extends the local emulator to support the full SDK hierarchy (client, server, and admin) by adding publishableClientKey to the emulator credentials flow, introducing _updateEmulatorCredentials overrides at each interface level, and wiring up an async init promise that fetches real project credentials from the local emulator on SDK construction.

Key changes and findings:

  • Backend (route.tsx): The getOrCreateCredentials query now validates publishableClientKey via an assertion, but the findFirst query still lacks a publishableClientKey: { not: null } filter. Pre-existing key sets without this field will trigger an assertion error instead of being skipped in favour of creating a new valid key set.
  • server-app-impl.ts / admin-app-impl.ts: _emulatorInitPromise is set but no prepareRequest is passed to StackServerInterface / StackAdminInterface. Since the parent's prepareRequest branch is only entered when no interface override is provided, server and admin requests are not guarded against racing ahead of the credential fetch. There is also no .catch() on the init promise, which creates an unhandled promise rejection if the emulator is unreachable.
  • client-app-impl.ts: Correctly awaits _emulatorInitPromise inside prepareRequest, so client-level requests are safe.
  • entrypoint.sh: Correctly assigns predictable well-known keys in emulator mode so the SDK bootstrap call can authenticate against the internal project before real credentials are loaded.

Confidence Score: 2/5

  • Not safe to merge — two logic bugs can cause authentication failures for server/admin SDK users and data inconsistency when pre-existing emulator key sets are encountered.
  • The missing publishableClientKey filter in the Prisma query and the absent prepareRequest wiring for server/admin interfaces are both correctness bugs that will surface in real usage: the first during upgrades that hit existing key sets, the second for any server/admin SDK call made promptly after app construction in emulator mode.
  • apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx (missing DB filter), packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts and admin-app-impl.ts (missing prepareRequest wiring and unhandled rejection).

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx Adds publishableClientKey to the response and assertion, but the existingKeySet query is missing a publishableClientKey: { not: null } filter — pre-existing key sets without this field would trigger an assertion error instead of being bypassed and a new valid key set being created.
packages/stack-shared/src/interface/client-interface.ts Adds _projectIdOverride and _publishableClientKeyOverride mutable fields and the _updateEmulatorCredentials method so the interface can switch to project-specific credentials after the async emulator init resolves; the override is correctly prioritized in request headers and the projectId getter.
packages/template/src/lib/stack-app/apps/implementations/common.ts Introduces fetchEmulatorProjectCredentials, emulator URL/key constants, and updates getDefaultProjectId/getBaseUrl/getDefaultSecretServerKey/getDefaultSuperSecretAdminKey to handle the emulator case gracefully without throwing.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Correctly gates emulator credential fetching behind prepareRequest for client-level requests; _emulatorInitPromise is properly awaited before each request when using StackClientInterface.
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts Sets _emulatorInitPromise but does not pass prepareRequest to StackServerInterface, meaning server-level requests may proceed with placeholder credentials before the real emulator credentials are loaded; also no .catch() means emulator init failures become unhandled promise rejections.
packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts Same prepareRequest/_emulatorInitPromise wiring issue as server-app-impl.ts — admin requests are not guarded; also no .catch() on the init promise.

Sequence Diagram

sequenceDiagram
    participant App as SDK App (Client/Server/Admin)
    participant CI as StackClientInterface
    participant SI as StackServerInterface
    participant BE as Local Emulator Backend
    participant DB as Prisma DB

    App->>CI: new StackClientInterface({ prepareRequest })
    App->>App: _emulatorInitPromise = fetchEmulatorProjectCredentials()
    App->>BE: POST /api/v1/internal/local-emulator/project
    BE->>DB: findFirst(apiKeySet where secretServerKey!=null AND superSecretAdminKey!=null)
    alt Key set found (may lack publishableClientKey — bug)
        DB-->>BE: existing keySet
        BE->>BE: assert publishableClientKey != null (throws if null)
    else No key set found
        DB-->>BE: null
        BE->>DB: create apiKeySet (all 3 keys)
        DB-->>BE: new keySet
    end
    BE-->>App: { project_id, publishable_client_key, secret_server_key, super_secret_admin_key }
    App->>CI: _updateEmulatorCredentials(credentials)
    App->>SI: _updateEmulatorCredentials(credentials)

    Note over App,SI: Client requests: prepareRequest awaits _emulatorInitPromise ✓
    Note over App,SI: Server/Admin requests: NO prepareRequest set — may race ✗

    App->>CI: sendClientRequest (awaits prepareRequest → init promise)
    App->>SI: sendServerRequest (NO prepareRequest → may use placeholder keys)
Loading

Comments Outside Diff (1)

  1. apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx, line 113-130 (link)

    Missing publishableClientKey filter in existingKeySet query

    The assertion at line 144 now requires publishableClientKey to be non-null, but the findFirst query that fetches existingKeySet does not filter for publishableClientKey: { not: null }. If there are any previously-created key sets (from before this PR, when the assertion didn't require publishableClientKey) that have a null publishableClientKey but valid secretServerKey and superSecretAdminKey, the query will return that key set. The subsequent assertion will then throw a StackAssertionError instead of falling through to create a new valid key set.

Last reviewed commit: 01ca850

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.

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx (1)

113-130: ⚠️ Potential issue | 🟡 Minor

Potential issue: Prisma query doesn't filter for publishableClientKey existence.

The query at lines 113-130 filters for secretServerKey: { not: null } and superSecretAdminKey: { not: null } but not publishableClientKey: { not: null }. If there are existing key sets in the database without a publishableClientKey, this could return them, and the assertion at line 144 would throw.

Consider adding publishableClientKey: { not: null } to the query filter for consistency:

Proposed fix
   const existingKeySet = await globalPrismaClient.apiKeySet.findFirst({
     where: {
       projectId,
       manuallyRevokedAt: null,
       expiresAt: {
         gt: new Date(),
       },
+      publishableClientKey: {
+        not: null,
+      },
       secretServerKey: {
         not: null,
       },
       superSecretAdminKey: {
         not: null,
       },
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx`
around lines 113 - 130, The Prisma query in
globalPrismaClient.apiKeySet.findFirst (assigned to existingKeySet) filters for
secretServerKey and superSecretAdminKey but not publishableClientKey, so it may
return rows missing publishableClientKey and later cause the assertion at line
144 to fail; update the findFirst where clause to include publishableClientKey:
{ not: null } so only key sets that contain all three keys (secretServerKey,
superSecretAdminKey, publishableClientKey) are returned by existingKeySet.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/stack-shared/src/interface/client-interface.ts`:
- Around line 53-70: The emulator override only updates
_projectIdOverride/_publishableClientKeyOverride via _updateEmulatorCredentials
but fetchNewAccessToken, getOAuthUrl, and callOAuthCallback still read
this.options.publishableClientKey (and possibly this.options.projectId), causing
inconsistent behavior; update those methods to read the effective accessors
(this.publishableClientKey and this.projectId) instead of directly using
this.options.* so the OAuth/token refresh flow honors the emulator overrides
from _updateEmulatorCredentials.

In `@packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts`:
- Around line 154-164: AdminAppImpl (and similarly ServerAppImpl) sets
this._emulatorInitPromise but never wires a prepareRequest callback on the
created interface, allowing requests to race before emulator credentials load;
add a prepareRequest async callback when constructing/assigning this._interface
that awaits this._emulatorInitPromise if present (i.e., implement
prepareRequest: async () => { if (this._emulatorInitPromise) await
this._emulatorInitPromise; }) so any request via the interface waits for
_emulatorInitPromise to resolve before proceeding.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 503-538: The emulator credentials are applied asynchronously after
the StackClientInterface is created, so synchronous methods that derive cookie
names (notably _getSession and _getOrCreateTokenStore) can use the pre-override
projectId and miss emulator sessions; fix by ensuring emulator credentials are
resolved before any code can synchronously read projectId or cookie names:
either await fetchEmulatorProjectCredentials(emulatorConfigFilePath) and apply
the returned project_id/publishable_client_key to projectId/publishableClientKey
before constructing StackClientInterface, or modify _getSession and
_getOrCreateTokenStore to await this._emulatorInitPromise (if present) before
deriving cookie names (and ensure any hook/path that calls them will suspend
while _emulatorInitPromise resolves); update references to prepareRequest only
for network protection and keep a single source of truth by applying the
emulator override prior to exposing the app interface.

In `@packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts`:
- Around line 420-438: The server interface is created immediately so initial
requests can run with pre-override keys; to fix, mirror the client-side behavior
by ensuring emulator credential fetch completes before the server request path
uses the interface: when isEmulator and no extraOptions.interface, set
this._emulatorInitPromise = fetchEmulatorProjectCredentials(...), then either
await that promise before constructing/assigning the StackServerInterface or
ensure this._interface’s request-building methods internally await
this._emulatorInitPromise (i.e., update server-app-impl.ts so the code around
StackServerInterface, this._interface and _emulatorInitPromise ensures the
credentials are applied before any inherited request code runs).

---

Outside diff comments:
In `@apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx`:
- Around line 113-130: The Prisma query in
globalPrismaClient.apiKeySet.findFirst (assigned to existingKeySet) filters for
secretServerKey and superSecretAdminKey but not publishableClientKey, so it may
return rows missing publishableClientKey and later cause the assertion at line
144 to fail; update the findFirst where clause to include publishableClientKey:
{ not: null } so only key sets that contain all three keys (secretServerKey,
superSecretAdminKey, publishableClientKey) are returned by existingKeySet.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1b977d5-c56f-409f-a728-6a208f15751d

📥 Commits

Reviewing files that changed from the base of the PR and between 612cb71 and 01ca850.

📒 Files selected for processing (11)
  • apps/backend/src/app/api/latest/internal/local-emulator/project/route.tsx
  • apps/backend/src/lib/local-emulator.ts
  • docker/server/entrypoint.sh
  • packages/stack-shared/src/interface/admin-interface.ts
  • packages/stack-shared/src/interface/client-interface.ts
  • packages/stack-shared/src/interface/server-interface.ts
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/common.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts

Add prepareRequest callbacks to StackServerInterface and
StackAdminInterface that await _emulatorInitPromise, matching
the existing pattern in the client app. Prevents race conditions
where requests use placeholder credentials before the emulator
credentials are fetched.
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)
packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts (1)

435-435: Consider replacing non-null assertion with explicit error.

While the non-null assertion is logically safe here (the isEmulator guard on line 433 ensures emulatorConfigFilePath is truthy), the coding guidelines prefer explicit error messages that state the assumption.

💡 Suggested improvement
-      this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath!).then((data) => {
+      this._emulatorInitPromise = fetchEmulatorProjectCredentials(emulatorConfigFilePath ?? throwErr("emulatorConfigFilePath must be defined when isEmulator is true")).then((data) => {

As per coding guidelines: "Prefer ?? throwErr(...) over non-null assertions with good error messages explicitly stating the assumption."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts`
at line 435, Replace the non-null assertion on emulatorConfigFilePath when
initializing this._emulatorInitPromise with an explicit null-check that throws a
clear error; specifically, change the call to
fetchEmulatorProjectCredentials(emulatorConfigFilePath!) in the
_emulatorInitPromise assignment to use emulatorConfigFilePath ??
throwErr("expected emulatorConfigFilePath to be set when isEmulator is true")
(or your project's throw helper), keeping the surrounding isEmulator guard and
the fetchEmulatorProjectCredentials call intact so the code fails with a
descriptive message instead of relying on a non-null assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts`:
- Line 435: Replace the non-null assertion on emulatorConfigFilePath when
initializing this._emulatorInitPromise with an explicit null-check that throws a
clear error; specifically, change the call to
fetchEmulatorProjectCredentials(emulatorConfigFilePath!) in the
_emulatorInitPromise assignment to use emulatorConfigFilePath ??
throwErr("expected emulatorConfigFilePath to be set when isEmulator is true")
(or your project's throw helper), keeping the surrounding isEmulator guard and
the fetchEmulatorProjectCredentials call intact so the code fails with a
descriptive message instead of relying on a non-null assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1420a7b-0c62-4608-9dff-9f228ac2008c

📥 Commits

Reviewing files that changed from the base of the PR and between 01ca850 and 25f8b1c.

📒 Files selected for processing (2)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts
  • packages/template/src/lib/stack-app/apps/implementations/server-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/template/src/lib/stack-app/apps/implementations/admin-app-impl.ts

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.

1 participant