YYayaw
Docs

Environment Variables

Runtime configuration variables used by the application.

This page lists the runtime variables consumed by the application. Use Deployment Environment Setup for the operational runbook that explains where to retrieve each value, how to scope hosted provider environments, and how to validate a deployment environment before promotion.

Core

NODE_ENV=development
NEXT_PUBLIC_BASE_URL=http://localhost:3080
DATABASE_URL=postgresql://user:password@localhost:5432/yayaw
DATABASE_POOL_MAX=
DATABASE_IDLE_TIMEOUT_SECONDS=
DATABASE_MAX_LIFETIME_SECONDS=
DATABASE_CONNECT_TIMEOUT_SECONDS=
DATABASE_STATEMENT_TIMEOUT_SECONDS=
DATABASE_PREPARE_STATEMENTS=
CMS_DASHBOARD_FULL_METRICS=false
BETTER_AUTH_SECRET=
BETTER_AUTH_TRUSTED_ORIGINS=http://localhost:3080
BETTER_AUTH_DEVICE_CLIENT_IDS=kyber-desktop,kyber-web
BETTER_AUTH_DEVICE_TRUSTED_ORIGINS=http://localhost:5080,tauri://localhost,http://tauri.localhost
  • The DATABASE_* pool knobs are optional. Runtime DB pools default to 10 connections in development and 3 in production/serverless. Production also defaults to 10 seconds connect timeout, 20 seconds idle timeout, 300 seconds max lifetime, 20 seconds statement timeout, and disabled prepared statements for pooler compatibility. Raise DATABASE_POOL_MAX or the lifecycle values only when the deployment target can support the extra per-runtime connections.
  • BETTER_AUTH_SECRET must be a stable, high-entropy server-only secret in production and self-hosted runtimes.
  • BETTER_AUTH_DEVICE_CLIENT_IDS is optional and controls the Better Auth Device Authorization client IDs allowed to request device codes. It defaults to kyber-desktop,kyber-web.
  • BETTER_AUTH_DEVICE_TRUSTED_ORIGINS is optional and adds origins that can call only the Device Authorization and bearer-token auth endpoints. Use it for local/native Kyber clients instead of broadening credentialed cookie origins.
  • DATABASE_STATEMENT_TIMEOUT_SECONDS applies a per-statement timeout when the database client opens a connection. Leave it empty unless the target Postgres provider or self-hosted database needs an explicit guardrail.
  • CMS_DASHBOARD_FULL_METRICS=true enables the full CMS publication and activity query set on /dashboard/content. Leave it false by default so the CMS dashboard uses fast indexed counters and never blocks the dashboard shell on expensive aggregate reads.

Build-Time Knobs

NEXT_BUILD_WORKERS=1
NEXT_STATIC_PAGE_GENERATION_TIMEOUT=180
  • NEXT_BUILD_WORKERS controls the number of Next.js workers used during next build. Self-hosted Docker builds default to 1 for stability on smaller hosts.
  • NEXT_STATIC_PAGE_GENERATION_TIMEOUT raises Next.js' static generation timeout for slower builders. Increase it only when legitimate static pages time out.

Production Host

Yayaw production uses https://yayaw.app as the canonical public origin. Preview deployments use https://preview.yayaw.app for the branch-backed preview domain.

NEXT_PUBLIC_BASE_URL=https://yayaw.app
BETTER_AUTH_TRUSTED_ORIGINS=https://*.yayaw.app,https://*.vercel.app
  • NEXT_PUBLIC_BASE_URL is the application source of truth for canonical URLs, Better Auth base URL, OAuth metadata, sitemap/robots links, and generated absolute asset URLs.
  • NEXT_PUBLIC_SITE_URL is not read by the application.
  • BETTER_AUTH_URL is not required by the current runtime because Better Auth receives baseURL from NEXT_PUBLIC_BASE_URL. If a legacy deployment still defines it, keep it aligned with https://yayaw.app.
  • preview.yayaw.app is the branch-backed preview domain and tracks the durable preview Git branch.
  • Keep www.yayaw.app as a Vercel project-domain redirect to yayaw.app; the app also normalizes www page requests before i18n routing.
  • Keep retired .eu hosts only as Vercel 308 redirects to their .app replacements, such as yayaw.eu and www.yayaw.eu to yayaw.app. Do not add retired hosts to BETTER_AUTH_TRUSTED_ORIGINS.
  • Organization public domains are separate from the canonical app host. They are verified through the configured public-domain provider and then mapped through organization_public_domains; they do not belong in BETTER_AUTH_TRUSTED_ORIGINS because dashboard/auth are not served from those hosts.

