Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
01ca850
client sdk local emulator
BilalG1 Mar 13, 2026
25f8b1c
Gate server and admin emulator requests on credential init
BilalG1 Mar 19, 2026
51e71cf
Resolve merge conflicts in common.ts with envVars + emulator support
BilalG1 Apr 9, 2026
7d9e156
local emulator image opt
BilalG1 Apr 9, 2026
d724eb2
emulator fixes
BilalG1 Apr 10, 2026
cd087c5
Merge branch 'dev' into local-emulator-image-optimization
BilalG1 Apr 10, 2026
784f17c
emulator: fail-fast on provision errors, diagnose smoke test failures
BilalG1 Apr 10, 2026
7bf4a15
emulator: make cross-arch arm64 build survive TCG
BilalG1 Apr 10, 2026
6c5615b
emulator: drop --jitless, capture migration errors on failure
BilalG1 Apr 10, 2026
2538382
ci: run arm64 emulator build on ubuntu-24.04-arm (same-arch TCG)
BilalG1 Apr 10, 2026
54ecd7c
emulator: bounded dep wait with per-service diagnostics
BilalG1 Apr 10, 2026
5c3c436
emulator: only use -cpu cortex-a72 for cross-arch TCG
BilalG1 Apr 11, 2026
e9dfda7
Merge remote-tracking branch 'origin/dev' into client-sdk-local-emula…
BilalG1 Apr 13, 2026
e636151
emulator: move arm64 back to ubicloud cross-arch, run migrations with…
BilalG1 Apr 13, 2026
f4aca6d
emulator: swap --jitless for --no-opt on migration exec
BilalG1 Apr 13, 2026
144866a
emulator: pass --no-opt on node CLI, not via NODE_OPTIONS
BilalG1 Apr 13, 2026
5077bb2
pr comment fixes
BilalG1 Apr 13, 2026
95054ca
emulator: don't strip the clickhouse binary (breaks self-extractor)
BilalG1 Apr 13, 2026
999843b
emulator: bump cross-arch TCG -cpu to cortex-a76 (LSE for ClickHouse)
BilalG1 Apr 13, 2026
0896f14
ci: skip emulator boot/verify on arm64 (cross-arch TCG)
BilalG1 Apr 13, 2026
44e4079
emulator: add --no-wasm-tier-up to migration exec
BilalG1 Apr 13, 2026
b111ef2
Merge branch 'dev' into client-sdk-local-emulator-support
BilalG1 Apr 13, 2026
9ec08f4
emulator: dedupe probe list, factor log-stream and console-marker hel…
BilalG1 Apr 13, 2026
0ffd898
Merge branch 'emulator-arm64-ubicloud-jitless' into client-sdk-local-…
BilalG1 Apr 13, 2026
f8524e9
Merge branch 'dev' into emulator-arm64-ubicloud-jitless
BilalG1 Apr 14, 2026
9f26df0
Merge branch 'emulator-arm64-ubicloud-jitless' into client-sdk-local-…
BilalG1 Apr 14, 2026
9e38bc6
local emulator changes
BilalG1 Apr 14, 2026
9cf900a
Merge remote-tracking branch 'origin/dev' into client-sdk-local-emula…
BilalG1 Apr 14, 2026
9431105
Revert examples/middleware to origin/dev state
BilalG1 Apr 14, 2026
6e34776
Randomize CRON_SECRET in local emulator entrypoint
BilalG1 Apr 14, 2026
576a3cc
Use console.warn for emulator-already-running notice
BilalG1 Apr 14, 2026
383a036
Merge branch 'dev' into client-sdk-local-emulator-support
BilalG1 Apr 14, 2026
e30b9e0
Inject throwaway SEED keys into emulator smoke test
BilalG1 Apr 14, 2026
11567e8
emulator email-rendering, pck, and stripe fixes
BilalG1 Apr 14, 2026
73da966
Merge remote-tracking branch 'origin/dev' into client-sdk-local-emula…
BilalG1 Apr 14, 2026
04640d4
fix
BilalG1 Apr 14, 2026
4ae826d
fix copy emulator assets
BilalG1 Apr 14, 2026
0d98d55
emulator stripe fixes and gh action fix
BilalG1 Apr 14, 2026
224bc6b
Merge branch 'dev' into client-sdk-local-emulator-support
BilalG1 Apr 14, 2026
75aaf0c
emulator fix ai-chat, clickhouse queries, db sync
BilalG1 Apr 14, 2026
fa67984
Merge branch 'client-sdk-local-emulator-support' of https://github.co…
BilalG1 Apr 14, 2026
9552be8
Merge remote-tracking branch 'origin/dev' into client-sdk-local-emula…
BilalG1 Apr 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
emulator email-rendering, pck, and stripe fixes
  • Loading branch information
