# Runtime environments and service URLs

> Concept: Environment values, local versus dev versus production URL resolution, CORS rules, and frontend service selection.

- Repository: macro-inc/macro
- GitHub: https://github.com/macro-inc/macro
- Human docs: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e
- Complete Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/llms-full.txt

## Source Files

- `rust/cloud-storage/macro_env/src/lib.rs`
- `rust/cloud-storage/macro_service_urls/src/lib.rs`
- `rust/cloud-storage/macro_cors/src/lib.rs`
- `js/app/packages/core/constant/servers.ts`
- `js/app/scripts/services.ts`
- `docker-compose.local-e2e.yml`

---

---
title: "Runtime environments and service URLs"
description: "Concept: Environment values, local versus dev versus production URL resolution, CORS rules, and frontend service selection."
---

Macro resolves runtime environments through separate backend, frontend, and tooling selectors: Rust services read `ENVIRONMENT`, the Vite app reads `import.meta.env.MODE` plus `VITE_LOCAL_SERVERS`, and generation scripts read Node process variables such as `MODE` and `LOCAL_BACKEND`.

## Runtime selectors

| Surface | Selector | Accepted values | Default behavior |
| --- | --- | --- | --- |
| Rust services | `ENVIRONMENT` | `prod`, `dev`, `local` | `Environment::new_or_prod()` falls back to `Production` if the value is missing or invalid. |
| Rust local app URL | `FRONTEND_PORT` | port string | Defaults to `3000` for local app redirects and local app URL construction. |
| Frontend runtime | `MODE` via Vite config | commonly `development` or `production` | `development` enables local-server selection logic; any other mode uses remote hosts. |
| Frontend local overrides | `VITE_LOCAL_SERVERS` | empty, `ALL`, or comma-separated service names | Empty in development still points at remote dev services. |
| Client/tool generation | `MODE`, `LOCAL_BACKEND` | `production`, `local`, or fallback | `MODE=production` selects prod schema URLs; `MODE=local` or `LOCAL_BACKEND=true` selects local; otherwise dev. |

<Warning>
For Rust services, missing `ENVIRONMENT` does not mean local. It resolves as production through `new_or_prod()`.
</Warning>

## Backend environment model

The backend environment enum has three variants:

| Variant | `ENVIRONMENT` string | Display string | Meaning in URL resolution |
| --- | --- | --- | --- |
| `Production` | `prod` | `prod` | Public production domains. |
| `Develop` | `dev` | `dev` | Shared dev domains, usually with `-dev` hostnames. |
| `Local` | `local` | `local` | Localhost ports and local WebSocket schemes. |

`macro_entrypoint` loads `.env` when present, reads the environment, and uses it for logging/tracing behavior. Local uses pretty tracing without the OpenTelemetry sidecar; dev and prod initialize OTLP tracing to the Datadog agent endpoint on `127.0.0.1:4317`.

## Backend service URL resolver

Rust code uses `EnvExtMacroServiceUrls` on `macro_env::Environment` for canonical backend URLs.

| Method | Production | Develop | Local |
| --- | --- | --- | --- |
| `app()` | `https://macro.com` | `https://dev.macro.com` | `http://localhost:${FRONTEND_PORT:-3000}` |
| `auth_service()` | `https://auth-service.macro.com` | `https://auth-service-dev.macro.com` | `http://localhost:8080` |
| `pdf_service()` | `https://pdf-service.macro.com` | `https://pdf-service-dev.macro.com` | `http://localhost:4567` |
| `document_storage_service()` | `https://cloud-storage.macro.com` | `https://cloud-storage-dev.macro.com` | `http://localhost:8086` |
| `websocket_service()` | `wss://services.macro.com` | `wss://services-dev.macro.com` | `ws://localhost:6969` |
| `cognition_service()` | `https://document-cognition.macro.com` | `https://document-cognition-dev.macro.com` | `http://localhost:8085` |
| `connection_gateway()` | `wss://connection-gateway.macro.com` | `wss://connection-gateway-dev.macro.com` | `ws://localhost:8082` |
| `notification_service()` | `https://notifications.macro.com` | `https://notifications-dev.macro.com` | `http://localhost:8089` |
| `static_file_service()` | `https://static-file-service.macro.com` | `https://static-file-service-dev.macro.com` | `http://localhost:8100` |
| `unfurl_service()` | `https://unfurl-service.macro.com` | `https://unfurl-service-dev.macro.com` | `http://localhost:8095` |
| `contacts_service()` | `https://contacts.macro.com` | `https://contacts-dev.macro.com` | `http://localhost:8083` |
| `email_service()` | `https://email-service.macro.com` | `https://email-service-dev.macro.com` | `http://localhost:8087` |
| `image_proxy_service()` | `https://image-proxy.macro.com` | `https://image-proxy-dev.macro.com` | `http://localhost:8097` |