Billing and Stripe

STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
STRIPE_ONE_TIME_WEBHOOK_SECRET=
BILLING_GRACE_PERIOD_DAYS=7
BILLING_PRO_SEAT_LIMIT=10
BILLING_BUSINESS_SEAT_LIMIT=100
BILLING_CODE_ACCESS_REPOSITORY_URL=
BILLING_CODE_ACCESS_DOWNLOAD_URL=
BILLING_CODE_ACCESS_DOCUMENTATION_URL=
BILLING_CODE_ACCESS_SUPPORT_URL=
BILLING_CODE_ACCESS_GITHUB_REPOSITORY=
BILLING_CODE_ACCESS_GITHUB_APP_ID=
BILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID=
BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY=
BILLING_CODE_ACCESS_GITHUB_TOKEN=
  • STRIPE_WEBHOOK_SECRET is used by the Better Auth Stripe plugin webhook endpoint.
  • STRIPE_ONE_TIME_WEBHOOK_SECRET is used by the custom one-time webhook endpoint.
  • Billing product prices are managed from admin or MCP and synced to Stripe; resulting Stripe price IDs are stored internally in billing_products.
  • BILLING_CODE_ACCESS_* URLs are optional non-secret deliverable links shown on /dashboard/organization/code-access after an eligible purchase or active subscription. Leave a value empty when that deliverable requires manual provisioning.
  • GitHub repository access for paid code access is configured from /dashboard/admin/billing-settings. Non-secret values can also be supplied as environment fallbacks with BILLING_CODE_ACCESS_GITHUB_REPOSITORY, BILLING_CODE_ACCESS_GITHUB_APP_ID, and BILLING_CODE_ACCESS_GITHUB_APP_INSTALLATION_ID.
  • Keep GitHub secrets in environment variables only: BILLING_CODE_ACCESS_GITHUB_APP_PRIVATE_KEY for production GitHub App provisioning, or BILLING_CODE_ACCESS_GITHUB_TOKEN as an optional local/staging fallback.
  • See Deployment Environment Setup for the GitHub App creation, installation ID, private key, and token fallback steps.

Canonical Host and Auth Sessions

  • Set NEXT_PUBLIC_BASE_URL to the canonical production host.
  • Set preview NEXT_PUBLIC_BASE_URL to the branch-backed preview host when a stable preview domain is configured.
  • Do not mix www and non-www hosts for authenticated sessions.
  • Keep BETTER_AUTH_TRUSTED_ORIGINS for required preview/local hosts. The canonical host and its www variant are already derived from NEXT_PUBLIC_BASE_URL.
  • For locale-prefixed routes, prefer @/i18n/navigation (Link, useRouter, usePathname) instead of next/link and next/navigation in app navigation components.

OAuth (Optional)

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
FACEBOOK_CLIENT_ID=
FACEBOOK_CLIENT_SECRET=
RESEND_API_KEY=
EMAIL_SENDER=team@yayaw.app
EMAIL_SUPPORT=support@yayaw.app
EMAIL_USERNAME=Yayaw Team

EMAIL_SENDER is the verified Resend sender address used in From headers. EMAIL_SUPPORT is shown in templates and support copy. EMAIL_USERNAME is the display name paired with the sender address. These variables are bootstrap defaults and fallbacks; runtime values saved in Admin > Site Settings > Email take priority after setup.

Storage (Optional for media features)

Media storage uses an S3-compatible API such as MinIO, AWS S3, or R2. STORAGE_PROVIDER should remain s3.