BilalG1 committed Apr 14, 2026
commit 11567e82b244f6e65f88c0a143edbee288913cf2
5 changes: 5 additions & 0 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ export async function seed() {
onboarding: {
requireEmailVerification: false,
},
...(localEmulatorEnabled ? {
project: {
requirePublishableClientKey: false,
},
} : {}),
dataVault: {
stores: {
'neon-connection-strings': {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Prisma } from "@/generated/prisma/client";
import { overrideBranchConfigOverride } from "@/lib/config";
import { overrideEnvironmentConfigOverride } from "@/lib/config";
import {
LOCAL_EMULATOR_ADMIN_USER_ID,
LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE,
Expand Down Expand Up @@ -100,6 +100,25 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom
},
});

const created = existingRow === undefined;

// Seed environment-level defaults BEFORE registering as a LocalEmulatorProject:
// once registered, setEnvironmentConfigOverride refuses to write.
// - domains.allowLocalhost: fresh emulator projects allow localhost redirects
// so developers don't hit "Redirect URL not whitelisted" before configuring
// trustedDomains.
// - payments.testMode: emulator payments always go through stripe-mock.
if (created) {
await overrideEnvironmentConfigOverride({
projectId,
branchId: DEFAULT_BRANCH_ID,
environmentConfigOverrideOverride: {
"domains.allowLocalhost": true,
"payments.testMode": true,
},
});
}

await globalPrismaClient.$executeRaw(Prisma.sql`
INSERT INTO "LocalEmulatorProject" ("absoluteFilePath", "projectId", "createdAt", "updatedAt")
VALUES (${absoluteFilePath}, ${projectId}, NOW(), NOW())
Expand All @@ -109,7 +128,7 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom
"updatedAt" = NOW()
`);

return { projectId, created: existingRow === undefined };
return { projectId, created };
}