The resolver is used by backend features such as auth, GitHub redirects, notification preferences, and email formatting. URL parse tests verify every resolver method returns a valid `Url` for all three environments.

## Frontend service selection

`js/app/packages/core/constant/servers.ts` owns browser-facing runtime hosts.

### Remote host selection

Remote hosts use a suffix based on Vite mode:

| `import.meta.env.MODE` | Remote suffix | Example |
| --- | --- | --- |
| `development` | `-dev` | `https://cloud-storage-dev.macro.com` |
| any other mode | none | `https://cloud-storage.macro.com` |

`auth-logout` is special: development uses a FusionAuth dev logout URL, while production uses `https://auth.macro.com/oauth2/logout...`.

### Local host selection

Local hosts are only selected when `MODE === 'development'`.

| `VITE_LOCAL_SERVERS` | Result |
| --- | --- |
| unset or empty | Use remote dev hosts. |
| `ALL` | Use every local frontend host. |
| `document-storage-service,email-service` | Use local hosts for those names and remote dev hosts for the rest. |
| `document-storage-service:9000` | Use the local URL for that service but replace its port with `9000`. |
| unknown service name | Throw `Error("unknown server name ...")`. |

Example:

```bash
MODE=development VITE_LOCAL_SERVERS=document-storage-service:9000,email-service bun run dev
```

Frontend local host keys include the backend services plus `auth-logout` and `scheduled-action`. Runtime code imports `SERVER_HOSTS` directly in generated service clients, auth flows, WebSocket clients, image proxying, observability, and static-file helpers.

<Note>
The frontend local `static-file` host is `http://localhost:8100`, which points at the local CDN/nginx layer. The static file service container itself is exposed on `8094` for API health and OpenAPI-related usage.
</Note>

## Sync service host selection

The sync service has a separate host map:

| Mode | Worker URL | WebSocket URL |
| --- | --- | --- |
| Local | `http://localhost:8787` | `ws://localhost:8787` |
| Development remote | `https://sync-service-dev3.macroverse.workers.dev` | `wss://sync-service-dev3.macroverse.workers.dev` |
| Production remote | `https://sync-service-prod2.macroverse.workers.dev` | `wss://sync-service-prod2.macroverse.workers.dev` |

Local sync is selected when the frontend is in development mode and `VITE_LOCAL_SERVERS` is `ALL` or contains `sync-service`.

`SYNC_PERMISSION_TOKEN_DSS_HOST` intentionally follows the sync service. If sync is remote, permission tokens come from remote document storage so the token is signed with the JWT secret that matches the remote sync service. If sync is local, it uses the selected document storage host from `SERVER_HOSTS`.

## Local Docker and local E2E

`docker-compose.yml` exposes most Rust services on stable localhost ports while containers listen internally on `8080`. Important browser-facing ports include:

| Service | Local port |
| --- | --- |
| `authentication-service` | `8080` |
| `connection_gateway` | `8082` |
| `contacts_service` | `8083` |
| `document_cognition_service` | `8085` |
| `document_storage_service` | `8086` |
| `email_service` | `8087` |
| `notification_service` | `8089` |
| `static_file_service` | `8094` |
| `static_file_cdn` | `8100` |
| `unfurl_service` | `8095` |
| `image_proxy_service` | `8097` |
| `websocket_service` | `6969` |
| `sync_service` | `8787` |