STORAGE_PROVIDER=s3
STORAGE_MEDIA_BUCKET=media
STORAGE_MEDIA_STAGING_BUCKET=media-staging
STORAGE_CMS_FORM_BUCKET=cms-form-submissions
STORAGE_PUBLIC_BASE_URL=
CMS_MEDIA_STAGED_UPLOADS_ENABLED=false
STORAGE_TRANSFER_SIGNING_SECRET=
CMS_PERSONALIZATION_RATE_LIMIT_SECRET=
CMS_PERSONALIZATION_FORM_CONTEXT_SECRET=
CMS_PERSONALIZATION_CLIENT_IP_HEADER=x-real-ip
S3_ENDPOINT=
S3_SIGNED_PUBLIC_ENDPOINT=
S3_REGION=us-east-1
S3_ACCESS_KEY_ID=
S3_SECRET_ACCESS_KEY=
S3_FORCE_PATH_STYLE=true
  • STORAGE_MEDIA_BUCKET defaults to media.
  • CMS_MEDIA_STAGED_UPLOADS_ENABLED defaults to false. Enable it only after every application instance runs the staged-upload-compatible release.
  • STORAGE_MEDIA_STAGING_BUCKET selects the private bucket for signed client uploads. Development can fall back to STORAGE_MEDIA_BUCKET, but production staged uploads fail closed unless this value names a distinct bucket. Keep that bucket private and configure a provider lifecycle rule to remove abandoned staging objects after the upload-session retention window.
  • STORAGE_CMS_FORM_BUCKET selects the durable private bucket for customer attachments submitted through registered public CMS forms. It can fall back to STORAGE_MEDIA_STAGING_BUCKET, but it must never equal the public media bucket. Prefer a dedicated bucket. When both flows share one private bucket, scope staging lifecycle rules to their staging object prefix so they do not remove durable form attachments.
  • STORAGE_TRANSFER_SIGNING_SECRET optionally separates opaque, short-lived same-origin storage grants from other signatures. It falls back to CMS_PREVIEW_SIGNING_SECRET, then BETTER_AUTH_SECRET, and the effective production secret must contain at least 32 characters. The application encrypts and authenticates the bucket, object key, operation, MIME type, byte bound, and expiry; client URLs use NEXT_PUBLIC_BASE_URL and never expose S3_ENDPOINT. Keep private buckets non-anonymous. By default, staged uploads stream through the application with an exact 250 MB ceiling, conditional no-overwrite storage, and a deadline no later than the grant expiry.
  • S3_SIGNED_PUBLIC_ENDPOINT is an explicit opt-in public S3 API origin for direct signed uploads when the application host has a lower request-body limit than the 250 MB CMS media contract. It never falls back to S3_ENDPOINT; production accepts only a safe HTTPS hostname. The returned direct PUT signs If-None-Match: *, and clients must send every returned header. Its bucket CORS policy must allow the application origin, PUT, and the Content-Type, Content-Length, and If-None-Match request headers. Downloads, including personalization photos, always use the opaque application proxy. Leave this blank for Coolify and self-hosted deployments where the app can stream the request to the internal S3 endpoint. Personalization photo downloads use an audience-bound, durable one-use grant with a maximum two-minute TTL. The proxy atomically consumes the current grant before reading storage, and issuing another URL invalidates the prior one.
  • CMS_PERSONALIZATION_RATE_LIMIT_SECRET is an optional dedicated HMAC secret for the public personalization-request abuse bucket. It must contain at least 32 characters and falls back to CMS_PREVIEW_SIGNING_SECRET, then BETTER_AUTH_SECRET. The abuse bucket stores only the HMAC subject, never the raw client IP, and the customer request row is not linked to that subject.
  • CMS_PERSONALIZATION_FORM_CONTEXT_SECRET is required and must contain at least 32 characters. It signs the public runtime's short-lived form context, binding submissions to the registered handler and action, page identity, immutable revision, scope, organization, locale, path, and origin. Keep it distinct so it can be rotated independently.
  • CMS_PERSONALIZATION_CLIENT_IP_HEADER is required in production and names one proxy-owned header containing exactly one canonical client IP. Configure the ingress to remove any client-supplied value and set the trusted value. x-forwarded-for is rejected because its first hop is not an authenticated client identity. The bundled Caddy configurations overwrite x-real-ip, so that is the self-host default; choose the equivalent canonical header for another ingress only after verifying that it overwrites client input. The endpoint atomically enforces six submissions per trusted IP and 1,200 submissions globally per hour, then the worker prunes expired abuse buckets.
  • STORAGE_PUBLIC_BASE_URL is required for S3-compatible storage and must be a public URL that can serve public /<bucket>/<object-key> paths. It is never used for private staging or CMS form buckets.
  • If STORAGE_PUBLIC_BASE_URL uses the app or CDN origin, route each public bucket prefix to the object store before the app fallback. The built-in public prefixes are /media/* and /organization-logos/*.
  • S3-compatible storage requires endpoint, region, access key, secret key, and public base URL. Keep S3_FORCE_PATH_STYLE=true for MinIO and most local S3 compatible endpoints. S3_ENDPOINT may remain an internal Docker or private network hostname because browsers never receive it. A successfully promoted staged object remains as a private conditional-write tombstone through grant expiry and a 24-hour settlement window. The post-expiry reaper repeats deletion and records cleanup only after its final delete. This prevents a valid bearer grant or a late direct PUT from recreating durable staging after promotion, including across app instances.
  • bun run seed uploads default global site-variable assets into STORAGE_MEDIA_BUCKET when storage is configured. Without storage credentials, the seed keeps local public-asset fallbacks so local setup can still complete.
  • Existing media rows store absolute public URLs, so changing providers later requires either keeping old URLs reachable or running a deliberate media URL migration.

OpenAI (Optional for AI builder features)

OPENAI_API_KEY=
OPENAI_COMPONENTS_AI_FALLBACK=true
OPENAI_IMAGE_GENERATION_ENABLED=true
OPENAI_IMAGE_MODEL=gpt-image-1.5
PAGE_AI_QUEUE_DRIVER=direct
PAGE_AI_DEEP_REFINEMENT=false
PAGE_AI_WORKER_POLL_MS=1500
PAGE_AI_WORKER_MAINTENANCE_MS=60000
PAGE_AI_WORKER_ID=
  • OPENAI_COMPONENTS_AI_FALLBACK controls text/object AI fallbacks for component and page builder flows.
  • OPENAI_IMAGE_GENERATION_ENABLED controls page-builder image generation.
  • Runtime site settings can also disable these AI features through ai-components-enabled and media-image-generation-enabled without changing environment variables.
  • Generated page-builder images use the configured OpenAI image model, default to gpt-image-1.5, and are stored as webp media assets through the organization media library.
  • PAGE_AI_QUEUE_DRIVER controls the durable Page AI wake-up transport: direct for local development, vercel-queue for Vercel, and db-worker for a long-lived worker process. In production, the default is vercel-queue only when Vercel runtime variables are present; otherwise it is db-worker.
  • PAGE_AI_DEEP_REFINEMENT is an internal environment-only quality toggle and is not exposed as an admin site setting.
  • PAGE_AI_WORKER_POLL_MS, PAGE_AI_WORKER_MAINTENANCE_MS, and PAGE_AI_WORKER_ID are only used by bun run worker:page-ai when PAGE_AI_QUEUE_DRIVER=db-worker. The maintenance interval defaults to one minute and drives durable personalization retention, orphan-photo recovery, and expired abuse-bucket pruning on an independent non-overlapping timer, including while a Page AI run is long-lived or blocked. Personalization PUTs are aborted after two minutes; ambiguous uploads keep a PII-free cleanup tombstone until a second object delete runs after a three-minute settle window.

PostHog (Optional for analytics and flags)

NEXT_PUBLIC_ANALYTICS_PROVIDER=posthog
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybrid
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://eu.i.posthog.com
NEXT_PUBLIC_POSTHOG_ENABLE_LOCAL=false
POSTHOG_PERSONAL_API_KEY=
POSTHOG_PROJECT_ID=
POSTHOG_API_HOST=https://eu.posthog.com
POSTHOG_ORG_ID_PROPERTY=organization_id
POSTHOG_FLAG_LOOKUP_TIMEOUT_MS=

NEXT_PUBLIC_POSTHOG_* values are used for capture, feature flags, and client identification. POSTHOG_PERSONAL_API_KEY, POSTHOG_PROJECT_ID, and POSTHOG_API_HOST are private server-only values used by /dashboard analytics to query PostHog through the private Query API. POSTHOG_ORG_ID_PROPERTY defaults to organization_id and must match the event property registered for organization-scoped dashboard metrics. POSTHOG_FLAG_LOOKUP_TIMEOUT_MS optionally shortens or lengthens the server-side PostHog feature-flag lookup timeout; it defaults to 1500.

NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE controls where events are captured: hybrid keeps browser analytics for product behavior and sends server-side billing/auth events, server disables browser analytics scripts and tracks CMS page views plus conversions from the server, and client preserves legacy browser-only capture. Server-side billing events include conversion properties such as revenue, value, currency, plan, product_key, and Stripe IDs.

Umami (Optional Analytics Provider)

NEXT_PUBLIC_ANALYTICS_PROVIDER=umami
NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=hybrid
NEXT_PUBLIC_UMAMI_HOST_URL=
NEXT_PUBLIC_UMAMI_SCRIPT_URL=
NEXT_PUBLIC_UMAMI_WEBSITE_ID=
NEXT_PUBLIC_UMAMI_DOMAINS=
NEXT_PUBLIC_UMAMI_AUTO_TRACK=true
UMAMI_API_URL=
UMAMI_WEBSITE_ID=
UMAMI_API_TOKEN=
UMAMI_API_KEY=
UMAMI_USERNAME=
UMAMI_PASSWORD=
UMAMI_CMS_EVENT_PAGE_SIZE=1000

NEXT_PUBLIC_UMAMI_* values configure the browser tracking script and are baked into the client bundle at build time. UMAMI_API_URL, UMAMI_WEBSITE_ID, and either UMAMI_API_TOKEN (or the legacy alias UMAMI_API_KEY) or UMAMI_USERNAME plus UMAMI_PASSWORD are server-only values used by dashboard analytics data providers.

For a no-browser-analytics setup, set NEXT_PUBLIC_ANALYTICS_CAPTURE_MODE=server. The app will not render the Umami script, and server events are sent directly to Umami /api/send with a server User-Agent. This improves privacy and avoids client cookies, but session, unique visitor, device, and referrer quality is less precise than browser capture.

Control Plane MCP (Optional)

YAYAW_MCP_API_KEY=
YAYAW_MCP_LOCAL_USER_ID=
CMS_PREVIEW_SIGNING_SECRET=
CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=false
CMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false
CMS_PROTOTYPE_ROLLOUT_OPERATION_ID=
CMS_LEGACY_PAGE_DESIGN_ADMIN_BYPASS_ENABLED=false
CMS_LOW_LEVEL_PAGE_ADMIN_BYPASS_ENABLED=false
  • YAYAW_MCP_API_KEY is used by the local stdio MCP launcher when you want local development to verify a real Better Auth API key.
  • YAYAW_MCP_LOCAL_USER_ID is only used by the local stdio launcher when no API key is provided.
  • Production MCP clients connect to /api/mcp with Authorization: Bearer <Yayaw API key or OAuth access token> and should store secrets outside the repository.
  • OAuth MCP metadata is derived from NEXT_PUBLIC_BASE_URL; production ChatGPT/App clients need that value to be the public HTTPS origin so issuer, resource, JWKS, and redirect metadata are stable.
  • CMS_PREVIEW_SIGNING_SECRET optionally separates short-lived immutable CMS draft preview signatures from the authentication secret. Use a stable high-entropy server-only value in production; the runtime falls back to BETTER_AUTH_SECRET when it is empty.
  • Production prototype writers use three explicit states. Reader-first uses CMS_LEGACY_PROTOTYPE_WRITES_ENABLED=true and CMS_PROTOTYPE_RUNTIME_V2_WRITES_ENABLED=false. The deployment freeze and every later rolling renderer deployment use both values as false. Exact runtime activation uses legacy false and v2 true, after private staged uploads are verified. Both true fails closed. Never re-enable legacy writes after the first v2 activation.
  • CMS_PROTOTYPE_ROLLOUT_OPERATION_ID is managed by the trusted manual rollout. It is empty in reader/freeze and identifies the activating, eventually consumed durable receipt in exact mode. Do not set or rotate it independently of CMS Prototype Runtime Rollout.
  • CMS_LEGACY_PAGE_DESIGN_ADMIN_BYPASS_ENABLED defaults to false. Set it to true only for a time-bounded migration or incident response by a client with control-plane:admin; normal page design must use the prepared contextual workflow.
  • CMS_LOW_LEVEL_PAGE_ADMIN_BYPASS_ENABLED defaults to false. Enable it only for a time-bounded, audited maintenance operation that must call yayaw_pages_create_draft or yayaw_pages_save_draft; normal creation and rework use the contextual design tools.

Dynamic Data Runtime Transforms (Optional)

DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET=
  • DYNAMIC_DATA_RUNTIME_TRANSFORM_SECRET is required only when a deployed dynamic runtime route declares an hmac_sha256 value transform. Generate a stable high-entropy server-only value and rotate it deliberately, because existing transformed lookup values depend on it.

Maintenance Mode (Optional)

MAINTENANCE_MODE=false
MAINTENANCE_MODE_END_DATE=

Deployment Runtime

DEPLOYMENT_PROVIDER=
DEPLOYMENT_URL=
DEPLOYMENT_ENV=
DEPLOYMENT_GIT_COMMIT_SHA=
DEPLOYMENT_GIT_COMMIT_REF=
PUBLIC_DOMAIN_PROVIDER=
APP_MANAGED_HOSTS=
RESERVED_PUBLIC_DOMAIN_SUFFIXES=
PUBLIC_DOMAIN_CNAME_TARGET=
PUBLIC_DOMAIN_IPV4_TARGETS=
PUBLIC_DOMAIN_TXT_PREFIX=_yayaw
VERCEL_URL=
VERCEL_TOKEN=
VERCEL_PROJECT_ID=
VERCEL_TEAM_ID=
  • DEPLOYMENT_PROVIDER can be vercel, static, or local. Leave it empty to auto-detect Vercel from VERCEL/VERCEL_URL, static deployments from DEPLOYMENT_URL, and local otherwise.
  • DEPLOYMENT_URL, DEPLOYMENT_ENV, DEPLOYMENT_GIT_COMMIT_SHA, and DEPLOYMENT_GIT_COMMIT_REF provide dashboard/control-plane deployment metadata for Docker or other non-Vercel runtimes.
  • PUBLIC_DOMAIN_PROVIDER can be vercel or manual-dns. When unset, Vercel is selected only if VERCEL_PROJECT_ID and VERCEL_TOKEN are configured; otherwise manual DNS verification is used.
  • APP_MANAGED_HOSTS adds comma-separated app-owned hosts that organization public domains may not claim.
  • RESERVED_PUBLIC_DOMAIN_SUFFIXES adds suffixes, such as .preview.example, that custom public domains may not claim. .vercel.app is always reserved.
  • PUBLIC_DOMAIN_CNAME_TARGET, PUBLIC_DOMAIN_IPV4_TARGETS, and PUBLIC_DOMAIN_TXT_PREFIX drive the manual DNS provider's hints and ownership TXT challenge.
  • VERCEL_URL is supplied by Vercel and lets the app recognize deployment hosts as managed app hosts.
  • VERCEL_TOKEN, VERCEL_PROJECT_ID, and optional VERCEL_TEAM_ID are server-only values used to add, inspect, and verify organization public domains through the Vercel project-domain API.

Source of Truth

Keep .env.example in sync with real usage in source code when adding or removing variables. Document retrieval steps in Deployment Environment Setup whenever a deployment operator needs to collect the value from an external provider.

Local Tooling Troubleshooting

Some local machines may hit an esbuild service hang when running Drizzle or docs generators.

If a command appears blocked:

  1. Use the safe scripts with timeouts:
bun run docs:generate
bun run db:generate
bun run db:push
  1. If it still fails, reinstall dependencies:
rm -rf node_modules
bun install
  1. Retry the command and verify with:
bun run check
bun run build