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
local emulator changes
  • Loading branch information
BilalG1 committed Apr 14, 2026
commit 9e38bc621103e9197086d4d78a91fdecac2a6e16
61 changes: 39 additions & 22 deletions apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,45 @@ export async function seed() {
console.log('Internal team created');
}

// Upsert the internal API key set before any flake-prone work (dummy-project
// seed, email/svix, clickhouse). The emulator CLI authenticates against the
// internal project using the pck stored here, so it must land before the rest
// of the seed even if something later fails.
const isLocalEmulator = process.env.NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR === 'true';
const rawPck = process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY;
if (isLocalEmulator && !rawPck) {
// Emulator images build before a per-VM pck is available. Runtime boots set
// STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY from the VM-generated
// random value and re-run the seed, which upserts the internal key set then.
console.log('Skipping internal API key set (no pck provided; emulator mode).');
} else {
const keySet = {
publishableClientKey: rawPck || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
secretServerKey: isLocalEmulator
? (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY ?? null)
: (process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set')),
superSecretAdminKey: isLocalEmulator
? (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY ?? null)
: (process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set')),
};

await globalPrismaClient.apiKeySet.upsert({
where: { projectId_id: { projectId: 'internal', id: apiKeyId } },
update: {
...keySet,
},
create: {
id: apiKeyId,
projectId: 'internal',
description: "Internal API key set",
expiresAt: new Date('2099-12-31T23:59:59Z'),
...keySet,
}
});

console.log('Updated internal API key set');
}

const shouldSeedDummyProject = process.env.STACK_SEED_ENABLE_DUMMY_PROJECT === 'true';
if (shouldSeedDummyProject) {
await seedDummyProject({
Expand All @@ -268,28 +307,6 @@ export async function seed() {
});
}

const keySet = {
publishableClientKey: process.env.STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is not set'),
secretServerKey: process.env.STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY is not set'),
superSecretAdminKey: process.env.STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY || throwErr('STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY is not set'),
};

await globalPrismaClient.apiKeySet.upsert({
where: { projectId_id: { projectId: 'internal', id: apiKeyId } },
update: {
...keySet,
},
create: {
id: apiKeyId,
projectId: 'internal',
description: "Internal API key set",
expiresAt: new Date('2099-12-31T23:59:59Z'),
...keySet,
}
});

console.log('Updated internal API key set');

// Create optional default admin user if credentials are provided.
// This user will be able to login to the dashboard with both email/password and magic link.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Prisma } from "@/generated/prisma/client";
import { overrideBranchConfigOverride } from "@/lib/config";
import {
LOCAL_EMULATOR_ADMIN_USER_ID,
LOCAL_EMULATOR_ONLY_ENDPOINT_MESSAGE,
Expand Down Expand Up @@ -58,14 +59,15 @@ async function assertLocalEmulatorOwnerTeamReadiness() {
}
}

async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<string> {
async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Promise<{ projectId: string, created: boolean }> {
const existingRows = await globalPrismaClient.$queryRaw<LocalEmulatorProjectMappingRow[]>(Prisma.sql`
SELECT "projectId"
FROM "LocalEmulatorProject"
WHERE "absoluteFilePath" = ${absoluteFilePath}
LIMIT 1
`);
const projectId = existingRows[0] ? existingRows[0].projectId : generateUuid();
const existingRow = existingRows.length > 0 ? existingRows[0] : undefined;
const projectId = existingRow ? existingRow.projectId : generateUuid();

await globalPrismaClient.project.upsert({
where: {
Expand Down Expand Up @@ -107,7 +109,7 @@ async function getOrCreateLocalEmulatorProjectId(absoluteFilePath: string): Prom
"updatedAt" = NOW()
`);

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

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

await assertLocalEmulatorOwnerTeamReadiness();

const projectId = await getOrCreateLocalEmulatorProjectId(absoluteFilePath);
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 credentials = await getOrCreateCredentials(projectId);
const fileConfig = await readConfigFromFile(absoluteFilePath);

Expand Down
4 changes: 0 additions & 4 deletions apps/backend/src/lib/local-emulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ import fs from "fs/promises";
import { createJiti } from "jiti";
import path from "path";

export const LOCAL_EMULATOR_INTERNAL_PUBLISHABLE_CLIENT_KEY = "local-emulator-publishable-client-key";
export const LOCAL_EMULATOR_INTERNAL_SECRET_SERVER_KEY = "local-emulator-secret-server-key";
export const LOCAL_EMULATOR_INTERNAL_SUPER_SECRET_ADMIN_KEY = "local-emulator-super-secret-admin-key";

export const LOCAL_EMULATOR_ADMIN_USER_ID = "63abbc96-5329-454a-ba56-e0460173c6c1";
export const LOCAL_EMULATOR_OWNER_TEAM_ID = "5a0c858b-d9e9-49d4-9943-8ce385d86428";
export const LOCAL_EMULATOR_ADMIN_EMAIL = "local-emulator@stack-auth.com";
Expand Down
58 changes: 55 additions & 3 deletions docker/local-emulator/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ COPY docs ./docs

# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV NEXT_CONFIG_OUTPUT=standalone
ENV NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator

# Build the backend NextJS app
RUN pnpm turbo run docker-build --filter=@stackframe/backend... --filter=@stackframe/dashboard...
Expand Down Expand Up @@ -89,8 +90,47 @@ RUN cp -a /app/node_modules /pruned-node_modules && \
date-fns@2* date-fns@3*


# ── Freestyle mock build ─────────────────────────────────────────────────────

FROM node-base AS freestyle-mock-builder
WORKDIR /freestyle-mock
COPY docker/dependencies/freestyle-mock/Dockerfile /tmp/freestyle-mock-dockerfile
# Extract the inline package.json and server.mjs from the Dockerfile's RUN cat commands,
# then install dependencies. This avoids duplicating the source.
RUN node -e " \
const fs = require('fs'); \
const df = fs.readFileSync('/tmp/freestyle-mock-dockerfile', 'utf8'); \
const pkgMatch = df.match(/cat <<'EOF' > package\\.json\\n([\\s\\S]*?)\\nEOF/); \
fs.writeFileSync('package.json', pkgMatch[1]); \
const srvMatch = df.match(/cat <<'EOF' > server\\.mjs\\n([\\s\\S]*?)\\nEOF/); \
let server = srvMatch[1]; \
server = server.replace('server.listen(8080)', 'server.listen(process.env.PORT || 8080)'); \
server = server.replace( \
'from \"fs/promises\"', \
'from \"fs/promises\"; import { symlinkSync } from \"fs\"' \
); \
server = server.replace( \
'await mkdir(workDir, { recursive: true });', \
'await mkdir(workDir, { recursive: true }); try { symlinkSync(\"/app/freestyle-mock/node_modules\", join(workDir, \"node_modules\")); } catch {}' \
); \
fs.writeFileSync('server.mjs', server); \
"
RUN npm install
Comment on lines +93 to +116
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail fast if the extracted freestyle sources stop matching.

This stage assumes both regexes and both exact replace() calls succeed. If docker/dependencies/freestyle-mock/Dockerfile changes formatting or the server.listen(...) text, the build still succeeds but ships a server that keeps port 8080 or misses the node_modules symlink. Please assert each match/rewrite before writing the files.

Suggested hardening
 RUN node -e " \
   const fs = require('fs'); \
   const df = fs.readFileSync('/tmp/freestyle-mock-dockerfile', 'utf8'); \
   const pkgMatch = df.match(/cat <<'EOF' > package\\.json\\n([\\s\\S]*?)\\nEOF/); \
+  if (!pkgMatch) throw new Error('Could not extract package.json from freestyle mock Dockerfile'); \
   fs.writeFileSync('package.json', pkgMatch[1]); \
   const srvMatch = df.match(/cat <<'EOF' > server\\.mjs\\n([\\s\\S]*?)\\nEOF/); \
+  if (!srvMatch) throw new Error('Could not extract server.mjs from freestyle mock Dockerfile'); \
   let server = srvMatch[1]; \
-  server = server.replace('server.listen(8080)', 'server.listen(process.env.PORT || 8080)'); \
+  const originalServer = server; \
+  server = server.replace('server.listen(8080)', 'server.listen(process.env.PORT || 8080)'); \
   server = server.replace( \
     'from \"fs/promises\"', \
     'from \"fs/promises\"; import { symlinkSync } from \"fs\"' \
   ); \
   server = server.replace( \
     'await mkdir(workDir, { recursive: true });', \
     'await mkdir(workDir, { recursive: true }); try { symlinkSync(\"/app/freestyle-mock/node_modules\", join(workDir, \"node_modules\")); } catch {}' \
   ); \
+  if (server === originalServer) throw new Error('Freestyle mock patch step did not modify server.mjs'); \
   fs.writeFileSync('server.mjs', server); \
 "
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docker/local-emulator/Dockerfile` around lines 95 - 118, The inline Node
script that extracts package.json and server.mjs must fail-fast when patterns or
replacements don't match: after running the regexes (pkgMatch and srvMatch)
assert they are non-null and, if null, console.error a clear message and
process.exit(1); after performing the three replacements (the
'server.listen(8080)' -> 'server.listen(process.env.PORT || 8080)', the 'from
"fs/promises"' import augmentation, and the 'await mkdir(workDir, { recursive:
true });' symlink insertion) verify the original substrings no longer exist and
the expected snippets were inserted, and if any verification fails console.error
and process.exit(1) before writing server.mjs or package.json so the build fails
loudly rather than producing a broken image.



# ── Mock OAuth server build ───────────────────────────────────────────────────

FROM node-base AS mock-oauth-builder
WORKDIR /mock-oauth
COPY apps/mock-oauth-server/package.json .
RUN pnpm install && pnpm add esbuild --save-dev
COPY apps/mock-oauth-server/src ./src
RUN npx esbuild src/index.ts --bundle --platform=node --target=node22 --outfile=dist/index.cjs


# ── Service binary stages ─────────────────────────────────────────────────────

FROM stripe/stripe-mock:v0.195.0 AS stripe-mock-bin
FROM inbucket/inbucket:3.1.0 AS inbucket-bin
FROM svix/svix-server:v1.88.0 AS svix-bin
FROM clickhouse/clickhouse-server:25.10 AS clickhouse-bin
Expand Down Expand Up @@ -161,6 +201,9 @@ COPY --from=node-base /usr/local/bin/node /usr/local/bin/node
# Inbucket
COPY --from=inbucket-bin /opt/inbucket /opt/inbucket

# Stripe mock
COPY --from=stripe-mock-bin /bin/stripe-mock /usr/local/bin/stripe-mock

# Svix (UPX-compressed)
COPY --from=upx-compress /out/svix-server /usr/local/bin/svix-server

Expand Down Expand Up @@ -193,6 +236,14 @@ RUN cp -a /app/node_modules /app/node_modules.standalone 2>/dev/null || mkdir -p
COPY --from=migration-pruner /pruned-node_modules ./node_modules
COPY --from=builder /app/packages ./packages

# Mock OAuth server (bundled single file)
COPY --from=mock-oauth-builder /mock-oauth/dist/index.cjs /app/mock-oauth-server/index.cjs

# Freestyle mock (JS execution for email rendering)
COPY --from=freestyle-mock-builder /freestyle-mock /app/freestyle-mock
COPY --from=node-base /usr/local/bin/npm /usr/local/bin/npm
COPY --from=node-base /usr/local/lib/node_modules/npm /usr/local/lib/node_modules/npm

RUN mkdir -p \
/data/postgres \
/data/redis \
Expand All @@ -209,17 +260,18 @@ RUN mkdir -p \
&& chown -R postgres:postgres /data/postgres

COPY docker/local-emulator/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
COPY docker/local-emulator/run-cron-jobs.sh /run-cron-jobs.sh
COPY docker/local-emulator/entrypoint.sh /entrypoint.sh
COPY docker/local-emulator/init-services.sh /init-services.sh
COPY docker/local-emulator/start-app.sh /start-app.sh
COPY docker/local-emulator/clickhouse-config.xml /etc/clickhouse-server/config.xml
COPY docker/local-emulator/clickhouse-users.xml /etc/clickhouse-server/users.xml
COPY docker/server/entrypoint.sh /app-entrypoint.sh
RUN chmod +x /entrypoint.sh /init-services.sh /start-app.sh /app-entrypoint.sh
RUN chmod +x /entrypoint.sh /init-services.sh /start-app.sh /app-entrypoint.sh /run-cron-jobs.sh

# PostgreSQL: 5432, Redis: 6379, Inbucket: 2500/9001/1100,
# Svix: 8071, ClickHouse: 8123/9009, MinIO: 9090, QStash: 8080
# Backend: 8102, Dashboard: 8101
EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102
# Backend: 8102, Dashboard: 8101, Mock OAuth: 8114
EXPOSE 5432 6379 2500 9001 1100 8071 8123 9009 9090 8080 8101 8102 8114

ENTRYPOINT ["/entrypoint.sh"]
8 changes: 5 additions & 3 deletions docker/local-emulator/generate-env-development.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,11 @@ const entries = [
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY"),
// STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY is generated per-VM at boot
// by docker/local-emulator/qemu/cloud-init/emulator/user-data and injected via
// /run/stack-auth/local-emulator.env. SECRET_SERVER_KEY and SUPER_SECRET_ADMIN_KEY
// are intentionally omitted so the seed script leaves them null on the internal
// project; per-project credentials come from /api/v1/internal/local-emulator/project.
blank(),
comment("# Third-party/test integrations"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SVIX_API_KEY"),
Expand Down
88 changes: 73 additions & 15 deletions docker/local-emulator/qemu/cloud-init/emulator/user-data
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ write_files:
#!/bin/bash
set -euo pipefail

mkdir -p /mnt/stack-runtime /run/stack-auth
mkdir -p /mnt/stack-runtime /run/stack-auth /var/lib/stack-auth
runtime_device="$(readlink -f /dev/disk/by-label/STACKCFG)"
mountpoint -q /mnt/stack-runtime || mount -o ro "$runtime_device" /mnt/stack-runtime

Expand All @@ -67,6 +67,24 @@ write_files:
source /mnt/stack-runtime/base.env
set +a

# Generate and persist the internal-project keys on first boot; reuse
# across container restarts so the dashboard keeps its internal-project
# session. Reset via `stack emulator reset`.
#
# pck: used by stack-cli to auth against /api/v1/internal/local-emulator/project
# ssk/sak: required by the emulator's own dashboard (StackServerApp
# construction throws without them). Not used by user-app flows; the
# /local-emulator/project route mints separate per-project credentials.
umask 077
for key in internal-pck internal-ssk internal-sak; do
if [ ! -s "/var/lib/stack-auth/$key" ]; then
openssl rand -hex 32 > "/var/lib/stack-auth/$key"
fi
done
INTERNAL_PCK="$(cat /var/lib/stack-auth/internal-pck)"
INTERNAL_SSK="$(cat /var/lib/stack-auth/internal-ssk)"
INTERNAL_SAK="$(cat /var/lib/stack-auth/internal-sak)"

# Container-local dependencies run on localhost. Host-only development
# services (such as the OAuth mock server) are reachable via the QEMU
# user-network host alias.
Expand All @@ -78,6 +96,9 @@ write_files:
# Static vars from base config and runtime (e.g. API keys, feature flags)
cat /mnt/stack-runtime/base.env
cat /mnt/stack-runtime/runtime.env
printf 'STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=%s\n' "$INTERNAL_PCK"
printf 'STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=%s\n' "$INTERNAL_SSK"
printf 'STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=%s\n' "$INTERNAL_SAK"

# Computed vars — depend on port prefix or deps host
# Host-side ports (for browser URLs — browser runs on host, not in VM)
Expand Down Expand Up @@ -108,7 +129,10 @@ write_files:
STACK_CLICKHOUSE_URL=http://${DEPS_HOST}:8123
STACK_EMAIL_MONITOR_VERIFICATION_CALLBACK_URL=http://localhost:${HP_DASHBOARD}/handler/email-verification
STACK_EMAIL_MONITOR_INBUCKET_API_URL=http://${DEPS_HOST}:9001
STACK_OAUTH_MOCK_URL=http://${HOST_SERVICES_HOST}:${P}14
STACK_OAUTH_MOCK_URL=http://localhost:${P}14
STACK_FREESTYLE_API_ENDPOINT=http://${DEPS_HOST}:8180
STACK_STRIPE_MOCK_PORT=12111
NEXT_PUBLIC_STACK_STRIPE_PUBLISHABLE_KEY=pk_test_mock_publishable_key_for_local_emulator
BACKEND_PORT=${P}02
DASHBOARD_PORT=${P}01
COMPUTED
Expand All @@ -135,20 +159,54 @@ write_files:

/usr/local/bin/mount-host-fs
/usr/local/bin/render-stack-env

# Publish the internal publishable client key to the host via 9p so the
# stack-cli can authenticate its bootstrap call to
# /api/v1/internal/local-emulator/project.
set -a
source /mnt/stack-runtime/runtime.env
set +a
if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ] && [ -s /var/lib/stack-auth/internal-pck ]; then
install -m 0600 /var/lib/stack-auth/internal-pck \
"/host${STACK_EMULATOR_VM_DIR_HOST}/internal-pck"
fi

docker rm -f stack >/dev/null 2>&1 || true
exec docker run \
--rm \
--name stack \
--network host \
--add-host host.docker.internal:host-gateway \
--env-file /run/stack-auth/local-emulator.env \
-v stack-postgres-data:/data/postgres \
-v stack-redis-data:/data/redis \
-v stack-clickhouse-data:/data/clickhouse \
-v stack-minio-data:/data/minio \
-v stack-inbucket-data:/data/inbucket \
-v /host:/host \
stack-local-emulator

# Mirror container stdout/stderr to a host-visible log for debugging.
# The container already bind-mounts /host:/host, so we reuse that path.
# Falls back to stdout (captured by systemd-journald) when no host log is set.
if [ -n "${STACK_EMULATOR_VM_DIR_HOST:-}" ]; then
host_log="/host${STACK_EMULATOR_VM_DIR_HOST}/stack.log"
: > "$host_log" 2>/dev/null || true
exec docker run \
--rm \
--name stack \
--network host \
--add-host host.docker.internal:host-gateway \
--env-file /run/stack-auth/local-emulator.env \
-v stack-postgres-data:/data/postgres \
-v stack-redis-data:/data/redis \
-v stack-clickhouse-data:/data/clickhouse \
-v stack-minio-data:/data/minio \
-v stack-inbucket-data:/data/inbucket \
-v /host:/host \
stack-local-emulator 2>&1 | tee -a "$host_log"
else
exec docker run \
--rm \
--name stack \
--network host \
--add-host host.docker.internal:host-gateway \
--env-file /run/stack-auth/local-emulator.env \
-v stack-postgres-data:/data/postgres \
-v stack-redis-data:/data/redis \
-v stack-clickhouse-data:/data/clickhouse \
-v stack-minio-data:/data/minio \
-v stack-inbucket-data:/data/inbucket \
-v /host:/host \
stack-local-emulator
fi

- path: /usr/local/bin/wait-for-deps
permissions: '0755'
Expand Down
Loading