Enhance local emulator setup with environment generation and configuration updates

- Introduced a new script to generate the local emulator environment file (`.env.development`) based on existing backend and dashboard configurations, ensuring consistency and ease of use.
- Updated `docker-compose.yaml` to reference the new environment file, improving configuration management for the local emulator.
- Modified emulator startup scripts to include environment generation, streamlining the process for developers.
- Added a new environment file for local development, containing necessary credentials and settings for the emulator.

These changes improve the local emulator's usability and configuration management, facilitating a smoother development experience.
This commit is contained in:
mantrakp04 2026-03-19 18:05:12 -07:00
parent d5d2a0916d
commit e5099471e4
6 changed files with 225 additions and 6 deletions

View File

@ -1,3 +1,7 @@
# Generated by docker/local-emulator/generate-env-development.mjs
# Do not edit manually; update apps/backend/.env.development, apps/dashboard/.env.development, or this generator.
# Public emulator/app credentials
NEXT_PUBLIC_STACK_DOCS_BASE_URL=https://docs.stack-auth.com
NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR=true
NEXT_PUBLIC_STACK_PROJECT_ID=internal
@ -5,6 +9,8 @@ NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-loca
STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
STACK_SERVER_SECRET=23-wuNpik0gIW4mruTz25rbIvhuuvZFrLOLtL7J4tyo
STACK_CHANGELOG_URL=https://raw.githubusercontent.com/stack-auth/stack-auth/refs/heads/dev/CHANGELOG.md
# Seed/project defaults
STACK_SEED_ENABLE_DUMMY_PROJECT=true
STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true
STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=true
@ -14,6 +20,8 @@ STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true
STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only
STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only
STACK_SEED_INTERNAL_PROJECT_SUPER_SECRET_ADMIN_KEY=this-super-secret-admin-key-is-for-local-development-only
# Third-party/test integrations
STACK_SVIX_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NTUxNDA2MzksImV4cCI6MTk3MDUwMDYzOSwibmJmIjoxNjU1MTQwNjM5LCJpc3MiOiJzdml4LXNlcnZlciIsInN1YiI6Im9yZ18yM3JiOFlkR3FNVDBxSXpwZ0d3ZFhmSGlyTXUifQ.En8w77ZJWbd0qrMlHHupHUB-4cx17RfzFykseg95SUk
STACK_OPENAI_API_KEY=mock_openai_api_key
STACK_OPENROUTER_API_KEY=FORWARD_TO_PRODUCTION
@ -27,6 +35,8 @@ STACK_DNSIMPLE_API_BASE_URL=https://api.dnsimple.com/v2
STACK_FREESTYLE_API_KEY=mock_stack_freestyle_key
STACK_VERCEL_SANDBOX_TOKEN=vercel_sandbox_disabled_for_local_development
CRON_SECRET=mock_cron_secret
# Storage, queueing, and analytics
STACK_S3_REGION=us-east-1
STACK_S3_ACCESS_KEY_ID=s3mockroot
STACK_S3_SECRET_ACCESS_KEY=s3mockroot
@ -41,6 +51,8 @@ STACK_QSTASH_NEXT_SIGNING_KEY=sig_5ZB6DVzB1wjE8S6rZ7eenA8Pdnhs
STACK_CLICKHOUSE_ADMIN_USER=stackframe
STACK_CLICKHOUSE_ADMIN_PASSWORD=PASSWORD-PLACEHOLDER--9gKyMxJeMx
STACK_CLICKHOUSE_EXTERNAL_PASSWORD=PASSWORD-PLACEHOLDER--EZeHscBMzE
# Email and dashboard integration
STACK_EMAIL_PORT=2500
STACK_EMAIL_SECURE=false
STACK_EMAIL_USERNAME=does-not-matter
@ -54,6 +66,8 @@ STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY=this-is-a-fake-key
STACK_EMAIL_MONITOR_SECRET_TOKEN=this-secret-token-is-for-local-development-only
STACK_EMAIL_MONITOR_USE_INBUCKET=true
STACK_FEATUREBASE_JWT_SECRET=secret-value
# Mock OAuth defaults
STACK_FORWARD_MOCK_OAUTH_SERVER=false
STACK_GITHUB_CLIENT_ID=MOCK
STACK_GITHUB_CLIENT_SECRET=MOCK

View File

@ -27,7 +27,7 @@ services:
- inbucket-data:/data/inbucket
- "${HOME}:${HOME}"
- "/tmp:/tmp"
env_file: ./base.env
env_file: ./.env.development
environment:
# Port-prefixed URLs — need shell interpolation, can't go in env_file
NEXT_PUBLIC_STACK_PORT_PREFIX: "${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}"

View File