async function getOrCreateCredentials(projectId: string) {
Expand Down Expand Up @@ -219,20 +238,7 @@ export const POST = createSmartRouteHandler({

await assertLocalEmulatorOwnerTeamReadiness();

const { projectId, created } = await getOrCreateLocalEmulatorProjectId(absoluteFilePath);
if (created) {
// Emulator projects are for local development. Default-allow localhost
// redirects so fresh projects don't hit "Redirect URL not whitelisted"
// before the developer has configured trustedDomains. User-supplied
// stack.config.ts still overrides this if they set domains explicitly.
await overrideBranchConfigOverride({
projectId,
branchId: DEFAULT_BRANCH_ID,
branchConfigOverrideOverride: {
"domains.allowLocalhost": true,
},
});
}
const { projectId } = await getOrCreateLocalEmulatorProjectId(absoluteFilePath);
const credentials = await getOrCreateCredentials(projectId);
const fileConfig = await readConfigFromFile(absoluteFilePath);

Expand Down
3 changes: 2 additions & 1 deletion apps/backend/src/lib/ai/models.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isLocalEmulatorEnabled } from "@/lib/local-emulator";
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";

Expand Down Expand Up @@ -59,7 +60,7 @@ export const ALLOWED_MODEL_IDS: ReadonlySet<string> = new Set([
]);

export function createOpenRouterProvider() {
const baseURL = getNodeEnvironment() === "development"
const baseURL = (getNodeEnvironment() === "development" || isLocalEmulatorEnabled())
? "http://localhost:8102/api/latest/integrations/ai-proxy/v1"
: "https://api.stack-auth.com/api/latest/integrations/ai-proxy/v1";
return createOpenRouter({
Expand Down
11 changes: 7 additions & 4 deletions apps/backend/src/lib/js-execution.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { traceSpan } from '@/utils/telemetry';
import { runAsynchronouslyAndWaitUntil } from '@/utils/background-tasks';
import { getEnvVariable, getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env';
import { isLocalEmulatorEnabled } from "@/lib/local-emulator";
import { StackAssertionError, captureError } from '@stackframe/stack-shared/dist/utils/errors';
import { Result } from '@stackframe/stack-shared/dist/utils/results';
import { Sandbox } from '@vercel/sandbox';
Expand All @@ -27,11 +28,13 @@ function createFreestyleEngine(): JsEngine {
let baseUrl = getEnvVariable("STACK_FREESTYLE_API_ENDPOINT", "") || undefined;

if (apiKey === "mock_stack_freestyle_key") {
if (!["development", "test"].includes(getNodeEnvironment())) {
if (!["development", "test"].includes(getNodeEnvironment()) && !isLocalEmulatorEnabled()) {
throw new StackAssertionError("Mock Freestyle key used in production; please set the STACK_FREESTYLE_API_KEY environment variable.");
}
const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
baseUrl = `http://localhost:${prefix}22`;
if (!baseUrl) {
const prefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
baseUrl = `http://localhost:${prefix}22`;
}
}

const freestyle = new FreestyleClient({
Expand Down Expand Up @@ -147,7 +150,7 @@ export async function executeJavascript(code: string, options: ExecuteJavascript

return await runWithFallback(code, options);
} else {
if (getNodeEnvironment().includes("prod")) {
if (getNodeEnvironment().includes("prod") && !isLocalEmulatorEnabled()) {
throw new StackAssertionError("STACK_VERCEL_SANDBOX_TOKEN is set to the disabled sentinel value in production. Please configure a real Vercel Sandbox token.");
}

Expand Down
5 changes: 1 addition & 4 deletions apps/backend/src/lib/payments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,16 @@ import type { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/use
import type { inlineProductSchema, productSchema, productSchemaWithMetadata } from "@stackframe/stack-shared/dist/schema-fields";
import { SUPPORTED_CURRENCIES } from "@stackframe/stack-shared/dist/utils/currency-constants";
import { FAR_FUTURE_DATE, addInterval, getIntervalsElapsed } from "@stackframe/stack-shared/dist/utils/dates";
import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env";
import { StackAssertionError, StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { filterUndefined, getOrUndefined, has, typedEntries, typedFromEntries, typedKeys, typedValues } from "@stackframe/stack-shared/dist/utils/objects";
import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings";
import { isUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import Stripe from "stripe";
import * as yup from "yup";
import { Tenancy } from "./tenancies";
import { getStripeForAccount } from "./stripe";
import { getStripeForAccount, useStripeMock } from "./stripe";

const DEFAULT_PRODUCT_START_DATE = new Date("1973-01-01T12:00:00.000Z"); // monday
const stripeSecretKey = getEnvVariable("STACK_STRIPE_SECRET_KEY", "");
const useStripeMock = stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment());

type Product = yup.InferType<typeof productSchema>;
type ProductWithMetadata = yup.InferType<typeof productSchemaWithMetadata>;
Expand Down
7 changes: 5 additions & 2 deletions apps/backend/src/lib/stripe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ import { getEnvVariable, getNodeEnvironment } from "@stackframe/stack-shared/dis
import { captureError, StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import Stripe from "stripe";
import type * as yup from "yup";
import { isLocalEmulatorEnabled } from "./local-emulator";
import { createStripeProxy, type StripeOverridesMap } from "./stripe-proxy";

const stripeSecretKey = getEnvVariable("STACK_STRIPE_SECRET_KEY", "");
const useStripeMock = stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment());
export const useStripeMock = isLocalEmulatorEnabled()
|| (stripeSecretKey === "sk_test_mockstripekey" && ["development", "test"].includes(getNodeEnvironment()));
const stackPortPrefix = getEnvVariable("NEXT_PUBLIC_STACK_PORT_PREFIX", "81");
const stripeMockPort = Number(getEnvVariable("STACK_STRIPE_MOCK_PORT", "") || `${stackPortPrefix}23`);
const stripeConfig: Stripe.StripeConfig = useStripeMock ? {
protocol: "http",
host: "localhost",
port: Number(`${stackPortPrefix}23`),
port: stripeMockPort,
} : {};

/** Product type as stored in Stripe metadata (same as config product schema) */
Expand Down
4 changes: 2 additions & 2 deletions docker/local-emulator/qemu/run-emulator.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=common.sh
source "$SCRIPT_DIR/common.sh"

IMAGE_DIR="$SCRIPT_DIR/images"
RUN_DIR="${EMULATOR_RUN_DIR:-$SCRIPT_DIR/run}"
IMAGE_DIR="${EMULATOR_IMAGE_DIR:-$HOME/.stack/emulator/images}"
RUN_DIR="${EMULATOR_RUN_DIR:-$HOME/.stack/emulator/run}"

VM_RAM="${EMULATOR_RAM:-4096}"
VM_CPUS="${EMULATOR_CPUS:-4}"
Expand Down
4 changes: 3 additions & 1 deletion docker/server/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ else
fi

export NEXT_PUBLIC_STACK_PROJECT_ID=internal
export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}
if [ "$NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR" != "true" ]; then
export NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=${STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY}
fi
if [ -n "${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY:-}" ]; then
export STACK_SECRET_SERVER_KEY=${STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY}
fi
Expand Down
2 changes: 1 addition & 1 deletion packages/stack-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"scripts": {
"clean": "rimraf node_modules && rimraf dist",
"build": "tsdown",
"build": "tsdown && node scripts/copy-emulator-assets.mjs",
"dev": "tsdown --watch",
"lint": "eslint --ext .tsx,.ts .",
"typecheck": "tsc --noEmit"
Expand Down
23 changes: 23 additions & 0 deletions packages/stack-cli/scripts/copy-emulator-assets.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env node
import { chmodSync, cpSync, mkdirSync } from "fs";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const packageRoot = resolve(__dirname, "..");
const qemuSrc = resolve(packageRoot, "../../docker/local-emulator/qemu");
const envSrc = resolve(packageRoot, "../../docker/local-emulator/.env.development");
const distDir = join(packageRoot, "dist");
const emulatorDist = join(distDir, "emulator");

mkdirSync(emulatorDist, { recursive: true });

for (const name of ["run-emulator.sh", "common.sh", "cloud-init"]) {
cpSync(join(qemuSrc, name), join(emulatorDist, name), { recursive: true });
}

chmodSync(join(emulatorDist, "run-emulator.sh"), 0o755);

cpSync(envSrc, join(distDir, ".env.development"));

console.log(`Copied emulator assets into ${emulatorDist} (+ .env.development into ${distDir}).`);
69 changes: 50 additions & 19 deletions packages/stack-cli/src/commands/emulator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Command } from "commander";
import { execFileSync, spawn } from "child_process";
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync } from "fs";
import { join, resolve } from "path";
import { homedir } from "os";
import { dirname, join, resolve } from "path";
import { fileURLToPath } from "url";
import { CliError } from "../lib/errors.js";

const DEFAULT_EMULATOR_BACKEND_PORT = 26701;
Expand All @@ -16,8 +18,20 @@ function emulatorBackendPort(): number {
return parsed;
}

function emulatorHome(): string {
return process.env.STACK_EMULATOR_HOME ?? join(homedir(), ".stack", "emulator");
}

function emulatorRunDir(): string {
return join(emulatorHome(), "run");
}

function emulatorImageDir(): string {
return join(emulatorHome(), "images");
}

function internalPckPath(): string {
return join(findQemuDir(), "run", "vm", "internal-pck");
return join(emulatorRunDir(), "vm", "internal-pck");
}

async function readInternalPck(timeoutMs = 60_000): Promise<string> {
Expand Down Expand Up @@ -79,39 +93,56 @@ function gh(args: string[]): string {
}
}

function findQemuDir(): string {
for (const rel of ["docker/local-emulator/qemu", "../docker/local-emulator/qemu"]) {
const dir = resolve(process.cwd(), rel);
if (existsSync(join(dir, "run-emulator.sh"))) return dir;
}
throw new CliError("Could not find QEMU emulator directory. Run this from the stack-auth repo root.");
function emulatorScriptsDir(): string {
const here = dirname(fileURLToPath(import.meta.url));
const bundled = join(here, "emulator");
if (existsSync(join(bundled, "run-emulator.sh"))) return bundled;
const repo = resolve(here, "../../../docker/local-emulator/qemu");
if (existsSync(join(repo, "run-emulator.sh"))) return repo;
throw new CliError("Emulator scripts not found in CLI bundle.");
}

function emulatorSpawnEnv(extra?: Record<string, string>): NodeJS.ProcessEnv {
return {
...process.env,
EMULATOR_RUN_DIR: emulatorRunDir(),
EMULATOR_IMAGE_DIR: emulatorImageDir(),
...extra,
};
}

function runEmulator(action: string, env?: Record<string, string>): Promise<void> {
const qemuDir = findQemuDir();
return new Promise((resolve, reject) => {
const child = spawn(join(qemuDir, "run-emulator.sh"), [action], {
const scriptsDir = emulatorScriptsDir();
mkdirSync(emulatorRunDir(), { recursive: true });
mkdirSync(emulatorImageDir(), { recursive: true });
return new Promise((resolvePromise, reject) => {
const child = spawn(join(scriptsDir, "run-emulator.sh"), [action], {
stdio: "inherit",
env: { ...process.env, ...env },
cwd: qemuDir,
env: emulatorSpawnEnv(env),
cwd: scriptsDir,
});
child.on("close", (code) => code === 0 ? resolve() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`)));
child.on("close", (code) => code === 0 ? resolvePromise() : reject(new CliError(`run-emulator.sh ${action} exited with code ${code}`)));
child.on("error", (err) => reject(new CliError(`Failed to run run-emulator.sh: ${err.message}`)));
});
}

function isEmulatorRunning(): boolean {
const qemuDir = findQemuDir();
const scriptsDir = emulatorScriptsDir();
try {
execFileSync(join(qemuDir, "run-emulator.sh"), ["status"], { stdio: "pipe", cwd: qemuDir });
execFileSync(join(scriptsDir, "run-emulator.sh"), ["status"], {
stdio: "pipe",
cwd: scriptsDir,
env: emulatorSpawnEnv(),
});
return true;
} catch {
return false;
}
}

async function startEmulator(arch: "arm64" | "amd64"): Promise<void> {
const img = join(findQemuDir(), "images", `stack-emulator-${arch}.qcow2`);
mkdirSync(emulatorImageDir(), { recursive: true });
const img = join(emulatorImageDir(), `stack-emulator-${arch}.qcow2`);
if (!existsSync(img)) {
console.log("No emulator image found. Pulling latest...");
pullRelease(arch);
Expand All @@ -130,7 +161,7 @@ function pullRelease(arch: "arm64" | "amd64", opts: { repo?: string; branch?: st
const branch = opts.branch ?? "dev";
const tag = opts.tag ?? `emulator-${branch}-latest`;
const asset = `stack-emulator-${arch}.qcow2`;
const imageDir = join(findQemuDir(), "images");
const imageDir = emulatorImageDir();
mkdirSync(imageDir, { recursive: true });
const dest = join(imageDir, asset);
const tmpDest = `${dest}.download`;
Expand Down Expand Up @@ -172,7 +203,7 @@ export function registerEmulatorCommand(program: Command) {
runId = String(runs[0].databaseId);
}

const imageDir = join(findQemuDir(), "images");
const imageDir = emulatorImageDir();
mkdirSync(imageDir, { recursive: true });
const dest = join(imageDir, `stack-emulator-${arch}.qcow2`);
if (existsSync(dest)) unlinkSync(dest);
Expand Down