Local E2E uses `docker-compose.local-e2e.yml` to override shared dev infrastructure with local Docker dependencies such as Postgres, Redis, and LocalStack. The Playwright local E2E web server starts Vite with:

```bash
VITE_LOCAL_SERVERS=ALL VITE_ENABLE_BEARER_TOKEN_AUTH=true bun run dev
```

The repository-level harness is:

```bash
just local-e2e
```

## CORS rules

Backend Axum services generally install `macro_cors::cors_layer()`; authentication adds the refresh-token header through `cors_layer_with_headers(...)`.

Default CORS behavior:

| Rule | Behavior |
| --- | --- |
| Credentials | Enabled. |
| Methods | `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `OPTIONS`. |
| Base headers | `authorization`, `content-type`. |
| Extra headers | `x-permissions-token`, `traceparent`, `tracestate`. |
| Additional headers | Per-service callers can pass extra `HeaderName` values. |
| Static origins | Built-in Macro origins include localhost dev, dashboard, dev, staging, production, Tauri, and Apollo testing origins. |
| `ALLOWED_ORIGINS` | Comma-separated env var that replaces the static origin list. |
| Preview origins | HTTPS origins ending in `preview.macro.com` are allowed. |
| Localhost dev ports | `http://localhost:3000` through `http://localhost:3999` are allowed by predicate. |

The sync service has its own Worker-side CORS implementation. It allows a similar but not identical origin set and accepts preview origins shaped like `https://{subdomain}.preview.macro.com`.

## Service-client and schema generation URLs

`js/app/scripts/services.ts` defines service metadata for generated clients and tooling:

- `services[]` maps each service name to dev/prod/local OpenAPI URLs, output directories, and Orval project keys.
- `serviceUrl(service)` selects local when `MODE=local` or `LOCAL_BACKEND=true`, production when `MODE=production`, and dev otherwise.
- `generate-api-schema.ts` uses the service list for names, output directories, and Orval keys, but generates OpenAPI JSON by building and running local Rust OpenAPI binaries.
- `generate-dcs-models.ts` can fetch Document Cognition models from the selected service URL, or read from `MODELS_JSON` when supplied.

## Contribution checklist

When adding or changing a service URL:

<Steps>
  <Step title="Update the backend resolver if Rust code needs the URL">
    Add a method or branch in `macro_service_urls` and extend the parse tests.
  </Step>
  <Step title="Update frontend runtime hosts if browser code calls the service">
    Add the key to both local and remote maps in `SERVER_HOSTS`; keep the `Servers` type satisfied.
  </Step>
  <Step title="Update Docker Compose if the service runs locally">
    Expose a stable local port and align the frontend local map with the intended browser-facing endpoint.
  </Step>
  <Step title="Update generation metadata if clients depend on OpenAPI output">
    Add or adjust the entry in `scripts/services.ts` and the Orval project in `packages/service-clients/orval.config.ts`.
  </Step>
  <Step title="Check CORS before exposing browser traffic">
    Confirm the app origin is covered by default rules, `ALLOWED_ORIGINS`, the preview-origin predicate, or the localhost `3000-3999` rule.
  </Step>
</Steps>

## Troubleshooting

| Symptom | Likely cause | Check |
| --- | --- | --- |
| Local Rust service redirects to `macro.com` | `ENVIRONMENT` is missing or invalid and fell back to production. | Set `ENVIRONMENT=local`. |
| Vite dev server calls shared dev services | `VITE_LOCAL_SERVERS` is empty. | Use `VITE_LOCAL_SERVERS=ALL` or a comma-separated service list. |
| Only one local service should be overridden | `ALL` is too broad. | Use `VITE_LOCAL_SERVERS=service-name` or `service-name:port`. |
| Browser CORS failure on a local frontend port | Port is outside the accepted `3000-3999` range or origin is not in `ALLOWED_ORIGINS`. | Use a `3000-3999` frontend port or configure `ALLOWED_ORIGINS`. |
| Sync permission token fails against remote sync | Token came from local DSS while sync is remote. | Use `SYNC_PERMISSION_TOKEN_DSS_HOST` behavior instead of hard-coding `SERVER_HOSTS['document-storage-service']`. |