@ -0,0 +1,203 @@
#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const rootDir = path.resolve(scriptDir, "..", "..");
const outputPath = path.join(scriptDir, ".env.development");
const backendEnvPath = path.join(rootDir, "apps", "backend", ".env.development");
const dashboardEnvPath = path.join(rootDir, "apps", "dashboard", ".env.development");
const args = process.argv.slice(2);
if (args.length > 1 || (args[0] != null && args[0] !== "--check")) {
throw new Error("Usage: node docker/local-emulator/generate-env-development.mjs [--check]");
}
const parseEnvFile = (filePath) => {
const env = new Map();
for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/)) {
const trimmedLine = rawLine.trim();
if (trimmedLine === "" || trimmedLine.startsWith("#")) {
continue;
}
const separatorIndex = rawLine.indexOf("=");
if (separatorIndex < 0) {
throw new Error(`Invalid env line in ${filePath}: ${rawLine}`);
}
const key = rawLine.slice(0, separatorIndex).trim();
const value = rawLine.slice(separatorIndex + 1);
env.set(key, value);
}
return env;
};
const backendEnv = parseEnvFile(backendEnvPath);
const dashboardEnv = parseEnvFile(dashboardEnvPath);
const getRequiredEnvValue = (sourceName, envMap, key) => {
const value = envMap.get(key);
if (value == null) {
throw new Error(`Missing ${key} in ${sourceName}; update the generator or source env file.`);
}
return value;
};
const fromSource = (sourceName, envMap, key) => ({
type: "entry",
key,
value: getRequiredEnvValue(sourceName, envMap, key),
});
const literal = (key, value) => ({
type: "entry",
key,
value,
});
const comment = (value) => ({
type: "comment",
value,
});
const blank = () => ({
type: "blank",
});
const entries = [
comment("# Generated by docker/local-emulator/generate-env-development.mjs"),
comment("# Do not edit manually; update apps/backend/.env.development, apps/dashboard/.env.development, or this generator."),
blank(),
comment("# Public emulator/app credentials"),
literal("NEXT_PUBLIC_STACK_DOCS_BASE_URL", "https://docs.stack-auth.com"),
literal("NEXT_PUBLIC_STACK_IS_LOCAL_EMULATOR", "true"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PROJECT_ID"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_SECRET_SERVER_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SERVER_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CHANGELOG_URL"),
blank(),
comment("# Seed/project defaults"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_ENABLE_DUMMY_PROJECT"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED"),
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"),
blank(),
comment("# Third-party/test integrations"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SVIX_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENAI_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_OPENROUTER_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_SECRET_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_STRIPE_WEBHOOK_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_RESEND_WEBHOOK_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_ACCOUNT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DNSIMPLE_API_BASE_URL"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_FREESTYLE_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_VERCEL_SANDBOX_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "CRON_SECRET"),
blank(),
comment("# Storage, queueing, and analytics"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_REGION"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_ACCESS_KEY_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_SECRET_ACCESS_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_BUCKET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_S3_PRIVATE_BUCKET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_REGION"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_ACCESS_KEY_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_AWS_SECRET_ACCESS_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_CURRENT_SIGNING_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_QSTASH_NEXT_SIGNING_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_USER"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_ADMIN_PASSWORD"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_CLICKHOUSE_EXTERNAL_PASSWORD"),
blank(),
comment("# Email and dashboard integration"),
literal("STACK_EMAIL_PORT", "2500"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SECURE"),
literal("STACK_EMAIL_USERNAME", "does-not-matter"),
literal("STACK_EMAIL_PASSWORD", "does-not-matter"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_SENDER"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_DEFAULT_EMAIL_CAPACITY_PER_HOUR"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PROJECT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_PUBLISHABLE_CLIENT_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_DOMAIN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_RESEND_EMAIL_API_KEY"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_SECRET_TOKEN"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_MONITOR_USE_INBUCKET"),
fromSource("apps/dashboard/.env.development", dashboardEnv, "STACK_FEATUREBASE_JWT_SECRET"),
blank(),
comment("# Mock OAuth defaults"),
literal("STACK_FORWARD_MOCK_OAUTH_SERVER", "false"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GITHUB_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_GOOGLE_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_MICROSOFT_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_ID"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_SPOTIFY_CLIENT_SECRET"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS"),
blank(),
comment("# Internal service endpoints (defaults for docker-compose; overridden in QEMU)"),
literal("STACK_DATABASE_CONNECTION_STRING", "postgres://postgres:PASSWORD-PLACEHOLDER--uqfEC1hmmv@127.0.0.1:5432/stackframe"),
fromSource("apps/backend/.env.development", backendEnv, "STACK_EMAIL_HOST"),
literal("STACK_SVIX_SERVER_URL", "http://127.0.0.1:8071"),
literal("STACK_S3_ENDPOINT", "http://127.0.0.1:9090"),
literal("STACK_QSTASH_URL", "http://127.0.0.1:8080"),
literal("STACK_CLICKHOUSE_URL", "http://127.0.0.1:8123"),
literal("STACK_CLICKHOUSE_DATABASE", "analytics"),
literal("STACK_EMAIL_MONITOR_INBUCKET_API_URL", "http://127.0.0.1:9001"),
literal("BACKEND_PORT", "8102"),
literal("DASHBOARD_PORT", "8101"),
];
const seenKeys = new Set();
for (const entry of entries) {
if (entry.type !== "entry") {
continue;
}
if (seenKeys.has(entry.key)) {
throw new Error(`Duplicate env key in generator: ${entry.key}`);
}
seenKeys.add(entry.key);
}
const content = `${entries.map((entry) => {
if (entry.type === "blank") {
return "";
}
if (entry.type === "comment") {
return entry.value;
}
return `${entry.key}=${entry.value}`;
}).join("\n")}\n`;
if (args[0] === "--check") {
const currentContent = fs.readFileSync(outputPath, "utf8");
if (currentContent !== content) {
throw new Error(`${path.relative(rootDir, outputPath)} is out of date. Run pnpm run generate-local-emulator-env.`);
}
console.log(`${path.relative(rootDir, outputPath)} is up to date.`);
} else {
fs.writeFileSync(outputPath, content);
console.log(`Wrote ${path.relative(rootDir, outputPath)}.`);
}

View File

@ -55,7 +55,7 @@ write_files:
set -a
source /mnt/stack-runtime/runtime.env
source /mnt/stack-runtime/base.env
source /mnt/stack-runtime/.env.development
set +a
# Container-local dependencies run on localhost. Host-only development
@ -67,7 +67,7 @@ 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/.env.development
cat /mnt/stack-runtime/runtime.env
# Computed vars — depend on port prefix or deps host

View File

@ -79,7 +79,7 @@ prepare_runtime_config_iso() {
{
printf "STACK_EMULATOR_PORT_PREFIX=%s\n" "$PORT_PREFIX"
} > "$cfg_dir/runtime.env"
cp "$SCRIPT_DIR/../base.env" "$cfg_dir/base.env"
cp "$SCRIPT_DIR/../.env.development" "$cfg_dir/.env.development"
make_iso_from_dir "$cfg_iso" "STACKCFG" "$cfg_dir"
}

View File

@ -24,14 +24,16 @@
"codegen": "pnpm pre && turbo run codegen && pnpm run generate-sdks",
"codegen:backend": "pnpm pre && turbo run codegen --filter=@stackframe/backend...",
"deps-compose": "docker compose -p stack-dependencies-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} -f docker/dependencies/docker.compose.yaml",
"generate-local-emulator-env": "node ./docker/local-emulator/generate-env-development.mjs",
"check-local-emulator-env": "node ./docker/local-emulator/generate-env-development.mjs --check",
"emulator-compose": "docker compose -p stack-local-emulator-${NEXT_PUBLIC_STACK_PORT_PREFIX:-81} -f docker/local-emulator/docker-compose.yaml",
"start-emulator": "pnpm pre && pnpm run emulator-compose up --detach --build && pnpm run wait-until-emulator-is-ready && echo \"\\nLocal emulator started. Dashboard: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 Backend: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02. 'pnpm run stop-emulator' to stop.\"",
"start-emulator": "pnpm pre && pnpm run generate-local-emulator-env && pnpm run emulator-compose up --detach --build && pnpm run wait-until-emulator-is-ready && echo \"\\nLocal emulator started. Dashboard: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01 Backend: http://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02. 'pnpm run stop-emulator' to stop.\"",
"stop-emulator": "pnpm run emulator-compose kill && pnpm run emulator-compose down -v",
"restart-emulator": "pnpm pre && pnpm run stop-emulator && pnpm run start-emulator",
"wait-until-emulator-is-ready": "pnpx wait-on http-get://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}02/health?db=1 http-get://localhost:${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}01/handler/sign-in",
"wait-until-postgres-is-ready:emulator": "until pg_isready -h localhost -p ${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}28 2>/dev/null; do sleep 1; done",
"emulator-qemu:build": "docker/local-emulator/qemu/build-image.sh",
"emulator-qemu:start": "docker/local-emulator/qemu/run-emulator.sh start",
"emulator-qemu:start": "pnpm run generate-local-emulator-env && docker/local-emulator/qemu/run-emulator.sh start",
"emulator-qemu:stop": "docker/local-emulator/qemu/run-emulator.sh stop",
"emulator-qemu:reset": "docker/local-emulator/qemu/run-emulator.sh reset",
"emulator-qemu:status": "docker/local-emulator/qemu/run-emulator.sh status",