**Tags:** "Hello World" Examples · Frameworks Examples · React

# Wasp Hello World

A full-stack [Wasp](https://wasp.sh/) application — React client, Node.js API, and [PostgreSQL](https://www.postgresql.org) — deployed on [Zerops](https://zerops.io). The client ships as static Nginx assets; the API runs on Node.js 24 with Prisma migrations.

### Available Environments

- **AI Agent** ← current
- [Remote (CDE)](https://app.zerops.io/recipes/wasp-hello-world.md?environment=remote-cde)
- [Local](https://app.zerops.io/recipes/wasp-hello-world.md?environment=local)
- [Stage](https://app.zerops.io/recipes/wasp-hello-world.md?environment=stage)
- [Small Production](https://app.zerops.io/recipes/wasp-hello-world.md?environment=small-production)
- [Highly-available Production](https://app.zerops.io/recipes/wasp-hello-world.md?environment=highly-available-production)

### Services in this Environment

**Services:**

- **core** (core:single@2)
  - Containers: 1 × Shared Core, 0.00 GB RAM, 0 GB Disk
- **apidev** (ubuntu/nodejs@24) :3000, :3001
  - Containers: 1 × Shared Core, 1.06 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- **apistage** (ubuntu/nodejs@24) :3001
  - Containers: 1 × Shared Core, 0.75 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- **appdev** (ubuntu/nodejs@24) :3001, :3000
  - Containers: 1 × Shared Core, 1.06 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- **appstage** (alpine/static@1.0)
  - Containers: 1 × Shared Core, 0.38 GB RAM, 1 GB Disk
  - Repository: [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- **db** (postgresql:single@16) :5432, :6432
  - Containers: 1 × Shared Core, 0.38 GB RAM, 1 GB Disk

**Total Resources:** 6 containers, 3.62 GB RAM, 5 GB Disk

### One-Click Deploy (Import YAML)

Use this YAML with `zcli project import` to deploy this environment:

```yaml
#zeropsPreprocessor=on

# AI Agent environment — a dev + stage pair for the Wasp client and
# API from one git repo, plus PostgreSQL. Project vault are a
# value store (not runtime keys): APP_URL / API_URL point at the staged
# subdomains; DEV_* at the interactive appdev / apidev runtimes.
# zerops.yaml maps APP_URL → WASP_WEB_CLIENT_URL and API_URL →
# WASP_SERVER_URL / REACT_APP_API_URL for prod-api / prod-client.
project:
  name: wasp-hello-world-agent
  vault:
    APP_URL: https://appstage-${zeropsSubdomainHost}.prg1.zerops.app
    API_URL: https://apistage-${zeropsSubdomainHost}-3001.prg1.zerops.app
    DEV_APP_URL: https://appdev-${zeropsSubdomainHost}-3000.prg1.zerops.app
    DEV_API_URL: https://apidev-${zeropsSubdomainHost}-3001.prg1.zerops.app

services:
  # Deploy the Wasp API in a dev + stage pair — `apidev` is the
  # development workspace (idle until `wasp start` or server-only
  # iteration over SSH); `apistage` runs the production API bundle
  # with Prisma migrations. Subdomain access on both so curl and
  # browser walks can hit :3001 from outside the project network.
  - hostname: apidev
    type: nodejs@24
    priority: 5
    zeropsSetup: dev
    buildFromGit: https://github.com/zerops-recipe-apps/wasp-hello-world-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      # Wasp CLI + server iteration over SSH — same floor as nestjs-showcase dev.
      minRam: 1

  - hostname: apistage
    type: nodejs@24
    priority: 5
    zeropsSetup: prod-api
    buildFromGit: https://github.com/zerops-recipe-apps/wasp-hello-world-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      # Bundled Node + Prisma server — 0.5 GB floor per research.md.
      minRam: 0.5
      minFreeRamGB: 0.25

  # Deploy the Wasp client in a dev + stage pair — `appdev` ships
  # full source with the Wasp CLI for `wasp start` (client :3000,
  # server :3001); `appstage` serves the compiled SPA on static
  # Nginx. API_URL bakes apistage into the stage bundle at build
  # time (zerops.yaml → REACT_APP_API_URL).
  - hostname: appdev
    type: nodejs@24
    zeropsSetup: dev
    buildFromGit: https://github.com/zerops-recipe-apps/wasp-hello-world-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      # `wasp start` runs Vite (:3000) + server (:3001) in one container.
      minRam: 1

  - hostname: appstage
    type: static
    zeropsSetup: prod-client
    buildFromGit: https://github.com/zerops-recipe-apps/wasp-hello-world-app
    enableSubdomainAccess: true
    verticalAutoscaling:
      # Nginx static — minimal RAM.
      minRam: 0.25
      minFreeRamGB: 0.125

  # Set higher priority on db than the app services, so PostgreSQL
  # accepts connections before the API initCommands run migrate
  # deploy against Prisma.
  - hostname: db
    type: postgresql:single@16
    profile: oltp-hobby
    priority: 10

```

---

## Next Steps

After deploying one of the environments and getting to know Zerops, you have two paths to choose from:

1. **Template Flow** — Clone our GitHub repositories and use the whole recipe as a template
2. **Integrate Flow** — If you already have an existing application on a similar stack, integrate the recipe setup with your application

Select a flow: [Template Flow](https://app.zerops.io/recipes/wasp-hello-world.md?environment=ai-agent&guideFlow=template) or [Integrate Flow](https://app.zerops.io/recipes/wasp-hello-world.md?environment=ai-agent&guideFlow=integrate)

Both flows are shown below:

## How to take over the AI Agent environment

### 📦 Clone the template repositories

Fork or clone the following repositories to your local machine or GitHub account:

- [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)
- [zerops-recipe-apps/wasp-hello-world-app](https://github.com/zerops-recipe-apps/wasp-hello-world-app)

### Prerequisite: check authorization

After you deploy the AI Agent environment, an authorization dialog should appear.

Complete the Claude Code authorization before continuing. This is separate from your Zerops access token and uses your own Claude Code subscription or API credentials.

<img src="https://storage-prg1.zerops.io/4gfos-storage/quickstart_zcp_service_ready_863e8ba704.png" style="display: block; margin: 0 auto;" alt="Claude Code authorization dialog in Zerops" width="600" />

### 1. Open your ZCP workspace

The deployment creates your app services and a `zcp@1` workspace.

The agent, terminal, and browser IDE run inside the ZCP service. Your app still runs in the app services, not in the ZCP workspace.

<img src="https://storage-prg1.zerops.io/4gfos-storage/Screenshot_2026_03_10_at_01_19_49_e487f14e63.png" style="display: block; margin: 0 auto;" alt="ZCP workspace service in Zerops" width="600" />

### 2. Start in the browser IDE

The fastest way to work with the agent is to open the browser IDE from the ZCP service.

- No local setup is required.
- The agent runs close to your Zerops project.
- It can inspect project context and use available MCP tools.
- After the agent makes a change, open the app URL and verify the result.

<img src="https://storage-prg1.zerops.io/4gfos-storage/quickstart_cloud_ide_claude_code_f80940d851.jpg" style="display: block; margin: 0 auto;" alt="Browser IDE with Claude Code in Zerops" width="600" />

### 3. Connect from your local IDE or terminal

If you prefer local tooling, connect your machine to the Zerops project network.

Install zCLI and log in:

```bash
npm i -g @zerops/zcli
zcli login <personal-access-token>
```

Start the VPN:

```bash
zcli vpn up
```

Connect to the remote service:

```bash
ssh -A <service-name>.zerops
```

### 4. Work with the dev/stage flow

Use the development environment for fast iteration with the agent. Use staging to verify the result before moving toward production.

Typical flow:

1. Ask the agent for a focused change.
2. Review and test the result.
3. Push the change to your repository or deploy with `zcli push`.
4. Verify the result in staging.
5. Continue with the production guide when ready.

### 5. Move toward production

When the staging version is ready, finish the production setup:

- configure the deployment pipeline
- set up your domain
- check logs and runtime behavior
- configure backups if the recipe includes a database
- review scaling settings before real traffic arrives

## How to integrate apidev with Zerops

### 1. Adding `zerops.yaml`

The main application configuration file you place at the root of your repository. It tells Zerops how to build, deploy, and run your Wasp app.

Wasp deploys as **two production services** from one repo:

- `prod-client` — builds the React SPA (`wasp build` + `vite build`) and serves static assets via Nginx.
- `prod-api` — runs the bundled Node.js server from `.wasp/out/server/bundle/` and applies Prisma migrations on deploy.

```yaml
zerops:
  - setup: prod-client
    build:
      base: nodejs@24
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
        - node scripts/generate-build-env.cjs
        - npx wasp build
        - npm run build:client
      envVariables:
        RUNTIME_APP_ENV: production
      deployFiles:
        - .wasp/out/web-app/build/~
      cache:
        - node_modules
        - .wasp/out
    run:
      base: static

  - setup: prod-api
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - NPM_CONFIG_PRODUCTION=false NPM_CONFIG_ENGINE_STRICT=false NPM_CONFIG_AUDIT=false npm install --min-release-age=0
        - npx wasp install
        - npx wasp build
        - cp -R migrations/. .wasp/out/db/migrations/
        - sh scripts/bundle-server.sh
        - node scripts/prepare-api-deploy.cjs
      deployFiles:
        - .zerops/deploy/~
      cache:
        - node_modules
        - .wasp/out
    deploy:
      readinessCheck:
        httpGet:
          port: 3001
          path: /auth/me
        failureTimeout: 300
        retryPeriod: 10
    run:
      base: nodejs@24
      os: ubuntu
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/migrate-prod.cjs
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/seed-demo-user.cjs
      ports:
        - port: 3001
          httpSupport: true
      envVariables:
        NODE_ENV: production
        PORT: 3001
        DATABASE_URL: postgresql://${db_user}:${db_password}@${db_hostname}:${db_port}/${db_dbName}
        APP_DB_NAME: ${db_dbName}
        JWT_SECRET: wasp-zerops-hello-world-demo-jwt-v1
      start: sh -c 'cd .wasp/out/server && NODE_ENV=production node --enable-source-maps bundle/server.js'

  - setup: dev
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
      deployFiles: ./
      cache:
        - node_modules
    run:
      base: nodejs@24
      os: ubuntu
      ports:
        - port: 3000
          httpSupport: true
        - port: 3001
          httpSupport: true
      envVariables:
        DATABASE_URL: ${db_connectionString}
      start: zsc noop --silent
```

### 2. Environment variables

Set these at the **project** level in your recipe `import.yaml` as a value store (not Wasp runtime key names). `zerops.yaml` maps them into the app:

| Import variable | Mapped in `zerops.yaml` | Purpose |
|-----------------|-------------------------|---------|
| `API_URL` | `REACT_APP_API_URL` (prod-client build), `WASP_SERVER_URL` (prod-api) | Public API URL (port 3001) |
| `APP_URL` | `WASP_WEB_CLIENT_URL` (prod-api) | Public SPA URL (hostname `app`) |

Keep them at the project level, because both services read them. `envVariables` on a single service in `import.yaml` are kept as well, but only that service sees them.

Demo login (seeded on deploy): **username** `demo`, **password** `demo-zerops1`. The home page requires auth; unauthenticated users are redirected to `/login`.

The API service receives `DATABASE_URL` from `${db_*}` placeholders and runs `migrate-prod.cjs` plus `seed-demo-user.cjs` on deploy. If the API returns 502, verify `APP_URL` and `API_URL` exist on the project (re-import `import.yaml` env if needed).

### 3. Local development

```bash
npm install --min-release-age=0
npx wasp install
npx wasp start db   # optional local Postgres via Wasp
npx wasp db migrate-dev
npx wasp db seed seedDemoUser
npx wasp start      # client :3000, server :3001 — sign in with demo / demo-zerops1
```

Production build locally:

```bash
export DATABASE_URL='postgresql://user:pass@localhost:5432/wasp'
wasp build
REACT_APP_API_URL='http://localhost:3001' npm run build:client
```

## How to integrate apistage with Zerops

### 1. Adding `zerops.yaml`

The main application configuration file you place at the root of your repository. It tells Zerops how to build, deploy, and run your Wasp app.

Wasp deploys as **two production services** from one repo:

- `prod-client` — builds the React SPA (`wasp build` + `vite build`) and serves static assets via Nginx.
- `prod-api` — runs the bundled Node.js server from `.wasp/out/server/bundle/` and applies Prisma migrations on deploy.

```yaml
zerops:
  - setup: prod-client
    build:
      base: nodejs@24
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
        - node scripts/generate-build-env.cjs
        - npx wasp build
        - npm run build:client
      envVariables:
        RUNTIME_APP_ENV: production
      deployFiles:
        - .wasp/out/web-app/build/~
      cache:
        - node_modules
        - .wasp/out
    run:
      base: static

  - setup: prod-api
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - NPM_CONFIG_PRODUCTION=false NPM_CONFIG_ENGINE_STRICT=false NPM_CONFIG_AUDIT=false npm install --min-release-age=0
        - npx wasp install
        - npx wasp build
        - cp -R migrations/. .wasp/out/db/migrations/
        - sh scripts/bundle-server.sh
        - node scripts/prepare-api-deploy.cjs
      deployFiles:
        - .zerops/deploy/~
      cache:
        - node_modules
        - .wasp/out
    deploy:
      readinessCheck:
        httpGet:
          port: 3001
          path: /auth/me
        failureTimeout: 300
        retryPeriod: 10
    run:
      base: nodejs@24
      os: ubuntu
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/migrate-prod.cjs
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/seed-demo-user.cjs
      ports:
        - port: 3001
          httpSupport: true
      envVariables:
        NODE_ENV: production
        PORT: 3001
        DATABASE_URL: postgresql://${db_user}:${db_password}@${db_hostname}:${db_port}/${db_dbName}
        APP_DB_NAME: ${db_dbName}
        JWT_SECRET: wasp-zerops-hello-world-demo-jwt-v1
      start: sh -c 'cd .wasp/out/server && NODE_ENV=production node --enable-source-maps bundle/server.js'

  - setup: dev
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
      deployFiles: ./
      cache:
        - node_modules
    run:
      base: nodejs@24
      os: ubuntu
      ports:
        - port: 3000
          httpSupport: true
        - port: 3001
          httpSupport: true
      envVariables:
        DATABASE_URL: ${db_connectionString}
      start: zsc noop --silent
```

### 2. Environment variables

Set these at the **project** level in your recipe `import.yaml` as a value store (not Wasp runtime key names). `zerops.yaml` maps them into the app:

| Import variable | Mapped in `zerops.yaml` | Purpose |
|-----------------|-------------------------|---------|
| `API_URL` | `REACT_APP_API_URL` (prod-client build), `WASP_SERVER_URL` (prod-api) | Public API URL (port 3001) |
| `APP_URL` | `WASP_WEB_CLIENT_URL` (prod-api) | Public SPA URL (hostname `app`) |

Keep them at the project level, because both services read them. `envVariables` on a single service in `import.yaml` are kept as well, but only that service sees them.

Demo login (seeded on deploy): **username** `demo`, **password** `demo-zerops1`. The home page requires auth; unauthenticated users are redirected to `/login`.

The API service receives `DATABASE_URL` from `${db_*}` placeholders and runs `migrate-prod.cjs` plus `seed-demo-user.cjs` on deploy. If the API returns 502, verify `APP_URL` and `API_URL` exist on the project (re-import `import.yaml` env if needed).

### 3. Local development

```bash
npm install --min-release-age=0
npx wasp install
npx wasp start db   # optional local Postgres via Wasp
npx wasp db migrate-dev
npx wasp db seed seedDemoUser
npx wasp start      # client :3000, server :3001 — sign in with demo / demo-zerops1
```

Production build locally:

```bash
export DATABASE_URL='postgresql://user:pass@localhost:5432/wasp'
wasp build
REACT_APP_API_URL='http://localhost:3001' npm run build:client
```

## How to integrate appdev with Zerops

### 1. Adding `zerops.yaml`

The main application configuration file you place at the root of your repository. It tells Zerops how to build, deploy, and run your Wasp app.

Wasp deploys as **two production services** from one repo:

- `prod-client` — builds the React SPA (`wasp build` + `vite build`) and serves static assets via Nginx.
- `prod-api` — runs the bundled Node.js server from `.wasp/out/server/bundle/` and applies Prisma migrations on deploy.

```yaml
zerops:
  - setup: prod-client
    build:
      base: nodejs@24
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
        - node scripts/generate-build-env.cjs
        - npx wasp build
        - npm run build:client
      envVariables:
        RUNTIME_APP_ENV: production
      deployFiles:
        - .wasp/out/web-app/build/~
      cache:
        - node_modules
        - .wasp/out
    run:
      base: static

  - setup: prod-api
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - NPM_CONFIG_PRODUCTION=false NPM_CONFIG_ENGINE_STRICT=false NPM_CONFIG_AUDIT=false npm install --min-release-age=0
        - npx wasp install
        - npx wasp build
        - cp -R migrations/. .wasp/out/db/migrations/
        - sh scripts/bundle-server.sh
        - node scripts/prepare-api-deploy.cjs
      deployFiles:
        - .zerops/deploy/~
      cache:
        - node_modules
        - .wasp/out
    deploy:
      readinessCheck:
        httpGet:
          port: 3001
          path: /auth/me
        failureTimeout: 300
        retryPeriod: 10
    run:
      base: nodejs@24
      os: ubuntu
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/migrate-prod.cjs
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/seed-demo-user.cjs
      ports:
        - port: 3001
          httpSupport: true
      envVariables:
        NODE_ENV: production
        PORT: 3001
        DATABASE_URL: postgresql://${db_user}:${db_password}@${db_hostname}:${db_port}/${db_dbName}
        APP_DB_NAME: ${db_dbName}
        JWT_SECRET: wasp-zerops-hello-world-demo-jwt-v1
      start: sh -c 'cd .wasp/out/server && NODE_ENV=production node --enable-source-maps bundle/server.js'

  - setup: dev
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
      deployFiles: ./
      cache:
        - node_modules
    run:
      base: nodejs@24
      os: ubuntu
      ports:
        - port: 3000
          httpSupport: true
        - port: 3001
          httpSupport: true
      envVariables:
        DATABASE_URL: ${db_connectionString}
      start: zsc noop --silent
```

### 2. Environment variables

Set these at the **project** level in your recipe `import.yaml` as a value store (not Wasp runtime key names). `zerops.yaml` maps them into the app:

| Import variable | Mapped in `zerops.yaml` | Purpose |
|-----------------|-------------------------|---------|
| `API_URL` | `REACT_APP_API_URL` (prod-client build), `WASP_SERVER_URL` (prod-api) | Public API URL (port 3001) |
| `APP_URL` | `WASP_WEB_CLIENT_URL` (prod-api) | Public SPA URL (hostname `app`) |

Keep them at the project level, because both services read them. `envVariables` on a single service in `import.yaml` are kept as well, but only that service sees them.

Demo login (seeded on deploy): **username** `demo`, **password** `demo-zerops1`. The home page requires auth; unauthenticated users are redirected to `/login`.

The API service receives `DATABASE_URL` from `${db_*}` placeholders and runs `migrate-prod.cjs` plus `seed-demo-user.cjs` on deploy. If the API returns 502, verify `APP_URL` and `API_URL` exist on the project (re-import `import.yaml` env if needed).

### 3. Local development

```bash
npm install --min-release-age=0
npx wasp install
npx wasp start db   # optional local Postgres via Wasp
npx wasp db migrate-dev
npx wasp db seed seedDemoUser
npx wasp start      # client :3000, server :3001 — sign in with demo / demo-zerops1
```

Production build locally:

```bash
export DATABASE_URL='postgresql://user:pass@localhost:5432/wasp'
wasp build
REACT_APP_API_URL='http://localhost:3001' npm run build:client
```

## How to integrate appstage with Zerops

### 1. Adding `zerops.yaml`

The main application configuration file you place at the root of your repository. It tells Zerops how to build, deploy, and run your Wasp app.

Wasp deploys as **two production services** from one repo:

- `prod-client` — builds the React SPA (`wasp build` + `vite build`) and serves static assets via Nginx.
- `prod-api` — runs the bundled Node.js server from `.wasp/out/server/bundle/` and applies Prisma migrations on deploy.

```yaml
zerops:
  - setup: prod-client
    build:
      base: nodejs@24
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
        - node scripts/generate-build-env.cjs
        - npx wasp build
        - npm run build:client
      envVariables:
        RUNTIME_APP_ENV: production
      deployFiles:
        - .wasp/out/web-app/build/~
      cache:
        - node_modules
        - .wasp/out
    run:
      base: static

  - setup: prod-api
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - NPM_CONFIG_PRODUCTION=false NPM_CONFIG_ENGINE_STRICT=false NPM_CONFIG_AUDIT=false npm install --min-release-age=0
        - npx wasp install
        - npx wasp build
        - cp -R migrations/. .wasp/out/db/migrations/
        - sh scripts/bundle-server.sh
        - node scripts/prepare-api-deploy.cjs
      deployFiles:
        - .zerops/deploy/~
      cache:
        - node_modules
        - .wasp/out
    deploy:
      readinessCheck:
        httpGet:
          port: 3001
          path: /auth/me
        failureTimeout: 300
        retryPeriod: 10
    run:
      base: nodejs@24
      os: ubuntu
      initCommands:
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/migrate-prod.cjs
        - zsc execOnce ${appVersionId} --retryUntilSuccessful -- node scripts/seed-demo-user.cjs
      ports:
        - port: 3001
          httpSupport: true
      envVariables:
        NODE_ENV: production
        PORT: 3001
        DATABASE_URL: postgresql://${db_user}:${db_password}@${db_hostname}:${db_port}/${db_dbName}
        APP_DB_NAME: ${db_dbName}
        JWT_SECRET: wasp-zerops-hello-world-demo-jwt-v1
      start: sh -c 'cd .wasp/out/server && NODE_ENV=production node --enable-source-maps bundle/server.js'

  - setup: dev
    build:
      base: nodejs@24
      os: ubuntu
      buildCommands:
        - npm install --min-release-age=0
        - npx wasp install
      deployFiles: ./
      cache:
        - node_modules
    run:
      base: nodejs@24
      os: ubuntu
      ports:
        - port: 3000
          httpSupport: true
        - port: 3001
          httpSupport: true
      envVariables:
        DATABASE_URL: ${db_connectionString}
      start: zsc noop --silent
```

### 2. Environment variables

Set these at the **project** level in your recipe `import.yaml` as a value store (not Wasp runtime key names). `zerops.yaml` maps them into the app:

| Import variable | Mapped in `zerops.yaml` | Purpose |
|-----------------|-------------------------|---------|
| `API_URL` | `REACT_APP_API_URL` (prod-client build), `WASP_SERVER_URL` (prod-api) | Public API URL (port 3001) |
| `APP_URL` | `WASP_WEB_CLIENT_URL` (prod-api) | Public SPA URL (hostname `app`) |

Keep them at the project level, because both services read them. `envVariables` on a single service in `import.yaml` are kept as well, but only that service sees them.

Demo login (seeded on deploy): **username** `demo`, **password** `demo-zerops1`. The home page requires auth; unauthenticated users are redirected to `/login`.

The API service receives `DATABASE_URL` from `${db_*}` placeholders and runs `migrate-prod.cjs` plus `seed-demo-user.cjs` on deploy. If the API returns 502, verify `APP_URL` and `API_URL` exist on the project (re-import `import.yaml` env if needed).

### 3. Local development

```bash
npm install --min-release-age=0
npx wasp install
npx wasp start db   # optional local Postgres via Wasp
npx wasp db migrate-dev
npx wasp db seed seedDemoUser
npx wasp start      # client :3000, server :3001 — sign in with demo / demo-zerops1
```

Production build locally:

```bash
export DATABASE_URL='postgresql://user:pass@localhost:5432/wasp'
wasp build
REACT_APP_API_URL='http://localhost:3001' npm run build:client
```

### 🎯 What's next?

**Deploy other environments** — Ready to scale? Deploy additional environments for different stages of your workflow:

- [Remote (CDE)](https://app.zerops.io/recipes/wasp-hello-world.md?environment=remote-cde)
- [Local](https://app.zerops.io/recipes/wasp-hello-world.md?environment=local)
- [Stage](https://app.zerops.io/recipes/wasp-hello-world.md?environment=stage)
- [Small Production](https://app.zerops.io/recipes/wasp-hello-world.md?environment=small-production)
- [Highly-available Production](https://app.zerops.io/recipes/wasp-hello-world.md?environment=highly-available-production)

## Knowledge Base

### Platform Reference

- [Routing & Domains](https://docs.zerops.io/features/access)
- [Scaling](https://docs.zerops.io/features/scaling)
- [Environment Variables](https://docs.zerops.io/features/env-variables)
- [CLI (zcli)](https://docs.zerops.io/references/cli)

### Service Type Reference

**Node.js**

- [Build & Deploy](https://docs.zerops.io/nodejs/how-to/build-pipeline)
- [Customize Runtime](https://docs.zerops.io/nodejs/how-to/customize-runtime)

**Static**

- [Build & Deploy Pipeline](https://docs.zerops.io/alpine/how-to/build-pipeline)
- [Customize Runtime](https://docs.zerops.io/alpine/how-to/customize-runtime)

**PostgreSQL**

- [Connect](https://docs.zerops.io/postgresql/how-to/connect)
- [Backup & Restore](https://docs.zerops.io/postgresql/how-to/backup)
- [Manage](https://docs.zerops.io/postgresql/how-to/manage)
- [Scale](https://docs.zerops.io/postgresql/how-to/scale)

---

## Related Recipes

- [TanStack Start Hello World](https://app.zerops.io/recipes/tanstack-start-hello-world-app.md)
- [React (Vite) Static Hello World](https://app.zerops.io/recipes/react-static-hello-world.md)
- [React Router Static Hello World](https://app.zerops.io/recipes/react-router-static-hello-world.md)
- [React Router SSR Hello World](https://app.zerops.io/recipes/react-router-ssr-hello-world.md)

