@sashimikun_void: Found an interesting open-source all-in-one comms + crm + agent OSS project @macrodotcom yesterday. I was very impresse…

X AI KOLs Following Tools

Summary

Introduces grok-wiki v0.0.16, a tool that automatically generates documentation from a GitHub URL or folder path, enabling agents to read docs. The author also highlights the Macro open-source comms/CRM/agent project.

Found an interesting open-source all-in-one comms + crm + agent OSS project @macrodotcom yesterday. I was very impressed but they don't have a documentation site available. Introducing the new grok-wiki 0 to 1 documentation feature. Automatically generate a beautiful documentation by providing a GitHub URL or folder path. It's live in the latest v0.0.16 release. You can also point your agent directly to https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e… to gain immediate context.
Original Article
View Cached Full Text

Cached at: 06/02/26, 05:37 PM

Found an interesting open-source all-in-one comms + crm + agent OSS project @macrodotcom yesterday.

I was very impressed but they don’t have a documentation site available.

Introducing the new grok-wiki 0 to 1 documentation feature. Automatically generate a beautiful documentation by providing a GitHub URL or folder path.

It’s live in the latest v0.0.16 release.

You can also point your agent directly to https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e… to gain immediate context.


Macro Engineering Documentation

Source: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e Agent-readable docs

Technical documentation for the Macro monorepo: SolidJS app, Rust services, sync worker, AI tooling, infrastructure, local development, APIs, configuration, and operations.

Full Markdownllms.txtMarkdown aliasGitHub## Pages

  1. OverviewOverview: Public entry points, repository surfaces, runtime assumptions, and the shortest successful path through the docs.
  2. InstallationInstall: Required tools, Nix option, encrypted environment setup, Docker resources, LocalStack, FusionAuth, and first setup command.
  3. Local development quickstartQuickstart: Bring up local services with just recipes, shared Compose resources, database initialization, and expected health signals.
  4. Frontend quickstartQuickstart: Run the SolidJS app, choose local or remote services, understand app routing, and start Tauri development.
  5. Local E2E smoke testsGuide: Run deterministic Playwright and ignored Rust smoke tests against the local-only stack and shared seed fixtures.
  6. Monorepo mapConcept: Workspace boundaries, package managers, major runtime folders, generated documentation, and where source-of-truth manifests live.
  7. Runtime environments and service URLsConcept: Environment values, local versus dev versus production URL resolution, CORS rules, and frontend service selection.
  8. Service topologyConcept: Rust services, workers, queues, storage dependencies, ports, and deployment boundaries used by the local and cloud stacks.
  9. Entities and blocksConcept: Item types, document-backed and non-document blocks, aliases, load sources, nesting rules, and file type resolution.
  10. Split layout and navigationConcept: Route encoding, component registry entries, split history, navigation causes, popovers, and desktop versus mobile split behavior.
  11. Real-time document syncConcept: Sync-service worker routes, Durable Object sessions, permission tokens, Bebop messages, snapshot lifecycle, and client reconnect behavior.
  12. Run backend services locallyGuide: Start databases, LocalStack, FusionAuth, Rust services, optional processor profiles, and local health checks.

Complete Markdown

# Macro Engineering Documentation

> Technical documentation for the Macro monorepo: SolidJS app, Rust services, sync worker, AI tooling, infrastructure, local development, APIs, configuration, and operations.

## Context Links

- [Agent index](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/llms.txt)
- [Human interactive docs](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e)
- [GitHub repository](https://github.com/macro-inc/macro)

## Repository Metadata

- Repository: macro-inc/macro

- Generated: 2026-06-01T01:07:41.847Z
- Updated: 2026-06-01T02:14:57.236Z
- Runtime: Pi · Codex · gpt-5.5
- Format: Documentation
- Pages: 29

## Page Index

- 01. [Overview](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/01-overview.md) - Overview: Public entry points, repository surfaces, runtime assumptions, and the shortest successful path through the docs.
- 02. [Installation](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/02-installation.md) - Install: Required tools, Nix option, encrypted environment setup, Docker resources, LocalStack, FusionAuth, and first setup command.
- 03. [Local development quickstart](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/03-local-development-quickstart.md) - Quickstart: Bring up local services with just recipes, shared Compose resources, database initialization, and expected health signals.
- 04. [Frontend quickstart](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/04-frontend-quickstart.md) - Quickstart: Run the SolidJS app, choose local or remote services, understand app routing, and start Tauri development.
- 05. [Local E2E smoke tests](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/05-local-e2e-smoke-tests.md) - Guide: Run deterministic Playwright and ignored Rust smoke tests against the local-only stack and shared seed fixtures.
- 06. [Monorepo map](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/06-monorepo-map.md) - Concept: Workspace boundaries, package managers, major runtime folders, generated documentation, and where source-of-truth manifests live.
- 07. [Runtime environments and service URLs](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/07-runtime-environments-and-service-urls.md) - Concept: Environment values, local versus dev versus production URL resolution, CORS rules, and frontend service selection.
- 08. [Service topology](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/08-service-topology.md) - Concept: Rust services, workers, queues, storage dependencies, ports, and deployment boundaries used by the local and cloud stacks.
- 09. [Entities and blocks](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/09-entities-and-blocks.md) - Concept: Item types, document-backed and non-document blocks, aliases, load sources, nesting rules, and file type resolution.
- 10. [Split layout and navigation](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/10-split-layout-and-navigation.md) - Concept: Route encoding, component registry entries, split history, navigation causes, popovers, and desktop versus mobile split behavior.
- 11. [Real-time document sync](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/11-real-time-document-sync.md) - Concept: Sync-service worker routes, Durable Object sessions, permission tokens, Bebop messages, snapshot lifecycle, and client reconnect behavior.
- 12. [Run backend services locally](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/12-run-backend-services-locally.md) - Guide: Start databases, LocalStack, FusionAuth, Rust services, optional processor profiles, and local health checks.
- 13. [Develop SolidJS and Tauri apps](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/13-develop-solidjs-and-tauri-apps.md) - Guide: Use Bun and Vite, run local service overrides, build app bundles, start Tauri targets, and avoid iOS worker deadlocks.
- 14. [Change a Rust service API](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/14-change-a-rust-service-api.md) - Guide: Update Axum handlers, OpenAPI emitters, generated client specs, Orval output, and CI checks for service API changes.
- 15. [Generate service clients and tool schemas](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/15-generate-service-clients-and-tool-schemas.md) - Guide: Regenerate OpenAPI JSON, TypeScript clients, DCS model types, AI tool schemas, and MCP documentation pages from Rust sources.
- 16. [Work with local seed data](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/16-work-with-local-seed-data.md) - Guide: Use seed_cli, local_e2e fixtures, manifest aliases, reset SQL, and shared Playwright and Rust fixture loaders.
- 17. [Storage and workspace APIs](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/17-storage-and-workspace-apis.md) - Reference: Document, channel, project, call, CRM, soup, pin, history, permissions, properties, and search endpoints.
- 18. [Auth and team APIs](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/18-auth-and-team-apis.md) - Reference: Login, OAuth callbacks, JWT refresh, account links, team membership, invites, permissions, billing, and user endpoints.
- 19. [Email and notification APIs](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/19-email-and-notification-apis.md) - Reference: Gmail init and sync, drafts, threads, labels, attachments, notification preferences, unread state, and unsubscribe routes.
- 20. [AI chat streaming API](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/20-ai-chat-streaming-api.md) - Reference: DCS chat endpoints, stream payloads, toolset selection, model identifiers, extraction statuses, structured completion, and stop semantics.
- 21. [MCP server and tool registry](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/21-mcp-server-and-tool-registry.md) - Reference: Remote MCP endpoint, OAuth-backed server, toolset composition, SendEmail boundary, generated schemas, and docs tool pages.
- 22. [Environment variables reference](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/22-environment-variables-reference.md) - Reference: Required and optional service environment variables, defaults, secrets, queue names, ports, and provider-specific keys.
- 23. [Database migrations and SQLx cache](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/23-database-migrations-and-sqlx-cache.md) - Operations: MacroDB migrations, local database just recipes, SQLx offline cache, fixture data, and migration validation workflow.
- 24. [Feature flags and server selection](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/24-feature-flags-and-server-selection.md) - Reference: Vite environment overrides, feature flag defaults, local service selection syntax, sync-service host selection, and runtime host helpers.
- 25. [Infrastructure stacks reference](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/25-infrastructure-stacks-reference.md) - Reference: Pulumi stack layout, reusable resources, service stacks, buckets, queues, Datadog sidecars, FusionAuth, and deployment inputs.
- 26. [Build, test, and quality gates](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/26-build-test-and-quality-gates.md) - Operations: Rust checks, clippy, SQLx preparation, TypeScript checks, Biome, Tailwind hygiene, Vitest, Playwright, and CI path filters.
- 27. [Deploy services and web app](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/27-deploy-services-and-web-app.md) - Operations: Generic service deployment, web app deployment, production release workflow, database migrations, Pulumi stacks, and deployment concurrency.
- 28. [Observability and debugging](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/28-observability-and-debugging.md) - Operations: Backend tracing initialization, Datadog ECS sidecars, browser RUM and logs, sourcemaps, local debug signals, and iOS WebView freeze diagnosis.
- 29. [Documentation site maintenance](https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/29-documentation-site-maintenance.md) - Contribution: Maintain the Mintlify docs site, navigation manifest, generated MCP pages, local preview, broken-link checks, and writing conventions.

## Source File Index

- `.github/workflows/code-check-cloud-storage.yml`
- `.github/workflows/deploy-service-generic.yml`
- `.github/workflows/deploy-web-app.yml`
- `.github/workflows/migrate-macro-db.yml`
- `.github/workflows/release-production.yml`
- `.github/workflows/reusable-deploy-service.yml`
- `.github/workflows/web-app-check-main.yml`
- `docker-compose-databases.yml`
- `docker-compose.local-e2e.yml`
- `docker-compose.yml`
- `docs/AGENTS.md`
- `docs/AI/mcp/overview.mdx`
- `docs/AI/mcp/tools/index.mdx`
- `docs/config/tool-pages.json`
- `docs/CONTRIBUTING.md`
- `docs/docs.json`
- `docs/package.json`
- `docs/README.md`
- `docs/scripts/generate-mcp-tool-pages.ts`
- `flake.nix`
- `infra/packages/lambda/src/index.ts`
- `infra/packages/resources/src/index.ts`
- `infra/packages/resources/src/resources/datadog.ts`
- `infra/packages/service/src/index.ts`
- `infra/packages/shared/src/ai_tools.ts`
- `infra/README.md`
- `infra/stacks/document-storage/index.ts`
- `infra/stacks/fusionauth-instance/README.md`
- `infra/stacks/mcp-server/index.ts`
- `js/app/AGENTS.md`
- `js/app/justfile`
- `js/app/package.json`
- `js/app/packages/app/component/Root.tsx`
- `js/app/packages/app/component/split-layout/componentRegistry.tsx`
- `js/app/packages/app/component/split-layout/layout.ts`
- `js/app/packages/app/component/split-layout/layoutManager.ts`
- `js/app/packages/app/component/split-layout/SplitLayoutRoute.tsx`
- `js/app/packages/app/component/split-layout/tests/layoutManager.test.ts`
- `js/app/packages/app/index.tsx`
- `js/app/packages/app/vite.config.ts`
- `js/app/packages/block-md/definition.ts`
- `js/app/packages/block-project/definition.ts`
- `js/app/packages/core/auth/logout.ts`
- `js/app/packages/core/auth/sso.ts`
- `js/app/packages/core/block.ts`
- `js/app/packages/core/component/AI/types/index.ts`
- `js/app/packages/core/constant/allBlocks.ts`
- `js/app/packages/core/constant/featureFlags.ts`
- `js/app/packages/core/constant/servers.ts`
- `js/app/packages/core/tests/featureFlags.test.ts`
- `js/app/packages/observability/src/index.ts`
- `js/app/packages/service-clients/orval.config.ts`
- `js/app/packages/service-clients/service-auth/client.ts`
- `js/app/packages/service-clients/service-auth/openapi.json`
- `js/app/packages/service-clients/service-cognition/client.ts`
- `js/app/packages/service-clients/service-cognition/openapi.json`
- `js/app/packages/service-clients/service-email/client.ts`
- `js/app/packages/service-clients/service-email/openapi.json`
- `js/app/packages/service-clients/service-notification/client.ts`
- `js/app/packages/service-clients/service-notification/openapi.json`
- `js/app/packages/service-clients/service-properties/client.ts`
- `js/app/packages/service-clients/service-properties/openapi.json`
- `js/app/packages/service-clients/service-search/client.ts`
- `js/app/packages/service-clients/service-search/openapi.json`
- `js/app/packages/service-clients/service-storage/client.ts`
- `js/app/packages/service-clients/service-storage/openapi.json`
- `js/app/packages/service-clients/service-sync/client.ts`
- `js/app/packages/service-clients/service-sync/source.ts`
- `js/app/README.md`
- `js/app/scripts/generate-api-schema.ts`
- `js/app/scripts/generate-dcs-tools.ts`
- `js/app/scripts/services.ts`
- `js/app/tauri/src-tauri/README.md`
- `js/app/tests/e2e/fixtures/local-e2e-seed.ts`
- `js/package.json`
- `justfile`
- `local_stack.just`
- `README.md`
- `RUNNING_LOCALLY.md`
- `rust/cloud-storage/agent/src/model.rs`
- `rust/cloud-storage/AGENTS.md`
- `rust/cloud-storage/ai_tools/src/bin/gen_tool_schemas.rs`
- `rust/cloud-storage/ai_tools/src/lib.rs`
- `rust/cloud-storage/authentication_service/src/config.rs`
- `rust/cloud-storage/authentication_service/src/openapi.rs`
- `rust/cloud-storage/Cargo.toml`
- `rust/cloud-storage/chat/.sqlx/query-57fc603adf83fd7c07968425c98f9a57924573692cf0529ad021b6eef00ce99e.json`
- `rust/cloud-storage/database.just`
- `rust/cloud-storage/document_cognition_service/src/api/stream/chat_message/mod.rs`
- `rust/cloud-storage/document_cognition_service/src/api/stream/stop.rs`
- `rust/cloud-storage/document_cognition_service/src/config.rs`
- `rust/cloud-storage/document_cognition_service/src/model/stream.rs`
- `rust/cloud-storage/document_cognition_service/src/openapi.rs`
- `rust/cloud-storage/document_storage_service/src/config.rs`
- `rust/cloud-storage/document_storage_service/src/openapi.rs`
- `rust/cloud-storage/email_service/src/config.rs`
- `rust/cloud-storage/email_service/src/openapi.rs`
- `rust/cloud-storage/integration_tests/local_e2e/README.md`
- `rust/cloud-storage/justfile`
- `rust/cloud-storage/local_e2e_test_support/README.md`
- `rust/cloud-storage/macro_cors/src/lib.rs`
- `rust/cloud-storage/macro_db_client/migrations/0001_baseline.sql`
- `rust/cloud-storage/macro_db_client/README.md`
- `rust/cloud-storage/macro_entrypoint/src/lib.rs`
- `rust/cloud-storage/macro_env/src/lib.rs`
- `rust/cloud-storage/macro_service_urls/src/lib.rs`
- `rust/cloud-storage/mcp_service/src/main.rs`
- `rust/cloud-storage/mcp_service/src/tool_service.rs`
- `rust/cloud-storage/models_soup/src/lib.rs`
- `rust/cloud-storage/notification_service/src/config.rs`
- `rust/cloud-storage/notification_service/src/openapi.rs`
- `rust/cloud-storage/README.md`
- `rust/cloud-storage/search_service/src/config.rs`
- `rust/cloud-storage/seed_cli/README.md`
- `rust/cloud-storage/seed_cli/seed/local_e2e/manifest.json`
- `rust/cloud-storage/seed_cli/seed/local_e2e/reset.sql`
- `rust/cloud-storage/seed_cli/seed/local_e2e/users.json`
- `rust/cloud-storage/seed_cli/src/main.rs`
- `rust/cloud-storage/sqlx.just`
- `rust/sync-service/Cargo.toml`
- `rust/sync-service/src/cf_worker.rs`
- `rust/sync-service/src/durable_object.rs`
- `rust/sync-service/src/generated/schema.rs`
- `rust/sync-service/src/websocket.rs`

---

## 01. Overview

> Overview: Public entry points, repository surfaces, runtime assumptions, and the shortest successful path through the docs.

- Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/01-overview.md
- Generated: 2026-06-01T00:46:58.329Z

### Source Files

- `README.md`
- `RUNNING_LOCALLY.md`
- `js/app/README.md`
- `rust/cloud-storage/AGENTS.md`
- `docker-compose.yml`
- `docs/docs.json`

---
title: "Overview"
description: "Overview: Public entry points, repository surfaces, runtime assumptions, and the shortest successful path through the docs."
---

Macro is a SolidJS and Rust monorepo for the Macro workspace: a static SPA frontend, Rust backend services, generated MCP tool documentation, Pulumi infrastructure, Docker Compose local services, and deterministic local smoke-test fixtures.

## Public entry points

| Surface | Entry point | Backing repository area |
| --- | --- | --- |
| Web application | `https://macro.com/app` | `js/app`, `infra/stacks/web-app` |
| Product docs | `docs.macro.com` site config | `docs/docs.json`, `docs/index.mdx`, `docs/product/*` |
| MCP server | `https://mcp-server.macro.com/mcp` | `rust/cloud-storage/mcp_service`, `rust/cloud-storage/ai_tools`, `docs/AI/mcp/*` |
| Local backend stack | `just run_local` | `justfile`, `docker-compose.yml`, `docker-compose-databases.yml` |
| Local E2E smoke stack | `just local-e2e` | `docker-compose.local-e2e.yml`, `rust/cloud-storage/seed_cli`, `js/app/tests/e2e` |

<Note>
The local setup is explicitly single-instance. The root `justfile` exports `COMPOSE_PROJECT_NAME=macro`, and the Compose files use the fixed project name `macro`, so multiple checkouts share the same local containers, volumes, networks, LocalStack, and FusionAuth resources.
</Note>

## Repository surfaces

:::files
```text
.
├── README.md                         # product, security, license, community
├── RUNNING_LOCALLY.md                # supported local setup and E2E path
├── justfile                          # root setup, local stack, E2E orchestration
├── docker-compose*.yml               # local services, databases, E2E overrides
├── local_stack.just                  # LocalStack SQS, DynamoDB, and S3 setup
├── docs/                             # public docs site and generated MCP docs
├── js/
│   ├── app/                          # SolidJS/Vite frontend workspace
│   ├── lexical-service/              # Lexical conversion service
│   └── websocket-service/            # Bun websocket service
├── rust/
│   ├── cloud-storage/                # Rust service workspace
│   └── sync-service/                 # sync service container
├── infra/                            # Pulumi stacks and shared service package
└── agents/transcription/             # transcription agent service

:::

Frontend

The frontend lives in js/app and uses Bun, Vite, SolidJS, Tailwind, TypeScript, and workspace packages under js/app/packages/*. The app package builds a static SPA with base path /app; development serves from / on port 3000 by default.

Common commands:

cd js/app
bun i
just local
bun run check
bun run local:e2e

The Vite config exposes build-time values such as import.meta.env.__APP_VERSION__, ASSETS_PATH, __LOCAL_DOCKER__, __LOCAL_JWT__, and __GIT_BRANCH__.

Rust services

The main backend surface is the Cargo workspace in rust/cloud-storage. It contains service binaries, worker binaries, Lambda handlers, database clients, domain crates, generated schemas, and integration tests.

Representative service binaries include:

BinaryLocal Compose servicePort mapping
authentication_serviceauthentication-service8080:8080
connection_gateway_serviceconnection_gateway8082:8080
contacts_servicecontacts_service8083:8080
document_storage_servicedocument_storage_service8086:8080
email_serviceemail_service8087:8080
notification_servicenotification_service8089:8080
search_processing_servicesearch_processing_service8092:8080
static_file_servicestatic_file_service8094:8080
unfurl_serviceunfurl_service8095:8080
image_proxy_serviceimage_proxy_service8097:8080

rust/cloud-storage/Dockerfile.dev builds the standard service bundle into the macro-local-rust-services:dev image. search_processing_service uses a separate Dockerfile and is behind the processors Compose profile.

Docs and MCP reference

The docs site is configured by docs/docs.json. Current navigation contains:

  • Getting Started: docs/index.mdx
  • Product pages: AI Chat, Email, Channels, Docs, Canvas, Tasks, Search, Files
  • MCP setup: docs/AI/mcp/overview.mdx
  • Generated MCP tool pages: docs/AI/mcp/tools/*
  • Changelog: docs/changelog/introduction

MCP tool pages are generated from the Rust tool registry:

cd docs
bun install
bun run generate:tools
bun run dev

The generator builds gen_tool_schemas from rust/cloud-storage, reads rust/cloud-storage/ai_tools/schemas/tools.json, writes MDX pages under docs/AI/mcp/tools, and updates docs/config/tool-pages.json.

The MCP docs and transport are portable at the repository boundary: the public setup page points clients at the hosted HTTP MCP endpoint and authenticates through Macro OAuth. Tool references are generated from repository schemas rather than from a specific client, editor, or model provider.

Infrastructure

Infrastructure lives under infra/stacks/* and shared Pulumi packages under infra/packages/*.

Important stacks include:

Stack areaPurpose
infra/stacks/web-appPublishes the built SPA to S3 and wires route/encoding Lambda functions
infra/stacks/document-storageCreates document storage buckets, replication, and the shared ECS cluster
infra/stacks/mcp-serverDeploys the MCP service and configures OAuth, Redis, queues, buckets, and secrets
infra/stacks/fusionauth-instanceLocal and deployed FusionAuth configuration
infra/packages/serviceShared ECS Fargate service component with load balancer, IAM role, Datadog sidecars, and environment variables

Runtime assumptions

Required local tools

RUNNING_LOCALLY.md lists these local prerequisites:

  • sops
  • Docker
  • AWS CLI/configuration
  • SQLx / sqlx-cli
  • Pulumi
  • Bun
  • Node
  • just

If Nix is available, the repo supports nix develop for toolchain management.

Local data plane

The default local stack uses:

DependencyLocal implementation
PostgreSQLpgvector/pgvector:pg16 on localhost:5432
Redisredis/redis-stack on localhost:6379 and UI/tools on 8001
OpenSearchopensearchproject/opensearch on 9200
AWS SQS/DynamoDB/S3LocalStack v4 on 4566
FusionAuthinfra/stacks/fusionauth-instance/docker-compose.yml
Static CDN emulationnginx static_file_cdn on 8100

Local E2E overrides force services onto local Postgres, Redis, LocalStack queues, LocalStack buckets, and deterministic seed data.

Environment and secrets

The root setup path decrypts .env-local*.enc into .env with sops, patches local FusionAuth values when available, creates shared Docker networks and volumes, initializes local AWS resources, initializes databases, and builds the Rust dev service image.

just setup

Use just get_environment when only the .env file is needed.

Shortest successful path

Start with README.md for the public product surface, security contact, AGPLv3 license, and self-hosting note.

Run the repository setup from the root:

just setup

This prepares .env, Docker networks and volumes, LocalStack resources, local databases, FusionAuth, and Rust service images.

just run_local

If service images changed, rebuild during startup:

just run_local --build

Add the processors profile only when you need processor services such as search_processing_service.

cd js/app
bun i
just local

The Vite dev server defaults to port 3000.

just local-e2e

For Rust ignored integration tests against the same local stack:

just local-e2e-rust

For both Rust and Playwright smoke coverage:

just local-e2e-all

Verification signals

CommandExpected signal
just run_localCompose services start and healthchecks pass
just local-e2eLocalStack setup completes, seed data loads, Playwright smoke tests run
cd rust/cloud-storage && just checkRust workspace type-checks with SQLX_OFFLINE=true
cd rust/cloud-storage && just clippyWorkspace clippy passes with warnings denied
cd js/app && bun run checkTypeScript and Biome checks pass
cd docs && bun run generate:toolsMCP tool pages regenerate from Rust schemas
cd docs && bun run lintDocs broken-link check runs through Mintlify

Failure modes to check first

SymptomLikely causeCheck
Local services share unexpected stateCompose project is intentionally frozen to macroDo not run two local stacks at the same time
just run_local fails before services startMissing .envRun just get_environment or just setup
FusionAuth values are missingLocal Pulumi/FusionAuth setup not completeRun just setup or inspect infra/stacks/fusionauth-instance
Local E2E seed refuses to runSafety guard rejected the target databaseEnsure LOCAL_E2E_SEED=true and DATABASE_URL targets local macrodb
SQLx reports missing cached query dataRust SQL changed without refreshing offline cacheRun just prepare_db from rust/cloud-storage
Docs tool pages drift from codeGenerated pages were not rebuiltRun cd docs && bun run generate:tools

Related pages

Connect clients to Macro's hosted MCP endpoint. Inspect generated tool schemas from the Rust registry. Navigate the public product docs surface.

02. Installation

Install: Required tools, Nix option, encrypted environment setup, Docker resources, LocalStack, FusionAuth, and first setup command.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/02-installation.md
  • Generated: 2026-06-01T00:46:44.840Z

Source Files

  • RUNNING_LOCALLY.md
  • flake.nix
  • justfile
  • local_stack.just
  • docker-compose-databases.yml
  • rust/cloud-storage/README.md

Macro local installation is driven by just setup at the repository root. The setup path decrypts .env, creates shared Docker networks and volumes, provisions LocalStack AWS resources, initializes the local Postgres database, configures a local FusionAuth stack, and builds local Rust service images.

Prerequisites

ToolRequired for
justRepository task runner
Docker with Compose v2Databases, services, FusionAuth, LocalStack
sopsDecrypting .env-local*.enc files
AWS CLI and AWS credentialsLocalStack provisioning and encrypted environment access
pulumiLocal FusionAuth stack configuration
bunFrontend and FusionAuth stack dependencies
Node.jsJavaScript tooling and Pulumi Node runtime
sqlx-cliDatabase create, migrate, prepare, and reset commands
Rust toolchainBackend builds and local Rust tests
The repository currently documents local development as work in progress and primarily supports running services against dev assets or the provided local E2E stack.

Nix option

If Nix is available, use the repository flake instead of installing most tools manually:

nix develop

The default shell provides the Rust toolchain, just, bun, pulumi, sops, sqlx-cli, Node.js 24, Docker Compose tooling, jq, and related backend/frontend utilities. It also sets SOPS_KMS_ARN for encrypted environment decryption.

For Tauri/frontend platform work, use the JavaScript app shell:

nix develop .#js-app
Nix supplies CLI tools, not the Docker daemon. Docker must still be running on the host.

Encrypted environment setup

The root justfile decrypts encrypted dotenv bundles into a plain root .env file.

CommandInputOutputNotes
just get_environment.env-local.enc.envFully local backing services
just get_environment dev.env-localdev.enc.envDev backing services, used for ad-hoc dev-service work
just edit_environment.env-local.encencrypted file edited through sopsMaintainer workflow
just edit_environment dev.env-localdev.encencrypted file edited through sopsMaintainer workflow
just fix_environment [dev]encrypted dotenvre-encrypted dotenvDecrypts with --ignore-mac, re-encrypts, removes temporary plaintext

If you are not inside nix develop, export the KMS ARNs before decrypting:

export SOPS_KMS_ARN="arn:aws:kms:us-east-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93,arn:aws:kms:us-west-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93"

When just get_environment dev is used and ~/.aws/credentials exists, the recipe replaces AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in .env from the [default] profile.

`just setup` always runs `just get_environment` with no suffix, so it regenerates `.env` from `.env-local.enc`.

First setup command

Run from the repository root:

just setup

just setup performs this sequence:

Generates `.env` from `.env-local.enc`. Creates the `databases` and `auth` networks plus the shared local volumes. Starts the `localstack` container on port `4566`, then creates local SQS queues, DynamoDB tables, S3 buckets, CORS rules, and the document upload finalizer notification. Starts Postgres and Redis, creates `macrodb`, runs migrations through SQLx, then stops the database Compose file. Starts the local FusionAuth stack, waits for health, initializes the Pulumi `local` stack, patches root `.env` with local FusionAuth values, and stops FusionAuth. Builds `macro-local-rust-services:dev` and the local search processing image.

Expected final output:

Setup complete.

Docker resources

Local Docker resources are intentionally frozen to the macro Compose project. Multiple checkouts and worktrees share the same containers, volumes, networks, LocalStack instance, and FusionAuth instance.

Do not run two local stacks at the same time from different checkouts.

Shared networks and volumes

just create_networks creates:

ResourceName
Networkdatabases
Networkauth
Volumemacro_postgres_data
Volumemacro_redis_data
Volumemacro_opensearch_data
Volumefusionauth_db_data
Volumefusionauth_config

Database Compose services

docker-compose-databases.yml defines:

ServiceImagePortsPurpose
postgrespgvector/pgvector:pg165432Local macrodb database
redisredis/redis-stack:latest6379, 8001Redis plus Redis Stack UI/tools
searchopensearchproject/opensearch:latest9200, 9600Local OpenSearch

The local database URL used by cloud-storage justfiles is:

postgres://user:password@localhost:5432/macrodb

LocalStack

Local AWS emulation is handled by local_stack.just.

just setup_localstack

The setup starts localstack/localstack:4 with:

SettingValue
Container namelocalstack
Networkdatabases
Port4566
Servicessqs,dynamodb,s3
Health endpointhttp://localhost:4566/_localstack/health

The recipe provisions SQS queues used by notification, email, contacts, conversion, document deletion, document upload finalization, document text extraction, search events, and static-file events. It also creates these DynamoDB tables:

TableKey shape
bulk-uploadPK + SK, with DocumentPkIndex
connection-gateway-tablePK + SK, with ConnectionPkIndex
static-file-metadatafile_id hash key

S3 buckets created locally:

Bucket
macro-email-attachments
doc-storage
docx-upload
static-file-storage
bulk-upload-staging

All buckets receive CORS rules for http://localhost:3000 through http://localhost:3009. The doc-storage bucket is also wired so s3:ObjectCreated:* events send messages to document-upload-finalizer-queue.

Backend AWS clients switch to LocalStack when LOCAL_AWS_URL is set. In that mode, the Rust AWS config uses us-east-1, test credentials, the configured endpoint URL, and path-style S3 behavior.

FusionAuth

The local FusionAuth stack lives under infra/stacks/fusionauth-instance.

just setup_fusionauth

The root setup command calls the same stack setup through:

just infra/stacks/fusionauth-instance/setup

Local FusionAuth uses:

SettingValue
App imagefusionauth/fusionauth-app:1.62.1
Database imagepostgres:16.0-bookworm
Port9011
Runtime modedevelopment
App URL inside Dockerhttp://fusionauth:9011
Local issuerlocal.macro.com
Pulumi stacklocal

Local admin credentials documented by the stack:

username: [email protected]
password: macroIsGreat!
api-key: bf69486b-4733-4954-a44e-2e1b5f2c8a91
Create the local Pulumi stack as `local`; do not use a `macro-inc/` stack prefix for the local FusionAuth instance.

During setup, the FusionAuth recipe patches root .env with local values including:

Key
AUDIENCE
FUSIONAUTH_TENANT_ID
FUSIONAUTH_CLIENT_ID
FUSIONAUTH_CLIENT_SECRET_KEY
JWT_SECRET_KEY
ISSUER
FUSIONAUTH_BASE_URL
FUSIONAUTH_API_KEY_SECRET_KEY
FUSIONAUTH_OAUTH_REDIRECT_URI

just run_local also calls just patch_local_fusionauth_env, which patches .env again if the local Pulumi stack exists. If FusionAuth is not running, that recipe starts it temporarily to read the OAuth client secret, then stops it.

Running after installation

Start backend services:

just run_local

If service images changed, rebuild selected images while starting:

just run_local --build

By default, convert_service and search_processing_service are not required for the frontend dev path. search_processing_service is behind the processors Compose profile and is pinned to linux/amd64 because its local PDFium library is amd64-only.

Start the frontend against local services:

cd js/app
bun i
just local

Local E2E smoke setup

After installation:

just local-e2e

This uses docker-compose.local-e2e.yml overrides so services use local Postgres and LocalStack instead of shared dev assets, seeds deterministic fixture data, launches the frontend with local bearer-token auth, and runs Playwright.

Additional E2E commands:

just local-e2e-rust
just local-e2e-all
just local-e2e-ui

The local E2E seed path is guarded: it requires LOCAL_E2E_SEED=true and rejects database URLs that are not the local Docker database shape postgres://user:...@(localhost|127.0.0.1|postgres):5432/macrodb.

Cleanup and reset

CommandEffect
just stop-localdocker compose down for local services
just stop-databasesStops the database Compose file
just stop_fusionauthStops the FusionAuth Compose file
just destroyDestroys the local FusionAuth stack and runs docker compose down -v
just docker_cache_clearClears all BuildKit cache
just docker_cache_clear_targetsClears Rust target cache mounts only
just docker_cache_usageShows BuildKit cache usage

Troubleshooting

SymptomCauseFix
.env not found during local runEnvironment was not decryptedRun just get_environment or rerun just setup
Pulumi local stack not foundFusionAuth local stack has not been initializedRun just setup
LocalStack AWS commands failAWS CLI is missing or unavailableInstall/configure AWS CLI; LocalStack recipes call aws --endpoint-url=http://localhost:4566 ...
Browser cannot resolve LocalStack bucket hostnamesPresigned URLs generated inside Docker use localstack hostnamesUse local mode with LOCAL_AWS_URL; the Rust AWS helper rewrites local URLs to localhost
Different checkout changes local containersDocker resources are shared under Compose project macroStop the other checkout before starting this one
FusionAuth client secret cannot be readFusionAuth is not running or local stack is incompleteRun just setup_fusionauth or rerun just setup

Related pages

Backend, frontend, and local E2E commands after installation. Rust backend services, database setup, and deployment notes. Local FusionAuth stack behavior, Pulumi configuration, and credentials.

03. Local development quickstart

Quickstart: Bring up local services with just recipes, shared Compose resources, database initialization, and expected health signals.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/03-local-development-quickstart.md
  • Generated: 2026-06-01T00:46:27.751Z

Source Files

  • RUNNING_LOCALLY.md
  • justfile
  • docker-compose.yml
  • docker-compose-databases.yml
  • local_stack.just
  • infra/stacks/fusionauth-instance/README.md

Local development is orchestrated from the root justfile: just setup prepares .env, shared Docker resources, LocalStack, Postgres migrations, FusionAuth, and cached Rust service images; just run_local starts the Docker Compose service stack under the fixed macro Compose project.

Prerequisites

Install the tools expected by RUNNING_LOCALLY.md and the recipes:

ToolUsed for
Docker / Docker ComposeDatabases, LocalStack, FusionAuth, backend services
justRepository task runner
sopsDecrypting .env-local*.enc into .env
AWS CLICreating LocalStack SQS, DynamoDB, and S3 resources
SQLx CLICreating and migrating macrodb
PulumiCreating the local FusionAuth stack
Bun and NodeFrontend, FusionAuth stack dependencies, Playwright

If not using the repository Nix shell, export the configured SOPS KMS ARN before decrypting local environment files.

export SOPS_KMS_ARN="arn:aws:kms:us-east-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93,arn:aws:kms:us-west-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93"
The local Docker resources are intentionally frozen to the `macro` Compose project. Multiple checkouts and worktrees share the same containers, networks, and volumes. Do not run two local stacks at the same time.

Quickstart

Run the repository setup recipe from the repository root.
```bash
just setup
```

This decrypts `.env`, creates shared Docker networks and volumes, initializes LocalStack, creates and migrates the local database, configures FusionAuth, and prebuilds development service images.
Start the Compose stack.
```bash
just run_local
```

To rebuild service containers after local service changes, pass the build flag.

```bash
just run_local --build
```
In a second terminal, run the frontend against local services.
```bash
cd js/app
bun i
just local
```

`just local` runs the app with `VITE_LOCAL_SERVERS=ALL` and defaults to `PORT=3000`.

Shared Docker resources

create_networks creates the shared resources used across the root Compose file, the database Compose file, and the FusionAuth Compose file.

ResourceName
Compose projectmacro
External networksdatabases, auth
Compose service networkservices
Postgres volumemacro_postgres_data
Redis volumemacro_redis_data
OpenSearch volumemacro_opensearch_data
FusionAuth database volumefusionauth_db_data
FusionAuth config volumefusionauth_config

The root docker-compose.yml includes docker-compose-databases.yml and infra/stacks/fusionauth-instance/docker-compose.yml, so just run_local can start application services together with Postgres, Redis, OpenSearch, and FusionAuth.

What just setup initializes

StageRecipeResult
Environmentjust get_environmentDecrypts .env-local.enc into root .env; optionally patches AWS credentials from the default local AWS profile when an environment suffix is used
Docker resourcesjust create_networksCreates shared networks and volumes
Local AWSjust setup_localstackStarts localstack/localstack:4 on port 4566; creates SQS queues, DynamoDB tables, S3 buckets, bucket CORS, and the document upload finalizer notification
Local databasejust setup_local_dbsStarts Postgres and Redis, creates macrodb, runs migrations, then stops the database Compose services
FusionAuthinfra/stacks/fusionauth-instance/setupStarts FusionAuth, waits for /api/status, applies the Pulumi local stack, patches root .env, then stops FusionAuth
Service image cacherust/cloud-storage/build_dev_service_imagesBuilds macro-local-rust-services:dev and the search-processing image

The local Macro database URL used by the Rust database recipes is:

postgres://user:password@localhost:5432/macrodb

LocalStack resources

local_stack.just starts LocalStack as a standalone Docker container named localstack on the shared databases network.

TypeLocal resources
Servicessqs, dynamodb, s3
Endpointhttp://localhost:4566
Bucketsmacro-email-attachments, doc-storage, docx-upload, static-file-storage, bulk-upload-staging
DynamoDB tablesbulk-upload, connection-gateway-table, static-file-metadata
Document upload eventS3 ObjectCreated events from doc-storage are wired to document-upload-finalizer-queue

All buckets receive a local CORS policy for http://localhost:3000 through http://localhost:3009.

Service ports and health signals

Most Rust services expose container port 8080 and map it to a distinct host port.

ServiceHost portHealth check
authentication-service8080GET /health
connection_gateway8082GET /health
contacts_service8083GET /health
document_cognition_service8085GET /health
document_storage_service8086GET /health
email_service8087GET /health
notification_service8089GET /health
search_processing_service8092GET /health; behind the processors profile
static_file_service8094GET /api/health
static_file_cdn8100Nginx proxy to S3/static file service
unfurl_service8095GET /health
image_proxy_service8097GET /health
lexical_service8096GET /health
sync_service8787GET /health
websocket_service6969WebSocket endpoint; no Compose health check
FusionAuth9011GET /api/status returns {"status":"Ok"}
LocalStack4566GET /_localstack/health
Postgres5432pg_isready -U user
Redis6379, UI on 8001Container availability
OpenSearch9200, analyzer on 9600GET /

Use Docker Compose waiting when starting a subset or detached stack:

just run_local -d --wait

Frontend local service selection

js/app/justfile provides local frontend shortcuts. The default local command selects all local service hosts.

cd js/app
just local

The underlying app config reads VITE_LOCAL_SERVERS:

ValueBehavior
unsetUses remote development service hosts in development mode
ALLUses all local service hosts
comma-separated service namesUses local hosts only for the selected services
service-name:portUses a local host with a port override for that service

List recognized local service names with:

cd js/app
just local-services

Local E2E smoke path

The local smoke harness runs the backend with local-only service overrides, seeds deterministic fixtures, starts the frontend with local bearer-token auth, and runs Playwright.

just local-e2e

Related harnesses:

just local-e2e-rust
just local-e2e-all
just local-e2e-ui

The E2E path uses docker-compose.local-e2e.yml to override database, Redis, LocalStack, bucket, table, and queue environment variables so the smoke suite does not mutate shared dev assets. The seed command is guarded by LOCAL_E2E_SEED=true and rejects any DATABASE_URL outside the local Docker database shape:

postgres://user:...@(localhost|127.0.0.1|postgres):5432/macrodb

Stopping and resetting

CommandEffect
just stop-localRuns docker compose down for the root stack
just stop-databasesStops the database Compose file
just stop_fusionauthStops the FusionAuth Compose file
just destroyDestroys the local FusionAuth Pulumi stack and runs Compose down with volumes for the root stack
just docker_cache_clearClears all BuildKit build cache
just docker_cache_clear_targetsClears Rust target cache mounts only

Troubleshooting

.env not found

just run_local patches local FusionAuth values into .env. If .env is missing, run:

just get_environment

For a fresh checkout, prefer the full setup:

just setup

FusionAuth local stack not found

If patch_local_fusionauth_env reports that the Pulumi local stack is missing, run:

just setup

FusionAuth local admin defaults are:

username: [email protected]
password: macroIsGreat!
api-key: bf69486b-4733-4954-a44e-2e1b5f2c8a91

Local E2E token generation fails

Prefer the repository-level harness:

just local-e2e

If running Playwright directly, seed first and ensure .env exists:

just local-e2e-seed
cd js/app
LOCAL_E2E=true bunx playwright test

You can bypass token generation by exporting LOCAL_JWT.

Search processing on Apple Silicon

search_processing_service is pinned to linux/amd64 because its local pdfium library is AMD64-only. On Apple Silicon, enabling the processor profile uses QEMU emulation for build and runtime.

Related pages

How `VITE_LOCAL_SERVERS` maps frontend clients to local or remote service hosts. Deterministic seed data, Playwright auth, and Rust local E2E integration tests.

04. Frontend quickstart

Quickstart: Run the SolidJS app, choose local or remote services, understand app routing, and start Tauri development.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/04-frontend-quickstart.md
  • Generated: 2026-06-01T00:46:48.071Z

Source Files

  • js/app/README.md
  • js/app/package.json
  • js/app/justfile
  • js/app/packages/app/index.tsx
  • js/app/packages/app/component/Root.tsx
  • js/app/AGENTS.md

The Macro frontend is a Bun-managed Vite/SolidJS SPA in js/app; the web app serves under /app, while Tauri loads the same frontend bundle through native desktop and mobile shells.

Prerequisites

  • Bun
  • just
  • Optional: nix develop from the repository root to enter a shell with the expected toolchain
  • For Tauri: Rust, Tauri CLI, and platform SDKs for Android or iOS

Run the web app

```bash cd js/app bun i ``` ```bash bun run dev ``` ```text http://localhost:3000/app ```

bun run dev runs cd packages/app && bun run --bun dev, which starts Vite with packages/app/vite.config.ts. The dev server defaults to port 3000, binds to 0.0.0.0, uses strict port mode, and enables HMR over WebSocket.

Choose remote or local services

In development mode, service endpoints come from packages/core/constant/servers.ts.

Command or environmentService behavior
bun run devUses remote development Macro services.
VITE_LOCAL_SERVERS=ALL bun run devUses all registered localhost service URLs.
VITE_LOCAL_SERVERS=document-storage-service,email-service bun run devUses local URLs for selected services and remote development URLs for the rest.
VITE_LOCAL_SERVERS=document-storage-service:9090 bun run devUses the selected local service with a port override.
just localStarts the app with VITE_LOCAL_SERVERS=ALL and PORT=3000.

Useful shortcuts from js/app/justfile:

cd js/app

just local-services
just local
just local-dss
just local-search
just local-email
Local backend services require the repository-level local setup. The local stack is documented by `RUNNING_LOCALLY.md`; the frontend command there is `cd js/app && bun i && just local`.

App routing model

The runtime selects the router by platform:

RuntimeRouterBase
WebRouter/app
TauriHashRouter/

The canonical authenticated landing route is:

/component/inbox

The root route / preloads user info and history, stores known campaign query parameters as short-lived cookies, normalizes short IDs in the URL, then redirects:

StateResult
AuthenticatedNavigate to /component/inbox, preserving query parameters.
UnauthenticatedNavigate to /welcome, which redirects to /login.
Native mobile with login cookie and failed user-info queryShow an offline retry fallback.

The split-layout route decodes URL segments as type/id pairs:

/component/inbox
/<block-or-alias>/<id>
/component/inbox/<block-or-alias>/<id>

If no valid pair is present, the layout falls back to the inbox component.

Top-level auth and utility routes include:

PathBehavior
/loginLogin UI
/signupSignup UI
/email-signup-callbackEmail signup callback
/inbox-link-callbackEmail link callback
/login/popup/successBroadcasts login-success and closes the popup
/team-inviteTeam invite acceptance
*404Redirects native mobile to the default route; web navigates to the origin

Runtime initialization

packages/app/index.tsx performs the browser entrypoint work:

  • Imports global CSS and font packages.
  • Initializes Lexical markdown support.
  • Initializes monochrome icon behavior.
  • Sets document.documentElement.dataset.platform to web, desktop, ios, or android.
  • Tracks current input modality as keyboard, mouse, or touch.
  • In Tauri, proxies non-localhost fetch calls through @tauri-apps/plugin-http for native compatibility.
  • In development mode, wraps Root in a Solid ErrorBoundary.
  • Outside Vite HMR sessions, lazy-loads observability and listens for vite:preloadError to prompt a refresh after deployments.

Root mounts global providers for analytics, PostHog, entities, user context, query sync, undo, channels, calls, quick access, search, chat attachments, notifications, split layout routing, onboarding, and toasts.

Build and preview

cd js/app

bun run build
bun run preview

Build output is emitted from packages/app into packages/app/dist. Production-style builds use /app as the Vite base.

Additional build recipes:

cd js/app

just build-dev
just build-staging
just build-prod

Local smoke tests

Prefer the repository-level harness for deterministic local E2E:

just local-e2e

That command starts the local service subset with compose overrides, seeds deterministic data, and runs Playwright with LOCAL_E2E=true.

For Playwright UI mode:

just local-e2e-ui

Start Tauri development

The Tauri shell lives under js/app/tauri and packages the same packages/app frontend.

cd js/app/tauri

cargo tauri dev
cargo tauri android dev
cargo tauri ios dev

Tauri config uses:

Config keyValue
build.devUrlhttp://localhost:3000
build.frontendDist../../packages/app/dist
beforeDevCommandjust dev-tauri from js/app
beforeBuildCommandjust build-tauri from js/app

Set TAURI_DEV_HOST when a device or emulator needs HMR to connect to a host other than localhost.

TAURI_DEV_HOST=192.168.1.20 cargo tauri android dev

Contributor checks

Common frontend checks from js/app/AGENTS.md:

cd js/app

bun run test
bun run check
bun run lint
bun run format
bun run knip

Next

  • Use RUNNING_LOCALLY.md for backend stack setup.
  • Use js/app/AGENTS.md for frontend contribution conventions.
  • Use js/app/tauri/src-tauri/README.md for native shell development notes.

05. Local E2E smoke tests

Guide: Run deterministic Playwright and ignored Rust smoke tests against the local-only stack and shared seed fixtures.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/05-local-e2e-smoke-tests.md
  • Generated: 2026-06-01T00:49:10.463Z

Source Files

  • RUNNING_LOCALLY.md
  • justfile
  • docker-compose.local-e2e.yml
  • rust/cloud-storage/seed_cli/README.md
  • js/app/tests/e2e/fixtures/local-e2e-seed.ts
  • rust/cloud-storage/integration_tests/local_e2e/README.md
  • rust/cloud-storage/local_e2e_test_support/README.md

just local-e2e, just local-e2e-rust, and just local-e2e-all start the Macro local stack with docker-compose.local-e2e.yml overrides, reset and seed deterministic fixtures from rust/cloud-storage/seed_cli/seed, then run Playwright or ignored Rust integration smoke suites against localhost services.

Prerequisites

Run the normal local setup first:

just setup

The harness expects the same local toolchain used by the repository: Docker, Bun, Rust/Cargo, AWS CLI, SQLx tooling, Pulumi, and a decrypted .env. Local Docker resources are intentionally frozen to the macro Compose project, so do not run two local stacks from different checkouts at the same time.

The local E2E seed path is destructive for its deterministic fixture ranges. The seed command is guarded by `LOCAL_E2E_SEED=true` and rejects database URLs outside the local Docker database shape `postgres://user:...@(localhost|127.0.0.1|postgres):5432/macrodb`.

Command reference

CommandRunsNotes
just local-e2ePlaywright local smoke testsStarts LocalStack, starts the local service subset, seeds fixtures, then runs LOCAL_E2E=true bunx playwright test from js/app.
just local-e2e-uiPlaywright UI modeSame setup and seed pass, then opens Playwright UI mode.
just local-e2e-rustIgnored Rust integration testsRuns SQLX_OFFLINE=true cargo test -p local_e2e_integration_tests -- --ignored --nocapture.
just local-e2e-allRust tests, then PlaywrightStarts and seeds once, runs the ignored Rust suite, then runs Playwright.
just local-e2e-seedSeed onlyStarts local Postgres/Redis, drops and recreates Macro DB state, initializes DBs, then runs the local E2E seed scenario.

Forward Playwright file filters or flags through the just target:

just local-e2e tests/e2e/local-smoke.spec.ts
just local-e2e --project=local-chromium

For Rust filters, pass the test name after the target:

just local-e2e-rust channel_message_posts_to_http_and_delivers_to_websocket

Runtime shape

just local-e2e*
  ├─ setup_localstack
  │  ├─ LocalStack on localhost:4566
  │  ├─ SQS queues
  │  ├─ DynamoDB tables
  │  └─ S3 buckets + doc-storage notification wiring
  ├─ COMPOSE_FILE=docker-compose.yml:docker-compose.local-e2e.yml just run_local -d --wait ...
  │  ├─ Postgres localhost:5432
  │  ├─ Redis localhost:6379
  │  └─ Macro service subset on local ports
  ├─ just local-e2e-seed
  └─ Playwright and/or ignored Rust tests

The compose override pins application services to local Postgres, Redis, and LocalStack instead of shared dev assets. It also supplies deterministic bucket, queue, table, AWS credential, and region values for the local smoke stack.

The startup service subset is defined in the root justfile as:

authentication-service
connection_gateway
contacts_service
document_storage_service
email_service
notification_service
static_file_service
static_file_cdn
sync_service
websocket_service

Common local ports used by the tests include:

SurfaceDefault local endpoint
Postgrespostgres://user:password@localhost:5432/macrodb
Redisredis://localhost:6379
LocalStackhttp://localhost:4566
Authentication servicehttp://localhost:8080
Connection gateway WebSocketws://localhost:8082/
Document storage servicehttp://localhost:8086
Notification servicehttp://localhost:8089
Frontend dev serverhttp://localhost:${PORT:-3000}/app

Seed fixtures

The shared seed contract lives under rust/cloud-storage/seed_cli/seed.

FilePurpose
local_e2e/manifest.jsonStable smoke aliases: primary user email, project roadmap document, general channel, and canonical welcome message.
local_e2e/users.jsonLocal users including [email protected], [email protected], [email protected], [email protected], and [email protected].
local_e2e/reset.sqlDeletes deterministic local E2E channels, channel access rows, mentions, activity, and documents before reseeding.
documents/documents.jsonSeeded document metadata.
channels.jsonSeeded channels.
channel_messages.jsonSeeded channel messages.

The seed CLI scenario resets fixture ranges, inserts users and verification rows, then seeds documents, channels, and channel messages. A successful seed prints:

Local e2e smoke seed data ready for macro|[email protected]

Playwright local smoke suite

When LOCAL_E2E=true, js/app/playwright.config.ts switches to a local-only browser project named local-chromium. The Playwright web server command sets:

PORT=${PORT:-3000}
VITE_LOCAL_SERVERS=ALL
VITE_ENABLE_BEARER_TOKEN_AUTH=true
LOCAL_JWT=<generated token>
bun run dev

If LOCAL_JWT is not already exported, Playwright runs js/app/scripts/generate-local-e2e-token.ts. That script calls the Rust local_e2e_test_support binary generate_local_e2e_token, which loads the shared seed user and signs a Macro API token using .env or process environment values. The default local token lifetime is eight hours.

The local Playwright specs skip unless LOCAL_E2E=true:

SpecMain verification
local-smoke.spec.tsDocuments list shows the seeded Project Roadmap; channels list and channel page show the seeded general channel and welcome message.
local-sidebar.spec.tsSidebar routes open expected list views and show seeded entities where applicable.
local-channel-actions.spec.tsSends a message, reacts with 👍, and opens the reply composer in the seeded channel.

Tests import localE2ESeed from js/app/tests/e2e/fixtures/local-e2e-seed.ts. That fixture loader reads the Rust seed JSON files directly and exposes raw arrays, lookup maps, and smoke aliases.

Rust local E2E suite

The Rust suite lives in rust/cloud-storage/integration_tests/local_e2e and is intentionally marked #[ignore] so normal workspace test runs do not require Docker services. Run it through:

just local-e2e-rust

The crate uses local_e2e_test_support to load the same seed files, generate local JWTs, and resolve local service endpoints. The support crate rejects non-local service URLs for mutating tests.

Supported local overrides include:

Environment variableDefaultConstraint
LOCAL_E2E_DOCUMENT_STORAGE_URLhttp://localhost:8086Must use http or https and a localhost host.
LOCAL_E2E_CONNECTION_GATEWAY_WS_URLws://localhost:8082/Must use ws or wss and a localhost host.
LOCAL_E2E_NOTIFICATION_URLhttp://localhost:8089Must use http or https and a localhost host.
LOCAL_E2E_DATABASE_URLpostgres://user:password@localhost:5432/macrodbUsed by Rust verification queries.
LOCAL_E2E_CHANNELS_BASE_URL${document_storage_url}/channelsChannel mutation API under test.
LOCAL_E2E_CHANNELS_READ_BASE_URL${document_storage_url}/channelsChannel read API under test.

The ignored Rust tests cover channel mutations, persistence checks, notifications/contacts side effects where workers are available, and connection gateway WebSocket delivery.

Running Playwright directly

Prefer the repository-level harness. If you need a manual loop, start and seed the stack first:

AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 just setup_localstack
COMPOSE_FILE=docker-compose.yml:docker-compose.local-e2e.yml just run_local -d --wait authentication-service connection_gateway contacts_service document_storage_service email_service notification_service static_file_service static_file_cdn sync_service websocket_service
just local-e2e-seed
cd js/app
LOCAL_E2E=true bunx playwright test

You can bypass token generation by exporting LOCAL_JWT, but the generated token path is the default and keeps Playwright aligned with the Rust helper.

Troubleshooting

Failed to generate LOCAL_JWT for LOCAL_E2E Playwright

Ensure .env exists from just get_environment or just setup, then rerun the repo-level harness:

just local-e2e

If running Playwright directly, seed first with just local-e2e-seed. As a temporary bypass, export LOCAL_JWT.

Seed refuses to run

Use just local-e2e-seed rather than invoking the seed CLI manually. The recipe sets LOCAL_E2E_SEED=true, local AWS values, and the expected local database URL.

Tests refuse a service URL

Rust helpers reject non-local service URLs. Check LOCAL_E2E_DOCUMENT_STORAGE_URL, LOCAL_E2E_CONNECTION_GATEWAY_WS_URL, and LOCAL_E2E_NOTIFICATION_URL; they must point at localhost or 127.0.0.1.

Local stack conflicts across checkouts

The Compose project name, networks, and volumes are shared as macro. Stop the current stack before starting another checkout:

just stop-local
just stop-databases

Adding local E2E coverage

  1. Put stable shared fixture rows in rust/cloud-storage/seed_cli/seed.
  2. Add only aliases to local_e2e/manifest.json; keep the source data in the fixture files.
  3. Use localE2ESeed in Playwright and LocalE2eSeed in Rust instead of duplicating IDs.
  4. Keep Playwright specs gated by LOCAL_E2E=true.
  5. Keep Rust integration tests marked #[ignore].
  6. Use generated unique text or IDs for mutating assertions so repeated smoke runs can coexist with the deterministic base seed.

Related pages

  • RUNNING_LOCALLY.md
  • rust/cloud-storage/seed_cli/README.md
  • rust/cloud-storage/integration_tests/local_e2e/README.md
  • rust/cloud-storage/local_e2e_test_support/README.md

06. Monorepo map

Concept: Workspace boundaries, package managers, major runtime folders, generated documentation, and where source-of-truth manifests live.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/06-monorepo-map.md
  • Generated: 2026-06-01T00:50:29.936Z

Source Files

  • rust/cloud-storage/Cargo.toml
  • rust/sync-service/Cargo.toml
  • js/package.json
  • js/app/package.json
  • infra/README.md
  • docs/docs.json

Macro is organized as several adjacent workspaces rather than one repository-wide package workspace: Rust backend services live under rust/cloud-storage, the Cloudflare Durable Object sync worker lives under rust/sync-service, the Solid/Tauri app and JS services live under js, Pulumi infrastructure lives under infra, and the Mintlify documentation site lives under docs.

Repository boundary map

.
├── justfile                         # Local orchestration entrypoint
├── docker-compose*.yml              # Local service, database, and E2E topology
├── flake.nix                        # Nix shells and cached Rust/API build jobs
├── rust/
│   ├── rust-toolchain.toml          # Rust toolchain source of truth
│   ├── cloud-storage/               # Main Rust Cargo workspace
│   └── sync-service/                # Cloudflare Worker/Durable Object sync runtime
├── js/
│   ├── package.json                 # Bun workspace for app + selected JS packages
│   ├── app/                         # Solid app, package libraries, tests, Tauri frontend
│   ├── lexical-core/                # Shared Lexical package
│   ├── lexical-service/             # Wrangler service for Lexical conversion
│   ├── websocket-service/           # Standalone Bun WebSocket service
│   └── analytics-proxy/             # Standalone Wrangler proxy
├── infra/
│   ├── package.json                 # Bun workspace for Pulumi stacks/packages
│   ├── stacks/                      # Deployable Pulumi projects
│   └── packages/                    # Shared Pulumi resource libraries
└── docs/
    ├── docs.json                    # Rendered docs navigation/config
    ├── package.json                 # Mintlify scripts
    └── AI/mcp/tools/                # Generated MCP tool reference pages
The repository root is orchestration-oriented. Do not infer JavaScript workspace membership from the root `package.json`; the active JS workspaces are declared in `js/package.json`, `js/app/package.json` scripts, and `infra/package.json`.

Package managers and workspace manifests

BoundarySource-of-truth manifestPackage manager/toolingLock/config filesScope
Rust backend servicesrust/cloud-storage/Cargo.tomlCargo, just, SQLxrust/cloud-storage/Cargo.lock, .sqlx/, rust/rust-toolchain.tomlAxum services, Lambda handlers, model crates, service clients, database migrations
Sync workerrust/sync-service/Cargo.toml, rust/sync-service/wrangler.tomlCargo, Wrangler, npm for Docker installrust/sync-service/Cargo.lock, package.json, D1 migrationsCloudflare Worker with Durable Objects, D1, R2, KV, Bebop-generated bindings
JS app workspacejs/package.json, js/app/package.jsonBun 1.3.5 at js/js/bun.lock, js/app/bun.lock, js/app/biome.jsonc, js/app/tsconfig.jsonSolid app, package libraries, generated service clients, Playwright/Vitest tests
Tauri appjs/app/tauri/Cargo.toml, js/app/tauri/src-tauri/tauri.conf.jsonCargo, Tauri CLI, Bun build hooksjs/app/tauri/Cargo.lockDesktop/mobile shell around js/app/packages/app/dist
Infrastructureinfra/package.jsonBun 1.2.0, Pulumi, TypeScriptinfra/bun.lock, infra/tsconfig.json, per-stack Pulumi*.yamlAWS infrastructure stacks and shared resource packages
Docsdocs/docs.json, docs/package.jsonBun, Mintlify CLIgenerated docs/config/tool-pages.jsonProduct docs and generated MCP tool reference
Nix shells/buildsflake.nixNix, Fenix, Craneflake.lockReproducible Rust, JS app, Pulumi, SQLx, Bun, pnpm, and Tauri tools

Nested package-manager exceptions

  • js/loro-mirror/package.json has pnpm scripts and pnpm overrides. It is included by the js/package.json workspace list, but its own scripts call pnpm.
  • js/websocket-service and js/analytics-proxy have package manifests but are not listed in the js/package.json workspace array.
  • infra/stacks/web-app has a Pulumi project, but infra/tsconfig.json explicitly excludes stacks/web-app from the infra TypeScript check.

Major runtime folders

FolderRuntime rolePrimary manifests
rust/cloud-storageMain Rust backend workspace for services such as authentication, document storage, document cognition, email, notifications, MCP, search, static files, image proxy, unfurling, and Lambda-style workersCargo.toml, justfile, database.just, sqlx.just, crate-level Cargo.toml files
rust/cloud-storage/macro_db_clientMacroDB client and SQL migrationsCargo.toml, migrations/*.sql, crate justfile
rust/cloud-storage/ai_toolsRust AI/MCP tool registry used by app and docs generatorsCargo.toml, src/, generated schemas/tools.json after running schema generation
rust/sync-serviceCloudflare Worker sync service using Durable Objects, D1, R2, KV, and Bebop bindingsCargo.toml, wrangler.toml, Dockerfile, database/user-peer-mapping/migrations, bebop/schema.bop
js/app/packages/appSolid/Vite frontend app packagepackage.json, vite.config.ts, tsconfig.json
js/app/packages/service-clientsGenerated and checked-in frontend API clientsorval.config.ts, per-service openapi.json, generated client/schema folders
js/app/tauriTauri Rust workspace and plugins for desktop/mobile packagingworkspace Cargo.toml, plugin crates, src-tauri/tauri.conf.json
js/lexical-serviceWrangler service for Lexical document conversionpackage.json, wrangler scripts
infra/stacks/*Deployable Pulumi projects; run Pulumi from inside a stack directoryPulumi.yaml, Pulumi.dev.yaml, Pulumi.prod.yaml, index.ts
infra/packages/*Shared Pulumi abstractions for services, VPC, Lambda, resources, and stack constantspackage src/index.ts exports
docsMintlify documentation sitedocs.json, MDX files, generator scripts
There are two Rust packages named `sync_service` in different workspaces: `rust/sync-service` is the Cloudflare Worker runtime, while `rust/cloud-storage/sync_service` is a crate inside the backend workspace. Keep workspace-relative paths explicit when changing sync code.

Backend Cargo workspace

rust/cloud-storage/Cargo.toml owns the backend Cargo workspace membership and shared dependency versions. Most backend crates set publish = false and depend on sibling crates by relative path.

Common commands run from rust/cloud-storage:

just check          # SQLX_OFFLINE=true cargo check with warnings denied
just clippy         # workspace clippy with all features
just format         # cargo fmt
just prepare_db     # refresh SQLx offline cache through sqlx.just
just build          # SQLX_OFFLINE=true cargo build

Database lifecycle commands are routed through rust/cloud-storage/macro_db_client/justfile:

just macro_db_client/create_db
just macro_db_client/migrate_db
just macro_db_client/drop_db -y -f

MacroDB schema migrations live under rust/cloud-storage/macro_db_client/migrations. Several worker/test crates also carry local migrations for their own test schemas.

Sync worker boundary

rust/sync-service is a separate Cargo project compiled to WebAssembly for Cloudflare Workers. Its wrangler.toml defines:

BindingPurpose
DOCUMENT_SYNC_SESSIONDurable Object class
USER_PEER_MAPPINGD1 database with migrations under database/user-peer-mapping/migrations
DOCUMENT_SNAPSHOT_BUCKETR2 snapshot bucket
DOCUMENT_VERSIONING_KVKV namespace
SNAPSHOT_STORE_KVKV namespace
INTERNAL_API_SECRET_KEY, SPS_URL, ENVIRONMENTWorker runtime variables per environment

The Dockerfile installs Node 22, worker-build, Wrangler dependencies, builds Bebop TypeScript bindings from bebop/schema.bop, applies local D1 migrations, and starts Wrangler on port 8787.

Frontend and app packages

js/package.json declares the Bun workspace boundary:

[
  "app",
  "app/packages/*",
  "app/infra/*",
  "loro-mirror",
  "lexical-core",
  "lexical-service"
]

js/app/package.json is the app-level command surface:

cd js/app
bun run dev          # Vite app through packages/app
bun run build        # Vite production-style build
bun run test         # Vitest
bun run local:e2e    # Playwright with LOCAL_E2E=true
bun run check        # TypeScript + Biome
bun run gen-api      # Rust OpenAPI binaries -> service-clients
bun run gen-tools    # Rust AI tool schemas -> DCS tool types

js/app/tsconfig.json includes packages/**/*, ../loro-mirror/**/*, and ../lexical-core/**/*, and defines path aliases for generated service clients such as @service-storage/*, @service-cognition/*, @service-email/*, and @service-sync/*.

Tauri packaging

The Tauri workspace lives in js/app/tauri. src-tauri/tauri.conf.json points frontendDist at ../../packages/app/dist, uses http://localhost:3000 for development, and delegates build hooks back to the app justfile:

{
  "beforeDevCommand": { "script": "just dev-tauri", "cwd": "../.." },
  "beforeBuildCommand": { "script": "just build-tauri", "cwd": "../.." }
}

Use js/app/justfile for app-local run modes:

cd js/app
just local                    # Vite against local services
just local-dcs                # Local cognition service only
just local-dss                # Local document storage service only
just build-prod               # Production-mode frontend bundle
just dist-archive             # Zip dist for native_app_server tests

Infrastructure workspace

Infrastructure code is TypeScript Pulumi under infra. The root infra/package.json declares Bun workspaces for stacks/* and packages/*, requires Node >=21, and enforces Bun through a preinstall script.

Run Pulumi from inside a stack directory:

cd infra/stacks/document-storage
pulumi up --stack dev
pulumi up --stack prod

Operational defaults from the infra README:

SettingValue
Cloud providerAWS
Main AWS regionus-east-1
Logging providerDatadog
Datadog regionus-central-1

Shared stack constants and exported helpers live in infra/packages/shared/src/index.ts; reusable resource constructors live in infra/packages/resources/src/index.ts.

Local orchestration

The root justfile is the local runtime entrypoint. It freezes Docker Compose resources under the macro project name so multiple checkouts share the same local containers, networks, and volumes.

Primary commands:

just setup          # Environment, networks, LocalStack, DBs, FusionAuth, Rust images
just run_dbs -d     # Postgres + Redis
just run_local      # Docker Compose services
just run_local --build
just stop-local
just local-e2e
just local-e2e-rust
just local-e2e-all

Local service topology is defined by:

FileRole
docker-compose.ymlRust services, sync worker, lexical service, WebSocket service, FusionAuth include, service networks
docker-compose-databases.ymlPostgres/pgvector, Redis Stack, OpenSearch, external volumes
docker-compose.local-e2e.ymlLocal-only overrides for deterministic E2E data and LocalStack resources
local_stack.justLocalStack setup recipes imported by the root justfile

The local E2E harness seeds deterministic data through rust/cloud-storage/seed_cli and runs Playwright from js/app.

Generated code and documentation

Generated surfaceCommandSource of truthOutput
OpenAPI JSON and frontend clientscd js/app && bun scripts/generate-api-schema.tsRust service OpenAPI binaries built from rust/cloud-storagejs/app/packages/service-clients/service-*/openapi.json and generated/ folders
OpenAPI freshness checkcd js/app && bun scripts/generate-api-schema.ts --checkSame as aboveFails if generated files differ from Git
DCS tool TypeScript/Zod typescd js/app && bun run gen-toolsrust/cloud-storage/ai_tools and gen_tool_schemasjs/app/packages/service-clients/service-cognition/generated/tools
MCP docs pagescd docs && bun run generate:toolsRust AI tool schemas from rust/cloud-storage/ai_toolsdocs/AI/mcp/tools/*.mdx, docs/config/tool-pages.json
Docs navigationedit docs/docs.jsonMintlify configRendered docs tabs/groups/pages
AI tool references are generated from local Rust schema code and checked-in files. The generation path does not require a specific model provider or hosted AI connector.

Source-of-truth checklist

When changing a boundary, update the owning manifest first:

ChangeUpdate firstThen verify
Add a Rust backend craterust/cloud-storage/Cargo.tomlcd rust/cloud-storage && just check
Add a Rust service OpenAPI clientRust service *_openapi binary and js/app/scripts/services.tscd js/app && bun scripts/generate-api-schema.ts --check
Add a database migrationrust/cloud-storage/macro_db_client/migrationsjust rust/cloud-storage/macro_db_client/migrate_db or local setup recipe
Add a frontend packagejs/app/packages/<name>/package.jsoncd js/app && bun run check
Add a Tauri pluginjs/app/tauri/Cargo.toml workspace membersTauri dev/build command
Add a Pulumi stackinfra/stacks/<stack>/Pulumi.yaml and infra/package.json workspace matchcd infra && bun run check unless intentionally excluded
Add generated MCP docsRust tool registry and docs generatorcd docs && bun run generate:tools && bun run lint
Change local runtime wiringdocker-compose*.yml and root justfilejust run_local --build or just local-e2e

Related pages

Commands and environment notes for running Macro locally. Product-facing documentation for Macro MCP surfaces. Generated tool pages derived from the Rust tool registry.

07. Runtime environments and service URLs

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

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/07-runtime-environments-and-service-urls.md
  • Generated: 2026-06-01T00:49:31.354Z

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

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

SurfaceSelectorAccepted valuesDefault behavior
Rust servicesENVIRONMENTprod, dev, localEnvironment::new_or_prod() falls back to Production if the value is missing or invalid.
Rust local app URLFRONTEND_PORTport stringDefaults to 3000 for local app redirects and local app URL construction.
Frontend runtimeMODE via Vite configcommonly development or productiondevelopment enables local-server selection logic; any other mode uses remote hosts.
Frontend local overridesVITE_LOCAL_SERVERSempty, ALL, or comma-separated service namesEmpty in development still points at remote dev services.
Client/tool generationMODE, LOCAL_BACKENDproduction, local, or fallbackMODE=production selects prod schema URLs; MODE=local or LOCAL_BACKEND=true selects local; otherwise dev.
For Rust services, missing `ENVIRONMENT` does not mean local. It resolves as production through `new_or_prod()`.

Backend environment model

The backend environment enum has three variants:

VariantENVIRONMENT stringDisplay stringMeaning in URL resolution
ProductionprodprodPublic production domains.
DevelopdevdevShared dev domains, usually with -dev hostnames.
LocallocallocalLocalhost 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.

MethodProductionDevelopLocal
app()https://macro.comhttps://dev.macro.comhttp://localhost:${FRONTEND_PORT:-3000}
auth_service()https://auth-service.macro.comhttps://auth-service-dev.macro.comhttp://localhost:8080
pdf_service()https://pdf-service.macro.comhttps://pdf-service-dev.macro.comhttp://localhost:4567
document_storage_service()https://cloud-storage.macro.comhttps://cloud-storage-dev.macro.comhttp://localhost:8086
websocket_service()wss://services.macro.comwss://services-dev.macro.comws://localhost:6969
cognition_service()https://document-cognition.macro.comhttps://document-cognition-dev.macro.comhttp://localhost:8085
connection_gateway()wss://connection-gateway.macro.comwss://connection-gateway-dev.macro.comws://localhost:8082
notification_service()https://notifications.macro.comhttps://notifications-dev.macro.comhttp://localhost:8089
static_file_service()https://static-file-service.macro.comhttps://static-file-service-dev.macro.comhttp://localhost:8100
unfurl_service()https://unfurl-service.macro.comhttps://unfurl-service-dev.macro.comhttp://localhost:8095
contacts_service()https://contacts.macro.comhttps://contacts-dev.macro.comhttp://localhost:8083
email_service()https://email-service.macro.comhttps://email-service-dev.macro.comhttp://localhost:8087
image_proxy_service()https://image-proxy.macro.comhttps://image-proxy-dev.macro.comhttp://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.MODERemote suffixExample
development-devhttps://cloud-storage-dev.macro.com
any other modenonehttps://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_SERVERSResult
unset or emptyUse remote dev hosts.
ALLUse every local frontend host.
document-storage-service,email-serviceUse local hosts for those names and remote dev hosts for the rest.
document-storage-service:9000Use the local URL for that service but replace its port with 9000.
unknown service nameThrow Error("unknown server name ...").

Example:

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.

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.

Sync service host selection

The sync service has a separate host map:

ModeWorker URLWebSocket URL
Localhttp://localhost:8787ws://localhost:8787
Development remotehttps://sync-service-dev3.macroverse.workers.devwss://sync-service-dev3.macroverse.workers.dev
Production remotehttps://sync-service-prod2.macroverse.workers.devwss://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:

ServiceLocal port
authentication-service8080
connection_gateway8082
contacts_service8083
document_cognition_service8085
document_storage_service8086
email_service8087
notification_service8089
static_file_service8094
static_file_cdn8100
unfurl_service8095
image_proxy_service8097
websocket_service6969
sync_service8787

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:

VITE_LOCAL_SERVERS=ALL VITE_ENABLE_BEARER_TOKEN_AUTH=true bun run dev

The repository-level harness is:

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:

RuleBehavior
CredentialsEnabled.
MethodsGET, POST, PUT, PATCH, DELETE, OPTIONS.
Base headersauthorization, content-type.
Extra headersx-permissions-token, traceparent, tracestate.
Additional headersPer-service callers can pass extra HeaderName values.
Static originsBuilt-in Macro origins include localhost dev, dashboard, dev, staging, production, Tauri, and Apollo testing origins.
ALLOWED_ORIGINSComma-separated env var that replaces the static origin list.
Preview originsHTTPS origins ending in preview.macro.com are allowed.
Localhost dev portshttp://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:

Add a method or branch in `macro_service_urls` and extend the parse tests. Add the key to both local and remote maps in `SERVER_HOSTS`; keep the `Servers` type satisfied. Expose a stable local port and align the frontend local map with the intended browser-facing endpoint. Add or adjust the entry in `scripts/services.ts` and the Orval project in `packages/service-clients/orval.config.ts`. Confirm the app origin is covered by default rules, `ALLOWED_ORIGINS`, the preview-origin predicate, or the localhost `3000-3999` rule.

Troubleshooting

SymptomLikely causeCheck
Local Rust service redirects to macro.comENVIRONMENT is missing or invalid and fell back to production.Set ENVIRONMENT=local.
Vite dev server calls shared dev servicesVITE_LOCAL_SERVERS is empty.Use VITE_LOCAL_SERVERS=ALL or a comma-separated service list.
Only one local service should be overriddenALL is too broad.Use VITE_LOCAL_SERVERS=service-name or service-name:port.
Browser CORS failure on a local frontend portPort 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 syncToken came from local DSS while sync is remote.Use SYNC_PERMISSION_TOKEN_DSS_HOST behavior instead of hard-coding SERVER_HOSTS['document-storage-service'].

08. Service topology

Concept: Rust services, workers, queues, storage dependencies, ports, and deployment boundaries used by the local and cloud stacks.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/08-service-topology.md
  • Generated: 2026-06-01T00:50:24.621Z

Source Files

  • docker-compose.yml
  • rust/cloud-storage/Cargo.toml
  • rust/cloud-storage/AGENTS.md
  • infra/stacks/document-storage/index.ts
  • infra/packages/resources/src/index.ts
  • infra/packages/shared/src/ai_tools.ts

macro-inc/macro runs the cloud-storage surface as a Rust Cargo workspace deployed either as local Docker Compose containers or as Pulumi-managed AWS ECS/Fargate services, Lambda handlers, SQS queues, S3 buckets, DynamoDB tables, Redis caches, OpenSearch, and Postgres databases.

Runtime boundaries

The repository has two primary topology descriptions:

BoundarySource of truthRuntime shape
Local stackdocker-compose.yml, docker-compose-databases.yml, docker-compose.local-e2e.ymlDocker services on bridge networks, one shared Rust service image, local Postgres/Redis/OpenSearch, FusionAuth compose include
Cloud stackinfra/stacks/**, infra/packages/resources/**, infra/packages/shared/**Pulumi stacks that create ECS/Fargate services, ALBs, Route53 records, SQS queues, Lambda handlers, S3 buckets, DynamoDB tables, Redis, OpenSearch, and RDS
Rust service workspacerust/cloud-storage/Cargo.tomlOne workspace containing HTTP services, queue workers, Lambda handlers, clients, models, and shared infrastructure crates
Most Rust HTTP binaries default to `PORT=8080`. Local Compose maps each container port to a distinct host port; cloud ECS services keep container port `8080` behind HTTPS ALBs.

Local Docker topology

docker-compose.yml freezes the Compose project name to macro, includes the database and FusionAuth compose files, and builds macro-local-rust-services:dev from rust/cloud-storage/Dockerfile.dev.

The dev Dockerfile builds a bundle of standard Rust binaries into /app/out/* and each service chooses a command from that bundle. search_processing_service uses a separate Dockerfile and is pinned to linux/amd64 because its Pdfium library is amd64-only.

Compose serviceHost portContainer command or runtimeMain dependencies
authentication-service8080/app/out/authentication_servicePostgres, FusionAuth, Redis
connection_gateway8082/app/out/connection_gateway_serviceRedis
contacts_service8083/app/out/contacts_servicePostgres/Redis via env
document_cognition_service8085/app/out/document_cognition_serviceDocument storage, email, static file, sync, lexical
document_storage_service8086/app/out/document_storage_serviceRedis, connection gateway, auth
document_upload_finalizernone/app/out/document_upload_finalizer_local_workerPostgres, sync, lexical; polls LocalStack SQS
email_service8087/app/out/email_serviceAuth, document storage, connection gateway, static file, Redis
notification_service8089/app/out/notification_serviceRedis, cognition, auth, connection gateway, document storage
search_processing_service8092Dockerfile.search_processing_service.devEmail service; profile processors
static_file_service8094/app/out/static_file_serviceDynamoDB/S3-style env
static_file_cdn8100nginx:alpineRoutes /file/* to LocalStack S3 and /api/* to static_file_service
unfurl_service8095/app/out/unfurl_serviceNo Compose dependency
image_proxy_service8097/app/out/image_proxy_serviceNo Compose dependency
websocket_service6969JS WebSocket serviceServices network
sync_service8787Rust sync-service DockerfileInternal API/document permission secrets
lexical_service8096Bun/JS lexical-service DockerfileSync service

Local infrastructure containers

docker-compose-databases.yml provides:

ServicePortsNotes
postgres5432pgvector/pgvector:pg16, external volume macro_postgres_data
redis6379, 8001redis/redis-stack:latest, external volume macro_redis_data
search9200, 9600OpenSearch single-node with security disabled
fusionauth9011Included from infra/stacks/fusionauth-instance/docker-compose.yml; uses its own Postgres container

Local networks separate service traffic:

NetworkPurpose
servicesService-to-service HTTP/WebSocket traffic
databasesPostgres, Redis, OpenSearch, LocalStack-style AWS endpoints
authFusionAuth access from the auth service
auth-internalFusionAuth internal database link
The local document upload finalizer defaults `LOCAL_AWS_URL` to `http://localstack:4566`, but the base Compose files do not define a `localstack` service. Local E2E overrides assume that endpoint exists and provide deterministic bucket, table, and queue names.

Cloud service boundary

Cloud services are provisioned with Pulumi stacks under infra/stacks. Public Rust HTTP services typically follow the same pattern:

  1. Build a service-specific ECR image from rust/cloud-storage/Dockerfile with SERVICE_NAME.
  2. Create an ECS/Fargate service in private subnets.
  3. Put an Application Load Balancer in public subnets when isPrivate: false.
  4. Terminate HTTPS on port 443; redirect HTTP 80 to HTTPS.
  5. Forward ALB traffic to container port 8080.
  6. Attach service-specific IAM policies for Secrets Manager, SQS, S3, DynamoDB, SNS, Lambda, or OpenSearch.
flowchart LR
  subgraph Local["Local Compose"]
    BrowserLocal["Host/browser"]
    ComposePorts["Host port mappings"]
    RustBundle["macro-local-rust-services:dev"]
    LocalDB["Postgres + Redis + OpenSearch"]
    Fusion["FusionAuth"]
    LocalS3["LocalStack-compatible AWS endpoint"]
  end

  subgraph Cloud["Pulumi AWS stacks"]
    Route53["Route53 + HTTPS ALB"]
    ECS["ECS/Fargate Rust services"]
    Workers["ECS workers + Lambda handlers"]
    Queues["SQS queues + DLQs"]
    Storage["S3 buckets + DynamoDB"]
    Data["RDS MacroDB + Redis + OpenSearch"]
  end

  BrowserLocal --> ComposePorts --> RustBundle
  RustBundle --> LocalDB
  RustBundle --> Fusion
  RustBundle --> LocalS3

  Route53 --> ECS
  ECS --> Data
  ECS --> Storage
  ECS --> Queues
  Queues --> Workers
  Storage --> Workers

Core cloud stacks

StackMain resourcesDeployment boundary
document-storageVersioned document S3 bucket, lifecycle rules, replication bucket, ECS cluster cloud-storage-cluster-${stack}Shared storage and cluster foundation for many Rust services
cloud-storage-servicedocument_storage_service ECS service, DOCX upload bucket, delete document/chat queues, docx unzip Lambda, document upload finalizer LambdaMain document API plus document upload/conversion event handlers
document-storage-bucket-integrationsEventBridge S3 Object Created rules and DLQsConnects document bucket events to search upload, text extraction, and upload finalization Lambdas
document-cognition-servicedocument_cognition_service ECS serviceAI/document cognition API and tool host
email-serviceemail_service ECS API, email-service-pubsub-workers ECS worker service, Redis, attachment bucket, CloudFront, many SQS queues, refresh/scheduled LambdasEmail API plus queue consumers
connection-gatewayconnection_gateway ECS service, Redis, DynamoDB connection tableRealtime connection tracking and message gateway
contacts-servicecontacts_service ECS service, contacts queueContact API plus SQS/outbox workers
notification-servicenotification_service ECS service, notification queues, SNS platform applications, push event handlerNotification API and push/event workers
static-file-servicestatic_file_service ECS service, static-file S3 bucket, CloudFront, DynamoDB metadata table, S3 event queue, image optimizer LambdaStatic file API/CDN boundary
search-processing-servicesearch_processing_service ECS service, backfill job DynamoDB tableSearch indexing workers plus backfill API
convert-serviceconvert_service ECS service, convert queueLibreOffice-based conversion API/worker
opensearchOpenSearch domainSearch index storage
macrodbRDS Postgres primary and read replicaMacroDB storage boundary

Service URLs and stack routing

Cloud service URL values are centralized in ServiceUrl and resolved by stack (dev or prod). Pulumi stacks inject these values as environment variables instead of hard-coding peer endpoints inside Rust binaries.

Env keyDev URLProd URL
DOCUMENT_STORAGE_SERVICE_URLhttps://cloud-storage-dev.macro.comhttps://cloud-storage.macro.com
AUTHENTICATION_SERVICE_URLhttps://auth-service-dev.macro.comhttps://auth-service.macro.com
CONNECTION_GATEWAY_URLhttps://connection-gateway-dev.macro.comhttps://connection-gateway.macro.com
DOCUMENT_COGNITION_SERVICE_URLhttps://document-cognition-dev.macro.comhttps://document-cognition.macro.com
EMAIL_SERVICE_URLhttps://email-service-dev.macro.comhttps://email-service.macro.com
STATIC_FILE_SERVICE_URLhttps://static-file-service-dev.macro.comhttps://static-file-service.macro.com
NOTIFICATION_SERVICE_URLhttps://notifications-dev.macro.comhttps://notifications.macro.com
SYNC_SERVICE_URLhttps://sync-service-dev3.macroverse.workers.devhttps://sync-service-prod2.macroverse.workers.dev
LEXICAL_SERVICE_URLhttps://lexical-service-dev.macroverse.workers.devhttps://lexical-service.macroverse.workers.dev

Storage dependencies

StorageLocal implementationCloud implementationPrimary consumers
MacroDBPostgres pgvector/pgvector:pg16 on 5432RDS Postgres, plus read replica in macrodb stackDocument storage, auth, email, contacts, notification, cognition, search processing
RedisRedis Stack on 6379ElastiCache Redis or secret-backed shared cache, depending on stackAuth sessions/rate limits, connection gateway, email rate limits, contacts, notification
OpenSearchSingle-node OpenSearch on 9200OpenSearch domain with prod VPC placement and multi-AZ settingsSearch processing, document storage search paths
Document filesLocalStack-compatible S3 endpoint when configuredVersioned S3 bucket from document-storage stackDocument storage, search/text extraction, upload finalizer
Static filesLocalStack bucket through static_file_cdnstatic-file-storage-${stack} S3 bucket + CloudFrontStatic file service, image optimizer
Connection stateLocal DynamoDB-style table name from env overridesDynamoDB connection table in connection-gatewayConnection gateway, cognition realtime integration
Metadata tablesLocal DynamoDB-style env namesDynamoDB tables such as static-file-metadata-${stack} and search backfill jobsStatic file service, search processing

Queues, workers, and event flows

Queue or eventProducerConsumerNotes
search-event-queue-${stack}Document storage, email, cognition, auth flowssearch_processing_serviceCloud queue has DLQ and production backlog/age alarms
Document text extractor queueDocument cognitiondocument_text_extractor LambdaLambda queue has DLQ and event source mapping with batch size 1
Document bucket Object CreatedS3 EventBridgeSearch upload Lambda, text extractor Lambda, document upload finalizer Lambdadocument-storage-bucket-integrations creates EventBridge rules and DLQs
document-upload-finalizer-queue localLocalStack S3 notificationdocument_upload_finalizer_local_workerLocal worker long-polls SQS, processes up to 10 messages, deletes only after success
convert-service-queue-${stack}DOCX unzip/document flowsconvert_serviceConvert service also exposes HTTP health/API and runs a queue worker unless built with disable_worker
Email queuesGmail webhooks, scheduled sends, backfill, SFS mapping, link manageremail_service, email-service-pubsub-workers, refresh/scheduled LambdasEmail stack owns multiple queues and exports names for other stacks
contacts-queue-${stack}Document/email/channel flowscontacts_service workerContacts service also runs an outbox worker
Notification ingress/egress queuesDocument/email/cognition/auth flowsnotification_serviceNotification service also integrates SNS platform applications
Static file S3 event queueStatic file bucket notificationsstatic_file_serviceUsed for static file metadata/event handling

Document upload and processing path

sequenceDiagram
  participant Client
  participant DSS as document_storage_service
  participant S3 as document-storage S3 bucket
  participant EB as EventBridge/SQS
  participant Finalizer as document_upload_finalizer
  participant Extractor as document_text_extractor
  participant Search as search_processing_service
  participant OS as OpenSearch

  Client->>DSS: Request document upload/update
  DSS->>S3: Write object or issue presigned upload path
  S3-->>EB: Object Created
  EB-->>Finalizer: Finalize versioned object
  EB-->>Extractor: Extract document text
  DSS-->>EB: Publish search event when applicable
  EB-->>Search: Search event queue message
  Search->>S3: Read source content
  Search->>OS: Index searchable representation

Local development replaces the EventBridge Lambda path with the document_upload_finalizer_local_worker polling a LocalStack SQS queue. Cloud uses EventBridge rules and Lambda targets with DLQs.

AI tool hosting topology

getAiToolsInfra() defines the shared infrastructure contract for services that host the Rust ai_tools crate. It returns:

FieldMeaning
envVarsService URLs, bucket names, queue names, CloudFront signer names, and internal auth values required by ai_tools context construction
secretArnsSecrets Manager ARNs for sync auth, CloudFront signing, and MCP credentials
queueArnsQueue access required by hosted tools, including email scheduled queue
bucketArnsDocument storage and DOCX upload bucket permissions

getAiToolsServiceRoleArns() centralizes the IAM role ARNs for tool-hosting services such as MCP server, document cognition, and agent schedule service. Resource-side policies can grant access to that set without coupling the topology to one model provider.

The AI tool infrastructure contract is provider-neutral at the topology layer: services receive AWS resources, service URLs, secrets, queues, and buckets. Model-provider keys are service-specific environment or secret inputs, not a requirement of the shared tool-hosting boundary.

Health checks and verification signals

RuntimeHealth path
Most Rust HTTP services/health
static_file_service/api/health
FusionAuth local container/api/status
sync_service/health on port 8787
lexical_service/health on port 8096

Local Compose health checks use the container port, usually 8080, even when the host port differs. Cloud ALB target groups use the configured healthCheckPath and forward to the ECS task container port.

Operations notes

Local startup constraints

  • Create the external Docker networks and volumes expected by Compose before starting the full stack.
  • Use the processors profile only when the local machine can run the amd64 search processing image or QEMU emulation.
  • Use COMPOSE_FILE=docker-compose.yml:docker-compose.local-e2e.yml for deterministic local E2E environment overrides.
  • Ensure a LocalStack-compatible endpoint exists when using S3/SQS-backed local flows.

Cloud deployment constraints

  • New service environment variables must be added to the matching Pulumi stack so ECS tasks or Lambda handlers receive them.
  • Services that call AWS APIs need both runtime environment values and IAM policy coverage.
  • Queue-backed workflows should define DLQs and alarms; the shared Queue component creates a queue, DLQ, and DLQ alarm by default.
  • Rust services use SQLX_OFFLINE=true during Docker builds; SQLx query metadata must be prepared from the Rust workspace when database queries change.

Related pages

Document API, bucket layout, upload/finalization flow, and storage permissions. Search event queues, extraction workers, OpenSearch indexes, and backfill operations. Compose startup, environment overrides, LocalStack setup, and health checks.

09. Entities and blocks

Concept: Item types, document-backed and non-document blocks, aliases, load sources, nesting rules, and file type resolution.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/09-entities-and-blocks.md
  • Generated: 2026-06-01T00:51:49.790Z

Source Files

  • js/app/packages/core/block.ts
  • js/app/packages/core/constant/allBlocks.ts
  • js/app/packages/block-md/definition.ts
  • js/app/packages/block-project/definition.ts
  • js/app/packages/service-clients/service-storage/client.ts
  • rust/cloud-storage/models_soup/src/lib.rs

Macro maps storage-service items and repository-defined block definitions into Solid block instances. The frontend resolves an entity-like item to a BlockName or BlockAlias, creates a Source, runs the selected block definition’s load(source, intent), and exposes the loaded data through block-scoped signals.

Core identifiers

IdentifierPurpose
BlockNameConcrete block type from BlockRegistry, such as pdf, md, code, image, canvas, chat, project, channel, email, video, automation, and unknown.
BlockAliasPseudo-block type that preserves UX semantics while reusing a base block implementation. Current aliases are task and csv.
ItemTypeStorage/client item category: cloud storage item types (document, chat, project) plus channel, email, channel_message, call, and automation.
SourceRuntime loading input. Remote sources are dss and sync-service; local/intermediate sources include blob, buffer, opfs, gen, and preload.
SoupItemBackend aggregate enum for list/search results: documents, chats, projects, email threads, channels, calls, CRM companies, and foreign entities.
`BlockRegistry` contains `write` and `contact`, but this checkout does not include `block-write/definition.ts` or `block-contact/definition.ts`. Treat them as registry-level names, not loadable block implementations, until definition modules are added.

Block definitions

Block definitions are collected with import.meta.glob('../../block-*/definition.ts'). Each definition provides:

defineBlock({
  name,
  description,
  component,
  accepted,
  load,
  defaultFilename?,
  aliases?,
  liveTrackingEnabled?,
  syncServiceEnabled?,
  editPermissionEnabled?,
})
BlockAccepted file typesPrimary load sourceNotes
pdfpdf, docxdssFetches binary document data, downloads the blob, and initializes pdf.js.
mdmdsync-serviceUses backend-owned sync-service content and Loro state. Alias: task.
codeCode extensions from FileTypeMap where app === 'code'dssLoads text via getTextDocument. Alias: csv.
imagepng, jpg, jpeg, gif, svg, webpdssFetches binary data and builds a DSS file wrapper.
canvascanvasdssFetches binary canvas data and builds a DSS file wrapper.
videoVideo extensions when ENABLE_VIDEO_BLOCK is enableddssFetches metadata and a presigned playback URL for supported formats.
unknownnonedssMetadata-only fallback for unsupported document file types.
projectnonedssSpecial-cases root and trash; otherwise fetches project metadata.
chatnonedssFetches chat data from cognition and synthesizes document-like metadata.
channelnonedssLoads by channel id only.
emailnonedssFetches and caches an email thread.
callnonedssEnabled only when ENABLE_CALLS() returns true.
automationnonedssLoads an automation schedule id.

Document-backed and non-document blocks

NonDocumentBlockTypes marks blocks that do not correspond to document file content:

[
  'call',
  'chat',
  'channel',
  'project',
  'email',
  'contact',
  'automation',
]

Everything else normally behaves as document-backed content. Aliases are not listed as non-document blocks: task resolves to md, and csv resolves to code, so both remain document-backed.

blockNameToItemType() converts blocks back to item types for history and storage operations:

Block or aliasItem type
chatchat
callcall
channelchannel
projectproject
emailemail
automationautomation
all other blocks and aliasesdocument

Aliases

Aliases are declared inside block definitions, not in a separate routing table.

AliasBase blockDefault filenameMeaning
taskmdNew TaskA markdown document with task subtype metadata.
csvcodeNew CSVA text/code document presented as a CSV-specific entity.

Alias handling has two layers:

  1. fileTypeToBlockName() may return the alias when the input is an alias or when an item subType.type is an alias.
  2. resolveBlockAlias() flattens aliases to the concrete block implementation before mounting or calling block methods.

Split-layout URLs preserve aliases with aliasContext, so a route can encode /task/:id while the mounted implementation is still the md block. Duplicate split checks compare resolved block names, preventing the same item from opening twice as both /md/:id and /task/:id.

File type resolution

allBlocks.ts builds lookup tables from every block definition’s accepted map:

LookupOutput
blockAcceptedMimetypeToFileExtensionMIME type to first registered extension.
blockAcceptedFileExtensionToMimeTypeExtension to MIME type.
blockAcceptedFileExtensionSetAll accepted extensions.
blockNameToFileExtensionsBlock name to accepted extensions.
blockNameToMimeTypesBlock name to accepted MIME types.
fileTypeToBlockName()Block name, alias, or unknown.
fileTypeToResolvedBlockName()Concrete block name with aliases flattened.

Resolution order for fileTypeToBlockName(input, icon?):

  1. Missing input returns unknown.
  2. channel_message maps to channel.
  3. With ENABLE_DOCX_TO_PDF, docx and write map to pdf for behavior, but icon: true returns write.
  4. Alias names return the alias.
  5. Registered block names return themselves.
  6. Accepted file extensions map to their owning block.
  7. Unrecognized input returns unknown.

itemToBlockName(item, icon?) gives document subtypes precedence over file types. If item.subType.type is a known alias, it returns that alias; otherwise it resolves item.fileType, then falls back to item.type.

Load sources and lifecycle

The orchestrator’s default source resolver returns sync-service when a block definition has syncServiceEnabled; otherwise it returns a dss source with the id and optional upload metadata from router state.

item or route
  -> block/file-type resolution
  -> createBlockInstance(type, id)
  -> Source: sync-service or dss
  -> definition.load(source, 'initial')
  -> BlockLoader signals

BlockLoader rejects nested preload results during initial load, records load errors as UNAUTHORIZED, MISSING, GONE, or INVALID, and publishes common fields when present:

Loaded fieldPublished signal behavior
dssFileSets block file signal.
textSets block text signal.
userAccessLevelSets user access; defaults to view when absent.
documentMetadataSets document metadata.
projectMetadataConverted into a document-metadata-shaped value for shared UI.
loroManagerSets collaborative document state manager.
syncSourceRegistered for cleanup on unmount.

Markdown is the main sync-service-backed block. It requires the storage location to be syncServiceContent; object-storage markdown content is treated as invalid because markdown initialization and persistence are backend-owned.

Backend entity shape

The Rust SoupItem enum is the backend list/search aggregate. Each variant can produce a canonical Entity with an entity type and string id. Documents with SoupDocumentSubType::Task report property entity type Task; ordinary documents report Document.

Property references are only available for documents, projects, email threads, and chats. Channels, calls, CRM companies, and foreign entities intentionally return no property reference in to_entity_reference().

Nesting rules

Nested blocks are gated by ValidNestingCombinations and checked through canNestBlock(name, parentName). The rule is keyed by child block and stores allowed parent blocks.

Child blockAllowed parent blocks
canvasmd
pdfmd
codemd
all other registered childrennone

Alias-aware callers resolve aliases before checking nesting. For example, task resolves to md, and csv resolves to code; a CSV/code preview can be nested in markdown if the caller resolves it to code.

Creation and upload paths

Document creation and upload use storage-service client methods:

MethodEndpointUse
createDocument()POST /documentsGeneric object-storage document upload. Supports fileType, mimeType, isTask, teamId, and related metadata.
createMarkdownDocument()POST /documents/create_markdownBackend-initialized markdown sync-service document.
createTask()POST /documents/create_taskBackend-initialized task document with task properties.

Upload file type inference first uses an explicit options.fileType, then MIME-to-extension lookup from block definitions, then the filename extension when the MIME type is empty. The backend owns object-created finalization and sync-service initialization after upload.

Related pages

Real-time collaborative markdown documents. Task documents and task-specific properties. Stored files and upload behavior.

10. Split layout and navigation

Concept: Route encoding, component registry entries, split history, navigation causes, popovers, and desktop versus mobile split behavior.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/10-split-layout-and-navigation.md
  • Generated: 2026-06-01T00:52:49.230Z

Source Files

  • js/app/packages/app/component/Root.tsx
  • js/app/packages/app/component/split-layout/SplitLayoutRoute.tsx
  • js/app/packages/app/component/split-layout/componentRegistry.tsx
  • js/app/packages/app/component/split-layout/layoutManager.ts
  • js/app/packages/app/component/split-layout/layout.ts
  • js/app/packages/app/component/split-layout/tests/layoutManager.test.ts

The app renders workspace content through a Solid Router catch-all split route whose path segments encode the visible split contents as type/id pairs, while createSplitLayout owns mounted content, per-split history, focus events, popovers, URL synchronization, and desktop/mobile layout decisions.

Route encoding

A split URL is parsed as alternating path segments:

/component/inbox/md/doc_123
└─ type ─┘└ id ┘└type┘└ id  ┘
   split 1       split 2
URLDecoded content
/component/inboxone registered component split: { type: 'component', id: 'inbox' }
/md/doc_123one block split for block or alias type md and id doc_123
/md/doc_123/component/searchtwo visible splits
/ or an invalid empty pair listdefault split { type: 'component', id: 'inbox' }

decodePairs() consumes pairs until a missing type or id is found. Non-component types are resolved through the block alias registry; aliases keep aliasContext so the URL can re-encode the alias instead of only the resolved base block type.

Only the current visible split contents are URL encoded. Component `params`, block `params`, per-entry `state`, and split history are runtime state and are not restored from the path after a full reload.

Route mounting and URL sync

LAYOUT_ROUTE uses path: '/*splits' and passes props.params.splits?.split('/') ?? [] into SplitLayoutContainer. The root route table also maps top-level app paths such as /inbox, /agents, /mail, /documents, /tasks, /channels, /calls, and /files to the same layout component, while DEFAULT_ROUTE is /component/inbox.

SplitLayoutContainer performs two-way synchronization:

DirectionBehavior
Manager to URLWhen splitManager.getUrlSegments() differs from the router segments, the container navigates to /${segments.join('/')}.
URL to managerWhen router segments change externally, the container calls splitManager.reconcile(decodedPairs()).
Excluded splitsgetUrlSegments() omits splits hidden by the current exclusion filter, used by mobile background panels.

Split content model

The layout manager treats all mounted content as SplitContent:

type SplitContent =
  | {
      type: BlockName | BlockAlias;
      id: string;
      params?: BlockComponentProps[BlockName];
      aliasContext?: BlockAliasContext;
      state?: EntryState;
    }
  | {
      type: 'component';
      id: string;
      params?: Record<string, unknown>;
      state?: EntryState;
    };

Block content is mounted through the global block orchestrator. Component content is resolved through the split component registry.

Component registry entries

Component splits use { type: 'component', id }. The id must be registered in componentRegistry.tsx; otherwise resolveComponent() throws Component '<name>' not registered.

Registered app-level component ids include:

Component idRuntime behavior
unified-listRedirects in-place to { type: 'component', id: 'inbox' }.
inbox, agents, mail, documents, tasks, channels, calls, foldersAuth-gated SoupView presets with page-view tracking.
searchAuth-gated SoupView that can receive initialQuery, initialFilters, and initialClientFilters when opened programmatically.
loadingLoading block placeholder.
channel-compose, email-compose, task-composeCompose surfaces.
settingsSettings panel wrapper.
Local/dev-only idsDebug and internal tools gated by LOCAL_ONLY or DEV_MODE_ENV.
A component opened with runtime `params` works during programmatic navigation, but those params are not encoded into the URL. Reloading the same path recreates the component with no params.

Opening and replacing splits

Most callers use useSplitLayout() rather than the manager directly.

HelperBehavior
openWithSplit(content, options)General navigation entry point. On mobile, preferNewSplit is forced to false by the hook.
replaceOrInsertSplit(content, referredFrom)Opens in the current panel when called inside a split panel, activates it, and records a referral cause.
replaceSplit({ content, mergeHistory, referredFrom })Replaces the current panel and forces preferNewSplit: false.
insertSplit(content, referredFrom)Prefers a new split and activates it.
popoverSplit(content)Creates a temporary dialog-backed split instead of a URL-backed panel.
resetSplit()Resets the current panel to the default split.

openWithSplit() follows this order:

  1. A registered navigation interceptor may consume the navigation. Mobile swipe layout uses this.
  2. Unless allowDuplicate is true, an existing visible split with the same type and id is activated and returned.
  3. If no explicit handle is provided, the active split becomes the target handle.
  4. The target handle is replaced when preferNewSplit is false or the resize zone cannot fit another 400px panel.
  5. Otherwise a new split is appended.

replaceWhenFull defaults to enabled. Setting replaceWhenFull: false allows callers to request a new split even when canAppendSplit() reports insufficient space, but desktop rendering still depends on the resize zone.

Split history and navigation causes

Each split owns an independent History<SplitContent> with push, merge, back, forward, replaceCurrent, goToIndex, and remove.

ActionHistory effectNavigationCause
Initial split creationPushes initial content. Optional initialHistory is pushed first.fresh
replace() with mergeHistory: falsePushes a new entry, forking away any forward entries.fresh
replace() with mergeHistory: trueMerges into the current entry.replace
goBack()Moves to the previous entry.history-back
goForward()Moves to the next entry.history-forward
goToEntry(predicate)Jumps to the closest matching prior entry, then closest forward entry.history-back or history-forward
removeFromHistory(predicate)Removes matching entries and reattaches the current surviving entry.replace

Components can read the latest cause with useNavigationCause(). This is intended for behavior that differs between fresh navigation and restoration, such as suppressing auto-focus after back/forward.

useEntryState(key, { default }) stores component-owned state on the current history entry. Registered captors run before navigation away, then the captured state is mirrored back onto split.content.state.

Referral metadata

referredFrom records where navigation originated. It is stored on SplitState and exposed through SplitHandle.referredFrom().

Allowed values are:

'list-view' | 'kommand-menu' | 'mention' | 'attachment' | 'launcher' |
'sidebar' | 'dock' | 'entity-actions-menu' | 'hotkey' | 'quick-access' |
'file-upload' | 'search' | null

Use the closest existing value when adding a navigation caller. If the source is not meaningful to downstream behavior or analytics, pass null.

Popover splits

Popover splits are temporary mounts managed by createPopoverSplit() and rendered by PopoverSplitRenderer.

A popover split:

  • creates a popover-... id;
  • acquires a focus lock before state updates;
  • mounts the same block/component content model as normal splits;
  • renders inside a Dialog and Panel;
  • provides a stub SplitHandle with isPopover() === true;
  • has no URL segments, no history navigation, and lastNavigationCause() === 'fresh';
  • closes on escape or dialog close;
  • releases the focus lock and removes its map entry after a short animation delay.

Popover content still receives SplitPanelContext, so components that depend on panel context can render inside the dialog. Do not expect URL persistence, back/forward history, or split resizing inside a popover.

Desktop layout behavior

Desktop split rendering uses a horizontal Resize.Zone with Resize.Panel children. Each panel has minSize={400} and renders a SplitPanel containing:

  • a split-specific hotkey DOM scope;
  • SoupContextProvider;
  • split header and toolbar slots;
  • the mounted block or registered component element;
  • spotlight support;
  • focus restoration and active split tracking.

Focus changes inside a panel activate that panel after a short debounce. Insert and remove events explicitly move focus to the inserted split or the nearest remaining split.

Relevant split hotkeys include:

HotkeyBehavior
cmd+\ or \Create a new inbox split when space allows.
cmd+escape / opt+escapeGo home or close split depending on current content and split count.
opt+[ / opt+]Per-split history back/forward.
shift+escapeToggle spotlight when multiple splits exist.
shift+h / shift+arrowleftFocus split left.
shift+l / shift+arrowrightFocus split right.

Mobile layout behavior

There are two mobile-related checks:

CheckEffect
isMobile() in useSplitLayout()Forces preferNewSplit to false for callers using the helper.
isNativeMobilePlatform() in SplitLayoutContainerSwitches rendering from desktop Resize.Zone to the native mobile two-slot swipe layout.

The native mobile layout uses slot A and slot B. One slot is foreground and the other may hold a background split for swipe-back. The background split is excluded from URL encoding, duplicate detection, and content lookup.

Mobile navigation behavior differs from desktop:

  • createMobileSwipeLayout() registers a navigation interceptor.
  • Non-mergeHistory navigation is handled as forward navigation into the background slot, then promoted to foreground after animation.
  • Swipe-back promotes the background slot, removes the old foreground split, and lazily mounts the promoted split’s previous history entry as the next background split.
  • mergeHistory navigation bypasses the interceptor and uses normal replace behavior.
  • MobileDock uses mergeHistory when switching between component/list views so dock tab switches do not create swipe-back entries.

Reconciliation contracts

reconcile(newSplits) is used for URL-driven changes. It compares the visible split key sequence with the decoded URL sequence, preserves excluded splits unchanged, and rebuilds changed visible positions. When a visible split exists at the same index, the new split reuses that split id to keep panel identity stable at that position.

Run the layout manager tests when changing reconciliation, history, or URL behavior. The test suite covers URL-state reconciliation, block-to-component replacement, and browser-back-like ordering scenarios.

Troubleshooting

SymptomLikely causeFix
No split manager foundA split helper ran before SplitLayoutContainer registered the global manager.Call split helpers only under the app layout route or guard for missing manager.
Component '<id>' not registeredA URL or caller opened { type: 'component', id } without a registry entry.Add registerComponent(id, factory) in the split component registry.
URL opens inbox instead of expected contentThe path did not contain a complete type/id pair.Use /component/<id> for registered components or /<blockType>/<id> for blocks.
Component state disappears after reloadRuntime params, entry state, and history are not URL encoded.Persist durable state outside split runtime state or encode it in a supported route/content id.
New block does not open in another splitDuplicate non-component content is prevented unless allowed.Pass allowDuplicate: true only when duplicate mounts are intentional.
Mobile background split is missing from URLThe mobile swipe layout excludes the background slot.Treat this as expected; only foreground-visible content is URL encoded.

11. Real-time document sync

Concept: Sync-service worker routes, Durable Object sessions, permission tokens, Bebop messages, snapshot lifecycle, and client reconnect behavior.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/11-real-time-document-sync.md
  • Generated: 2026-06-01T00:52:59.543Z

Source Files

  • rust/sync-service/src/cf_worker.rs
  • rust/sync-service/src/durable_object.rs
  • rust/sync-service/src/websocket.rs
  • rust/sync-service/src/generated/schema.rs
  • js/app/packages/service-clients/service-sync/client.ts
  • js/app/packages/service-clients/service-sync/source.ts
  • js/app/packages/block-md/definition.ts

The sync service is a Cloudflare Worker backed by one DocumentSyncSession Durable Object per document_id; it exposes HTTP document endpoints, upgrades /document/{document_id}/connect to a WebSocket session, serializes realtime messages with Bebop, and stores Loro snapshots plus pending operations for reconnect and recovery.

Runtime shape

flowchart LR
  subgraph Client
    MD["block-md load()"]
    Source["createSyncServiceSource()"]
    Rest["syncServiceClient"]
  end

  subgraph Worker["sync-service Worker"]
    Router["cf_worker::router"]
    Schema["/schema"]
    Copy["/document/{id}/copy"]
  end

  subgraph DO["DocumentSyncSession Durable Object"]
    WS["/connect WebSocket"]
    API["metadata/raw/snapshot/active_peers/initialize"]
    State["DocumentState(LoroDoc)"]
    Awareness["EphemeralStore"]
    Alarm["alarm()"]
  end

  subgraph Storage
    SQLKV["Durable Object SQL snapshot store"]
    KVFallback["SNAPSHOT_STORE_KV fallback"]
    DOKV["Durable Object KV pending ops"]
    D1["USER_PEER_MAPPING D1"]
  end

  MD --> Source
  Source --> WS
  Rest --> Router
  Router --> Schema
  Router --> Copy
  Router --> DO
  WS --> State
  WS --> Awareness
  API --> State
  State --> SQLKV
  SQLKV --> KVFallback
  DOKV --> State
  WS --> D1
  Alarm --> SQLKV
  Alarm --> DOKV

Worker routes

The top-level Worker routes health/schema traffic directly and forwards document traffic to the Durable Object namespace DOCUMENT_SYNC_SESSION using id_from_name(document_id).

RouteHandlerAuthenticationResponse
/WorkerNoneHello Sync Service!
/healthWorkerNonehealthy
/schemaWorkerNoneRaw bebop/schema.bop
/document/{document_id}/copyWorker orchestrationHeader token via forwarded Durable Object callsCopies a snapshot into another document
/document/{document_id}/{*rest}Durable Object pass-throughDepends on Durable Object routeDurable Object response or 408 on RPC timeout

pass_to_durable_object wraps Durable Object fetches with the service default timeout and returns status 408 if the RPC times out.

Durable Object routes

DocumentSyncSession owns the document session state. CORS validation runs before route handling. Allowed origins include local app ports, Macro production/dev/staging origins, Capacitor localhost, Apollo testing, and non-empty https://{subdomain}.preview.macro.com preview origins.

RoutePurposeAuth requirement
/document/{document_id}/connect?token=...WebSocket upgrade and initial syncJWT in query params
/document/{document_id}/existsExistence checkNone
/document/{document_id}/wakeupWarm in-memory state and keep worker aliveNone
/document/{document_id}/peer/{peer_id}Resolve registered peer to userNone
/document/{document_id}/metadataReturn { id, peers, version_id }Bearer JWT or internal admin
/document/{document_id}/rawReturn Loro deep JSONBearer JWT or internal admin
/document/{document_id}/snapshotReturn binary Loro snapshotBearer JWT or internal admin
/document/{document_id}/active_peersReturn active peer IDs as stringsBearer JWT or internal admin
/document/{document_id}/initializeStore initial binary snapshotBearer JWT with at least edit
/document/{document_id}/debug_dump_operationsReturn pending operationsAdmin
/document/{document_id}/debug_do_kv_get/{key}Inspect Durable Object KV keyAdmin
/document/{document_id}/debug_do_kv_list/{prefix}Inspect Durable Object KV prefixAdmin
Route matching is path-based. The implementation handles CORS `OPTIONS` separately, but most Durable Object handlers do not enforce a specific HTTP method after the path is matched.

Permission tokens

The service accepts two token sources:

SurfaceToken sourceDecoder mode
WebSocket connecttoken query paramTokenFrom::QueryParams
HTTP document APIsAuthorization: Bearer {jwt}TokenFrom::Headers
Internal/admin APIsx-internal-auth-keyTokenFrom::Headers admin shortcut

JWTs are HS256 tokens signed with DOCUMENT_PERMISSIONS_SECRET and contain:

type AuthToken = {
  user_id?: string;
  document_id: string;
  access_level: 'view' | 'comment' | 'edit' | 'owner' | 'admin';
};

Document-scoped requests must match document_id unless the token is internal admin. initialize requires edit or stronger. Debug routes require admin.

For WebSocket document updates, PeerUpdate is ignored when Wsm::can_edit() is false. The current runtime write gate treats comment, edit, owner, and admin as editable because AccessLevel::can_edit() checks for AccessLevel::Comment or stronger; view users cannot push document updates. Awareness updates are accepted for connected users independently of edit access.

WebSocket session lifecycle

  1. connect_handler decodes the query token.
  2. The Durable Object stores the document_id if not already set.
  3. A Cloudflare WebSocketPair is created.
  4. The server socket is accepted with a generated WebSocket tag.
  5. WebSocketMetadata is stored in Durable Object storage and memory:
    • user_id
    • access_level
    • peer_ids
  6. The current DocumentState is loaded or reused.
  7. The server sends RemoteInitialSync with:
    • a shallow Loro snapshot
    • encoded awareness state
  8. The client receives the paired WebSocket response.

String "ping" messages receive "pong". Binary messages are decoded as FromPeer Bebop messages.

Bebop message protocol

The schema lives in rust/sync-service/bebop/schema.bop and is generated into Rust and TypeScript clients.

Client to service: FromPeer

MessageFieldsBehavior
PeerUpdateupdate: byte[]Applies update to DocumentState, records pending op, broadcasts RemoteUpdate to other sockets, sends RemoteUpdateAck to sender
PeerAwarenessawareness: byte[]Applies awareness update and broadcasts RemoteAwareness to other sockets
PeerRequestSincefrontiers: byte[]Decodes Loro frontiers and returns RemoteUpdateSince
PeerRequestSnapshotnoneReturns RemoteSnapshot with a shallow snapshot
PeerRegisterIdpeerid: uint64Associates a Loro peer ID with the socket metadata and, when user_id exists, D1

Service to client: FromRemote

MessageFieldsEmitted when
RemoteInitialSyncsnapshot, awarenessImmediately after WebSocket connect
RemoteUpdateupdateAnother peer pushes an accepted update
RemoteAwarenessawarenessAnother peer updates or clears awareness
RemoteSnapshotsnapshotAlarm broadcast or explicit snapshot request
RemoteUpdateAckupdateSender’s update was accepted and processed
RemoteUpdateSinceupdate, frontiersResponse to PeerRequestSince

Messages larger than 1 MB log a warning before deserialization; they are not rejected solely because of that size check.

Snapshot and operation lifecycle

DocumentState wraps a Loro document. It imports snapshots with the from_service tag, imports client updates with the from_client tag, and tracks whether a snapshot should be saved by comparing last_update and last_export.

Storage layers

DataStorage
Current snapshotDefault feature: Durable Object SQL via DurableSQLStorage, with SNAPSHOT_STORE_KV fallback for reads
Pending operationsDurable Object KV keys under o/
All recent operationsDurable Object KV keys under a/
Last saved version vectorDurable Object KV key LAST_VERSION_VECTOR
User-peer mappingsD1 binding USER_PEER_MAPPING, table peer_user_map

The default Cargo feature set enables do-sqlite-snapshot-storage, so snapshots are written to Durable Object SQL. The combined storage backend reads SQL first and falls back to KV when SQL has no snapshot.

Save loop

After each binary WebSocket message, the Durable Object schedules an alarm roughly 5 seconds in the future if no later alarm is already scheduled.

When the alarm fires:

  1. The Durable Object loads DocumentState.
  2. If state.should_save() is true:
    • exports a full Loro snapshot
    • stores the snapshot
    • stores the Loro operation version vector
    • clears applied pending operation keys
    • marks the state exported
  3. If sockets are still connected:
    • schedules the next alarm
    • broadcasts RemoteSnapshot with a shallow snapshot
  4. If no sockets remain, it logs that the Durable Object reached zero connections.

wakeup calls warm the session storage and document state when the document exists, then use a JavaScript timeout keepalive with the default 60 second TTL. The TypeScript client’s safeWakeup debounces wakeups by 200 ms and suppresses repeat wakeups for 55 seconds per document.

Initialization and copy

initialize accepts a Bebop InitializeFromSnapshotRequest:

type InitializeFromSnapshotRequest = {
  snapshot: Uint8Array;
};

It fails if snapshot storage already contains a snapshot for the document. Otherwise it stores the snapshot, records the DOCUMENT_ID in Durable Object storage, and prepares SessionStorage.

copy is handled at the Worker layer because it touches two Durable Object instances:

  1. POST /document/{source_id}/snapshot with optional version_id.
  2. Wrap the returned binary snapshot in InitializeFromSnapshotRequest.
  3. POST /document/{target_id}/initialize.

The JSON copy request is:

{
  "target_document_id": "new-document-id",
  "version_id": {
    "peer": "123",
    "counter": 10
  }
}

version_id is optional. When present, the snapshot handler exports state at the requested Loro frontier.

TypeScript client behavior

syncServiceClient exposes HTTP helpers against SYNC_SERVICE_HOSTS.worker:

MethodRoute
wakeup({ documentId })GET /document/{id}/wakeup
safeWakeup(id)Debounced GET /document/{id}/wakeup
exists({ documentId })HEAD /document/{id}/exists
initializeFromSnapshot({ documentId, snapshot })POST /document/{id}/initialize
getDocumentMetadata({ documentId })GET /document/{id}/metadata
getSnapshot({ documentId })GET /document/{id}/snapshot
getRaw({ documentId })GET /document/{id}/raw

Tauri requests add an explicit Origin header of https://dev.macro.com in development and https://macro.com otherwise.

createSyncServiceSource(documentId, token) builds the WebSocket URL:

{SYNC_SERVICE_HOSTS.ws}/document/{documentId}/connect?token={token}

Connection settings:

SettingValue
Reconnect backoffConstant 500 ms
Max retries20
Initial sync timeout10 seconds
Update ACK timeout3 seconds
Snapshot request timeout10 seconds
Updates-since timeout10 seconds
Heartbeat interval10 seconds
Heartbeat timeout5 seconds
Max missed heartbeats2

The first connection uses the provided token. Reconnects request a fresh permission token from storageServiceClient.permissionsTokens.createPermissionToken({ document_id }); if token refresh fails, the client falls back to the last known WebSocket URL.

Heartbeat is intentionally started only after the first RemoteInitialSync arrives. On reconnect, the client starts heartbeat, waits for another RemoteInitialSync, and emits a reconnect sync event containing the new snapshot and awareness. If reconnect sync times out, it logs the failure and lets heartbeat monitoring close/retry the socket.

Markdown block integration

The Markdown block only loads realtime collaboration when the source is sync-service.

Load flow:

  1. Fetch document metadata, document location, and permission token in parallel.
  2. If the location is a pending presigned URL, wait for syncServiceContent readiness.
  3. Reject the load when the final location is not syncServiceContent.
  4. Create the sync-service source with the permission token.
  5. Create a Loro manager using MARKDOWN_LORO_SCHEMA.
  6. Initialize local Loro state from initialSync.snapshot.
  7. Return the block data with syncSource, loroManager, metadata, and user access level.

Markdown initialization and lifecycle persistence are backend-owned; the frontend does not repair an object-storage-backed Markdown document into sync-service content during block load.

Operational configuration

Cloudflare bindings in wrangler.toml:

BindingPurpose
DOCUMENT_SYNC_SESSIONDurable Object namespace
USER_PEER_MAPPINGD1 database for peer/user mappings
DOCUMENT_SNAPSHOT_BUCKETR2 bucket when the R2 snapshot feature is enabled
SNAPSHOT_STORE_KVKV snapshot fallback
DOCUMENT_VERSIONING_KVConfigured KV namespace
INTERNAL_API_SECRET_KEYVariable naming the secret binding for internal API auth
DOCUMENT_PERMISSIONS_SECRETJWT signing secret expected at runtime
SPS_URLSearch processing service URL when search-service is enabled

Useful local commands:

cd rust/sync-service

# Build the Worker used by Miniflare tests
just worker-build

# Run e2e tests and Rust unit tests
just test

# Apply local D1 migrations and start Wrangler
just dev

# Regenerate TypeScript Bebop bindings used by tests
cd bebop && npx bebopc build

Error and verification signals

SymptomLikely cause
401Missing/malformed Bearer token, invalid JWT, wrong document token, or missing internal key
403Request Origin is not allowed
404Snapshot/document does not exist for routes that check existence
408Worker-to-Durable-Object RPC timeout
Missing update ACKClient did not receive RemoteUpdateAck within 3 seconds
View-only edits do not propagatePeerUpdate was ignored by the WebSocket write gate
Initial sync timeoutClient did not receive RemoteInitialSync within 10 seconds
Snapshot already exists during initializeTarget document already has stored snapshot state

Related pages

TypeScript source wrapper, reconnect behavior, and SyncSource integration. Markdown-specific sync-service source loading and Loro initialization.

12. Run backend services locally

Guide: Start databases, LocalStack, FusionAuth, Rust services, optional processor profiles, and local health checks.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/12-run-backend-services-locally.md
  • Generated: 2026-06-01T00:53:22.606Z

Source Files

  • justfile
  • docker-compose.yml
  • docker-compose-databases.yml
  • docker-compose.local-e2e.yml
  • local_stack.just
  • rust/cloud-storage/justfile

The local backend runtime is owned by just recipes and Docker Compose files at the repository root. just setup prepares .env, external Docker networks and volumes, LocalStack AWS resources, the local Postgres database, FusionAuth configuration, and prebuilt Rust service images; just run_local then starts the Compose stack.

Local Docker resources are intentionally single-instance. The Compose project is fixed to `macro`, so multiple checkouts or worktrees share the same containers, volumes, networks, LocalStack container, and FusionAuth instance.

Prerequisites

Install the tools used directly by the local recipes:

ToolUsed by
justRepository command runner
Docker and Docker ComposeDatabases, FusionAuth, Rust services, JS services
sopsDecrypting .env-local*.enc into .env
AWS CLICreating LocalStack SQS, DynamoDB, and S3 resources
PulumiLocal FusionAuth stack outputs and configuration
SQLx CLIDatabase create, migrate, setup, and reset commands
Bun and NodeFusionAuth setup dependencies and JS-backed services/tests

If not using the repository shell environment, export SOPS_KMS_ARN before decrypting local environment files.

export SOPS_KMS_ARN="arn:aws:kms:us-east-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93,arn:aws:kms:us-west-1:569036502058:key/mrk-cab29bf948044eb79005a81f48d40e93"

One-command setup

just setup

just setup performs this sequence:

  1. Decrypts the root local environment into .env.
  2. Creates external Docker networks: databases and auth.
  3. Creates persistent local volumes for Postgres, Redis, OpenSearch, and FusionAuth.
  4. Starts and provisions LocalStack.
  5. Creates and migrates the local Macro Postgres database.
  6. Starts FusionAuth, deploys the local Pulumi stack, patches root .env, then stops FusionAuth.
  7. Builds the development Rust service images.
`setup_local_dbs` starts Postgres and Redis to initialize the database, then stops the database Compose stack. `run_local` starts the services again when needed.

Start only infrastructure

Use these commands when you want to rebuild or inspect dependencies before starting application services.

```bash just create_networks ``` ```bash just run_dbs -d ```
This targets only `postgres` and `redis` from `docker-compose-databases.yml`.
```bash just rust/cloud-storage/macro_db_client/create_db just rust/cloud-storage/macro_db_client/migrate_db ```
The local database URL is `postgres://user:password@localhost:5432/macrodb`.
```bash AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 just setup_localstack ``` ```bash just setup_fusionauth ```

Local infrastructure endpoints

ComponentContainer/imageHost endpointNotes
Postgrespgvector/pgvector:pg16localhost:5432User user, password password, database macrodb after setup
Redis Stackredis/redis-stack:latestlocalhost:6379, UI/tools on localhost:8001Uses persistent macro_redis_data volume
OpenSearchopensearchproject/opensearch:latestlocalhost:9200, analyzer on localhost:9600Defined in the database Compose file; not targeted by just run_dbs
LocalStacklocalstack/localstack:4localhost:4566Runs SQS, DynamoDB, and S3 on the databases network
FusionAuthfusionauth/fusionauth-app:1.62.1localhost:9011Exposed to backend services through the external auth network

LocalStack resources

just setup_localstack starts LocalStack with SERVICES=sqs,dynamodb,s3, waits for /_localstack/health, then creates queues, tables, buckets, and a document-upload S3 notification.

S3 buckets

BucketUsed for
macro-email-attachmentsEmail attachments
doc-storageDocument storage and document-upload finalizer events
docx-uploadDOCX upload flow
static-file-storageStatic file objects
bulk-upload-stagingBulk upload staging

All buckets receive a local CORS configuration allowing http://localhost:3000 through http://localhost:3009.

DynamoDB tables

TableKey shape
bulk-uploadPK hash, SK range, DocumentPkIndex GSI
connection-gateway-tablePK hash, SK range, ConnectionPkIndex GSI
static-file-metadatafile_id hash

SQS queues

Local setup creates queues for notifications, email jobs, contacts, conversion, document deletion, document text extraction, search events, static-file events, and document-upload finalization. The document-upload finalizer wires S3 ObjectCreated events from doc-storage to document-upload-finalizer-queue.

Inside Docker, services use http://localstack:4566. Host-side commands and generated browser-facing LocalStack URLs use http://localhost:4566.

FusionAuth local setup

FusionAuth local setup is split between Docker Compose and a Pulumi stack under infra/stacks/fusionauth-instance.

just setup_fusionauth

The setup recipe:

  1. Downloads the FusionAuth container .env if missing.
  2. Starts the FusionAuth Postgres database and app.
  3. Waits for http://localhost:9011/api/status to report {"status":"Ok"}.
  4. Initializes or updates the Pulumi local stack.
  5. Writes local FusionAuth values into the root .env.
  6. Stops FusionAuth after setup.

Useful local credentials:

FieldValue
Admin username[email protected]
Admin passwordmacroIsGreat!
API keybf69486b-4733-4954-a44e-2e1b5f2c8a91

just run_local calls patch_local_fusionauth_env before starting services. If FusionAuth is not already running, the patch recipe starts it temporarily, reads the client secret, updates .env, and stops the temporary container afterward.

Start backend services

just run_local

By default, this runs docker compose up in the foreground. Pass Docker Compose arguments through just when you want detached startup, waiting, or a subset of services.

just run_local -d --wait

For a clean rebuild after changing service code:

just run_local --build

run_local always builds the shared Rust services image target rust_services_image. With --build, it also rebuilds websocket_service, sync_service, and lexical_service.

Service ports and health endpoints

ServiceHost portHealth checkRuntime notes
authentication-service8080/healthDepends on Postgres, FusionAuth, and Redis
connection_gateway8082/healthWebSocket gateway; service alias connection-gateway
contacts_service8083/healthContact management service
document_cognition_service8085/healthDepends on document storage, email, static files, sync, and lexical
document_storage_service8086/healthDepends on Redis, connection gateway, and auth
document_upload_finalizernonenoneLocal SQS worker for doc-storage object-created events
email_service8087/healthDepends on auth, document storage, connection gateway, static files, and Redis
notification_service8089/healthDepends on Redis, cognition, auth, connection gateway, and document storage
search_processing_service8092/healthOptional processors profile
static_file_service8094/api/healthUses DynamoDB/static-file metadata
static_file_cdn8100noneNginx local CloudFront-style emulator
unfurl_service8095/healthURL unfurling service
image_proxy_service8097/healthExternal image proxy
websocket_service6969noneJS WebSocket service
sync_service8787/healthCloudflare Worker/Durable Object sync runtime via Docker
lexical_service8096/healthLexical conversion service

Check the stack:

docker compose ps
curl -fsS http://localhost:8080/health
curl -fsS http://localhost:8086/health
curl -fsS http://localhost:8094/api/health
curl -fsS http://localhost:8787/health
curl -fsS http://localhost:9011/api/status
curl -fsS http://localhost:4566/_localstack/health

Optional processor profile

search_processing_service is behind the Compose profile processors.

just run_local --profile processors

Rebuild it explicitly when changing processor code:

just run_local --build --profile processors

The processor image uses Dockerfile.search_processing_service.dev and is pinned to linux/amd64 because the bundled search_processing_service/pdfium-lib/linux/libpdfium.so is amd64-only. Apple Silicon hosts run this service through emulation.

Local E2E backend profile

The local E2E commands start LocalStack, start a narrowed backend service set with docker-compose.local-e2e.yml overrides, seed deterministic data, then run tests.

just local-e2e

Rust ignored integration tests use the same seeded stack:

just local-e2e-rust

Run Rust integration tests and Playwright after one stack startup and seed pass:

just local-e2e-all

Open Playwright UI mode:

just local-e2e-ui

The E2E override file forces services to use local dependencies instead of shared development infrastructure:

KeyLocal value
DATABASE_URLpostgres://user:password@postgres:5432/macrodb
DATABASE_URL_READONLYpostgres://user:password@postgres:5432/macrodb
LOCAL_AWS_URLhttp://localstack:4566
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYtest / test
REDIS_URIredis://redis:6379
DOCUMENT_STORAGE_SERVICE_REDIS_URIredis://redis:6379

The deterministic seed command is guarded. It requires LOCAL_E2E_SEED=true and refuses database URLs outside the local Docker database shape postgres://user:...@(localhost|127.0.0.1|postgres):5432/macrodb.

Stop and reset

CommandEffect
just stop-localRuns docker compose down for the main local stack
just stop-databasesStops the database Compose stack
just stop_fusionauthStops the FusionAuth Compose stack
docker rm -f localstackRemoves the LocalStack container
just rust/cloud-storage/macro_db_client/reset_dbDrops and recreates the local Macro database
just docker_cache_usageShows BuildKit cache disk usage
just docker_cache_clear_targetsClears Rust target cache mounts
just docker_cache_clearClears all BuildKit build cache

Troubleshooting

.env not found

Run:

just get_environment

For a fresh checkout, prefer:

just setup

FusionAuth local stack is missing

If run_local prints a warning that the Pulumi local stack was not found, run:

just setup_fusionauth

Then restart the backend stack.

LocalStack resources are missing

Recreate LocalStack resources:

AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1 just setup_localstack

The recipe uses localstack/localstack:4; it is intentionally not latest.

Processor builds are slow on Apple Silicon

search_processing_service runs as linux/amd64 because of the bundled PDFium library. Use the default stack unless you need the processors profile.

Local E2E seed refuses to run

Check that the seed command has LOCAL_E2E_SEED=true and that DATABASE_URL points at the local Compose database, not a shared development database.

Related pages

Run Playwright against the local backend stack and seeded fixtures. Populate local Macro data with deterministic scenarios. Maintain the local FusionAuth Pulumi stack and Docker runtime.

13. Develop SolidJS and Tauri apps

Guide: Use Bun and Vite, run local service overrides, build app bundles, start Tauri targets, and avoid iOS worker deadlocks.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/13-develop-solidjs-and-tauri-apps.md
  • Generated: 2026-06-01T00:54:19.836Z

Source Files

  • js/app/README.md
  • js/app/package.json
  • js/app/justfile
  • js/app/packages/app/index.tsx
  • js/app/packages/app/vite.config.ts
  • js/app/tauri/src-tauri/README.md
  • js/app/AGENTS.md

The Macro frontend is a SolidJS single-page app in js/app/packages/app built by Vite and run with Bun; the same Vite output is used for web, desktop, iOS, and Android through the Tauri wrapper in js/app/tauri.

Project layout

:::files js/app/ ├─ package.json # Bun scripts for dev, build, tests, codegen ├─ justfile # frontend build and local-service shortcuts ├─ packages/app/ │ ├─ index.tsx # Solid entry point and Tauri fetch override │ ├─ vite.config.ts # app Vite config entry │ └─ vite.base.ts # shared Vite server/build/env configuration ├─ packages/core/constant/ │ └─ servers.ts # remote/local service host selection ├─ packages/core/util/ │ ├─ platform.ts # web/desktop/iOS/Android runtime detection │ └─ platformFetch.ts # Tauri HTTP plugin fetch adapter └─ tauri/src-tauri/ ├─ tauri.conf.json # Tauri dev/build hooks and bundle settings └─ README.md # native target commands :::

Prerequisites

Use nix develop from the repository root when available. Otherwise install:

  • Bun
  • Node tooling used by Bun scripts
  • Rust and Cargo
  • Tauri CLI
  • Xcode for iOS targets
  • Android Studio and Android SDK tooling for Android targets

For local backend services, complete the repository-level local setup first:

just setup

The local stack uses the fixed Docker Compose project name macro; do not run two local stacks at the same time.

Run the web app with Vite

Run commands from js/app.

bun i
bun run dev

bun run dev changes into packages/app and starts:

bun run --bun dev

The package-level app script runs:

vite -c vite.config.ts

Vite serves on 0.0.0.0:${PORT:-3000} with strictPort: true, WebSocket HMR, CORS enabled, polling file watch, and filesystem access to the workspace root.

The dev app is available at `http://localhost:3000/app` for normal browser routing. Tauri development uses the same Vite server through `http://localhost:3000`.

Vite build behavior

The shared config sets different paths for serve and build:

SurfaceValue
Dev server base/
Built app base/app
Build outputjs/app/packages/app/dist
Build targetesnext
CSS transformer/minifierlightningcss
Worker formatES modules
Sourcemapsenabled

Build modes are selected with MODE:

just build-dev
just build-staging
just build-prod

Equivalent direct command shape:

cd js/app/packages/app
MODE=production NODE_ENV=production bun run --bun build

The app build also writes dist/semver.txt as:

<package-version>+<git-short-sha>

Use NO_MINIFY=true when a readable bundle is needed for debugging. The Vite config switches output names to stable assets/[name].js and disables minification.

Runtime environment values

vite.base.ts injects compile-time values through define.

IdentifierSource or defaultNotes
import.meta.env.__APP_VERSION__packages/app/package.json version plus Git short SHALogged at startup and passed to observability outside Vite HMR
import.meta.env.ASSETS_PATHderived from MODE and Vite command/local in dev server development mode, /dev for development builds, /staging for staging, / otherwise
import.meta.env.__LOCAL_DOCKER__LOCAL_DOCKER === "true"boolean define
import.meta.env.__LOCAL_JWT__LOCAL_JWTused by local bearer-token auth paths
import.meta.env.__GIT_BRANCH__current branch during vite serveupdated over a custom HMR event when .git/HEAD changes

Run against local service overrides

In development mode, service hosts come from packages/core/constant/servers.ts.

If VITE_LOCAL_SERVERS is unset or empty, the app uses remote development hosts. If it is ALL, all configured services use localhost. Otherwise, provide a comma-separated list of service names to override individually.

# All local services
just local

# Cognition HTTP + websocket services
just local-dcs

# Document storage only
just local-dss

# Search only
just local-search

# Email only
just local-email

Direct examples:

cd js/app/packages/app

# Use all local hosts
VITE_LOCAL_SERVERS=ALL bun run --bun dev

# Override one service
VITE_LOCAL_SERVERS=document-storage-service bun run --bun dev

# Override multiple services
VITE_LOCAL_SERVERS=document-storage-service,email-service bun run --bun dev

# Override a local port for one service
VITE_LOCAL_SERVERS=document-storage-service:18086 bun run --bun dev

Available service keys include:

Service keyDefault local host
auth-servicehttp://localhost:8080
pdf-servicehttp://localhost:4567
document-storage-servicehttp://localhost:8086
websocket-servicews://localhost:6969
cognition-servicehttp://localhost:8085
connection-gatewayws://localhost:8082
notification-servicehttp://localhost:8089
static-filehttp://localhost:8100
unfurl-servicehttp://localhost:8095
contactshttp://localhost:8083
email-servicehttp://localhost:8087
image-proxy-servicehttp://localhost:8097
scheduled-actionhttp://localhost:8098
sync-servicehttp://localhost:8787 and ws://localhost:8787 when selected
`sync-service` is handled separately: it switches to local hosts when `VITE_LOCAL_SERVERS=ALL` or the list contains `sync-service`. When sync remains remote, sync permission tokens use the remote document-storage host so secrets match the remote sync service.

Run the local smoke suite

From the repository root:

just local-e2e

The harness starts the local stack with docker-compose.local-e2e.yml, seeds deterministic smoke data, launches the frontend with:

LOCAL_E2E=true
VITE_LOCAL_SERVERS=ALL
VITE_ENABLE_BEARER_TOKEN_AUTH=true
LOCAL_JWT=<generated-or-exported-token>

Then it runs Playwright from js/app.

Open Playwright UI mode with:

just local-e2e-ui

Run the frontend Playwright command directly only after the local stack and seed data are ready:

cd js/app
LOCAL_E2E=true bunx playwright test

Start Tauri targets

Run native commands from the Tauri workspace under js/app/tauri.

cd js/app/tauri
cargo tauri dev
cd js/app/tauri
cargo tauri ios dev
cd js/app/tauri
cargo tauri android dev

tauri.conf.json defines:

Tauri build keyValue
devUrlhttp://localhost:3000
frontendDist../../packages/app/dist
beforeDevCommandjust dev-tauri with cwd ../..
beforeBuildCommandjust build-tauri with cwd ../..

just dev-tauri starts the same Vite app. just build-tauri builds the production frontend bundle before Tauri packages native artifacts.

For devices or emulators that cannot resolve localhost HMR, set:

export TAURI_DEV_HOST=<host-reachable-from-device>
cargo tauri ios dev

Vite uses TAURI_DEV_HOST as the HMR host while still serving on port 3000.

Build native bundles

cd js/app/tauri
cargo tauri build
cd js/app/tauri
cargo tauri ios build
cd js/app/tauri
cargo tauri android build

Tauri packages the static files emitted into js/app/packages/app/dist. The configured bundle target is all, with iOS minimum system version 14.0.

For updater-server testing, create the frontend archive from js/app:

just dist-archive

That recipe builds production assets, zips packages/app/dist, and writes:

rust/cloud-storage/tools/native_app_server/app-archive.zip

Platform-specific runtime behavior

The app detects platform at runtime instead of producing separate web/mobile bundles.

Runtime surfaceBehavior
getPlatform()returns web, desktop, ios, or android
isTauri()checks for window.__TAURI_INTERNALS__
Routerbrowser uses Router with /app; Tauri uses HashRouter with /
FetchTauri replaces global window.fetch with platformFetch except localhost requests
WebSocketTauri uses the Tauri WebSocket plugin wrapper; web uses the browser WebSocket
Native providerMaybeTauriProvider mounts Tauri update, safe-area, share-target, push, and navigation wiring only in Tauri

The fetch override intentionally skips localhost URLs so Vite HMR and dev-server requests continue to use normal browser fetch during Tauri development.

iOS worker deadlock rule

Do not construct ES module Web Workers at module-load time in code that can run inside iOS WKWebView under Tauri.

The problematic pattern is eager construction:

import WorkerImpl from './worker?worker';

const worker = new WorkerImpl(); // avoid at module load on iOS

Use lazy construction instead. Importing a worker constructor is safe; calling new WorkerImpl() must happen on first use.

import HeicWorker from './heic-worker?worker';

class Service {
  private static instance: Service | null = null;

  private constructor() {
    this.workerPool = WorkerPool.getInstance(HeicWorker);
  }

  static getInstance(): Service {
    if (!Service.instance) Service.instance = new Service();
    return Service.instance;
  }
}

export const service = new Proxy({} as Service, {
  get: (_target, prop, receiver) =>
    Reflect.get(Service.getInstance(), prop, receiver),
});

Safe examples in this codebase instantiate workers inside runtime paths such as a block load() function or a service singleton that is reached only when a conversion/upload operation starts.

Symptom signature

An iOS worker deadlock usually looks like:

  • HTML loads.
  • Initial JavaScript starts.
  • JavaScript silently stops.
  • Safari Web Inspector attaches but shows no useful app progress.
  • The WebContent process stays alive with 0.0% CPU.

Diagnose an iOS worker freeze

Use the iOS Simulator so logs flow through macOS unified logging.

cd js/app/tauri
cargo tauri ios dev "iPhone 15"

Use the full path because zsh has a log builtin.

/usr/bin/log stream --predicate 'process == "macro"' --info --debug --style compact
tail -200 <logfile> | grep -vE 'tauri:// request|tauri_protocol.rs|^\s*\\134'

Look for the last meaningful URL request before silence, especially a *-worker.js?worker_file&type=module request.

ps -o pid,pcpu,comm -p <webcontent_pid>

0.0% CPU with a live process indicates a parked deadlock rather than a hot loop.

sample <webcontent_pid> 3 -file /tmp/sample.txt

Confirm worker threads are stuck in a stack containing:

WorkerOrWorkletScriptController::loadModuleSynchronously
WorkerDedicatedRunLoop::runInMode
Condition::waitUntilUnchecked

Quality checks

Run these from js/app before handing off frontend changes:

bun run check
bun run lint
bun run test

Use the AGENTS guidance for SolidJS changes:

  • Avoid createEffect unless syncing with an external imperative system.
  • Use derived signals for derived state.
  • Use createMemo only for referential stability or expensive derivations.
  • Check solid-primitives before adding custom reactive utilities.
  • Keep reusable components small and decoupled from query/mutation state.
  • Use semantic color tokens instead of raw Tailwind color classes.

Related pages

Set up Docker, LocalStack, FusionAuth, seeded data, and local services. Use the local E2E harness and UI mode for smoke coverage. Native providers, bundle updates, share targets, mobile plugins, and platform adapters.

14. Change a Rust service API

Guide: Update Axum handlers, OpenAPI emitters, generated client specs, Orval output, and CI checks for service API changes.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/14-change-a-rust-service-api.md
  • Generated: 2026-06-01T00:55:38.799Z

Source Files

  • rust/cloud-storage/document_storage_service/src/openapi.rs
  • rust/cloud-storage/document_cognition_service/src/openapi.rs
  • js/app/scripts/generate-api-schema.ts
  • js/app/scripts/services.ts
  • js/app/packages/service-clients/orval.config.ts
  • .github/workflows/web-app-check-main.yml
  • rust/cloud-storage/AGENTS.md

Rust service API changes flow from Axum handlers and utoipa annotations into local *_openapi Rust binaries, then through js/app/scripts/generate-api-schema.ts into committed openapi.json files and Orval-generated TypeScript under js/app/packages/service-clients.

API generation path

Rust handler + request/response models
  └─ #[utoipa::path(...)] + ToSchema
      └─ api/swagger.rs ApiDoc paths(...) and components.schemas(...)
          └─ src/openapi.rs binary prints ApiDoc::openapi().to_pretty_json()
              └─ js/app/scripts/generate-api-schema.ts writes openapi.json
                  └─ orval.config.ts regenerates generated client/schema files
                      └─ biome formats service-clients
                          └─ --check mode fails if git diff or untracked generated files remain

The live services also mount Swagger UI at /docs and serve the same OpenAPI document at /api-doc/openapi.json.

Service registry

The generator accepts service names from js/app/scripts/services.ts and maps them to Rust crate names in js/app/scripts/generate-api-schema.ts.

Service argumentRust crateOpenAPI binaryGenerated packageOrval project
cloud-storagedocument_storage_servicedocument_storage_service_openapiservice-storagestorageService
properties-serviceproperties_serviceproperties_service_openapiservice-propertiespropertiesService
document-cognitiondocument_cognition_servicedocument_cognition_service_openapiservice-cognitioncognitionService
auth-serviceauthentication_serviceauthentication_service_openapiservice-authauthService
notification-servicenotification_servicenotification_service_openapiservice-notificationnotificationService
static-filesstatic_file_servicestatic_file_service_openapiservice-static-filesstaticFileService
connection-gatewayconnection_gatewayconnection_gateway_openapiservice-connectionconnectionGateway
contacts-servicecontacts_servicecontacts_service_openapiservice-contactscontactService
unfurl-serviceunfurl_serviceunfurl_service_openapiservice-unfurlunfurlService
email-serviceemail_serviceemail_service_openapiservice-emailemailService
search-servicesearch_servicesearch_service_openapiservice-searchsearchService
scheduled-actionscheduled_actionscheduled_action_openapiservice-scheduled-actionscheduledActionService

Change an existing endpoint

Change the handler, request type, response type, status codes, and router registration in the Rust service. For request and response structs that must appear in OpenAPI, derive `utoipa::ToSchema` and keep `serde` casing attributes aligned with the wire format.
Example surfaces:
- Storage handlers live under `rust/cloud-storage/document_storage_service/src/api/**`.
- Cognition handlers live under `rust/cloud-storage/document_cognition_service/src/api/**`.
- Shared request/response models may live under service-local `model/**` modules or shared crates such as `model`, `models_*`, `chat`, or `memory`.
Keep the OpenAPI operation synchronized with the handler: - HTTP method: `get`, `post`, `put`, `patch`, or `delete` - `path = "..."` matching the mounted API path - `tag = "..."` - request body and params, when used - `responses((status = ..., body = ...))`
If a route is mounted under both `/{version}` and the unversioned router, document the unversioned path in `#[utoipa::path]`, matching the existing service pattern.
Add new handlers to `paths(...)` and add request/response models to `components(schemas(...))`.
Storage uses `rust/cloud-storage/document_storage_service/src/api/swagger.rs`.

Cognition uses `rust/cloud-storage/document_cognition_service/src/api/swagger.rs`.

Missing `components.schemas` entries commonly produce incomplete generated TypeScript even when the Rust code compiles.
From the web app workspace, regenerate only the service you changed when possible.
```bash
cd js/app
bun run gen-api -- cloud-storage
bun run gen-api -- document-cognition
```

To regenerate every registered service:

```bash
cd js/app
bun run gen-api
```

The script builds OpenAPI binaries with `SQLX_OFFLINE=true cargo build`, runs each binary, writes `openapi.json`, removes the previous `generated/` directory, runs Orval, and then runs Biome on `packages/service-clients/`.
Commit the service `openapi.json` and all changed files under that service package’s `generated/` directory. Do not hand-edit generated files; fix the Rust schema source or Orval config and regenerate.

Document Cognition special generation

document-cognition has additional generated artifacts beyond Orval output.

When the target service includes document-cognition, generate-api-schema.ts also builds or runs:

  • document_cognition_service_models
  • gen_tool_schemas

It then runs scripts/generate-dcs-types.ts, which generates:

  • service-cognition/generated/schemas/model.ts
  • service-cognition/generated/tools/schemas.ts
  • service-cognition/generated/tools/types.ts
  • service-cognition/generated/tools/tool.ts
The models generator can read `MODELS_JSON` from a local file. The main API generation script sets this automatically after running the Rust `document_cognition_service_models` binary.

Add a new service to generation

For a new Rust service client, add all registry points together:

  1. Add a Rust OpenAPI binary, usually named <crate>_openapi, that prints ApiDoc::openapi().to_pretty_json().
  2. Add a [[bin]] entry in the service Cargo.toml.
  3. Add the service entry in js/app/scripts/services.ts with name, dev, prod, local, output, and orvalKey.
  4. Add the service-to-crate mapping in js/app/scripts/generate-api-schema.ts.
  5. Add a matching Orval project in js/app/packages/service-clients/orval.config.ts.
  6. Run targeted generation and commit openapi.json plus generated/.

Orval output modes

js/app/packages/service-clients/orval.config.ts controls the generated TypeScript shape.

Orval client modeServices in this repoOutput pattern
fetchauth, cognition, connection, contacts, email, scheduled action, search, static files, unfurlgenerated/client.ts plus schema files
zod with mode: "split"notification, properties, storagegenerated/zod.ts plus split schema files

Storage, properties, and notification generate Zod validators. Most other services generate fetch clients and TypeScript schemas. Search writes schemas under service-search/generated/models instead of generated/schemas.

Verification

Run the checks that match the change size.

cd js/app
bun run gen-api -- <service-name>
bun run gen-api -- --check
bun run --bun --silent tsc --project ./packages/app/tsconfig.json

For Rust changes:

cd rust/cloud-storage
cargo test -p <crate-name>
just check
just clippy
just format

If the API change includes SQLx query or schema changes, update the SQLx offline cache from the workspace root:

cd rust/cloud-storage
just prepare_db

CI behavior

The web app PR workflow has two path filters:

  • should_run for frontend package, app, lockfile, Biome, and workflow changes.
  • api_changed for rust/cloud-storage/**/*.rs, Rust lock/config files, flake files, API generation scripts, setup actions, and the workflow.

When api_changed is true, the Typecheck job:

  1. sets up web prerequisites,
  2. restores Rust cache for rust/cloud-storage,
  3. runs bun run gen-api -- --check in js/app,
  4. runs TypeScript checking.

--check mode fails when generated service-client files differ from the committed tree or when untracked generated files exist.

Troubleshooting

Generated types are out of sync with Rust API definitions

Run generation locally and commit the resulting changes:

cd js/app
bun run gen-api

For a smaller diff, target the changed service:

bun run gen-api -- cloud-storage

No matching services found

Use one of the name values from js/app/scripts/services.ts, such as cloud-storage, document-cognition, or search-service.

No crate mapping found, skipping

The service exists in services.ts but is missing from serviceToCrate in generate-api-schema.ts. Add the crate mapping before regenerating.

TypeScript type is missing or too broad

Check the Rust OpenAPI source:

  • the model derives ToSchema,
  • the model is included in components(schemas(...)),
  • the handler is included in paths(...),
  • the #[utoipa::path] response body names the expected type.

Orval writes to the wrong package

Keep these values aligned:

  • services.ts output
  • services.ts orvalKey
  • orval.config.ts project key
  • orval.config.ts input.target
  • orval.config.ts output.target and output.schemas

OpenAPI binary fails in generation

The generator runs binaries from rust/cloud-storage/target/debug unless OPENAPI_BINS_DIR is set. Each binary has a 120 second timeout and reports captured stderr on non-zero exit or timeout.

To skip the cargo build phase with prebuilt binaries:

cd js/app
OPENAPI_BINS_DIR=/path/to/bins bun run gen-api -- <service-name>

Related pages

Service testing, SQLx offline cache, and workspace commands. TypeScript client package layout and generated artifact conventions.

15. Generate service clients and tool schemas

Guide: Regenerate OpenAPI JSON, TypeScript clients, DCS model types, AI tool schemas, and MCP documentation pages from Rust sources.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/15-generate-service-clients-and-tool-schemas.md
  • Generated: 2026-06-01T00:55:20.399Z

Source Files

  • js/app/scripts/generate-api-schema.ts
  • js/app/scripts/generate-dcs-tools.ts
  • js/app/scripts/services.ts
  • rust/cloud-storage/ai_tools/src/lib.rs
  • rust/cloud-storage/ai_tools/src/bin/gen_tool_schemas.rs
  • docs/scripts/generate-mcp-tool-pages.ts
  • docs/package.json

Macro regenerates frontend service clients and AI tool artifacts from local Rust binaries under rust/cloud-storage; js/app/scripts/generate-api-schema.ts builds OpenAPI emitters, writes openapi.json, runs Orval, and applies DCS-specific model and tool generation when document-cognition is included.

Generated artifact map

Rust sources
  ├─ */src/openapi.rs                         -> service-*/openapi.json
  ├─ document_cognition_service/src/models_bin.rs -> service-cognition/generated/schemas/model.ts
  └─ ai_tools/src/bin/gen_tool_schemas.rs     -> ai_tools/schemas/tools.json
                                                   -> service-cognition/generated/tools/*
                                                   -> docs/AI/mcp/tools/*
ArtifactGeneratorOutput
OpenAPI JSONjs/app/scripts/generate-api-schema.tsjs/app/packages/service-clients/service-*/openapi.json
TypeScript service clientsOrval via orval.config.tsgenerated/client.ts, generated/zod.ts, and generated schemas/models
DCS model enumdocument_cognition_service_models + generate-dcs-models.tsservice-cognition/generated/schemas/model.ts
DCS AI tool validators and typesgen_tool_schemas + generate-dcs-tools.tsservice-cognition/generated/tools/{schemas,types,tool}.ts
MCP tool pagesdocs/scripts/generate-mcp-tool-pages.tsdocs/AI/mcp/tools/*.mdx, docs/config/tool-pages.json
The main OpenAPI workflow runs local Rust binaries instead of fetching deployed `/api-doc/openapi.json` endpoints.

Prerequisites

  • Bun installed for js/app and docs scripts.
  • Rust toolchain available for cargo build in rust/cloud-storage.
  • Run generation from the package directory shown in each command.
  • SQLx is built in offline mode by the scripts with SQLX_OFFLINE=true.

Regenerate service clients

cd js/app
bun run gen-api

This processes every service listed in js/app/scripts/services.ts.

cd js/app
bun run gen-api -- document-cognition

Valid service names include cloud-storage, properties-service, document-cognition, auth-service, notification-service, static-files, connection-gateway, contacts-service, unfurl-service, email-service, search-service, and scheduled-action.

cd js/app
bun run gen-api -- --check

Check mode regenerates artifacts, compares js/app/packages/service-clients, reports changed and untracked files, and exits non-zero when generated files are out of sync.

Service and crate mapping

generate-api-schema.ts maps public service names to Rust crate binaries. Each mapped crate must expose an OpenAPI binary named <crate>_openapi.

ServiceRust crateOrval projectOutput package
cloud-storagedocument_storage_servicestorageServiceservice-storage
properties-serviceproperties_servicepropertiesServiceservice-properties
document-cognitiondocument_cognition_servicecognitionServiceservice-cognition
auth-serviceauthentication_serviceauthServiceservice-auth
notification-servicenotification_servicenotificationServiceservice-notification
static-filesstatic_file_servicestaticFileServiceservice-static-files
connection-gatewayconnection_gatewayconnectionGatewayservice-connection
contacts-servicecontacts_servicecontactServiceservice-contacts
unfurl-serviceunfurl_serviceunfurlServiceservice-unfurl
email-serviceemail_serviceemailServiceservice-email
search-servicesearch_servicesearchServiceservice-search
scheduled-actionscheduled_actionscheduledActionServiceservice-scheduled-action

Build behavior

The script builds all requested OpenAPI binaries in one Cargo invocation:

cd rust/cloud-storage
SQLX_OFFLINE=true cargo build --bin <crate>_openapi ...

Then it runs each binary from target/debug, captures stdout as OpenAPI JSON, writes the service package openapi.json, removes the old generated directory, and runs:

cd js/app/packages/service-clients
bun run orval --config orval.config.ts --project <orvalKey>

Set OPENAPI_BINS_DIR to reuse prebuilt binaries and skip the Cargo build phase:

cd js/app
OPENAPI_BINS_DIR=/absolute/path/to/bins bun run gen-api

DCS model and tool generation

When document-cognition is part of the service set, generate-api-schema.ts performs extra work after Orval:

  1. Runs document_cognition_service_models.
  2. Writes temporary .models.json.
  3. Runs MODELS_JSON=.models.json bun scripts/generate-dcs-types.ts.
  4. Deletes .models.json.

generate-dcs-types.ts runs both DCS generators:

cd js/app
bun scripts/generate-dcs-types.ts

Model enum output

generate-dcs-models.ts writes:

js/app/packages/service-clients/service-cognition/generated/schemas/model.ts

When MODELS_JSON is set, it reads model metadata from the local file produced by the Rust model binary. Without MODELS_JSON, it fetches ${documentCognitionBase}/models, where MODE and LOCAL_BACKEND=true control the selected base URL.

The generated file exports:

  • Model
  • Model constant map
  • AllModels
  • ModelEnum

Tool schema output

Run the tool generator directly when only AI tool types changed:

cd js/app
bun run gen-tools

The generator builds and runs:

cd rust/cloud-storage
SQLX_OFFLINE=true cargo build --bin gen_tool_schemas

cd rust/cloud-storage/ai_tools
../target/debug/gen_tool_schemas

The Rust binary writes rust/cloud-storage/ai_tools/schemas/tools.json as a combined schema:

{
  "$defs": {},
  "tools": [
    {
      "name": "ContentSearch",
      "input": "ContentSearch",
      "output": "SearchToolResponse"
    }
  ]
}

generate-dcs-tools.ts validates that combined shape, dereferences shared definitions, and writes:

FilePurpose
generated/tools/schemas.tsZod v3 validators generated from JSON Schema definitions
generated/tools/types.tsTypeScript definition types generated from $defs
generated/tools/tool.tsToolName, NamedTool, deserializeToolCall, and deserializeToolResponse

deserializeToolCall and deserializeToolResponse return neverthrow Result values. Unknown tool names return not_found; schema parse failures return parse_error.

Regenerate MCP tool documentation pages

The docs package exposes a separate generator:

cd docs
bun install
bun run generate:tools

This script rebuilds gen_tool_schemas, removes the generated MCP tools directory, then writes:

docs/AI/mcp/tools/index.mdx
docs/AI/mcp/tools/<slug>.mdx
docs/config/tool-pages.json

docs/package.json also runs this generator during prepare.

In this checkout, `gen_tool_schemas` writes the combined `$defs`/`tools` shape used by the app tool generator, while `docs/scripts/generate-mcp-tool-pages.ts` is typed to read a `schemas` array with inline `inputSchema` and `outputSchema`. If `bun run generate:tools` fails while sorting `toolSchemas.schemas`, align the docs generator with the combined schema or change the Rust schema emitter intentionally.

Runtime and schema ownership

rust/cloud-storage/ai_tools/src/lib.rs owns toolset composition:

  • all_tools() builds the DCS chat toolset and prompt.
  • all_tool_combined_schema() merges all_tools() with the ReadThread phantom tool for schema generation.
  • mcp_tools() builds the MCP runtime toolset separately.

Do not assume generated MCP documentation is automatically scoped to the MCP runtime toolset unless the schema generator is changed to use mcp_tools().

Verification

After regeneration, check the generated files that match the source change:

git diff -- js/app/packages/service-clients
git diff -- rust/cloud-storage/ai_tools/schemas/tools.json
git diff -- docs/AI/mcp/tools docs/config/tool-pages.json

For frontend type safety:

cd js/app
bun run type-check

For docs links:

cd docs
bun run lint

CI runs bun run gen-api -- --check for Rust/API changes in the web app workflow and fails when generated service clients drift from Rust sources.

Troubleshooting

SymptomCauseFix
No matching services foundUnknown service argumentUse a name from services.ts
Service skippedMissing serviceToCrate entryAdd the service-to-crate mapping
Binary timeout after 120000msOpenAPI/model binary hung or took too longRun the specific Cargo binary locally and inspect stderr
Generated clients out of sync in CIRust API changed without committed generated filesRun cd js/app && bun run gen-api, then commit outputs
Biome binary fails on NixOSnpm-installed Biome dynamic linking issueThe scripts detect NixOS and use system biome when available
Docs tool generation fails on schemasDocs generator expects the old inline schema shapeUpdate generate-mcp-tool-pages.ts for $defs/tools or restore the expected Rust output

Related pages

Runtime context for Macro MCP integration. Generated tool pages produced from Rust tool schemas.

16. Work with local seed data

Guide: Use seed_cli, local_e2e fixtures, manifest aliases, reset SQL, and shared Playwright and Rust fixture loaders.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/16-work-with-local-seed-data.md
  • Generated: 2026-06-01T00:55:38.119Z

Source Files

  • rust/cloud-storage/seed_cli/README.md
  • rust/cloud-storage/seed_cli/src/main.rs
  • rust/cloud-storage/seed_cli/seed/local_e2e/manifest.json
  • rust/cloud-storage/seed_cli/seed/local_e2e/users.json
  • rust/cloud-storage/seed_cli/seed/local_e2e/reset.sql
  • js/app/tests/e2e/fixtures/local-e2e-seed.ts
  • rust/cloud-storage/local_e2e_test_support/README.md

seed_cli owns the deterministic local fixture set in rust/cloud-storage/seed_cli/seed, and the repo-level local E2E harness applies it with just local-e2e-seed before Playwright or ignored Rust integration tests run against the local Docker stack.

Fixture layout

:::files

rust/cloud-storage/seed_cli/
  README.md
  justfile
  seed/
    channels.json
    channel_messages.json
    documents/
      documents.json
      files/
    local_e2e/
      manifest.json
      reset.sql
      users.json
  src/
    main.rs
    entity/
      scenario/mod.rs
      document/mod.rs
      channel/mod.rs
      channel_message/mod.rs

:::

FilePurpose
seed/local_e2e/manifest.jsonStable aliases for the smoke user, smoke document, general channel, and canonical welcome message.
seed/local_e2e/users.jsonLocal user fixtures shared by database seed code, Playwright fixture loading, and Rust token generation.
seed/local_e2e/reset.sqlDestructive cleanup for seeded channel, message, document, mention, and share-permission ranges.
seed/documents/documents.jsonDocument rows with stable UUIDs, display names, fixture file names, and public flags.
seed/channels.jsonChannel rows with stable UUIDs, optional names, channel types, and participants.
seed/channel_messages.jsonMessage rows with stable UUIDs, senders, optional thread IDs, and optional entity mentions.

Run the local seed

```bash just local-e2e-seed ```
This starts local databases, drops and initializes `macrodb`, then runs the seed CLI local E2E scenario.
```bash just local-e2e ```
The harness starts the local E2E service subset, applies the seed, launches the frontend with local bearer-token auth, and runs `bunx playwright test` with `LOCAL_E2E=true`.
```bash just local-e2e-rust ```
Rust local E2E tests are ignored by default and run with `SQLX_OFFLINE=true`.
```bash just local-e2e-all ```
`scenario local-e2e-smoke` is destructive. It requires `LOCAL_E2E_SEED=true` and refuses database URLs that are not the local Docker database shape `postgres://user:...@(localhost|127.0.0.1|postgres):5432/macrodb`.

Seed CLI behavior

The CLI entrypoint initializes the local Macro runtime, loads required environment variables, connects to Postgres, initializes FusionAuth and S3 clients, then dispatches an entity command.

The local smoke scenario is:

cd rust/cloud-storage/seed_cli
just local-e2e-smoke

Internally it runs:

cargo r -- scenario local-e2e-smoke

The local-e2e-smoke recipe supplies local-only defaults, including:

VariableValue used by recipe
LOCAL_E2E_SEEDtrue
DATABASE_URLpostgres://user:password@localhost:5432/macrodb
LOCAL_AWS_URLhttp://localhost:4566
DOCUMENT_STORAGE_BUCKETdoc-storage
FUSIONAUTH_BASE_URLhttp://localhost:9011
SQLX_OFFLINEtrue
ENVIRONMENTlocal

Local smoke scenario order

  1. Load local_e2e/manifest.json and local_e2e/users.json.
  2. Resolve the primary smoke user from manifest.user.email.
  3. Delete local E2E contact backfill rows if public.contacts_backfill_outbox exists.
  4. Execute local_e2e/reset.sql.
  5. Delete and reinsert local user rows derived from users.json.
  6. Seed documents from seed/documents/documents.json.
  7. Seed channels from seed/channels.json.
  8. Seed channel messages from seed/channel_messages.json.
  9. Print Local e2e smoke seed data ready for <user_id>.

Reset scope

reset.sql removes fixture data by deterministic UUID prefixes:

DataReset condition
entity_accessSeed channel source IDs or seed document entity IDs.
ChannelSharePermissionSeed channel IDs.
comms_entity_mentionsSeed message IDs or seed document entity IDs.
comms_activitySeed channel IDs.
comms_channelsSeed channel IDs.
DocumentSeed document IDs.

User cleanup is generated from local_e2e/users.json and deletes matching rows from "User" and macro_user by auth user ID, macro user ID, or email before reinsert.

Manifest aliases

local_e2e/manifest.json is the stable contract for smoke tests. It does not duplicate full fixture rows; it names canonical fixture records that must exist in the seed files.

{
  "user": {
    "email": "[email protected]"
  },
  "documents": {
    "projectRoadmap": {
      "id": "00000000-0000-0000-0002-000000000001",
      "name": "Project Roadmap"
    }
  },
  "channels": {
    "general": {
      "id": "00000000-0000-0000-0000-000000000001",
      "name": "general",
      "message": "Welcome to the general channel everyone!"
    }
  }
}

When adding a new smoke alias, update the manifest and the underlying seed file in the same change. The shared Playwright and Rust loaders fail fast when an alias points at a missing user, document, channel, or message.

Playwright fixture loader

js/app/tests/e2e/fixtures/local-e2e-seed.ts reads the seed JSON directly from the repository root. It locates the root by walking upward until rust/cloud-storage/seed_cli/seed exists.

The exported localE2ESeed object includes:

PropertyContents
userPrimary smoke user resolved from manifest.user.email.
users, documents, channels, channelMessagesFull fixture arrays.
usersById, usersByEmail, usersByMacroUserIdUser lookup maps.
documentsById, documentsByNameDocument lookup maps.
channelsById, channelsByNameChannel lookup maps.
channelMessagesByIdMessage lookup map.
channelMessagesByChannelId(channelId)Messages filtered by channel ID.
smoke.projectRoadmapDocument row for manifest.documents.projectRoadmap.id.
smoke.generalChannelChannel row for manifest.channels.general.id.
smoke.generalWelcomeMessageMessage in the general channel matching manifest.channels.general.message.

Local Playwright specs skip unless LOCAL_E2E=true. The Playwright config generates LOCAL_JWT automatically for local E2E mode unless LOCAL_JWT is already exported.

cd js/app
LOCAL_E2E=true bunx playwright test

Prefer the repo-level harness:

just local-e2e

It seeds first and starts the frontend with:

VITE_LOCAL_SERVERS=ALL
VITE_ENABLE_BEARER_TOKEN_AUTH=true
LOCAL_JWT=<generated token>
bun run dev

Rust fixture loader

local_e2e_test_support is the Rust counterpart for ignored local integration tests. It reads the same seed files and exposes typed fixture accessors, service URL helpers, and local Macro API JWT generation.

use local_e2e_test_support::{
    LocalE2eConfig, LocalE2eSeed, LocalE2eServices, LocalJwtOptions,
    encode_local_jwt_with,
};

let config = LocalE2eConfig::load()?;
let seed = LocalE2eSeed::from_config(&config)?;
let services = LocalE2eServices::from_config(&config)?;

let user = seed.smoke_user()?;
let channel = seed.general_channel()?;
let token = encode_local_jwt_with(&config, LocalJwtOptions::new(user))?;
let ws_url = services.connection_gateway_ws_url_with_token(&token)?;

Rust helper defaults

HelperDefault
Seed directory<repo>/rust/cloud-storage/seed_cli/seed
Document storage URLhttp://localhost:8086
Connection gateway WebSocket URLws://localhost:8082/
Notification service URLhttp://localhost:8089
JWT issuerMACRO_API_TOKEN_ISSUER, or local
JWT expiryMACRO_API_TOKEN_EXPIRY_SECONDS, or 8 hours

Service URL overrides must remain local. The helper rejects non-local hosts and mismatched schemes for mutating local E2E tests.

OverrideAccepted schemes
LOCAL_E2E_DOCUMENT_STORAGE_URLhttp, https
LOCAL_E2E_CONNECTION_GATEWAY_WS_URLws, wss
LOCAL_E2E_NOTIFICATION_URLhttp, https

Generate a local E2E token

Playwright calls js/app/scripts/generate-local-e2e-token.ts, which shells out to the Rust binary in local_e2e_test_support.

cd js/app
bun scripts/generate-local-e2e-token.ts

Optional arguments are forwarded to the Rust generator:

bun scripts/generate-local-e2e-token.ts \
  --email [email protected] \
  --expiry-seconds 3600 \
  --organization-id 1
ArgumentDefault
--emailmanifest.user.email
--macro-user-idMatching users.json user_id, or `macro
--fusion-user-idMatching users.json fusion_user_id, then macro_user_id, then the first local fixture UUID
--expiry-secondsArgument, then MACRO_API_TOKEN_EXPIRY_SECONDS, then 8 hours
--organization-idOmitted
--issuerArgument, then MACRO_API_TOKEN_ISSUER, then local

MACRO_API_TOKEN_PRIVATE_SECRET_KEY is required in .env or the process environment.

Update fixture data safely

Add or change a seeded user

  1. Edit rust/cloud-storage/seed_cli/seed/local_e2e/users.json.
  2. Keep macro_user_id, fusion_user_id, and user_id stable once tests depend on them.
  3. If this is the primary smoke user, update manifest.user.email.
  4. Run just local-e2e-seed.
  5. Run at least one consumer:
    just local-e2e
    # or
    just local-e2e-rust
    

Add or change a seeded document

  1. Add a row to seed/documents/documents.json.
  2. Put the source file under seed/documents/files/.
  3. Use a deterministic document ID in the 00000000-0000-0000-0002-* range if it should be cleaned by reset.sql.
  4. Update manifest.documents only for canonical smoke-test aliases.
  5. Run just local-e2e-seed.

Document seeding creates database metadata and uploads the fixture file to the configured document storage bucket.

Add or change a seeded channel

  1. Edit seed/channels.json.
  2. Use a deterministic channel ID in the 00000000-0000-0000-0000-00000000000* range if it should be cleaned by reset.sql.
  3. Set channel_type to a value accepted by the seed CLI model, such as public, private, or direct_message.
  4. List participants excluding or including the owner; the seed command appends the scenario owner when absent.
  5. Update manifest.channels only for canonical smoke-test aliases.

Add or change a seeded message

  1. Edit seed/channel_messages.json.
  2. Use a deterministic message ID in the 00000000-0000-0000-0001-* range if it should be cleaned by reset.sql.
  3. Set channel_id to an existing seeded channel.
  4. Set sender_id to a seeded auth user ID such as macro|[email protected].
  5. Use thread_id only when the message is a reply.
  6. Add entity_mentions when the message content references a document or user.

For non-user shareable mentions, the seed command attempts to create message mentions and update channel share permissions for the mentioned entity.

Troubleshooting

SymptomCheck
refusing to run destructive local-e2e-smoke seed without LOCAL_E2E_SEED=trueUse just local-e2e-seed or just rust/cloud-storage/seed_cli/local-e2e-smoke instead of invoking the scenario without the guard variable.
refusing to run local-e2e-smoke seed against DATABASE_URL ...Point DATABASE_URL at the local Docker database with user user, database macrodb, and port 5432.
Playwright cannot generate LOCAL_JWTRun just local-e2e-seed, ensure .env exists from the local setup flow, or export LOCAL_JWT manually.
Missing fixture error in Playwright or RustVerify the alias in local_e2e/manifest.json matches an actual row in the corresponding JSON seed file.
Rust helper rejects a service URLUse localhost, 127.0.0.1, or ::1; the helper refuses non-local service URLs.
Seed command reports an empty JSON fileSeed commands bail on empty document, channel, or message arrays.

Related pages

Local stack setup, local E2E commands, and environment prerequisites. Rust test harness behavior for the deterministic local E2E stack.

17. Storage and workspace APIs

Reference: Document, channel, project, call, CRM, soup, pin, history, permissions, properties, and search endpoints.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/17-storage-and-workspace-apis.md
  • Generated: 2026-06-01T01:00:28.495Z

Source Files

  • js/app/packages/service-clients/service-storage/openapi.json
  • js/app/packages/service-clients/service-storage/client.ts
  • rust/cloud-storage/document_storage_service/src/openapi.rs
  • js/app/packages/service-clients/service-properties/openapi.json
  • js/app/packages/service-clients/service-properties/client.ts
  • js/app/packages/service-clients/service-search/openapi.json
  • js/app/packages/service-clients/service-search/client.ts

Macro’s workspace API surface is served primarily by Document Storage Service (DSS), with /properties and /search mounted on the same document-storage-service host and consumed from TypeScript clients that wrap fetchWithToken and return neverthrow Result values.

Runtime and client boundaries

SurfaceTypeScript entry pointHostServer mount
Documents, channels, projects, soup, pins, history, permissionsjs/app/packages/service-clients/service-storage/client.tsSERVER_HOSTS['document-storage-service']DSS root router
Callsjs/app/packages/service-clients/service-call/client.ts plus storage schemassame DSS host/call
Propertiesjs/app/packages/service-clients/service-properties/client.tssame DSS host/properties
Searchjs/app/packages/service-clients/service-search/client.tssame DSS host/search

The Rust OpenAPI JSON for DSS is produced from rust/cloud-storage/document_storage_service/src/openapi.rs, which prints the utoipa ApiDoc. The checked-in frontend OpenAPI snapshots under js/app/packages/service-clients/*/openapi.json and generated schema directories provide the wire types used by the clients.

Client methods return `Result`; callers should branch on `isOk()` / `isErr()` or use the project’s query helpers rather than assuming failed HTTP responses throw.

Documents

Core document operations

OperationMethod and pathClient methodNotes
List recent user documentsGET /documents?limit&offsetgetUserDocumentsClient maps data.documents, total, and next_offset to nextOffset.
Create upload-backed documentPOST /documentscreateDocumentReturns metadata plus S3 presigned upload URL, content type, and optional file type.
Create markdown documentPOST /documents/create_markdowncreateMarkdownDocumentBackend initializes sync-service content and returns documentId.
Create task documentPOST /documents/create_taskcreateTaskCreates task document, initializes markdown content, and attaches task properties.
Get metadataGET /documents/{document_id} or /{version}getDocumentMetadataClient retries with exponential delay up to five tries.
Edit metadata/share settingsPATCH /documents/{document_id}editDocumentUsed for name/project/share-permission edits.
Save PDF modification dataPUT /documents/{document_id}pdfSaveClient validates modification data and response metadata with Zod.
Simple file/text savePUT /documents/{document_id}/simple_savesimpleSave, simpleSaveTextUses FormData upload body.
Soft deleteDELETE /documents/{document_id}deleteDocumentReturns success data.
Permanent deleteDELETE /documents/{document_id}/permanentpermanentlyDeleteDocumentRemoves a soft-deleted document permanently.
Restore soft deletePUT /documents/{document_id}/revert_deleterevertDocumentDeleteReverts document deletion.
Copy documentPOST /documents/{document_id}/copy?version_id=copyDocumentCan copy a specific document version.
ExportGET /documents/{document_id}/exportexportDocumentReturns export payload.
Batch previewsPOST /documents/previewgetBatchDocumentPreviewsBody: { "document_ids": string[] }.
List searchable docsGET /documents/listlistDocumentsUsed to populate document search surfaces.

Creation and copy paths show the file-limit paywall when the returned error message includes 403.

Locations and document bytes

OperationMethod and pathClient methodCache behavior
Writer part URLsGET /documents/{uuid}/location?document_version_id=getWriterPartUrlsCached for 14 minutes.
Document location v3GET /documents/{document_id}/location_v3?get_converted_docx_url&document_version_id=getDocumentLocationCached for 14 minutes.
DOCX expanded filemetadata + writer partsgetDocxFileCached for 10 seconds; fails if file type is not docx.
Text documentmetadata + location + presigned fetchgetTextDocumentCached for 2 seconds; requires a presignedUrl location.
Binary documentmetadata + locationgetBinaryDocumentReturns blobUrl from the presigned URL.

Presigned URL caching uses a 14-minute client lifetime because the server expiration is treated as 15 minutes.

Processing and task helpers

OperationPathClient method
Document processing resultGET /documents/{document_id}/processinggetDocumentProcessingResult
Job processing resultGET /documents/{document_id}/processing/{job_id}getJobProcessingResult
Task short IDGET /documents/{document_id}/short_idgetDocumentShortId
Task branch nameGET /documents/{document_id}/branch_namegetDocumentBranchName
GitHub pull requestsGET /documents/{document_id}/github_prsgetDocumentGithubPullRequests
Wake sync servicePOST /sync_service/wakeupbulkWakeupSyncServiceDocuments

Processing helpers currently validate PREPROCESS results against CoParseSchema; missing or invalid JSON returns INVALID_RESPONSE.

Annotations

The storageServiceClient.annotations namespace wraps document comments and anchors.

OperationPath
List commentsGET /annotations/comments/document/{document_id}
List anchorsGET /annotations/anchors/document/{document_id}
Create commentPOST /annotations/comments/document/{document_id}
Create unthreaded anchorPOST /annotations/anchors/document/{document_id}
Edit commentPATCH /annotations/comments/comment/{comment_id}
Edit anchorPATCH /annotations/anchors
Delete commentDELETE /annotations/comments/comment/{comment_id}
Delete unthreaded anchorDELETE /annotations/anchors

Channels

Channel lifecycle

OperationMethod and pathClient method
Create channelPOST /channelscreateChannel
List channelsGET /comms/channelsgetChannels
Get or create DMPOST /channels/get_or_create_dmgetOrCreateDirectMessage
Get or create private channelPOST /channels/get_or_create_privategetOrCreatePrivateChannel
Rename/update channelPATCH /channels/{channel_id}patchChannel
Delete channelDELETE /channels/{channel_id}deleteChannel
Batch previewsPOST /channels/previewgetBatchChannelPreviews
Join / leavePOST /channels/{channel_id}/join, /leavejoinChannel, leaveChannel

getChannels still targets /comms/channels on the DSS host; the source comment says it should move to /channels when the comms teardown finishes.

Messages, replies, reactions, attachments

OperationMethod and pathClient method
Send messagePOST /channels/{channel_id}/messagepostMessage
Edit messagePATCH /channels/{channel_id}/message/{message_id}patchMessage
Delete messageDELETE /channels/{channel_id}/message/{message_id}?nonce=deleteMessage
Add reactionPOST /channels/{channel_id}/reactionpostReaction
Typing updatePOST /channels/{channel_id}/typingpostTypingUpdate
Page messagesGET /channels/{channel_id}/messages?limit&cursor&previous_cursor&load_around_message_idgetChannelMessages
Filter messagesPOST /channels/{channel_id}/messages?limit=postChannelMessages
Thread repliesGET /channels/{channel_id}/messages/{message_id}/repliesgetThreadReplies
Message contextGET /channels/{channel_id}/messages/{message_id}/context?before&aftergetMessageWithContext
Resolve messageGET /channels/{channel_id}/messages/{message_id}/resolveresolveChannelMessage
Attachments pageGET /channels/{channel_id}/attachments?limit&cursor&attachment_typegetChannelAttachments
ParticipantsGET/POST/DELETE /channels/{channel_id}/participantsgetChannelParticipants, addParticipantsToChannel, removeParticipantsFromChannel
Entity mentionsPOST /channels/mentions, DELETE /channels/mentions/{mention_id}createEntityMention, deleteEntityMention
Attachment referencesGET /channels/attachments/{entity_type}/{entity_id}/referencesattachmentReferences
Channel activityGET/POST /channels/activitygetActivity, postActivity

Message posting deduplicates mentions before sending. Several channel mutation methods accept a nonce and forward it in the JSON body or query string.

Calls

The call client is a separate wrapper over DSS /call endpoints.

OperationMethod and pathClient method
Get or create call tokenGET /call/{channel_id}getOrCreateCall
Leave or end callDELETE /call/{channel_id}leaveCall
Check active callGET /call/{channel_id}/activecheckActiveCall
Get call recordGET /call/record/{call_id}getCallRecord
Edit call recordPATCH /call/record/{call_id}editCallRecord
Delete call recordDELETE /call/record/{call_id}deleteCallRecord
Toggle team sharingPOST /call/record/{call_id}/share-with-team/toggletoggleShareWithTeam
Batch previewsPOST /call/record/previewstorageServiceClient.getBatchCallPreviews
Edit transcriptPATCH /call/record/{call_id}/transcriptno storage wrapper shown
Webhook ingestPOST /call/webhookserver route
Internal transcript ingestPOST /call/{channel_id}/transcriptinternal agent route

checkActiveCall treats an empty 204-style response as null. toggleShareWithTeam returns a primitive JSON boolean at runtime, so the client casts through an object type only to satisfy fetchWithToken’s generic constraint.

Projects

OperationMethod and pathClient method
List projectsGET /projects`projects.getAll
title: “Storage and workspace APIs”
description: “Reference: Document, channel, project, call, CRM, soup, pin, history, permissions, properties, and search endpoints.”

Macro’s workspace API surface is mounted on the document-storage-service host and consumed by TypeScript clients that wrap authenticated fetchWithToken calls in neverthrow Result values. Storage, properties, and search are separate client packages, but the browser-facing host is SERVER_HOSTS['document-storage-service'].

Implementation surface

SurfaceClient entry pointServer mountSpec artifact
Storage and workspacestorageServiceClient/documents, /channels, /projects, /items, /pins, /history, /crm, /callservice-storage/openapi.json
CallscallServiceClient/callstorage OpenAPI schemas
PropertiespropertiesServiceClient/propertiesservice-properties/openapi.json
SearchsearchClient/searchservice-search/openapi.json plus Rust router routes
The storage OpenAPI is generated from the Rust `utoipa` document. The `openapi.rs` binary prints `api::swagger::ApiDoc::openapi()` as JSON.

Transport and response conventions

  • All handwritten clients use fetchWithToken, so callers receive Result<T, ResultError[]>.
  • storageServiceClient, propertiesServiceClient, and searchClient all target the document-storage-service host.
  • Most JSON bodies are serialized with JSON.stringify; file uploads use FormData or direct presigned URL PUT.
  • Document permission token creation uses SYNC_PERMISSION_TOKEN_DSS_HOST instead of the normal DSS host so the token is signed by the DSS instance compatible with the sync service secret.
  • Generated response wrappers commonly use { error, data }; client methods often unwrap to the useful payload.
import { storageServiceClient } from '@service-storage/client';
import { propertiesServiceClient } from '@service-properties/client';
import { searchClient } from '@service-search/client';

const doc = await storageServiceClient.getDocumentMetadata({
  documentId: 'doc-id',
});

const props = await propertiesServiceClient.getEntityProperties({
  entity_type: 'DOCUMENT',
  entity_id: 'doc-id',
  query: { include_metadata: true },
});

const results = await searchClient.search({
  params: { page_size: 10 },
  request: {
    query: 'roadmap',
    match_type: 'partial',
    search_on: 'name_content',
  },
});

Documents

OperationEndpointClient methodNotes
List recent documentsGET /documents?limit=&offset=getUserDocumentsReturns documents, total, and next offset.
Create upload-backed documentPOST /documentscreateDocumentReturns metadata, presignedUrl, contentType, and optional fileType; client triggers file-limit paywall on 403.
Create markdown documentPOST /documents/create_markdowncreateMarkdownDocumentBackend initializes sync-service content.
Create taskPOST /documents/create_taskcreateTaskCreates task document, properties, and initialized markdown content.
Get metadataGET /documents/{document_id} or /{version}getDocumentMetadataClient retries metadata fetch up to 5 times with exponential delay.
Edit metadata/sharePATCH /documents/{document_id}editDocumentSupports name/project/share-permission edits; moving requires access to target project.
Save PDF statePUT /documents/{document_id}pdfSaveValidates server modification-data schema before sending.
Save simple file/textPUT /documents/{document_id}/simple_savesimpleSave, simpleSaveTextUses FormData file upload.
Copy documentPOST /documents/{document_id}/copy?version_id=copyDocumentCreates a new document without re-uploading source content.
Soft deleteDELETE /documents/{document_id}deleteDocumentOwner-only soft delete.
Permanent deleteDELETE /documents/{document_id}/permanentpermanentlyDeleteDocumentHard-delete path.
Restore deletePUT /documents/{document_id}/revert_deleterevertDocumentDeleteReverts soft deletion.
Batch previewPOST /documents/previewgetBatchDocumentPreviewsBody: { document_ids: string[] }.
ExportGET /documents/{document_id}/exportexportDocumentReturns export metadata/payload from DSS.
Processing resultGET /documents/{document_id}/processing[/job_id]getDocumentProcessingResult, getJobProcessingResultClient currently validates PREPROCESS results with CoParseSchema.
LocationGET /documents/{document_id}/location_v3getDocumentLocationAdds get_converted_docx_url and optional document_version_id. Cached for 14 minutes.
DOCX partsGET /documents/{id}/location?version_id=getWriterPartUrls, getDocxFileRequires version ID; cached for 14 minutes.

Document content helpers

getTextDocument, getBinaryDocument, and getDocxFile compose metadata and location APIs. They return client-side errors such as INVALID_DOCUMENT, INVALID_FILETYPE, or NOT_FOUND when the location payload does not contain a usable presigned URL or the resource is gone.

Channels and messages

OperationEndpointClient method
Create channelPOST /channelscreateChannel
List channelsGET /comms/channelsgetChannels
Get or create DMPOST /channels/get_or_create_dmgetOrCreateDirectMessage
Get or create private channelPOST /channels/get_or_create_privategetOrCreatePrivateChannel
Rename channelPATCH /channels/{channel_id}patchChannel
Delete channelDELETE /channels/{channel_id}deleteChannel
Send messagePOST /channels/{channel_id}/messagepostMessage
Edit messagePATCH /channels/{channel_id}/message/{message_id}patchMessage
Delete messageDELETE /channels/{channel_id}/message/{message_id}?nonce=deleteMessage
ReactPOST /channels/{channel_id}/reactionpostReaction
Typing statePOST /channels/{channel_id}/typingpostTypingUpdate
Add/remove participantsPOST / DELETE /channels/{channel_id}/participantsaddParticipantsToChannel, removeParticipantsFromChannel
Join/leavePOST /channels/{channel_id}/join, /leavejoinChannel, leaveChannel
Messages pageGET /channels/{channel_id}/messagesgetChannelMessages
Filtered messagesPOST /channels/{channel_id}/messagespostChannelMessages
Thread repliesGET /channels/{channel_id}/messages/{message_id}/repliesgetThreadReplies
Context around messageGET /channels/{channel_id}/messages/{message_id}/contextgetMessageWithContext
Resolve messageGET /channels/{channel_id}/messages/{message_id}/resolveresolveChannelMessage
Attachments pageGET /channels/{channel_id}/attachmentsgetChannelAttachments
ParticipantsGET /channels/{channel_id}/participantsgetChannelParticipants
Entity mentionPOST /channels/mentionscreateEntityMention
Delete mentionDELETE /channels/mentions/{mention_id}deleteEntityMention
Attachment referencesGET /channels/attachments/{entity_type}/{entity_id}/referencesattachmentReferences
Channel activityGET / POST /channels/activitygetActivity, postActivity

postMessage de-duplicates mentions before sending. Message mutations can pass a nonce; deletes serialize it as a query parameter while sends, edits, reactions, and typing include it in the JSON body.

Projects

OperationEndpointClient method
List projectsGET /projectsprojects.getAll
Get projectGET /projects/{id}projects.getProject
Get pending uploadsGET /projects/pendingprojects.getPending
Create projectPOST /projectsprojects.create
Edit projectPATCH /projects/{id}projects.edit
Soft delete projectDELETE /projects/{id}projects.delete
Permanent deleteDELETE /projects/{id}/permanentprojects.permanentlyDelete
RestorePUT /projects/{id}/revert_deleteprojects.revertDelete
Project contentGET /projects/{id}/contentprojects.getContent
Share permissionsGET /projects/{id}/permissionsprojects.getPermissions
User access levelGET /projects/{id}/access_levelprojects.getUserAccessLevel
Batch previewPOST /projects/previewprojects.getPreview
Zip upload requestPOST /projects/upload_extractprojects.createUploadZipRequest

Calls

OperationEndpointClient method
Join or create callGET /call/{channel_id}callServiceClient.getOrCreateCall
Leave or end callDELETE /call/{channel_id}callServiceClient.leaveCall
Active call checkGET /call/{channel_id}/activecallServiceClient.checkActiveCall
Get recordGET /call/record/{call_id}callServiceClient.getCallRecord
Edit recordPATCH /call/record/{call_id}callServiceClient.editCallRecord
Delete recordDELETE /call/record/{call_id}callServiceClient.deleteCallRecord
Toggle team sharingPOST /call/record/{call_id}/share-with-team/togglecallServiceClient.toggleShareWithTeam
Batch previewPOST /call/record/previewstorageServiceClient.getBatchCallPreviews
Edit transcript speakersPATCH /call/record/{call_id}/transcriptOpenAPI route
Provider webhookPOST /call/webhookServer route
Internal transcript ingestPOST /call/{channel_id}/transcriptInternal route guarded by x-macro-internal-call

checkActiveCall maps an empty 204-style response to null. toggleShareWithTeam returns a primitive boolean at runtime even though the generic type is constrained to object-like values.

CRM

OperationEndpointRequest bodyBehavior
List company contactsGET /crm/companies/{company_id}/contactsnoneReturns non-hidden contacts for a team-owned company; owned companies with no visible contacts return [].
Toggle email syncPUT /crm/companies/{company_id}/email-sync{ email_sync: boolean }Setting false permanently removes existing CRM contacts and contact sources. Enabling on hidden companies returns 409.
Toggle company hiddenPUT /crm/companies/{company_id}/hidden{ hidden: boolean }Hiding disables email_sync and clears contacts/contact sources. Unhiding does not re-enable email sync.
Toggle contact hiddenPUT /crm/contacts/{contact_id}/hidden{ hidden: boolean }Display-only opt-out for contacts.
List comment threadsGET /crm/comments/{entity_type}/{entity_id}noneentity_type is crm_company or crm_contact.
Create comment/replyPOST /crm/comments/{entity_type}/{entity_id}{ content, threadId? }Without threadId, creates a new thread; with threadId, appends a reply.
Edit commentPATCH /crm/comment/{comment_id}{ content }Returns the updated comment.
Delete commentDELETE /crm/comment/{comment_id}noneSoft-deletes comment; also soft-deletes thread if it was the last live comment.

Soup workspace feed

Soup endpoints return workspace items with next_cursor and a frecency_score. The router supports regular filters, AST filters, and grouped AST queries.

OperationEndpointClient methodKey params
Default feedGET /items/soupnot wrapped directlyexpand, limit, sort_method, cursor
Filtered feedPOST /items/soup?cursor=getSoupItemsTyped EntityFilters, email_view, expand, limit, sort_method
AST feedPOST /items/soup/ast?cursor=getSoupAstItemsAST filters, email_view, expand, limit, sort_method
Grouped AST feedPOST /items/soup/ast/grouped?cursor=getGroupedSoupAstItemsgroup_by, optional group_key, optional grouped sort

Supported feed sort values are viewed_at, created_at, updated_at, viewed_updated, and frecency. Grouped queries use simple sorts only and default to viewed_updated.

Grouped responses include:

Flat item page ordered by group and sort. Global cursor for the next page. Group metadata with key, label, display_order, total_count, page_count, start_index, and per-group next_cursor.

CRM-scoped soup filters require team membership. Hidden CRM company filters require admin or owner team role.

Pins and history

OperationEndpointClient methodDefaults
List pinsGET /pins?limit=&offset=getPinsClient defaults to limit=10, offset=0.
Add pinPOST /pins/{pinned_item_id}pinItemBody is AddPinRequest without the path id.
Remove pinDELETE /pins/{pinned_item_id}removePinBody is PinRequest without the path id.
Reorder pinsPATCH /pinsreorderPinsBody is an array of ReorderPinRequest.
List historyGET /historygetUsersHistoryReturns { data: Item[] }.
Upsert history itemPOST /history/{item_type}/{item_id}upsertItemToUserHistoryPerforms tracking side effects.
Remove history itemDELETE /history/{item_type}/{item_id}removeItemFromUserHistoryReturns success response.

Recognized client item types include document, chat, project, channel, email, channel_message, call, automation, and thread. blockNameToItemType maps unknown block names to document.

Permissions and views

OperationEndpointClient method
Entity permissionGET /entity/{entity_type}/{entity_id}/permissionsUse raw DSS fetch or generated schema
Document share permissionsGET /documents/{document_id}/permissionsgetDocumentPermissions
Document permission tokenPOST /documents/permissions_token/{document_id}permissionsTokens.createPermissionToken
Validate permission tokenPOST /documents/permissions_token/validatepermissionsTokens.validatePermissionToken
Project permissionsGET /projects/{id}/permissionsprojects.getPermissions
Project access levelGET /projects/{id}/access_levelprojects.getUserAccessLevel
Document viewersGET /documents/{document_id}/viewsgetDocumentViewers
Save view locationPOST /user_document_view_location/{document_id}upsertDocumentViewLocation
Delete view locationDELETE /user_document_view_location/{document_id}deleteDocumentViewLocation
The generated OpenAPI also advertises a v2 document permissions path for the response schema. The TypeScript storage client calls the runtime route at `/documents/{document_id}/permissions`.

Properties

propertiesServiceClient is a focused client for dynamic properties attached to entities. Entity types are uppercase wire values: CHANNEL, CHAT, COMPANY, DOCUMENT, PROJECT, TASK, THREAD, and USER.

OperationEndpointClient method
List definitionsGET /properties/definitionslistProperties
Create definitionPOST /properties/definitionscreatePropertyDefinition
Delete definitionDELETE /properties/definitions/{definition_id}deletePropertyDefinition
List optionsGET /properties/definitions/{definition_id}/optionsgetPropertyOptions
Add optionPOST /properties/definitions/{definition_id}/optionsaddPropertyOption
Delete optionDELETE /properties/definitions/{definition_id}/options/{option_id}deletePropertyOption
Get entity propertiesGET /properties/entities/{entity_type}/{entity_id}getEntityProperties
Set entity propertyPUT /properties/entities/{entity_type}/{entity_id}/{property_id}setEntityProperty
Delete entity propertyDELETE /properties/entity_properties/{entity_property_id}deleteEntityProperty
Bulk get propertiesPOST /properties/entities/bulkgetBulkEntityProperties
Mark status completePATCH /properties/entities/{entity_type}/{entity_id}/status/completesetPropertyStatusComplete

Definition filters

Scope filter for property definitions. When set, definitions include select options. Filters out definitions that cannot attach to the requested entity type.

Property values

Stored property values use capitalized response variants such as Boolean, Number, String, Date, SelectOption, EntityReference, and Link. Set requests use lower-case action variants such as boolean, number, string, date, select_option, multi_select_option, entity_reference, multi_entity_reference, link, and multi_link.

```json title="PUT /properties/entities/TASK/task-id/property-id" { "value": { "type": "select_option", "option_id": "00000000-0000-0000-0000-000000000000" } } ```

Bulk property requests accept { entities, property_ids? }. The public endpoint returns only entities the user can view; inaccessible entities are omitted.

Search

searchClient wraps unified and channel-specific search. Both endpoints accept page_size and optional base64 cursor query params. Server-side validation rejects page sizes outside 0..=100 and queries shorter than 3 characters.

OperationEndpointClient methodResponse
Unified searchPOST /searchsearch{ results, next_cursor }
Simple unified searchPOST /search/simplegenerated simpleUnifiedSearchFlat, non-grouped response
Channel content searchPOST /search/channelsearchChannel{ results, next_cursor, total_count }

Unified request shape:

Search string; must be at least 3 characters after trimming. Match mode passed to the search service. Defaults to content when omitted. Filter bundle for calls, channels, chats, CRM companies, documents, email, foreign entities, projects, and property filters.

Channel search accepts query or terms, match_type, optional sort, and channel filters. The server requires at least one channel_id for /search/channel, even though the generated filter model describes empty arrays as broadly searchable.

```json title="POST /search/channel?page_size=20" { "query": "launch notes", "match_type": "partial", "sort": "message", "channel_ids": ["00000000-0000-0000-0000-000000000000"] } ```

Operational checks for API changes

  • Update Rust utoipa route annotations and rerun OpenAPI generation when adding or changing server routes.
  • Regenerate TypeScript schemas before relying on new request or response types in clients.
  • Verify handwritten clients when specs lag server routes; current examples include grouped soup and channel search wrappers.
  • Keep client return mapping explicit when endpoints return primitives, 204 bodies, or wrapped { data } payloads.

18. Auth and team APIs

Reference: Login, OAuth callbacks, JWT refresh, account links, team membership, invites, permissions, billing, and user endpoints.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/18-auth-and-team-apis.md
  • Generated: 2026-06-01T00:59:40.293Z

Source Files

  • js/app/packages/service-clients/service-auth/openapi.json
  • js/app/packages/service-clients/service-auth/client.ts
  • rust/cloud-storage/authentication_service/src/openapi.rs
  • rust/cloud-storage/authentication_service/src/config.rs
  • js/app/packages/core/auth/sso.ts
  • js/app/packages/core/auth/logout.ts

The auth API is an Axum service exposed through SERVER_HOSTS['auth-service']; the frontend wraps it with authServiceClient, uses cookie credentials by default, and can switch to bearer-token requests with ENABLE_BEARER_TOKEN_AUTH.

Runtime surface

SurfaceRuntime behavior
Base URLSERVER_HOSTS['auth-service'] (http://localhost:8080 when VITE_LOCAL_SERVERS selects local auth)
Frontend clientjs/app/packages/service-clients/service-auth/client.ts
OpenAPIGenerated by rust/cloud-storage/authentication_service/src/openapi.rs from swagger::ApiDoc
Swagger UIMounted at /docs, spec at /api-doc/openapi.json
Auth transportHttpOnly cookies, or Authorization: Bearer ... for selected token flows
Standard JSON error{ "message": string }
Client result shapeneverthrow Result<T, ResultError[]> from safeFetch / fetchWithAuth
Some OpenAPI query parameters are marked `required: true` even when server code treats them as optional. Use the Rust handler behavior and the hand-written frontend client as the integration source of truth.

Login and session lifecycle

Password login

:::endpoint POST /login/password Password login Accepts:

{
  "email": "[email protected]",
  "password": "password"
}

Returns UserTokensResponse:

{
  "access_token": "jwt",
  "refresh_token": "refresh"
}

The server lowercases the email, validates email format, authenticates through FusionAuth, sets access and refresh cookies, and returns the same tokens in JSON. If FusionAuth reports an unregistered user, the handler registers the user from email and retries login. Unverified users receive 401. :::

Passwordless login

:::endpoint POST /login/passwordless Start passwordless login Accepts:

{
  "email": "[email protected]",
  "redirect_uri": "https://macro.com/app/login",
  "referral_code": "optional"
}

Responses:

StatusMeaning
200Login code was generated, cached against the email, and sent.
202The email belongs to an identity provider; response body is { "idp_id": string }.
400Invalid email or invalid request.
403Blocked email.
500FusionAuth, cache, or send failure.

The callback endpoint is GET /oauth/passwordless/{code}?email=.... The frontend uses disable_redirect=true when it wants JSON tokens instead of a redirect. :::

SSO login

:::endpoint GET /login/sso Start SSO Supported query parameters:

ParameterRequired by serverNotes
idp_nameOne of idp_name or idp_idIdentity provider display name, for example Google/Gmail.
idp_idOne of idp_name or idp_idDirect FusionAuth identity provider ID.
login_hintNoPre-fills provider login when supported.
original_urlNoWeb redirect target after successful login.
is_mobileNoCauses callback to issue a session code instead of only cookies.
referral_codeNoTracked after login when present.

The handler builds FusionAuth authorization state and returns a temporary redirect to the provider. :::

sequenceDiagram
  participant Web as Web app
  participant Auth as authentication_service
  participant IdP as OAuth provider
  participant Cache as Redis/session cache

  Web->>Auth: GET /login/sso?idp_name=...&original_url=...
  Auth-->>Web: Redirect to provider authorization URL
  Web->>IdP: Provider login
  IdP-->>Auth: GET /oauth/redirect?code=...&state=...
  Auth->>Auth: Complete authorization code grant
  alt mobile state
    Auth->>Cache: Store refresh token by generated session code
    Auth-->>Web: Redirect original_url?token=session_code
    Web->>Auth: GET /session/login/{session_code}
    Auth-->>Web: UserTokensResponse + cookies
  else web state
    Auth-->>Web: Set cookies + HTML redirect to original_url
  end

Native mobile SSO

startSsoLogin() uses the Tauri plugin:auth|authenticate command on iOS with:

{
  authUrl,
  callbackScheme: "macro",
  ephemeralSession: true
}

The plugin returns a token session code. The client calls authServiceClient.sessionLogin({ session_code }), stores returned tokens in macroAccessToken, clears any pending token refresh promise, and invalidates auth queries after success.

Token refresh and API tokens

:::endpoint POST /jwt/refresh Refresh access and refresh tokens Tokens can be supplied by cookies or by headers:

Authorization: Bearer <access_token>
x-macro-refresh-token: <refresh_token>

Behavior:

ConditionResult
Access token is still validReturns original access and refresh tokens.
Access token is expiredCalls FusionAuth refresh, sets new cookies, returns new tokens.
Token cannot be decoded401 unauthorized.
Refresh token is invalid400 invalid refresh token.

The frontend coalesces concurrent refreshes: fetchWithToken() keeps a single tokenPromise, and authServiceClient.getAccessToken() keeps a single ongoingRefresh. :::

:::endpoint GET /jwt/macro_api_token Generate Macro API token Uses the authenticated user context and optional email query parameter to issue a macro_api_token for the selected profile:

{
  "macro_api_token": "jwt"
}

service-auth/fetch.ts uses this token as the bearer credential for service calls when bearer-token auth is enabled. :::

Logout

authServiceClient.logout() clears persisted access-token state and posts to /logout. The server clears access and refresh cookies by setting expired cookies, then attempts FusionAuth logout for the current tenant.

useLogout() also clears the app’s login=false cookie, resets cached user info to an unauthenticated shape, tracks sign_out, resets analytics, and then:

PlatformRedirect behavior
Native mobileFetches SERVER_HOSTS['auth-logout'] with no-cors, then navigates to /login.
WebSets window.location.href to SERVER_HOSTS['auth-logout'].

Account links and OAuth callbacks

Account linking requires an authenticated caller.

EndpointMethodClient methodPurpose
/linkPOSTnoneCreates an in-progress link row and returns { link_id }. Limited to 5 in-progress links per FusionAuth user.
/link/githubPOSTinitGithubLink(originalUrl?)Creates a GitHub in-progress link and returns { authorization_url, link_id }.
/link/githubDELETEdeleteGithubLink()Deletes the user’s GitHub link; it does not uninstall the GitHub app from team repositories.
/link/gmailPOSTinitGmailLink(originalUrl?)Creates a Gmail in-progress link and returns Google OAuth URL plus link_id.
/user/link_existsGETcheckLinkExists({ idp_name?, idp_id? })Returns { link_exists: boolean }. One of idp_name or idp_id is expected.
/oauth2/{provider}/callbackGETprovider redirectCompletes custom Google/GitHub login or linking.

Gmail linking requests Google scopes for OpenID profile/email, Gmail modify, contacts read-only, other contacts read-only, and Gmail settings basic. After Google consent, the callback redirects back to original_url with link_id appended so the frontend can finish inbox provisioning.

GitHub linking either redirects to original_url or returns a small HTML page that posts { type: "github-linked", success: true } to window.opener and closes the popup.

The OpenAPI spec includes account merge paths (`/merge`, `/merge/verify/{code}`), but the auth service router shown in `api_router` does not mount `merge::router()`. Treat those paths as not available at runtime unless the router is updated.

User endpoints

EndpointMethodClient methodResponse
/user/meGETgetUserInfo(){ user_id, organization_id?, permissions }, mapped to { authenticated, permissions, userId, organizationId }.
/user/meDELETEdeleteUser(){ success: boolean }; client clears persisted token state first.
/user/legacy_user_permissionsGETgetLegacyUserPermissions()Legacy UI aggregate: permissions, email, name, license status, tutorial flag, group, Chrome extension state, trial state, AI consent, referral code.
/user/nameGETgetUserName(){ id, first_name?, last_name? }.
/user/namePUTputUserName({ first_name?, last_name? })Empty response.
/user/get_namesPOSTgetUserNames(){ names: UserName[] }.
/user/get_names_with_emailPOSTgetUserNamesWithEmail(){ names: UserName[] }, with email-contact fallback.
/user/profile_picturePUTputProfilePicture({ url })Empty response.
/user/profile_picturesPOSTpostProfilePictures({ user_id_list }){ pictures: UserProfilePicture[] }.
/user/organizationGETgetOrganization(){ organizationId, organizationName }, or 204; frontend maps errors/no-content to undefined organization fields.
/user/quotaGETuserQuota(){ documents, ai_chat_messages, max_documents, max_ai_chat_messages }, or 204 for premium users with no quota.
/user/tutorialPATCHpatchUserTutorial({ tutorialComplete })Empty response.
/user/onboardingPATCHcompleteOnboarding({ firstName, lastName, industry, title })Empty response.
/user/groupPATCHsetGroup({ group })Empty response.
/user/ai_consentPATCHpatchAiConsent({ aiDataConsent })Empty response.

Permissions

EndpointAuthResponse
GET /permissionsNot wrapped in JWT middleware in the routerPermission[] with { id, description }.
GET /permissions/meJWT requiredstring[] permission IDs for the current user.
GET /user/meJWT requiredIncludes current user permissions alongside user and organization IDs.

Teams, invites, and roles

All /team routes are protected by JWT middleware. Team access checks are enforced with typed extractors for member, admin, and owner roles.

RoleUsed for
memberRead team details.
adminPatch team metadata/roles, list team invites, toggle CRM.
ownerInvite users, delete invites, remove users, delete team.

Team models

type TeamRole = "member" | "admin" | "owner";

interface Team {
  id: string;
  name: string;
  owner_id: string;
  slug: string;
}

interface TeamWithMembers {
  team: Team;
  members: {
    team_id: string;
    user_id: string;
    role: TeamRole;
  }[];
}

Team endpoints

EndpointMethodClient methodAccess
/team/userGETgetUserTeams()Authenticated user.
/team/user/invitesGETgetUserInvites()Authenticated user.
/teamGETgetTeam()Team member.
/teamPOSTcreateTeam({ name })Authenticated user.
/teamPATCHpatchTeam({ name?, slug?, user_role_updates? })Admin or owner.
/teamDELETEdeleteTeam()Owner. Cancels team subscription and is irreversible.
/team/invitesGETgetTeamInvites()Admin or owner.
/team/invitePOSTinviteToTeam({ invites })Owner. Returns 201 when invites are created, 304 if nothing new was sent.
/team/invite/{team_invite_id}DELETEdeleteTeamInvite(id)Owner.
/team/join/{team_invite_id}GETjoinTeam(id)Invited authenticated user.
/team/join/{team_invite_id}DELETErejectInvitation(id)Invited authenticated user.
/team/remove/{remove_user_id}DELETEremoveUserFromTeam(userId)Owner.
/team/crmPATCHnot wrapped in authServiceClientAdmin or owner.

InviteToTeamRequest accepts:

{
  "invites": [{ "email": "[email protected]" }]
}

Emails are parsed and lowercased. Invalid emails, empty invite lists, and too many emails return 400.

Team CRM toggle

:::endpoint PATCH /team/crm Enable or disable team CRM Request:

{
  "enabled": true
}

Response:

{
  "enabled": true,
  "changed": true,
  "backfill_enqueued": 4,
  "backfill_failed": 0
}

Enabling CRM enqueues a best-effort PopulateCrmForUser message per team member. Disabling CRM purges the team’s CRM data through the CRM table cascade. :::

Billing and subscription endpoints

Billing endpoints live under /user/stripe/* and require an authenticated user.

EndpointMethodClient methodNotes
/user/stripe/checkoutPOSTcreateCheckoutSession()Legacy checkout. Accepts optional tier; defaults to haiku.
/user/stripe/checkoutv2POSTcreateCheckoutSessionV2()Current checkout. Backend uses configured price ID and can include team metadata when the caller owns a team.
/user/stripe/portalPOSTcreatePortalSession({ returnUrl })Returns Stripe billing portal URL.
/user/stripe/subscriptionPATCHpatchSubscriptionTier({ newTier })Swaps both RBAC subscription role and Stripe subscription line item.

Checkout request shape

{
  "successUrl": "https://macro.com/app/?subscriptionSuccess=true",
  "cancelUrl": "https://macro.com/app?subscriptionCancel=true",
  "discount": "PROMO",
  "metadata": {
    "gaClientId": "optional",
    "fbp": "optional",
    "fbc": "optional"
  }
}

The response is:

{
  "url": "https://checkout.stripe.com/..."
}

Subscription tier changes

Supported tiers are:

type StripeProductTier = "haiku" | "sonnet" | "opus";

PATCH /user/stripe/subscription rejects:

StatusFrontend semantic codeMeaning
400TIER_UNCHANGEDThe user is already on the requested tier.
403USER_IN_TEAMTeam members cannot manage their own tier.
404NO_SUBSCRIPTIONNo active subscription or subscription role exists.
409UPDATE_IN_PROGRESSAnother tier update holds the per-user subscription lock.
500SERVER_ERRORStripe, RBAC, DB, or inconsistent subscription state failure.

The backend performs the RBAC role swap before the Stripe line-item update and rolls the role swap back if Stripe fails. Stripe requests use a stable idempotency key for the logical tier-swap operation.

Configuration

The auth service loads configuration from environment variables in Config::from_env().

KeyRequiredPurpose
BASE_URLYesPublic auth service base URL; used for OAuth2 callback URIs.
DATABASE_URLYesPostgres connection.
REDIS_URIYesRedis/cache connection for login codes, session codes, and rate limits.
FUSIONAUTH_TENANT_IDYesFusionAuth tenant.
FUSIONAUTH_API_KEY_SECRET_KEYYesSecret name, or local raw value in local environment.
FUSIONAUTH_CLIENT_IDYesFusionAuth application client ID.
FUSIONAUTH_CLIENT_SECRET_KEYYesSecret name, or local raw value in local environment.
FUSIONAUTH_BASE_URLYesFusionAuth base URL.
FUSIONAUTH_OAUTH_REDIRECT_URIYesFusionAuth OAuth redirect URI.
GOOGLE_CLIENT_IDYesGoogle OAuth client ID.
GOOGLE_CLIENT_SECRET_KEYYesSecret name, or local raw value in local environment.
STRIPE_SECRET_KEYYesSecret name, or local raw value in local environment.
SERVICE_INTERNAL_AUTH_KEYYesInternal service auth key.
DOCUMENT_STORAGE_SERVICE_URLYesDocument storage service URL.
NOTIFICATION_QUEUEYesNotification SQS queue.
SEARCH_EVENT_QUEUEYesSearch-event SQS queue.
LINK_MANAGER_QUEUEYesEmail link manager queue.
EMAIL_BACKFILL_QUEUEYesTeam join / CRM backfill queue.
GITHUB_CLIENT_IDYesGitHub OAuth client ID.
GITHUB_CLIENT_SECRETYesGitHub OAuth secret.
GITHUB_IDP_IDYesGitHub identity provider ID.
PORTNoDefaults to 8080.
GA_MEASUREMENT_ID, GA_API_SECRETNoGoogle Analytics conversion tracking.
META_PIXEL_ID, META_ACCESS_TOKEN, META_TEST_EVENT_CODENoMeta conversion tracking.
POSTHOG_API_KEY, POSTHOG_HOSTNoPostHog analytics.

Cookie names and attributes depend on environment:

EnvironmentCookie prefixDomainSameSite
Productionnonemacro.comStrict
Developdev-macro.comNone
Locallocal-noneNone

Access and refresh cookies are Secure, HttpOnly, path /, and expire in 365 days.

Client integration notes

  • authApiFetch() always sends credentials: "include".
  • fetchWithToken() retries the original request after a 401 by calling POST /jwt/refresh.
  • service-auth/fetch.ts obtains a Macro API token via GET /jwt/macro_api_token and sends it as Authorization: Bearer ....
  • authServiceClient.sessionLogin() and passwordlessCallback() persist { accessToken, refreshToken, expiresAt } under macroAccessToken.
  • User info queries use getLegacyUserPermissions() for the main UI cache and mark data stale after 15 seconds.
  • Team mutations invalidate the relevant Solid Query team caches after create, patch, delete, invite, join, or reject operations.

Related pages

Frontend client wrappers, `safeFetch`, generated schemas, and mock client registration. Team ownership, Stripe subscriptions, CRM toggles, and operational recovery paths.

19. Email and notification APIs

Reference: Gmail init and sync, drafts, threads, labels, attachments, notification preferences, unread state, and unsubscribe routes.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/19-email-and-notification-apis.md
  • Generated: 2026-06-01T00:58:55.627Z

Source Files

  • js/app/packages/service-clients/service-email/openapi.json
  • js/app/packages/service-clients/service-email/client.ts
  • rust/cloud-storage/email_service/src/openapi.rs
  • js/app/packages/service-clients/service-notification/openapi.json
  • js/app/packages/service-clients/service-notification/client.ts
  • rust/cloud-storage/notification_service/src/openapi.rs

Macro exposes email and notification HTTP APIs through Rust utoipa OpenAPI specs, generated TypeScript schemas, and handwritten frontend clients: emailClient uses SERVER_HOSTS['email-service'], and notificationServiceClient uses SERVER_HOSTS['notification-service'].

Runtime surface

AreaFrontend clientOpenAPI artifactRust OpenAPI binary
Emailjs/app/packages/service-clients/service-email/client.tsjs/app/packages/service-clients/service-email/openapi.jsonemail_service_openapi
Notificationsjs/app/packages/service-clients/service-notification/client.tsjs/app/packages/service-clients/service-notification/openapi.jsonnotification_service_openapi

Both clients call fetchWithToken, so callers receive neverthrow Result values and get token refresh behavior on unauthorized cookie-based requests. In development, VITE_LOCAL_SERVERS=ALL maps email to http://localhost:8087 and notifications to http://localhost:8089; otherwise the remote service hosts are used.

Generated schemas are refreshed from local Rust OpenAPI binaries with `cd js/app && bun run gen-api email-service` or `bun run gen-api notification-service`.

Email API

Gmail initialization and backfill

POST /email/init initializes Gmail email for the authenticated user. It registers a Gmail watch, upserts an email_links row, stores the Gmail history id, creates a backfill job, and enqueues the initial backfill operation.

Optional query parameter from a `/link/gmail` flow. When supplied, init uses the linked email recorded on the in-progress link instead of the JWT email. The accessible email link id. Present when init enqueues a new backfill job. It can be absent when init delegates to an existing child inbox.

Common backfill routes:

MethodPathPurpose
GET/email/backfill/gmail/activeReturn the active backfill job or 204 when none exists.
GET/email/backfill/gmail/{id}Return a backfill job by id for the current email link.
DELETE/email/backfill/gmailCancel a job by JSON body { "job_id": "..." }.

/email/init limits non-@macro.com users to three recent jobs in the last 24 hours and rejects users already initialized for the same Gmail link.

Sync lifecycle

Client methodRouteStatus
emailClient.init({ linkId })POST /email/init?link_id=...Supported.
emailClient.stopSync()DELETE /email/syncSupported; enqueues asynchronous link deletion with reason ManuallyDisabled.
emailClient.startSync()POST /email/syncClient method exists, but the Rust router and OpenAPI currently expose only DELETE /email/sync. Use init to enable sync unless the server route is added.

Threads and unread state

Thread listing uses cursor pagination:

await emailClient.getPreviews({
  view: 'inbox',
  limit: 20,
  sort_method: 'viewed_updated',
  cursor,
});
RouteInputsResponse
GET /email/threads/previews/cursor/{view}view, optional limit, sort_method, cursor{ items, next_cursor? }
GET /email/threads/{thread_id}optional offset default 0, limit default 5, max 100{ thread }
GET /email/threads/{id}/messagesoptional since, limitParsedMessage[]
POST /email/threads/{id}/seenthread idEmptyResponse
PATCH /email/threads/{id}/archived{ value: boolean }EmptyResponse
PATCH /email/threads/{id}/labels{ label_id, value }updated thread labels
PATCH /email/threads/{thread_id}/project`{ projectId: stringnull }`

Email unread state is persisted on the thread and message records and mirrored to Gmail labels. POST /email/threads/{id}/seen upserts user history, marks the thread and unread messages as read, removes the UNREAD label locally, then enqueues Gmail operations to remove the provider UNREAD label.

Supported preview view values include inbox, sent, drafts, starred, all, important, other, and user:<label>.

Drafts, sending, and scheduling

Draft creation and sending use ApiDraftInput.

await emailClient.createDraft({
  draft: {
    subject: 'Review',
    to: [{ email_address: '[email protected]' }],
    body_text: 'Please review this.',
  },
});

await emailClient.sendMessage({
  message: {
    subject: 'Review',
    to: [{ email_address: '[email protected]' }],
    body_text: 'Please review this.',
  },
});
OperationRouteBody
Create draftPOST /email/drafts{ draft: ApiDraftInput, send_time? }
Delete draftDELETE /email/drafts/{id}none
Send messagePOST /email/messages{ message: ApiDraftInput }
Schedule draftPUT /email/drafts/scheduled/{id}scheduled send request
Unschedule draftDELETE /email/drafts/scheduled/{message_id}none
List scheduled draftsGET /email/drafts/scheduled?offset=&limit=pagination params

ApiDraftInput requires subject and supports to, cc, bcc, body_html, body_macro, body_text, provider_id, provider_thread_id, thread_db_id, replying_to_id, headers_json, and db_id.

Attachments

RoutePurpose
GET /email/attachments/{id}Return an attachment wrapper.
GET /email/attachments/{id}/document_idReturn or create the Macro document id for an email attachment.
POST /email/drafts/{id}/attachmentsCreate a draft attachment record and return a presigned upload URL.
DELETE /email/drafts/{id}/attachments/{attachment_id}Remove a draft attachment.
POST /email/drafts/{id}/forwarded-attachmentsAttach an existing email attachment to a draft.
DELETE /email/drafts/{id}/forwarded-attachments/{attachment_id}Remove a forwarded attachment.

AddDraftAttachmentRequest requires:

SHA-256 hex string. The server validates exactly 64 ASCII hex characters. Raw byte size. The server rejects non-positive sizes and enforces an 18 MB safe raw attachment limit.

The response contains attachment_id, upload_url, and content_type.

Labels, filters, contacts, and settings

FeatureRoutes
LabelsGET /email/labels, POST /email/labels, DELETE /email/labels/{id}
Message labelsPATCH /email/messages/labels with { message_ids, label_id, value }
Thread labelsPATCH /email/threads/{id}/labels with { label_id, value }
FiltersGET /email/filters, PUT /email/filters, DELETE /email/filters/{id}
ContactsGET /email/contacts, POST /email/contacts/block, POST /email/contacts/unblock
LinksGET /email/links
SettingsPATCH /email/settings with { settings }

The generated Settings schema currently exposes signature_on_replies_forwards.

Notification API

User notifications

The notification service mounts the internal router both under /{version} and unversioned paths. The OpenAPI annotations describe read routes under /v1/user_notifications and bulk mutation/delete routes under /v2/user_notifications, while the frontend client currently calls unversioned /user_notifications paths.

Client methodRoute used by clientShape
userNotifications({ limit, cursor })GET /user_notifications{ items, next_cursor? }
getUserNotificationById(id)GET /user_notifications/{id}ApiUserNotification
bulkGetUserNotificationsByEventItemId(...)POST /user_notifications/item/bulkbody { eventItemIds }
markNotificationAsSeen(...)PATCH /user_notifications/bulk/seenbody { notificationIds }
markNotificationAsDone(...)PATCH /user_notifications/bulk/donebody { notificationIds }
bulkMarkNotificationAsUndone(...)PATCH /user_notifications/bulk/undonebody { notificationIds }
markNotificationEntityAsSeen(...)PATCH /user_notifications/item/{event_item_id}/seenno body
markNotificationEntityAsDone(...)PATCH /user_notifications/item/{event_item_id}/doneno body

ApiUserNotification includes id, owner_id, entity_id, entity_type, notification_event_type, notification_metadata, sent, done, viewed_at, created_at, updated_at, optional deleted_at, and optional sender_id.

Notification unread state is represented by viewed_at; task/action completion is represented separately by done.

Notification preferences

OpenAPI exposes notification-type preferences:

MethodPathPurpose
GET/v1/user_notifications/preferencesReturn { disabled_types: string[] }.
PUT/v1/user_notifications/preferences/{notification_event_type}/disableDisable a notification event type.
PUT/v1/user_notifications/preferences/{notification_event_type}/enableRe-enable a notification event type.

The handwritten notificationServiceClient does not currently wrap these preference endpoints.

Device registration

The notification client exposes push device registration helpers:

await notificationServiceClient.registerDevice({
  deviceType: 'ios',
  token: '<push-token>',
});

await notificationServiceClient.unregisterDevice({
  deviceType: 'ios',
  token: '<push-token>',
});

The local API router nests the device router at /device; the OpenAPI artifact includes the DeviceRequest schema but does not list device paths.

Unsubscribe routes

RoutePurpose
GET /unsubscribeReturn UserUnsubscribe[], each with item_id and item_type.
POST /unsubscribe/item/{item_type}/{item_id}Unsubscribe the current user from one notification item.
DELETE /unsubscribe/item/{item_type}/{item_id}Remove one item-level unsubscribe.
POST /unsubscribe/muteMute all notifications for the current user.
DELETE /unsubscribe/muteUnmute global notifications; manually muted items remain muted.
POST /unsubscribe/emailOpenAPI handler for email unsubscribe.
The notification Rust router currently registers `/unsubscribe/email` with `GET`, while the handler annotation and generated OpenAPI declare `POST /unsubscribe/email`. The frontend client does not expose an `unsubscribeEmail` wrapper. Verify the runtime method before adding callers.

Client return pattern

Both handwritten clients return Result objects:

const result = await emailClient.getThread({
  thread_id,
  offset: 0,
  limit: 20,
});

if (result.isErr()) {
  // ResultError<FetchWithTokenErrorCode>[]
  return;
}

const { thread } = result.value;

Use generated schema types from ./generated/schemas for request and response bodies, but prefer the handwritten emailClient and notificationServiceClient for application code because they apply service host selection and authenticated fetch behavior.

Next

  • Update OpenAPI/client drift before adding new sync, device, or unsubscribe-email callers.
  • Regenerate schemas after Rust handler or utoipa annotation changes with bun run gen-api <service-name>.

20. AI chat streaming API

Reference: DCS chat endpoints, stream payloads, toolset selection, model identifiers, extraction statuses, structured completion, and stop semantics.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/20-ai-chat-streaming-api.md
  • Generated: 2026-06-01T00:59:46.491Z

Source Files

  • js/app/packages/service-clients/service-cognition/openapi.json
  • js/app/packages/service-clients/service-cognition/client.ts
  • rust/cloud-storage/document_cognition_service/src/model/stream.rs
  • rust/cloud-storage/document_cognition_service/src/api/stream/chat_message/mod.rs
  • rust/cloud-storage/document_cognition_service/src/api/stream/stop.rs
  • rust/cloud-storage/agent/src/model.rs
  • js/app/packages/core/component/AI/types/index.ts

DCS exposes chat generation as an HTTP initiation API plus durable stream delivery through connection_gateway; POST /stream/chat/message returns a stream_id and chat_id, then clients subscribe to the chat stream to receive ChatStream payloads until stream_end.

Runtime shape

UI / service-cognition client
  POST /stream/chat/message
        │ returns { stream_id, message_id, chat_id }
        ▼
DCS stream repo: id = { entity_type: "chat", entity_id: chat_id, stream_id }
        │ publishes payloads
        ▼
connection_gateway websocket
        │ message.type = "stream"
        ▼
@service-connection/stream.subscribe("chat", chat_id, stream_id)
The chat endpoint is not an SSE response. The HTTP response only acknowledges stream creation. Chunks arrive later as `connection_gateway` websocket stream events.

Endpoints

:::endpoint POST /stream/chat/message Initiate a streamed chat response

Creates or reuses a chat, stores the incoming user message, starts the agent loop, and publishes stream payloads under the returned stream_id.

Request body

FieldTypeRequiredBehavior
contentstringYesUser message text.
modelAgentModelYesRequired by schema. In the current chat message handler, the effective response model is resolved from ChatModelAccess rather than read directly from this field.
chat_idstring | nullNoExisting chat ID. If omitted, empty, or not found, DCS creates a new persistent chat named with the default chat name.
attachmentsEntity[] | nullNoEntities attached to the message. Resolved attachment parts from the current and prior message chain are included in the prompt.
additional_instructionsstring | nullNoAppended after the selected tool prompt. Frontend chat input also appends a model instruction string.
toolsetToolSetNoDefaults to { "type": "all" }. See Toolset selection.
POST /stream/chat/message
Content-Type: application/json

{
  "content": "Summarize the attached document.",
  "model": "smart",
  "chat_id": "chat_123",
  "attachments": [
    {
      "entity_type": "document",
      "entity_id": "doc_456"
    }
  ],
  "toolset": { "type": "all" },
  "additional_instructions": "Prefer concise bullets."
}

Response body

FieldTypeBehavior
stream_idstringStream identifier for connection_gateway subscription. Generated server-side.
message_idstringAssistant message ID. The implementation sets this to the same value as stream_id.
chat_idstringActual chat ID. May differ from the request if DCS created a new chat.
{
  "stream_id": "0de0fb91-3f54-4680-97a0-f3e97b9a2a69",
  "message_id": "0de0fb91-3f54-4680-97a0-f3e97b9a2a69",
  "chat_id": "chat_123"
}

Client helper

cognitionApiServiceClient.sendStreamChatMessage(args) posts to /stream/chat/message. The chat input flow then calls:

subscribe('chat', chat_id, stream_id)

and treats stream_end as the terminal marker.

:::endpoint POST /stream/chat/message/stop Stop an in-flight chat stream

Publishes a cancellation signal for an in-flight stream. The caller must have more than View or Comment access to the chat.

Request body

FieldTypeRequiredBehavior
chat_idstringYesChat used for permission checks.
stream_idstringYesStream/message ID returned by /stream/chat/message.
{
  "stopped": true
}

stopped: false means no matching live subscriber received the cancel signal, usually because the stream already finished or the serving DCS instance is gone.

:::endpoint POST /structured-completion Run a two-phase structured completion

Runs an agent loop to gather context, then asks the model to return JSON matching output_schema.

Request body

FieldTypeRequiredBehavior
promptstringYesUser prompt for the information-gathering phase.
modelAgentModelYesModel passed to the agent loop and structured output call.
output_schemaDynamicSchemaYes{ name, schema, description? }; schema is the JSON schema to validate against.
additional_instructionsstring | nullNoAppended to the selected tool prompt.
toolsetToolSetNoDefaults to all-tool prompt behavior.
{
  "result": {
    "summary": "The document describes renewal terms.",
    "risk_level": "medium"
  }
}

Stream event envelope

connection_gateway websocket messages use type: "stream" and carry a serialized stream item:

{
  "id": {
    "entity_type": "chat",
    "entity_id": "chat_123",
    "stream_id": "0de0fb91-3f54-4680-97a0-f3e97b9a2a69"
  },
  "payload": {
    "type": "chat_message_response",
    "stream_id": "0de0fb91-3f54-4680-97a0-f3e97b9a2a69",
    "chat_id": "chat_123",
    "message_id": "0de0fb91-3f54-4680-97a0-f3e97b9a2a69",
    "content": {
      "type": "text",
      "text": "Here is the summary..."
    }
  }
}

The frontend stream store appends payloads to the stream unless payload.type === "stream_end", in which case it marks the stream done.

ChatStream payloads

typeFieldsNotes
chat_user_messagestream_id, chat_id, message_id, content, attachmentsFirst item emitted by the HTTP chat message handler so other clients can display the user message.
chat_message_responsestream_id, message_id, chat_id, contentRepeated assistant chunks. content is an AssistantMessagePart.
stream_endstream_idTerminal event. Stop and normal completion both end with this marker.
errorstream_error payloadEmitted for stream creation errors, AI stream errors, and idle timeout.
chat_message_ackmessage_id, chat_idDefined in the shared stream schema.
chat_message_finishedmessage_id, chat_id, user_message_idDefined in the shared stream schema.
chat_renamedstream_id, chat_id, nameDefined in the shared stream schema.
model_selection_changedchat_id, available_models, new_model?Defined in the shared stream schema.
token_count_changedchat_id, token_countDefined in the shared stream schema.
chat_message_status_updatechat_id, message_id, messageDefined in the shared stream schema.
extraction_status_ackattachment_id, statusInitial extraction status response.
extraction_status_updateattachment_id, statusLater extraction status update.
completion_responsecompletion_id, content, donePDF completion payload.
completion_stream_chunkcompletion_id, content, donePDF completion chunk payload.

Assistant message parts

chat_message_response.content uses camelCase part tags:

content.typeFields
texttext
thinkingthinking
toolCallname, json, id
mcpToolCallname, service, display_name, json, id
toolCallResponseJsonname, json, id
toolCallErrname, description, id

Frontend message assembly ignores non-chat_message_response payloads and merges adjacent text chunks and adjacent thinking chunks into single parts.

Toolset selection

ToolSet is a tagged object:

{ "type": "all" }
{ "type": "none" }
ToolsetPrompt selected
allDCS all_tools_prompt
noneai_tools::prompts::BASE_PROMPT
In the current chat and structured-completion implementations, `toolset` selects the system prompt. The agent session is still constructed with the combined static and MCP toolset from enabled MCP records.

Model identifiers

Use AgentModel values in API payloads and frontend state, not provider API IDs.

API valueUI labelProvider route in agent model
smartSmartAnthropic Opus 4.7
fastFastAnthropic Haiku 4.5
opus4_7Opus 4.7Anthropic Opus 4.7
sonnet4_6Sonnet 4.6Anthropic Sonnet 4.6
haiku4_5Haiku 4.5Anthropic Haiku 4.5
retiredRetiredFalls back to the Smart/Opus route

Additional model behavior:

Model groupContext windowThinking parameters
smart, opus4_7, sonnet4_6, retired1_000_000 tokensOpus uses adaptive thinking; Sonnet uses enabled thinking with budget_tokens: 10000.
fast, haiku4_5200_000 tokensEnabled thinking with budget_tokens: 10000.

Unrecognized model strings deserialize to retired, which routes to the default Opus-backed path.

Extraction statuses

Extraction statuses are tagged objects:

{ "type": "incomplete" }
StatusMeaning
incompleteNo extracted document text row exists yet.
emptyExtraction completed, but extracted text is empty or has no content length.
insufficientExtracted text has fewer than 1000 non-whitespace characters.
completeExtracted text has at least 1000 non-whitespace characters.

extraction_status_ack tells the client the current status. If it is incomplete, clients should wait for later extraction_status_update events for the same attachment_id.

Stop and persistence semantics

Stopping a stream publishes to Redis on a per-stream_id cancellation channel. The DCS instance running the stream subscribes when generation starts; any instance can publish the stop signal.

When cancellation is observed:

  1. The streaming loop breaks.
  2. DCS emits stream_end.
  3. DCS persists the assistant parts already yielded.
  4. If a yielded tool call has no matching response, DCS inserts a persisted toolCallErr with description: "cancelled" so later conversation turns remain well-formed.
  5. DCS skips the notification summarization path for cancelled streams.

The chat stream also has a 3 minute idle timeout between AI stream items. Timeout emits an error payload with internal_error, then the stream ends.

Error responses

SurfaceStatuses in API schemaBody
POST /stream/chat/message400, 401, 402, 403400 returns { "error": string, "stream_id"?: string }.
POST /stream/chat/message/stop200, 403403 returns { "error": string }.
POST /structured-completion400, 401, 402, 500Error responses return { "error": string }.

Common failure points include invalid user IDs, missing chat permissions, message storage failure, request building failure, agent stream creation failure, AI stream errors, and structured-output validation or generation failure.

Related pages

  • Chat state and stream rendering
  • Connection gateway stream subscription
  • MCP tool registration and tool call rendering

21. MCP server and tool registry

Reference: Remote MCP endpoint, OAuth-backed server, toolset composition, SendEmail boundary, generated schemas, and docs tool pages.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/21-mcp-server-and-tool-registry.md
  • Generated: 2026-06-01T01:02:24.313Z

Source Files

  • docs/AI/mcp/overview.mdx
  • docs/AI/mcp/tools/index.mdx
  • rust/cloud-storage/mcp_service/src/main.rs
  • rust/cloud-storage/mcp_service/src/tool_service.rs
  • rust/cloud-storage/ai_tools/src/lib.rs
  • docs/scripts/generate-mcp-tool-pages.ts

Macro exposes a remote Streamable HTTP MCP endpoint at https://mcp-server.macro.com/mcp; the Rust mcp_service binary mounts the protected MCP service at /mcp, brokers OAuth through FusionAuth-backed routes, builds the runtime registry from ai_tools::mcp_tools(), and executes tool calls with the authenticated Macro user id.

Remote endpoint

https://mcp-server.macro.com/mcp

The public docs configure clients as an HTTP MCP server; no local Macro process is required.

claude mcp add --transport http macro https://mcp-server.macro.com/mcp
codex mcp add macro --url https://mcp-server.macro.com/mcp
{
  "mcpServers": {
    "macro": {
      "type": "http",
      "url": "https://mcp-server.macro.com/mcp"
    }
  }
}

At runtime, mcp_service binds 0.0.0.0:${PORT} with PORT=8090 as the default. The server config enables JSON responses, disables stateful mode, and allows the host from MCP_PUBLIC_URL plus localhost and 127.0.0.1.

Client
  ├─ OAuth discovery/register/authorize/token ──> mcp_auth_proxy routes
  └─ Bearer-authenticated MCP calls /mcp ───────> AuthenticatedToolService
                                                    └─ ai_tools::mcp_tools()
                                                       └─ Macro domain services

HTTP routes

RouteAuthPurpose
GET /healthNo bearer tokenALB health check returning ok.
GET /.well-known/oauth-protected-resourceNo bearer tokenOAuth protected-resource metadata.
GET /.well-known/oauth-protected-resource/mcpNo bearer tokenProtected-resource metadata for MCP clients.
GET /mcp/.well-known/oauth-protected-resourceNo bearer tokenAlternate protected-resource discovery path.
GET /.well-known/oauth-authorization-serverNo bearer tokenOAuth authorization-server metadata.
GET /.well-known/oauth-authorization-server/mcpNo bearer tokenAuthorization-server metadata for MCP clients.
GET /mcp/.well-known/oauth-authorization-serverNo bearer tokenAlternate authorization-server discovery path.
POST /registerNo bearer tokenDynamic public-client registration.
GET /authorizeNo bearer tokenStarts an OAuth authorization-code + PKCE flow.
GET /oauth/callbackNo bearer tokenReceives the upstream FusionAuth callback.
POST /tokenNo bearer tokenExchanges broker-issued codes or refresh tokens.
/mcpBearer token requiredStreamable HTTP MCP service.

CORS is applied outside the bearer middleware so browser MCP clients can complete OAuth preflights and still receive WWW-Authenticate challenges. Allowed methods are GET, POST, DELETE, and OPTIONS; allowed headers include Authorization, Content-Type, mcp-protocol-version, and mcp-session-id.

OAuth and access control

The MCP auth proxy presents itself as the OAuth authorization server to MCP clients while delegating the upstream sign-in flow to FusionAuth.

OAuth behavior

BehaviorValue
Supported response_typecode
Supported grant typesauthorization_code, refresh_token
Supported PKCE methodS256
Dynamic client auth methodnone
Pending authorization TTL10 minutes
Broker-issued authorization code TTL5 minutes
Redirect URI policyhttps or loopback http for localhost, 127.0.0.1, or [::1]
Upstream providerFusionAuth OAuth provider using the google_gmail identity provider
In-flight state storeRedis keys under mcp_auth_proxy:pending:* and mcp_auth_proxy:issued:*

The /mcp route validates Authorization: Bearer <token> with Macro JWT validation. Both Macro access tokens and Macro API tokens can supply the macro_user_id; the middleware stores that value as MacroUserIdStr in request extensions for the MCP service.

Subscription gate

list_tools and call_tool both require the authenticated user to have at least one paid AI permission:

  • write:opus
  • write:sonnet
  • write:haiku

If none are present, the MCP service returns an MCP invalid-request error with:

MCP access requires a paid subscription

Tool service contract

AuthenticatedToolService implements rmcp::handler::server::ServerHandler.

MCP methodRuntime behavior
get_infoAdvertises macro-tools, title Macro, package version, tool capability, and instructions for searching, reading, creating documents, and listing entities.
list_toolsExtracts the authenticated user, checks paid access, and returns every tool in toolset.tools with its name, description, and input schema.
call_toolExtracts the authenticated user, checks paid access, requires object arguments, then invokes toolset.try_tool_call(...) with RequestContext { user_id }.

Call results and errors

CaseMCP response
Missing argumentsInvalid params: No params provided.
Tool input deserialization failsParse error with the deserialization message.
Tool name is not registeredResource-not-found error.
Tool returns Ok(value)Structured MCP tool result.
Tool returns a domain ToolCallErrorMCP tool-result error containing the user-facing error description as text.
Missing user id in request extensionsInternal error: missing user identity — is auth configured?.

Runtime toolset composition

The MCP runtime registry is constructed in ai_tools::mcp_tools(), not from the checked-in MDX files.

FunctionComposition
subagent_toolset()Search tools, ListEntities, document tools, property tools, call tools, chat tools, channel tools, team tools, and Anthropic server-tool wrappers. It intentionally excludes email and the Subagent tool itself.
all_tools()subagent_toolset() plus notification tools, the full email toolset, and Subagent. This is the broader AI-provider-facing registry.
mcp_tools()subagent_toolset() plus notification tools, the MCP email toolset, and Subagent. This is what mcp_service exposes.
no_tools()Empty toolset with the base prompt.
The registry boundary is provider-neutral at the MCP layer: tools are composed as Rust `AsyncToolCollection` entries. The current registry includes a subtoolset for Anthropic server-tool wrappers, but the MCP server contract is the tool registry and Streamable HTTP transport, not a requirement that every deployment use one model provider.

SendEmail boundary

SendEmail is deliberately outside the MCP email toolset.

Email registryIncludes
email::inbound::toolset::mcp_toolset()UpdateThreadLabels, GetThread, ListLabels
email::inbound::toolset::email_toolset()Everything in mcp_toolset() plus SendEmail as a user tool
ai_tools::mcp_tools()Uses email_mcp_toolset(); therefore excludes direct SendEmail execution
ai_tools::all_tools()Uses the full email_toolset()

SendEmail composes and sends an email by resolving the user’s linked email account, creating message input from subject, Markdown/HTML body, recipients, and optional replyingToId, then calling the email service. In the full AI tool registry it is registered through add_user_tool, whose wrapper returns PendingUserExecution when called by the automatic tool loop. That user-action boundary is not exposed through the MCP runtime registry.

The checked-in generated MCP tool pages currently include a `SendEmail` page. Treat the live MCP `list_tools` response and `ai_tools::mcp_tools()` as the runtime source of truth.

Generated schemas and docs tool pages

The docs generator lives in docs/scripts/generate-mcp-tool-pages.ts and is wired through the docs package scripts.

{
  "scripts": {
    "generate:tools": "bun ./scripts/generate-mcp-tool-pages.ts",
    "prepare": "bun run generate:tools"
  }
}

The generator workflow is:

It runs `SQLX_OFFLINE=true cargo build --bin gen_tool_schemas` from `rust/cloud-storage`. It removes `rust/cloud-storage/ai_tools/schemas`, then runs the `gen_tool_schemas` binary from the `ai_tools` directory. The binary writes `schemas/tools.json`. It rewrites `docs/AI/mcp/tools/*.mdx`, including `index.mdx`. It writes `docs/config/tool-pages.json`, which is referenced by `docs/config/navigation.json` under the MCP Tool Reference group.

The Rust schema binary currently serializes ai_tools::all_tool_combined_schema(), which merges the broad all_tools() registry plus a schema-only ReadThread phantom tool into a combined schema with shared $defs and tools entries.

The TypeScript docs script expects a `schemas` array with `name`, `description`, `inputSchema`, and `outputSchema`, while the Rust binary emits the combined schema shape `{ "$defs": ..., "tools": [...] }`. Keep the Rust schema shape and docs script contract aligned before relying on `bun run generate:tools` in CI or release automation.

Configuration

McpEnvVars loads these required environment variables during context construction. PORT is read separately and defaults to 8090.

Environment variableUsed for
DATABASE_URLPostgres connection pool for tools, permissions, and domain services.
EMAIL_SCHEDULED_QUEUESQS email scheduling queue configuration.
DOCUMENT_STORAGE_SERVICE_URLSearch/document client base URL wiring.
SYNC_SERVICE_URLSync service client base URL.
SYNC_SERVICE_AUTH_KEYSecret-manager key for sync service authentication.
LEXICAL_SERVICE_URLLexical client base URL.
EMAIL_SERVICE_URLEmail service client base URL.
STATIC_FILE_SERVICE_URLLoaded as part of MCP environment configuration.
DOCUMENT_STORAGE_BUCKETDocument S3 upload adapter bucket.
DOCX_DOCUMENT_UPLOAD_BUCKETDOCX upload bucket.
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_DISTRIBUTION_URLCloudFront document distribution URL.
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_IDCloudFront signer public key id.
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY_SECRET_NAMESecret-manager key for the CloudFront private key.
MCP_PUBLIC_URLPublic base URL for OAuth metadata, callback URL construction, and allowed host derivation.
FUSIONAUTH_BASE_URLFusionAuth base URL.
FUSIONAUTH_CLIENT_IDFusionAuth OAuth client id.
FUSIONAUTH_TENANT_IDFusionAuth tenant id.
FUSIONAUTH_API_KEY_SECRET_KEYSecret-manager key for FusionAuth API access.
FUSIONAUTH_CLIENT_SECRET_KEYSecret-manager key for the FusionAuth client secret.
GOOGLE_CLIENT_IDGoogle OAuth client id passed to FusionAuth client setup.
GOOGLE_CLIENT_SECRET_KEYSecret-manager key for the Google client secret.
REDIS_URLRedis connection string for in-flight OAuth state.
PORTOptional listen port; defaults to 8090.

Adding or changing MCP tools

  1. Implement the tool as an AsyncTool with Deserialize input and Serialize + JsonSchema output.
  2. Register it in the narrow domain toolset first.
  3. Wire that domain toolset into ai_tools::mcp_tools() only if it should be exposed over remote MCP.
  4. Add or update FromRef<ToolServiceContext> wiring and build_context() dependencies when the tool needs a new service.
  5. Keep email::mcp_toolset() separate from email_toolset() if the operation requires user review or should not be remotely executable.
  6. Regenerate docs with cd docs && bun run generate:tools after fixing or confirming the schema contract.
  7. Verify the live runtime with an authenticated MCP list_tools call; do not use checked-in MDX pages as the only availability signal.

Troubleshooting

SymptomLikely causeVerification
401 with WWW-Authenticate and no errorMissing bearer token on /mcp.Check the client completed OAuth and sends Authorization: Bearer ....
401 with error="invalid_token"JWT validation failed or token user id was invalid.Re-run the OAuth flow or inspect token issuer/configuration.
redirect_uri must be https or a loopback addressOAuth client used a non-HTTPS, non-loopback redirect URI.Use HTTPS or http://localhost, http://127.0.0.1, or http://[::1].
unsupported code_challenge_methodClient did not use PKCE S256.Confirm MCP client OAuth settings.
invalid or expired codeBroker-issued code was already used or older than 5 minutes.Restart the OAuth flow.
MCP access requires a paid subscriptionUser lacks write:opus, write:sonnet, and write:haiku.Check user permissions in Macro.
No params providedcall_tool request omitted arguments.Send an object in MCP tool arguments, even when the tool has few parameters.
Tool page exists but tool is absent from MCPGenerated docs are broader or stale relative to mcp_tools().Compare against authenticated list_tools.

Related pages

Client connection commands and the public remote endpoint. Generated tool pages and navigation for documented tool schemas.

22. Environment variables reference

Reference: Required and optional service environment variables, defaults, secrets, queue names, ports, and provider-specific keys.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/22-environment-variables-reference.md
  • Generated: 2026-06-01T01:04:54.159Z

Source Files

  • RUNNING_LOCALLY.md
  • rust/cloud-storage/document_storage_service/src/config.rs
  • rust/cloud-storage/document_cognition_service/src/config.rs
  • rust/cloud-storage/authentication_service/src/config.rs
  • rust/cloud-storage/email_service/src/config.rs
  • rust/cloud-storage/notification_service/src/config.rs
  • rust/cloud-storage/search_service/src/config.rs
  • rust/cloud-storage/macro_env/src/lib.rs

Macro’s local and deployed service binaries read configuration directly from environment variables at startup. Most Rust services default PORT to 8080, derive ENVIRONMENT with prod fallback, and fail fast when required variables are missing or cannot be parsed.

Runtime conventions

ConventionBehavior
ENVIRONMENTAccepted values are prod, dev, and local. Missing or invalid values fall back to production behavior.
env_var! namesRust CamelCase config structs map to UPPER_SNAKE_CASE; for example DocumentStorageBucket reads DOCUMENT_STORAGE_BUCKET.
Required variablesenv_var! and direct std::env::var(...).context(...) reads fail service startup when absent.
Optional variablesmaybe_env_var! and .ok() reads return None when absent.
Local secretsIn ENVIRONMENT=local, secret variables are normally used as literal secret values.
Deployed secretsIn dev and prod, variables passed through LocalOrRemoteSecret or explicit secret-manager lookups are treated as AWS Secrets Manager secret IDs.
AWS local modeLOCAL_AWS_URL switches shared AWS clients to LocalStack-style endpoint and test credentials.
CORS overrideALLOWED_ORIGINS can replace the default comma-separated origin allowlist used by the shared CORS layer.
Some numeric variables use `.unwrap()` after parsing. Invalid values can panic instead of returning a structured startup error.

Local environment sources

Local setup expects an encrypted dotenv source and writes a runtime .env file.

just setup
just run_local

just setup decrypts .env-local*.enc, creates shared Docker networks and volumes, starts LocalStack, initializes databases, sets up FusionAuth, and builds local service images. just run_local patches local FusionAuth values into .env before starting Docker Compose.

Local E2E uses explicit overrides:

COMPOSE_FILE=docker-compose.yml:docker-compose.local-e2e.yml just run_local -d --wait

The local E2E override forces local Postgres, Redis, LocalStack, S3 buckets, DynamoDB tables, and queue names instead of shared dev assets.

Ports

Local service host ports

ServiceHost portContainer port / default
authentication-service80808080
connection_gateway80828080
contacts_service80838080
document_cognition_service80858080
document_storage_service80868080
email_service80878080
notification_service80898080
search_processing_service80928080
static_file_service80948080
unfurl_service80958080
lexical_service80968096
image_proxy_service80978080
static_file_cdn810080
websocket_service69696969
sync_service87878787
FusionAuth90119011
Postgres54325432
Redis63796379
Redis Stack UI80018001
OpenSearch REST92009200
OpenSearch performance analyzer96009600

Shared variables

Environment and infrastructure

VariableRequired byDefaultNotes
ENVIRONMENTMost servicesprod fallbackValues: prod, dev, local.
PORTHTTP servicesUsually 8080lexical_service defaults to 8096; websocket_service uses 6969.
DATABASE_URLMost database-backed servicesNonePrimary MacroDB connection URL or secret ID where explicitly resolved.
DATABASE_URL_READONLYdocument_storage_service, search_processing_serviceOptionalDSS falls back to primary if readonly connection fails; search backfills use primary when absent or unreachable.
MACRO_DB_URLemail_service, connection_gatewayNonePrimary MacroDB URL for services that use this name instead of DATABASE_URL.
REDIS_URIAuth, DSS, email, notification, contacts, workersNoneRedis URL.
REDIS_HOSTconnection_gateway, DCSNoneRedis URL-like host used by typed config.
REDIS_URLMCP service and some stream/test pathsNoneSeparate name used by MCP auth proxy wiring.
LOCAL_AWS_URLAWS client factoryOptionalEnables LocalStack endpoint and test credentials.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION, AWS_DEFAULT_REGIONLocalStack / AWS SDKLocal E2E sets test credentialsNeeded by local AWS CLI setup and SDK calls when not using ambient credentials.
DD_SERVICE, DD_ENVDeployed tracingunknown-service, unknownUsed by OpenTelemetry/Datadog entrypoint in dev and prod.

Shared auth and JWT

VariableRequired byDefaultNotes
INTERNAL_API_SECRET_KEYInternal-auth protected servicesNoneValidated against x-internal-auth-key. Local value is literal; deployed uses secret-manager resolution where wired through LocalOrRemoteSecret.
AUDIENCEServices constructing JWT validationNoneJWT validation config.
ISSUERServices constructing JWT validationNoneJWT validation config.
JWT_SECRET_KEYServices constructing JWT validationNoneLocal value or deployed secret ID.
MACRO_API_TOKEN_ISSUERAuth and JWT validationNoneUsed for Macro API tokens.
MACRO_API_TOKEN_PUBLIC_KEYJWT validationNoneLocal public key or deployed secret ID.
MACRO_API_TOKEN_PRIVATE_SECRET_KEYauthentication_serviceNoneLocal private key or deployed secret ID for Macro API token signing.
MACRO_API_TOKEN_EXPIRY_SECONDSauthentication_serviceNoneParsed as usize.
STRIPE_WEBHOOK_SECRET_KEYauthentication_serviceNoneLocal value or deployed secret ID.

Service reference

authentication_service

Required:

Variable groupVariables
Base URLs and storesBASE_URL, DATABASE_URL, REDIS_URI, DOCUMENT_STORAGE_SERVICE_URL
FusionAuthFUSIONAUTH_TENANT_ID, FUSIONAUTH_API_KEY_SECRET_KEY, FUSIONAUTH_CLIENT_ID, FUSIONAUTH_CLIENT_SECRET_KEY, FUSIONAUTH_BASE_URL, FUSIONAUTH_OAUTH_REDIRECT_URI
Google OAuthGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET_KEY
StripeSTRIPE_SECRET_KEY, STRIPE_PRICE_ID_HAIKU, STRIPE_PRICE_ID_SONNET, STRIPE_PRICE_ID_OPUS
GitHub OAuthGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_IDP_ID
QueuesNOTIFICATION_QUEUE, SEARCH_EVENT_QUEUE, LINK_MANAGER_QUEUE, EMAIL_BACKFILL_QUEUE
Internal authSERVICE_INTERNAL_AUTH_KEY, plus shared INTERNAL_API_SECRET_KEY in service startup

Optional:

VariableDefaultNotes
PORT8080HTTP listener.
GA_MEASUREMENT_ID, GA_API_SECRETNoneGoogle Analytics Measurement Protocol.
META_PIXEL_ID, META_ACCESS_TOKEN, META_TEST_EVENT_CODENoneMeta conversions tracking.
POSTHOG_API_KEY, POSTHOG_HOSTNonePostHog analytics.

The active Stripe price ID is selected in code by ENVIRONMENT: production uses the production price ID, while dev and local use the development price ID.

document_storage_service

Required:

Variable groupVariables
StoresDATABASE_URL, DATABASE_URL_READONLY, REDIS_URI, DOCUMENT_STORAGE_BUCKET, DOCX_DOCUMENT_UPLOAD_BUCKET, UPLOAD_STAGING_BUCKET, BULK_UPLOAD_REQUESTS_TABLE
QueuesDOCUMENT_DELETE_QUEUE, NOTIFICATION_QUEUE, SEARCH_EVENT_QUEUE, CONTACTS_QUEUE
Service URLsCONNECTION_GATEWAY_URL, SYNC_SERVICE_URL, LEXICAL_SERVICE_URL, GITHUB_SYNC_APP_URL
Sync and searchSYNC_SERVICE_AUTH_KEY, OPENSEARCH_URL, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORD
CloudFrontDOCUMENT_STORAGE_SERVICE_CLOUDFRONT_DISTRIBUTION_URL, DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_ID, DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY_SECRET_NAME
Secrets and integrationsDOCUMENT_PERMISSION_JWT_SECRET_KEY, GITHUB_WEBHOOK_SECRET_KEY, GITHUB_SYNC_APP_PEM_SECRET_KEY, GITHUB_SYNC_APP_CLIENT_ID, CAL_WEBHOOK_SECRET_KEY, CAL_EVENT_TYPE_CONTENT_NAMES_KEY, META_PIXEL_ID, META_ACCESS_TOKEN
LiveKit callsLIVEKIT_SERVER_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET

Optional and tunable:

VariableDefaultNotes
PORT8080HTTP listener.
DOCUMENT_LIMIT20Free-user document limit.
DOCUMENT_STORAGE_SERVICE_PRESIGNED_URL_EXPIRY_SECONDS900Signed document URL TTL.
DOCUMENT_STORAGE_SERVICE_PRESIGNED_URL_BROWSER_CACHE_EXPIRY_SECONDS840Browser cache suggestion for signed URLs.
QUEUE_MAX_MESSAGES10Delete-document worker poll batch size.
QUEUE_WAIT_TIME_SECONDS4Delete-document worker long-poll wait.
META_TEST_EVENT_CODENoneMeta test events.
LIVEKIT_TRANSCRIPTION_AGENT_NAMENoneRequires INTERNAL_CALL_SECRET when set.
INTERNAL_CALL_SECRETNoneShared secret for internal call endpoints.
CALL_RECORDING_S3_BUCKET, CALL_RECORDING_S3_REGION, CALL_RECORDING_S3_ACCESS_KEY, CALL_RECORDING_S3_SECRETNoneCall recording egress is enabled only when all four are present.
APPLE_BUNDLE_ID, SNS_APNS_VOIP_PLATFORM_ARNNoneOptional VoIP push sender. Empty VoIP ARN disables VoIP push.

document_cognition_service

Required:

Variable groupVariables
StoresDATABASE_URL, DOCUMENT_STORAGE_BUCKET, DOCX_DOCUMENT_UPLOAD_BUCKET, REDIS_HOST
Service URLsDOCUMENT_STORAGE_SERVICE_URL, DOCUMENT_COGNITION_SERVICE_URL, SYNC_SERVICE_URL, LEXICAL_SERVICE_URL, EMAIL_SERVICE_URL, STATIC_FILE_SERVICE_URL, AUTHENTICATION_SERVICE_URL
QueuesDOCUMENT_TEXT_EXTRACTOR_QUEUE, CHAT_DELETE_QUEUE, EMAIL_SCHEDULED_QUEUE, NOTIFICATION_QUEUE, SEARCH_EVENT_QUEUE
SecretsSYNC_SERVICE_AUTH_KEY, AUTHENTICATION_SERVICE_SECRET_KEY, DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY_SECRET_NAME, MCP_CREDENTIALS_KEY_SECRET_NAME
CloudFrontDOCUMENT_STORAGE_SERVICE_CLOUDFRONT_DISTRIBUTION_URL, DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_ID

Optional:

VariableDefaultNotes
PORT8080HTTP listener.
DOCUMENT_BATCH_LIMIT1000Maximum document query batch size.
OPENAI_API_KEYEmpty stringUsed by the non-streaming OpenAI chat completions proxy. Missing key still sends an upstream request with an empty bearer token.

email_service

Required:

Variable groupVariables
StoresMACRO_DB_URL, REDIS_URI, ATTACHMENT_BUCKET
QueuesLINK_MANAGER_QUEUE, EMAIL_SCHEDULED_QUEUE, GMAIL_INBOX_SYNC_QUEUE, GMAIL_INBOX_SYNC_RETRY_QUEUE, GMAIL_OPS_QUEUE, GMAIL_OPS_RETRY_QUEUE, SEARCH_EVENT_QUEUE, GMAIL_GCP_QUEUE, NOTIFICATION_QUEUE, BACKFILL_QUEUE, CONTACTS_QUEUE, SFS_UPLOADER_QUEUE, SFS_DELETE_QUEUE
Service URLsAUTHENTICATION_SERVICE_URL, STATIC_FILE_SERVICE_URL, DOCUMENT_STORAGE_SERVICE_URL, CONNECTION_GATEWAY_URL
SecretsAUTHENTICATION_SERVICE_SECRET_KEY, EMAIL_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY
CloudFrontEMAIL_SERVICE_CLOUDFRONT_DISTRIBUTION_URL, EMAIL_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_ID
Feature gatesNOTIFICATIONS_ENABLED

Optional and tunable:

VariableDefaultNotes
PORT8080HTTP listener.
SENT_UNDO_DELAY_SECS10Delay before processing sent mail.
USE_APOLLO_CRM_ENRICHMENTfalseEnables Apollo.io CRM enrichment.
APOLLO_API_KEYEmpty stringLiteral key locally; deployed comments indicate secret name/value resolution by startup wiring.
QUEUE_MAX_MESSAGES10Generic queue poll batch size.
QUEUE_WAIT_TIME_SECONDS20Generic long-poll wait.
BACKFILL_QUEUE_WORKERS25Backfill worker count.
BACKFILL_QUEUE_MAX_MESSAGES1Backfill batch size.
INBOX_SYNC_QUEUE_WORKERS10Gmail inbox sync worker count.
INBOX_SYNC_QUEUE_MAX_MESSAGES1Gmail inbox sync batch size.
INBOX_SYNC_RETRY_QUEUE_WORKERS10Gmail retry worker count.
INBOX_SYNC_RETRY_QUEUE_MAX_MESSAGES1Gmail retry batch size.
GMAIL_OPS_QUEUE_WORKERS5Gmail ops worker count.
GMAIL_OPS_QUEUE_MAX_MESSAGES10Gmail ops batch size.
GMAIL_OPS_RETRY_QUEUE_WORKERS2Gmail ops retry worker count.
GMAIL_OPS_RETRY_QUEUE_MAX_MESSAGES10Gmail ops retry batch size.
SFS_UPLOADER_WORKERS3Static-file upload mapper worker count.
REDIS_RATE_LIMIT_REQS14000Sliding-window request limit.
REDIS_RATE_LIMIT_REQS_BACKFILL13000Backfill-specific rate limit.
REDIS_RATE_LIMIT_WINDOW_SECS60Rate-limit window.
EMAIL_SERVICE_PRESIGNED_URL_TTL_SECS3600Attachment signed URL TTL.

notification_service

Required:

Variable groupVariables
Base and storesBASE_URL, DATABASE_URL, REDIS_URI
Internal authINTERNAL_API_SECRET_KEY, URL_SIGNING_HMAC
QueuesNOTIFICATION_QUEUE, NOTIFICATION_INGRESS_QUEUE, PUSH_NOTIFICATION_EVENT_HANDLER_QUEUE
Push providersSNS_APNS_PLATFORM_ARN, SNS_FCM_PLATFORM_ARN, APPLE_BUNDLE_ID
Service URLsCONNECTION_GATEWAY_URL
Email senderSENDER_BASE_ADDRESS

Optional and tunable:

VariableDefaultNotes
PORT8080HTTP listener.
NOTIFICATION_QUEUE_MAX_MESSAGES9Notification worker poll batch size.
NOTIFICATION_QUEUE_WAIT_TIME_SECONDS4Notification worker long-poll wait.
SNS_APNS_VOIP_PLATFORM_ARNRequired except localLocal can omit it; service uses an empty string locally to keep VoIP disabled.

SENDER_BASE_ADDRESS is transformed into no-reply@..., no-reply-dev@..., or no-reply-local@... according to ENVIRONMENT.

Search services

search_service

VariableRequiredDefaultNotes
DATABASE_URLYesNoneMacroDB connection.
OPENSEARCH_URLYesNoneOpenSearch endpoint.
OPENSEARCH_USERNAMEYesNoneOpenSearch username.
OPENSEARCH_PASSWORDYesNoneLocal password or deployed secret where caller resolves it.
INTERNAL_API_SECRET_KEYYesNoneInternal API auth.
PORTNo8080HTTP listener.

search_processing_service

VariableRequiredDefaultNotes
DATABASE_URLYesNoneLocal URL or deployed secret ID.
DATABASE_URL_READONLYNoNoneOptional read-replica URL or deployed secret ID for backfills.
SEARCH_EVENT_QUEUEYesNoneQueue consumed for indexing work.
OPENSEARCH_URLYesNoneOpenSearch endpoint.
OPENSEARCH_USERNAMEYesNoneOpenSearch username.
OPENSEARCH_PASSWORDYesNoneLocal password or deployed secret ID.
DOCUMENT_STORAGE_BUCKETYesNoneSource document bucket.
LEXICAL_SERVICE_URLYesNoneLexical conversion service.
BACKFILL_JOBS_TABLEYesNoneDynamoDB job registry table.
BACKFILL_JOB_TTL_SECONDSNo86400TTL for completed job records.
WORKER_COUNTNo10Search worker count.
QUEUE_MAX_MESSAGESNo10Queue poll batch size.
QUEUE_WAIT_TIME_SECONDSNo20Queue long-poll wait.
BACKFILL_CALLS_PAGE_SIZENo2000Must be > 0.
BACKFILL_CHATS_PAGE_SIZENo5000Must be > 0.
BACKFILL_CHANNELS_PAGE_SIZENo5000Must be > 0.
BACKFILL_DOCUMENTS_PAGE_SIZENo1000Must be > 0.
BACKFILL_EMAILS_PAGE_SIZENo1000Must be > 0.
PORTNo8080HTTP listener.

Other HTTP services and workers

ServiceRequired variablesOptional/defaults
connection_gatewayREDIS_HOST, MACRO_DB_URL, CONNECTION_GATEWAY_TABLE, shared JWT vars, INTERNAL_API_SECRET_KEYPORT=8080
contacts_serviceDATABASE_URL, REDIS_URI, CONTACTS_QUEUE, shared JWT varsPORT=8080, CONTACTS_QUEUE_MAX_MESSAGES=10, CONTACTS_QUEUE_WAIT_TIME_SECONDS=5, optional CONNECTION_GATEWAY_URL
static_file_serviceSTATIC_FILE_SERVICE_DYNAMODB_TABLE_NAME, STATIC_STORAGE_BUCKET, STATIC_FILE_SERVICE_URL, STATIC_FILE_SERVICE_S3_EVENT_QUEUE_URL, INTERNAL_API_SECRET_KEY, shared JWT varsPORT=8080
convert_serviceCONVERT_QUEUE, LOK_PATH, DATABASE_URL, DOCUMENT_STORAGE_BUCKET, WEB_SOCKET_RESPONSE_LAMBDAPORT=8080, QUEUE_MAX_MESSAGES=5, QUEUE_WAIT_TIME_SECONDS=5
deleted_item_pollerDATABASE_URL, DOCUMENT_DELETE_QUEUE, CHAT_DELETE_QUEUE, SEARCH_EVENT_QUEUENone
docx_unzip_handlerDATABASE_URL, REDIS_URI, DOCUMENT_STORAGE_BUCKET, WEB_SOCKET_RESPONSE_LAMBDA, CONVERT_QUEUENone
document_text_extractorDATABASE_URLPDFIUM_LIB_PATH is embedded at build time.
document_upload_finalizer local workerDATABASE_URL, INTERNAL_API_SECRET_KEY, SYNC_SERVICE_AUTH_KEY, LEXICAL_SERVICE_URL, SYNC_SERVICE_URL, DOCUMENT_UPLOAD_FINALIZER_QUEUE_URLLOCAL_AWS_URL=http://localstack:4566 in Compose.
email_refresh_handlerDATABASE_URL, LINK_MANAGER_QUEUE, DELETE_UNUSED_AFTER_DAYS, DELETE_INACTIVE_AFTER_DAYSNone
email_scheduled_handlerDATABASE_URL, EMAIL_SCHEDULED_QUEUENone
email_sfs_delete_handlerDATABASE_URL, SFS_DELETE_QUEUENone
sha_cleanup_workerREDIS_URI, DATABASE_URL, DOCUMENT_STORAGE_BUCKETNone
unfurl_serviceNone beyond shared runtimePORT=8080
image_proxy_serviceNone beyond shared runtimePORT=8080

Provider-specific variables

Provider / integrationVariablesRequired by
FusionAuthFUSIONAUTH_TENANT_ID, FUSIONAUTH_API_KEY_SECRET_KEY, FUSIONAUTH_CLIENT_ID, FUSIONAUTH_CLIENT_SECRET_KEY, FUSIONAUTH_BASE_URL, FUSIONAUTH_OAUTH_REDIRECT_URIAuth service; MCP service uses the same base/client/tenant secret-key pattern.
Google OAuthGOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET_KEYAuth service and MCP auth proxy.
StripeSTRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET_KEY, legacy STRIPE_PRICE_ID_* varsAuth service.
GitHubGITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, GITHUB_IDP_ID, GITHUB_WEBHOOK_SECRET_KEY, GITHUB_SYNC_APP_PEM_SECRET_KEY, GITHUB_SYNC_APP_CLIENT_ID, GITHUB_SYNC_APP_URLAuth and document storage.
OpenSearchOPENSEARCH_URL, OPENSEARCH_USERNAME, OPENSEARCH_PASSWORDSearch and document services.
OpenAIOPENAI_API_KEYDCS completions proxy.
AnthropicANTHROPIC_API_KEYAnthropic client / AI tools.
Slack MCPSLACK_MCP_CLIENT_ID, SLACK_MCP_CLIENT_SECRETPre-registered MCP provider registry; optional because registry creation tolerates absence.
Apollo.ioUSE_APOLLO_CRM_ENRICHMENT, APOLLO_API_KEYEmail CRM enrichment.
MetaMETA_PIXEL_ID, META_ACCESS_TOKEN, META_TEST_EVENT_CODEAuth analytics and DSS Cal-to-Meta tracking.
Google AnalyticsGA_MEASUREMENT_ID, GA_API_SECRETAuth analytics.
PostHogPOSTHOG_API_KEY, POSTHOG_HOST, frontend VITE_POSTHOG_API_KEYAuth analytics and web app builds.
LiveKitLIVEKIT_SERVER_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_TRANSCRIPTION_AGENT_NAMEDSS call service.
AWS SNS pushSNS_APNS_PLATFORM_ARN, SNS_FCM_PLATFORM_ARN, SNS_APNS_VOIP_PLATFORM_ARN, APPLE_BUNDLE_IDNotification service and optional DSS VoIP push.
Cal.comCAL_WEBHOOK_SECRET_KEY, CAL_EVENT_TYPE_CONTENT_NAMES_KEYDSS Cal webhook routing.

Local queues, tables, and buckets

LocalStack setup creates these queue names:

VariableLocal name
NOTIFICATION_QUEUEnotification-queue
NOTIFICATION_INGRESS_QUEUEnotification-ingress-queue
PUSH_NOTIFICATION_EVENT_HANDLER_QUEUEpush-delivery-queue
BACKFILL_QUEUE, EMAIL_BACKFILL_QUEUEemail-service-backfill-queue
CHAT_DELETE_QUEUEdelete-chat-handler-queue
CONTACTS_QUEUEcontacts-queue
CONVERT_QUEUEconvert-service-queue
DOCUMENT_DELETE_QUEUEdelete-document-handler-queue
DOCUMENT_UPLOAD_FINALIZER_QUEUE_URLdocument-upload-finalizer-queue URL
DOCUMENT_TEXT_EXTRACTOR_QUEUEdocument-text-extractor-lambda-queue
EMAIL_SCHEDULED_QUEUEemail-service-scheduled-queue
GMAIL_INBOX_SYNC_QUEUEemail-service-gmail-inbox-sync-queue
GMAIL_INBOX_SYNC_RETRY_QUEUEemail-service-gmail-inbox-retry-queue
GMAIL_OPS_QUEUEemail-service-gmail-ops-queue
GMAIL_OPS_RETRY_QUEUEemail-service-gmail-ops-retry-queue
LINK_MANAGER_QUEUEemail-service-refresh-queue
SEARCH_EVENT_QUEUEsearch-event-queue
SFS_DELETE_QUEUEemail-sfs-delete-queue
SFS_UPLOADER_QUEUEemail-service-sfs-mapper-queue
STATIC_FILE_SERVICE_S3_EVENT_QUEUE_URLstatic-file-s3-event-notification-queue URL

LocalStack setup creates these tables and buckets:

TypeNames
DynamoDB tablesbulk-upload, connection-gateway-table, static-file-metadata
S3 bucketsmacro-email-attachments, doc-storage, docx-upload, static-file-storage, bulk-upload-staging

Frontend and JS runtime variables

SurfaceVariablesDefaults / notes
Vite app dev serverPORT, MODE, LOCAL_DOCKER, LOCAL_JWT, TAURI_DEV_HOSTApp Vite config defaults PORT to 3000; LOCAL_JWT is injected as import.meta.env.__LOCAL_JWT__.
Local backend selectionVITE_LOCAL_SERVERS, VITE_ENABLE_BEARER_TOKEN_AUTHPlaywright local E2E sets VITE_LOCAL_SERVERS=ALL and bearer-token auth.
ObservabilityVITE_DD_WEB_APP_ID, VITE_DD_WEB_APP_TOKEN, VITE_POSTHOG_API_KEYUsed by web observability and analytics packages.
Feature flagsVITE_<FLAG_NAME>Feature flag helper reads Vite env keys by flag name.
Playwright local E2ELOCAL_E2E, LOCAL_JWT, PORT, CILOCAL_E2E=true switches tests to the local stack.
lexical_servicePORT, INTERNAL_AUTH_KEY or INTERNAL_API_SECRET_KEY, SYNC_SERVICE_AUTH_KEY, SYNC_SERVICE_URLCompose sets PORT=8096, INTERNAL_AUTH_KEY=${INTERNAL_API_SECRET_KEY}, and SYNC_SERVICE_URL=http://sync-service:8787.

Troubleshooting startup failures

SymptomCheck
Service exits with ... must be providedThe named required variable is missing from .env, Compose overrides, or deployment config.
Service panics while parsing a numberVerify numeric variables such as PORT, queue worker counts, TTLs, and page sizes contain only valid positive integers where required.
Local service attempts deployed AWSSet LOCAL_AWS_URL=http://localstack:4566 and local AWS test credentials.
Deployed service treats a secret value as a nameFor variables resolved through Secrets Manager in dev/prod, set the env var to the secret ID, not the plaintext secret.
JWT-protected routes failVerify AUDIENCE, ISSUER, JWT_SECRET_KEY, MACRO_API_TOKEN_ISSUER, and MACRO_API_TOKEN_PUBLIC_KEY are aligned across auth-producing and auth-consuming services.
Internal service calls return 401Verify caller and callee share INTERNAL_API_SECRET_KEY; requests must send x-internal-auth-key.
Local E2E refuses to seedThe seed path requires LOCAL_E2E_SEED=true and a local Docker database URL.
VoIP push is unexpectedly disabledDSS requires both APPLE_BUNDLE_ID and non-empty SNS_APNS_VOIP_PLATFORM_ARN; notification service allows missing VoIP ARN only in local mode.

Related pages

  • Running locally
  • Service architecture
  • Local E2E smoke tests

23. Database migrations and SQLx cache

Operations: MacroDB migrations, local database just recipes, SQLx offline cache, fixture data, and migration validation workflow.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/23-database-migrations-and-sqlx-cache.md
  • Generated: 2026-06-01T01:02:18.636Z

Source Files

  • rust/cloud-storage/macro_db_client/README.md
  • rust/cloud-storage/macro_db_client/migrations/0001_baseline.sql
  • rust/cloud-storage/database.just
  • rust/cloud-storage/sqlx.just
  • rust/cloud-storage/justfile
  • rust/cloud-storage/chat/.sqlx/query-57fc603adf83fd7c07968425c98f9a57924573692cf0529ad021b6eef00ce99e.json
  • .github/workflows/migrate-macro-db.yml

MacroDB migrations live in rust/cloud-storage/macro_db_client/migrations, are executed with SQLx CLI recipes, and feed both local PostgreSQL setup and compile-time SQLx query metadata used by the Rust cloud-storage workspace.

Migration ownership

SurfacePathPurpose
Migration filesrust/cloud-storage/macro_db_client/migrations/SQLx migration history for MacroDB.
Migration runner recipesrust/cloud-storage/macro_db_client/justfileDatabase create, migrate, reset, check, and remote dev/prod helpers.
Shared SQLx recipesrust/cloud-storage/sqlx.justLow-level sqlx database, sqlx migrate, and cargo sqlx prepare commands.
Local database URLrust/cloud-storage/database.justDefault local MacroDB URL: postgres://user:password@localhost:5432/macrodb.
Workspace recipesrust/cloud-storage/justfilesetup_macrodb, initialize_dbs, prepare_db, checks, and test .env setup.
Test migrator craterust/cloud-storage/macro_db_migratorLightweight static import of MacroDB migrations for crates that need migrations in tests without depending on macro_db_client.
GitHub migration action.github/actions/migrate-cloud-storage-db/action.ymlCI/CD migration runner for dev and prod databases.

Baseline migration behavior

0001_baseline.sql is a guarded baseline from the Prisma-to-SQLx migration. It counts existing public tables except _sqlx_migrations and only creates the baseline schema when the database is empty.

Do not treat the baseline as a normal production bootstrap script. Its header states that dev and prod already had the declared objects when the project moved to SQLx. The guard prevents it from applying over an existing schema, but new migrations should still be additive SQLx migrations after the baseline.

Typical later migrations are timestamped files such as:

20260511131821_enable_pgvector.sql
20260416135258_scheduled_agent.up.sql
20260416135258_scheduled_agent.down.sql
20260529164720_crm_domain_directory_apollo_fields.up.sql
20260529164720_crm_domain_directory_apollo_fields.down.sql

The migrations directory contains both single-file migrations and paired .up.sql / .down.sql migrations where rollback SQL is maintained.

Local database recipes

The local MacroDB URL is imported from database.just:

postgres://user:password@localhost:5432/macrodb

Start local database services from the repository root:

just run_dbs -d

Initialize MacroDB from the repository root:

just rust/cloud-storage/initialize_dbs

Equivalent cloud-storage workflow:

cd rust/cloud-storage
just setup_macrodb

setup_macrodb runs:

just macro_db_client/create_db
just macro_db_client/migrate_db

Root setup_local_dbs starts the database containers, creates and migrates MacroDB, prints Local databases initialized, then stops the database compose file.

MacroDB just commands

Run these from rust/cloud-storage/macro_db_client unless invoking through the root justfile path.

CommandEffect
just create_dbRuns sqlx database create against the local DATABASE_URL.
just migrate_dbRuns sqlx migrate run against the local DATABASE_URL.
just check_db through sqlx.justRuns sqlx migrate info.
just setupRuns sqlx database setup.
just reset_dbDrops and sets up the database.
just force_drop_dbPipes y into the drop command for local force drops.
just prepare_dbRuns SQLx workspace prepare for offline query metadata.
just prepare_db_devPrepares query metadata against the dev RDS URL returned by ../scripts/rds_database_url.sh.
just check_db_dev / just check_db_prodChecks pending migrations against remote dev/prod.
just migrate_db_devRuns migrations against remote dev.
just migrate_db_prodPrompts before running migrations against remote prod.

SQLx offline cache

The workspace uses SQLx compile-time query checking. Prepared query metadata is committed as JSON files under .sqlx directories, for example:

{
  "db_name": "PostgreSQL",
  "query": "INSERT INTO \"ChatMessage\" ... RETURNING id",
  "describe": {
    "columns": [{ "ordinal": 0, "name": "id", "type_info": "Text" }],
    "parameters": { "Left": ["Text", "Text", "Jsonb", "Text", "Text", "Timestamp", "Timestamp"] },
    "nullable": [false]
  },
  "hash": "57fc603adf83fd7c07968425c98f9a57924573692cf0529ad021b6eef00ce99e"
}

Refresh the cache after adding or changing SQLx queries:

cd rust/cloud-storage
just prepare_db

The shared recipe expands to:

cargo sqlx prepare --workspace -- --all-features

Additional flags can be passed through the shared sqlx.just recipe. The cloud-storage check, clippy, format, and build recipes set SQLX_OFFLINE=true, so stale or missing .sqlx metadata can surface as compile-time failures before runtime.

`macro_db_client/README.md` documents the expected test workflow: run local MacroDB in Docker, run `just setup_macrodb` from `cloud-storage`, then run `just prepare_db` before `cargo test` for code that depends on prepared SQLx metadata.

Test migrations and fixtures

Most database tests use #[sqlx::test] and fixture SQL files. Fixtures are stored near the crate that owns the behavior, for example:

rust/cloud-storage/macro_db_client/fixtures/basic_user_with_document.sql
rust/cloud-storage/name_search/fixtures/chat.sql
rust/cloud-storage/channels/fixtures/channels_repo.sql

macro_db_client tests can use the crate-local migration directory directly. Other crates import migrations through macro_db_migrator:

pub static MACRO_DB_MIGRATIONS: sqlx::migrate::Migrator =
    sqlx::migrate!("../macro_db_client/migrations");

A typical cross-crate test shape is:

#[sqlx::test(
    migrator = "MACRO_DB_MIGRATIONS",
    fixtures(path = "../../fixtures", scripts("chat"))
)]
async fn test_search_chat_names_empty_term(pool: Pool<Postgres>) -> anyhow::Result<()> {
    // test body
}

The cloud-storage CI test job prepares test .env files and initializes MacroDB before running cargo nextest:

just rust/cloud-storage/setup_test_envs
just rust/cloud-storage/initialize_dbs
cd rust/cloud-storage
cargo nextest run --all-features --lib --bins --tests

setup_test_envs appends the same DATABASE_URL to many crate-local .env files so SQLx tests and service tests resolve MacroDB consistently.

Fixture and seed data

There are two fixture paths with different purposes:

Data typeLocationUsed by
SQL test fixturesPer-crate fixtures/*.sql directories#[sqlx::test(fixtures(...))] database tests.
Local E2E seed datarust/cloud-storage/seed_cli/seed/Deterministic local Playwright and Rust local E2E smoke data.

The root local-e2e-seed recipe resets MacroDB and applies deterministic seed data:

just local-e2e-seed

It runs local databases, drops MacroDB, initializes migrations, then calls:

just rust/cloud-storage/seed_cli/local-e2e-smoke

The local-e2e-smoke seed command sets LOCAL_E2E_SEED=true, the local MacroDB URL, local AWS/FusionAuth settings, and SQLX_OFFLINE=true, then executes:

cargo r -- scenario local-e2e-smoke

The seed scenario validates that it is running against a local compose-style MacroDB URL before applying destructive reset SQL. Accepted hosts are localhost, 127.0.0.1, ::1, or postgres, with user user, database macrodb, and port 5432.

Remote migration workflow

Manual migration dispatch is defined in .github/workflows/migrate-macro-db.yml with an environment input of dev or prod.

The composite action:

  1. Checks out .github/ and rust/cloud-storage/macro_db_client.
  2. Reads either macro-db-dev or macro-db-prod from AWS Secrets Manager.
  3. Adds sslmode=require to the database URL when missing.
  4. Masks the database URL in GitHub logs.
  5. Runs SQLx migrations from rust/cloud-storage/macro_db_client.

Dev migrations run with:

cargo sqlx migrate run --ignore-missing

Prod migrations run with:

cargo sqlx migrate run

The main-branch cloud-storage deploy workflow runs the same migration action for dev before service deployment. The production release workflow runs the same action for prod before deploying cloud-storage services.

Migration validation checklist

For local work, start Docker databases with `just run_dbs -d`. For remote checks, use the `check_db_dev` or `check_db_prod` recipes from `rust/cloud-storage/macro_db_client`. Run `cd rust/cloud-storage && just setup_macrodb` for a fresh local database, or `cd rust/cloud-storage/macro_db_client && just migrate_db` when the database already exists. Run `just sqlx::check_db postgres://user:password@localhost:5432/macrodb` from `rust/cloud-storage`, or the crate-specific remote check recipes for dev/prod. Run `cd rust/cloud-storage && just prepare_db` after schema or query changes. Commit the changed `.sqlx/query-*.json` files with the migration and Rust query changes. Use `just check`, `just clippy`, and the relevant `cargo test` or `cargo nextest` command. Database-backed tests require local MacroDB and the crate `.env` setup when they read `DATABASE_URL`.

Troubleshooting

SymptomLikely causeFix
SQLx macro compile error for a changed queryOffline cache is missing or stale.Run cd rust/cloud-storage && just prepare_db against an up-to-date MacroDB.
Migration appears pending locally after pulling changesLocal database has not applied new files in macro_db_client/migrations.Run cd rust/cloud-storage/macro_db_client && just migrate_db.
Test crate cannot find DATABASE_URLCrate-local .env was not populated.Run just rust/cloud-storage/setup_test_envs from the repository root.
Local E2E seed refuses to runSafety guard did not detect LOCAL_E2E_SEED=true or the URL is not the local compose MacroDB.Use just local-e2e-seed or match postgres://user:password@localhost:5432/macrodb.
Remote prod migration needs confirmation locallymigrate_db_prod is intentionally interactive.Use it from a terminal and confirm only after reviewing pending migrations with check_db_prod.
GitHub dev migration ignores missing migrations but prod does notThe composite action passes --ignore-missing only for dev.Keep prod migration history complete and avoid deleting applied prod migration files.

Related pages

Build, check, clippy, test, and service-level Rust workflows. Docker compose databases, local services, and seeded E2E workflows.

24. Feature flags and server selection

Reference: Vite environment overrides, feature flag defaults, local service selection syntax, sync-service host selection, and runtime host helpers.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/24-feature-flags-and-server-selection.md
  • Generated: 2026-06-01T01:03:44.927Z

Source Files

  • js/app/packages/core/constant/featureFlags.ts
  • js/app/packages/core/tests/featureFlags.test.ts
  • js/app/packages/core/constant/servers.ts
  • js/app/justfile
  • js/app/packages/app/vite.config.ts
  • js/app/package.json

The frontend resolves feature gates and backend hosts at module load from import.meta.env, with Vite exposing VITE_* variables and packages/app/vite.base.ts injecting app metadata such as __APP_VERSION__, ASSETS_PATH, __LOCAL_JWT__, and __GIT_BRANCH__.

Vite mode and environment inputs

packages/app/vite.config.ts delegates to createAppViteConfig(). The base config sets Vite mode from process.env.MODE ?? mode, so the shell MODE value wins over Vite’s default mode.

InputUsed byBehavior
MODE=developmentfeature flags, remote host suffixes, sync-service suffixes, assets pathEnables DEV_MODE_ENV, uses -dev service hosts, uses sync-service -dev3, and allows VITE_LOCAL_SERVERS selection.
MODE=productionfeature flags, remote host suffixes, sync-service suffixesEnables PROD_MODE_ENV, uses production service hosts, and uses sync-service -prod2.
MODE=stagingVite assets pathUses /staging assets, but servers.ts treats it as non-development for host selection.
VITE_*Vite runtime envAutomatically exposed by Vite and read by feature flag and server selectors.
LOCAL_JWTdefineEnv()Injected as import.meta.env.__LOCAL_JWT__ for local bearer-token auth paths.
PORTVite dev/preview serverDefaults to 3000; used by just local and preview commands.

Common commands:

cd js/app
bun run dev
cd js/app
MODE=production NODE_ENV=production bun run build
cd js/app
just build-dev
just build-staging
just build-prod

Feature flag override rules

Feature flags are centralized in packages/core/constant/featureFlags.ts.

resolveFeatureFlag(flagName: string, defaultValue: boolean): boolean

For a flag named ENABLE_EMAIL, the runtime override key is VITE_ENABLE_EMAIL.

Override valueResult
trueForces the flag on.
falseForces the flag off.
unsetUses the code default.
any other value, such as 1 or yesIgnored; uses the code default.
Feature flag values are string comparisons. Use `VITE_ENABLE_EMAIL=true`, not `VITE_ENABLE_EMAIL=1`.

Example:

cd js/app/packages/app
VITE_ENABLE_BEARER_TOKEN_AUTH=true bun run --bun dev

The resolver behavior is covered by packages/core/tests/featureFlags.test.ts, including default fallback, true, false, and invalid values.

Feature flag defaults

Runtime mode flags

ExportValue
LOCAL_ONLYtrue when import.meta.hot is present, normally Vite dev with hot reload.
DEV_MODE_ENVtrue when import.meta.env.MODE === 'development'.
PROD_MODE_ENVtrue when import.meta.env.MODE === 'production'.

Boolean flags resolved through VITE_<flag>

ExportOverride keyDefault
ENABLE_PDF_MODIFICATION_DATA_AUTOSAVEVITE_ENABLE_PDF_MODIFICATION_DATA_AUTOSAVEtrue
ENABLE_PDF_LOCATION_AUTOSAVEVITE_ENABLE_PDF_LOCATION_AUTOSAVEtrue
ENABLE_PDF_TABSVITE_ENABLE_PDF_TABStrue
ENABLE_PDF_MARKUPVITE_ENABLE_PDF_MARKUPtrue
ENABLE_SCRIPTINGVITE_ENABLE_SCRIPTINGfalse
ENABLE_PDF_MULTISPLITVITE_ENABLE_PDF_MULTISPLITtrue
ENABLE_PROJECT_SHARINGVITE_ENABLE_PROJECT_SHARINGtrue
ENABLE_CANVAS_IMAGESVITE_ENABLE_CANVAS_IMAGEStrue
ENABLE_CANVAS_FILESVITE_ENABLE_CANVAS_FILEStrue
ENABLE_CANVAS_TEXTVITE_ENABLE_CANVAS_TEXTtrue
ENABLE_LIVE_INDICATORSVITE_ENABLE_LIVE_INDICATORStrue
ENABLE_PROFILE_PICTURESVITE_ENABLE_PROFILE_PICTUREStrue
ENABLE_VIDEO_BLOCKVITE_ENABLE_VIDEO_BLOCKtrue
ENABLE_DOCX_TO_PDFVITE_ENABLE_DOCX_TO_PDFtrue
ENABLE_MARKDOWN_LIVE_COLLABORATIONVITE_ENABLE_MARKDOWN_LIVE_COLLABORATIONtrue
ENABLE_EMAILVITE_ENABLE_EMAILtrue
ENABLE_BLOCK_IN_BLOCKVITE_ENABLE_BLOCK_IN_BLOCKtrue
ENABLE_SEARCH_SERVICEVITE_ENABLE_SEARCH_SERVICEtrue
ENABLE_MARKDOWN_DIFFVITE_ENABLE_MARKDOWN_DIFFtrue
ENABLE_HISTORY_COMPONENTVITE_ENABLE_HISTORY_COMPONENTfalse
ENABLE_BEARER_TOKEN_AUTHVITE_ENABLE_BEARER_TOKEN_AUTHfalse
ENABLE_MARKDOWN_SEARCH_TEXTVITE_ENABLE_MARKDOWN_SEARCH_TEXTDEV_MODE_ENV
CANVAS_SVG_IMPORTVITE_CANVAS_SVG_IMPORTtrue
ENABLE_CANVAS_VIDEOVITE_ENABLE_CANVAS_VIDEOtrue
ENABLE_CANVAS_HEICVITE_ENABLE_CANVAS_HEICfalse
ENABLE_MARKDOWN_COMMENTSVITE_ENABLE_MARKDOWN_COMMENTStrue
ENABLE_REFERENCES_MODALVITE_ENABLE_REFERENCES_MODALtrue
ENABLE_MENTION_TRACKINGVITE_ENABLE_MENTION_TRACKINGtrue
ENABLE_CHAT_CHANNEL_ATTACHMENTVITE_ENABLE_CHAT_CHANNEL_ATTACHMENTtrue
ENABLE_SVG_PREVIEWVITE_ENABLE_SVG_PREVIEWtrue
USE_WIDE_ICONSVITE_USE_WIDE_ICONStrue
ENABLE_ANIMATED_ICONSVITE_ENABLE_ANIMATED_ICONStrue
ENABLE_PREVIEWVITE_ENABLE_PREVIEWtrue
ENABLE_PROJECT_VIEW_PREVIEWVITE_ENABLE_PROJECT_VIEW_PREVIEWtrue
ENABLE_TTFTVITE_ENABLE_TTFTDEV_MODE_ENV
ENABLE_MULTI_INBOXVITE_ENABLE_MULTI_INBOXDEV_MODE_ENV
ENABLE_EMAIL_SHARINGVITE_ENABLE_EMAIL_SHARINGtrue
ENABLE_DOCUMENT_MENTION_NOTIFICATIONSVITE_ENABLE_DOCUMENT_MENTION_NOTIFICATIONSDEV_MODE_ENV
ENABLE_STATIC_DOCUMENT_CARDSVITE_ENABLE_STATIC_DOCUMENT_CARDSfalse
ENABLE_MARKDOWN_AI_GENERATEVITE_ENABLE_MARKDOWN_AI_GENERATEfalse
ENABLE_UNIFIED_LIST_AI_INPUTVITE_ENABLE_UNIFIED_LIST_AI_INPUTtrue
ENABLE_EMAIL_SCHEDULED_SENDVITE_ENABLE_EMAIL_SCHEDULED_SENDtrue
ENABLE_FEATURED_SEARCH_RESULTSVITE_ENABLE_FEATURED_SEARCH_RESULTStrue
ENABLE_PROXY_EMAIL_IMAGESVITE_ENABLE_PROXY_EMAIL_IMAGEStrue
ENABLE_CLIENT_EMAIL_SIGNAL_FILTERVITE_ENABLE_CLIENT_EMAIL_SIGNAL_FILTERfalse
ENABLE_APP_STORE_QR_CODEVITE_ENABLE_APP_STORE_QR_CODEtrue
ENABLE_RAIL_CHAT_TASK_COMMENTSVITE_RAIL_CHAT_TASK_COMMENTStrue
ENABLE_AUTO_UPDATE_UIVITE_ENABLE_AUTO_UPDATE_UItrue
ENABLE_CALLKITVITE_ENABLE_CALLKITfalse
ENABLE_MARKDOWN_SIDE_PANELVITE_ENABLE_MARKDOWN_SIDE_PANELtrue
ENABLE_REFOCUS_HIGHLIGHTVITE_ENABLE_REFOCUS_HIGHLIGHTtrue
ENABLE_CREATE_PROPERTYVITE_ENABLE_CREATE_PROPERTYfalse

PostHog and override-style flags

ExportBehavior
ENABLE_TEAMS_OVERRIDEtrue in development or when VITE_ENABLE_TEAMS=true; otherwise undefined. A false override becomes undefined, not exported false.
ENABLE_CALLS()Returns true in development; otherwise falls back to PostHog flag enable-calls.
ENABLE_NEW_ONBOARDING_OVERRIDEtrue in development; otherwise undefined.
ENABLE_NEW_LOGIN_OVERRIDEtrue in development; otherwise undefined.
ENABLE_INVITE_TEAM_ONBOARDING_OVERRIDEtrue in development; otherwise undefined.
ENABLE_TEAM_INVITE_TIERS_OVERRIDEtrue in development; otherwise undefined.
ENABLE_SOUP_GROUP_BY_OVERRIDEtrue in development; otherwise undefined.
ENABLE_NEW_PRICING_OVERRIDEtrue in development or when VITE_ENABLE_NEW_PRICING=true; otherwise undefined.

Server host selection

packages/core/constant/servers.ts exports:

export const SERVER_HOSTS: Servers
export const SYNC_SERVICE_HOSTS
export const SYNC_PERMISSION_TOKEN_DSS_HOST

SERVER_HOSTS is remote by default. Local host selection is only evaluated when import.meta.env.MODE === 'development'.

ModeVITE_LOCAL_SERVERSSERVER_HOSTS result
non-developmentany valueRemote production-style hosts.
developmentunset or emptyRemote dev hosts.
developmentALLAll regular services use local hosts.
developmentcomma-separated service namesOnly listed services use local hosts; all others remain remote dev hosts.
developmentservice-name:portListed service uses its local URL with only the port replaced.

Examples:

cd js/app
just local
cd js/app
just servers='document-storage-service' local
cd js/app
just servers='document-storage-service:9001,email-service' port='3001' local
cd js/app/packages/app
VITE_LOCAL_SERVERS='document-storage-service:9001' bun run --bun dev

When a partial local selection is used, the selector logs entries like:

Using local server document-storage-service: http://localhost:8086
Unknown service names throw at startup with `unknown server name `. The current `just local-dcs` and `just local-search` recipes include names that are not present in `SERVER_HOSTS`; use `just local-services` or the table below before adding a partial selector.

Regular service host reference

In development remote mode, most remote hosts use a -dev suffix. In non-development modes, the suffix is empty.

KeyLocal hostRemote host pattern
auth-servicehttp://localhost:8080https://auth-service[-dev].macro.com
auth-logouthttp://localhost:3000FusionAuth logout URL for dev or prod auth tenant
pdf-servicehttp://localhost:4567https://pdf-service[-dev].macro.com
document-storage-servicehttp://localhost:8086https://cloud-storage[-dev].macro.com
websocket-servicews://localhost:6969wss://services[-dev].macro.com
cognition-servicehttp://localhost:8085https://document-cognition[-dev].macro.com
connection-gatewayws://localhost:8082wss://connection-gateway[-dev].macro.com
notification-servicehttp://localhost:8089https://notifications[-dev].macro.com
static-filehttp://localhost:8100https://static-file-service[-dev].macro.com
unfurl-servicehttp://localhost:8095https://unfurl-service[-dev].macro.com
contactshttp://localhost:8083https://contacts[-dev].macro.com
email-servicehttp://localhost:8087https://email-service[-dev].macro.com
image-proxy-servicehttp://localhost:8097https://image-proxy[-dev].macro.com
scheduled-actionhttp://localhost:8098https://agent-schedule[-dev].macro.com

Sync-service host selection

SYNC_SERVICE_HOSTS is separate from SERVER_HOSTS.

Mode and selectionWorker URLWebSocket URL
non-developmenthttps://sync-service-prod2.macroverse.workers.devwss://sync-service-prod2.macroverse.workers.dev
development, remote synchttps://sync-service-dev3.macroverse.workers.devwss://sync-service-dev3.macroverse.workers.dev
development, local synchttp://localhost:8787ws://localhost:8787

The sync selector chooses local sync when VITE_LOCAL_SERVERS === 'ALL' or when the string contains sync-service.

`sync-service` is not a regular `SERVER_HOSTS` key. Because `SERVER_HOSTS` validates every comma-separated entry before sync host selection completes, `VITE_LOCAL_SERVERS=sync-service` currently throws `unknown server name sync-service`. Use `VITE_LOCAL_SERVERS=ALL` for local sync-service in the current implementation, or update the regular server selector before relying on partial sync-only selection.

SYNC_PERMISSION_TOKEN_DSS_HOST follows the sync-service host:

Sync-service selectedPermission token DSS host
Remote sync-serviceRemote document-storage-service, so JWT secrets match the remote sync-service.
Local sync-serviceCurrent SERVER_HOSTS['document-storage-service'], which may be local when ALL is selected.

Runtime host helpers

servers.ts also exposes static-file URL helpers:

staticFileIdEndpoint(id: string): string
staticFileSizedEndpoint(id: string, size: 'small' | 'medium'): string
staticFileSizedUrl(url: string, size: 'small' | 'medium'): string
HelperOutput shape
staticFileIdEndpoint('abc')${SERVER_HOSTS['static-file']}/file/abc
staticFileSizedEndpoint('abc', 'small')${SERVER_HOSTS['static-file']}/file/abc?size=320
staticFileSizedEndpoint('abc', 'medium')${SERVER_HOSTS['static-file']}/file/abc?size=1080
staticFileSizedUrl(url, 'small')${url}?size=320

staticFileSizedUrl() appends ?size= directly. Pass a base URL without an existing query string.

Verification

cd js/app
bunx --bun vitest packages/core/tests/featureFlags.test.ts
cd js/app
just local-services
cd js/app
VITE_LOCAL_SERVERS=ALL VITE_ENABLE_BEARER_TOKEN_AUTH=true LOCAL_JWT='<token>' bun run dev

Local E2E startup uses the same pattern: VITE_LOCAL_SERVERS=ALL, VITE_ENABLE_BEARER_TOKEN_AUTH=true, and LOCAL_JWT injected into Vite as __LOCAL_JWT__.

Next

Local backend stack setup, seed data, and local E2E smoke-test workflow. How frontend packages consume `SERVER_HOSTS` through typed service clients.

25. Infrastructure stacks reference

Reference: Pulumi stack layout, reusable resources, service stacks, buckets, queues, Datadog sidecars, FusionAuth, and deployment inputs.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/25-infrastructure-stacks-reference.md
  • Generated: 2026-06-01T01:05:52.608Z

Source Files

  • infra/README.md
  • infra/packages/resources/src/index.ts
  • infra/packages/lambda/src/index.ts
  • infra/packages/service/src/index.ts
  • infra/stacks/document-storage/index.ts
  • infra/stacks/mcp-server/index.ts
  • infra/stacks/fusionauth-instance/README.md

Macro infrastructure is a Bun/TypeScript Pulumi monorepo under infra/, with deployable projects in infra/stacks/* and reusable components in infra/packages/*. AWS resources target us-east-1, and each stack is deployed from its own stack directory with Pulumi.

Stack layout

infra/
  package.json                 # Bun workspaces: stacks/* and packages/*
  packages/
    resources/                 # Shared AWS components: buckets, queues, ALB, RDS, Redis, Datadog
    lambda/                    # Rust Lambda packaging and worker trigger helpers
    service/                   # ECR image builder component
    shared/                    # stack/config constants, service URLs, shared StackReference helpers
    vpc/                       # legacy Coparse API VPC/subnet IDs
  stacks/
    document-storage/          # document S3 bucket, replication bucket, shared ECS cluster
    cloud-storage-service/     # application service and docx upload bucket
    mcp-server/                # public ECS/Fargate MCP server
    fusion-auth/               # FusionAuth runtime service and RDS database
    fusionauth-instance/       # FusionAuth tenant/application configuration
    ...

Run Pulumi from a stack directory:

cd infra/stacks/mcp-server
pulumi up --stack dev
pulumi up --stack prod
The root `infra/package.json` enforces Bun as the package manager and exposes `bun run check`, `bun run lint`, and `bun run format` for TypeScript validation.

Shared packages

PackageMain exportsPurpose
packages/resourcescreateBucket, createBucketV2, Queue, QueueAlarms, serviceLoadBalancer, DynamoDBTable, Database, Redis, Datadog helpersReusable AWS building blocks for stacks
packages/lambdaLambda, WorkerTrigger, generateContentHash, SourceCodeHashRust Lambda deployment and ECS worker trigger helpers
packages/serviceEcrImageECR repository plus Docker image build/push
packages/sharedstack, config, constants, service URL helpers, stack-reference helpersShared Pulumi config, hard-coded account constants, cross-stack outputs
packages/vpcget_coparse_api_vpc, COPARSE_API_VPCExisting VPC and subnet IDs used by services

Cross-stack dependency model

flowchart LR
  subgraph Foundation
    DS["document-storage stack\nS3 bucket + ECS cluster"]
    VPC["packages/vpc\nget_coparse_api_vpc()"]
  end

  subgraph SharedOutputs
    CSS["cloud-storage-service\nDOCX bucket + queues"]
    Email["email-service\nscheduled queue"]
    Link["link-sharing\nCloudFront signer outputs"]
  end

  subgraph ServiceStacks
    MCP["mcp-server\nMcpServer component"]
    Cog["document-cognition-service\nDocumentCognitionService component"]
    Contacts["contacts-service\nContactsService component"]
  end

  DS --> MCP
  DS --> Cog
  DS --> Contacts
  VPC --> MCP
  VPC --> Cog
  VPC --> Contacts
  CSS --> MCP
  Email --> MCP
  Link --> MCP
  CSS --> Cog
  Email --> Cog
  Link --> Cog

document-storage is both a storage stack and a foundational service stack: it exports the document bucket and the shared ECS cluster outputs consumed by ECS service stacks.

Buckets

createBucket

createBucket creates an aws.s3.Bucket with common Macro defaults:

InputBehavior
idPulumi resource ID
bucketNamePhysical S3 bucket name
transferAccelerationEnables S3 acceleration when true
enableVersioningEnables bucket versioning with MFA delete disabled
lifecycleRulesPassed through to S3 bucket lifecycle rules
exposeHeadersAdded to default CORS exposed headers
tagsApplied to the bucket

Default behavior:

  • forceDestroy is enabled outside prod.
  • Production buckets log to macro-logging-bucket with prefix ${bucketName}/.
  • CORS allows GET, PUT, POST, DELETE, and HEAD.
  • CORS allows all headers and exposes ETag plus any caller-provided headers.
  • Allowed origins come from packages/resources/src/resources/cors.ts.

createBucketV2

createBucketV2 uses aws.s3.BucketV2 and creates separate resources for:

  • CORS configuration
  • lifecycle configuration
  • versioning
  • transfer acceleration
  • production logging

Use this helper when a stack needs the newer S3 V2 resource model or finer-grained Pulumi dependencies.

document-storage bucket

The document-storage stack creates the primary document bucket:

StackBucket name
prodmacro-document-storage-prod
non-proddoc-storage-${stack}

Configured behavior:

  • Versioning enabled.
  • Transfer acceleration enabled only in prod.
  • temp_files/ objects expire after 1 day.
  • expired delete markers are removed.
  • noncurrent versions expire after 30 days.
  • EventBridge bucket notifications are enabled.
  • CORS exposes Content-Length and Content-Range.

The stack exports:

OutputValue
documentStorageBucketIdbucket ID/name
documentStorageBucketArnbucket ARN
documentStorageBucketNamesame value as bucket ID
documentStorageBucketReplicationRoleArnreplication IAM role ARN
cloudStorageClusterNameshared ECS cluster name
cloudStorageClusterArnshared ECS cluster ARN

Replication bucket

document-storage/replication-bucket.ts creates a cross-region replication target in us-west-1.

ResourceNaming
bucketmacro-doc-storage-replication in prod, otherwise macro-doc-storage-replication-${stack}
IAM rolereplication-role-${stack}
admin groupdocument-store-admin-${stack}

The replication bucket is versioned. In production it logs to macro-access-log-bucket-uswest2. Its bucket policy denies non-admin access and allows the replication role to read replication-related object versions.

Queues

Queue

Queue is a Pulumi component that creates:

  • an SQS queue
  • a DLQ
  • a DLQ visible-message alarm
  • a default queue age alarm through QueueAlarms

Naming:

ResourceName pattern
main queue${name}-queue-${stack}
DLQ${name}-dlq-${stack}
FIFO queueappends .fifo to both names

Defaults:

OptionDefault
maxReceiveCount5
visibilityTimeoutSeconds30
DLQ retention1209600 seconds
fifoQueuefalse

The DLQ alarm fires when ApproximateNumberOfMessagesVisible > 0 and sends actions to CloudTrailSNS.

QueueAlarms

QueueAlarms creates an ApproximateAgeOfOldestMessage CloudWatch alarm for a queue.

OptionDefault
approximateAgeOfOldestMessageEvaluationPeriodsused as the alarm period, default 60 seconds
approximateAgeOfOldestMessageThreshold120 seconds
metric namespaceAWS/SQS
alarm actionCloudTrailSNS

Custom queue stacks

Some stacks define queues directly instead of using Queue. For example, search-event-queue creates:

  • search-event-queue-${stack}
  • search-event-queue-dlq-${stack}
  • an SNS topic named search-event-queue-alerts-${stack}
  • production-only alarms for DLQ messages, old messages, and visible message count

Service stacks

Service stacks usually follow this pattern:

  1. Read Pulumi config with config.require(...).
  2. Resolve AWS Secrets Manager values with aws.secretsmanager.getSecretVersionOutput(...).
  3. Import shared VPC values with get_coparse_api_vpc().
  4. Read other stack outputs with pulumi.StackReference.
  5. Create an ECS/Fargate component for the service.
  6. Export service URLs, role ARNs, queue names, bucket names, or Lambda names needed by downstream stacks.

Shared VPC

get_coparse_api_vpc() returns a fixed VPC ID and fixed public/private subnet IDs. Service tasks run in private subnets. ALBs use public subnets unless isPrivate is true.

Load balancers

serviceLoadBalancer creates:

ResourceBehavior
target groupHTTP target group with targetType: "ip" and caller-provided health check path
ALBpublic or internal application load balancer
HTTPS listenerport 443, TLS policy ELBSecurityPolicy-TLS13-1-2-2021-06, shared Macro ACM certificate
HTTP listenerport 80, redirects to HTTPS with HTTP_301
access logsenabled in prod to macro-alb-logging

isPrivate controls subnet selection:

isPrivateALB subnets
trueVPC private subnets
false/undefinedVPC public subnets

ECR images

EcrImage creates:

  • mutable ECR repository
  • force-delete repository behavior
  • skipped default AWSX lifecycle policy
  • explicit lifecycle rule to expire untagged images older than 1 day
  • Docker image tagged latest

If USE_PREBUILT_SERVICE_BINARIES=true, supported Dockerfiles are remapped:

DockerfilePrebuilt Dockerfile
DockerfileDockerfile.prebuilt
Dockerfile.convert_serviceDockerfile.convert_service.prebuilt
Dockerfile.search_processing_serviceDockerfile.search_processing_service.prebuilt

Unsupported Dockerfile names throw an error when prebuilt binaries are enabled.

ECS/Fargate service conventions

The mcp-server and document-cognition-service components show the current ECS service shape:

  • tasks run in private subnets
  • security group ingress is only from the ALB security group on the service port
  • ALB accepts public HTTP/HTTPS when the service is public
  • deployment circuit breaker is enabled with rollback
  • task role is passed through taskDefinitionArgs.taskRole
  • Route53 creates an A alias for the service domain
  • CPU, memory, ALB 5xx, and target-tracking scaling policies are declared in the service component

mcp-server uses:

SettingValue
domainmcp-server.macro.com in prod, mcp-server-${stack}.macro.com otherwise
container port8080
health check/health
desired count1
autoscaling min/maxmin 1, max 10 in prod, max 3 otherwise
scaling targetsALB request count 1000, CPU 70%, memory 70%
service alarmsCPU 80%, memory 80%, ALB 5xx count 25

Datadog integration

Datadog support is centralized in packages/resources.

Secrets and sidecars

DATADOG_API_KEY is read from Secrets Manager secret datadog-api-key.

fargateLogRouterSidecarContainer uses:

  • image amazon/aws-for-fluent-bit:latest
  • FireLens type fluentbit
  • config file /fluent-bit/configs/parse-json.conf
  • ECS_FARGATE=true
  • DD_ENV=${stack}
  • memory reservation 50

datadogAgentContainer uses:

  • image public.ecr.aws/datadog/agent:latest
  • DD_SITE=us5.datadoghq.com
  • APM enabled
  • OTLP gRPC endpoint 0.0.0.0:4317
  • sample rate 0.1 in prod, 1.0 otherwise
  • max traces per second 100
  • memory reservation 256

Service containers that use FireLens send logs to http-intake.logs.us5.datadoghq.com with dd_service, dd_source=fargate, dd_tags, and provider=ecs.

Lambda logs

The shared Lambda component creates a CloudWatch log group at:

/aws/lambda/${baseName}-${stack}

It also creates a log subscription filter to the shared Datadog Kinesis Firehose stream and filters out Lambda runtime boilerplate lines:

START RequestId
REPORT RequestId
END RequestId
INIT_START

Datadog Software Catalog

DatadogServiceEntity creates a Datadog Software Catalog service entity with:

  • service name
  • display name
  • owner
  • GitHub repository link
  • GitHub code path
  • language metadata
  • dependency on service:document-storage

Lambda components

Lambda

The generic Lambda<T> component deploys Rust Lambdas using the provided.al2023 runtime.

Key behavior:

FieldValue
handlerbootstrap
runtimeprovided.al2023
architecturex86_64
timeout default30 seconds
log retention7 days
deployment packagepulumi.asset.FileArchive(zipLocation)

If a VPC is supplied, the component creates a Lambda security group, attaches all-in/all-out rules, and places the Lambda in private subnets.

The component adds SOURCE_CODE_FILE_HASH to the Lambda environment and depends on a SourceCodeHash resource so Pulumi detects source-code changes after a build.

WorkerTrigger

WorkerTrigger creates a Lambda that runs an ECS task.

Environment variables:

NameValue
TASK_DEFINITIONtask definition ARN
CLUSTERECS cluster ARN
SUBNETScomma-separated private subnet IDs
ENVIRONMENTPulumi stack
RUST_LOGworker_trigger=info

The trigger role can call ecs:RunTask for the supplied task definition and cluster, and can iam:PassRole to ecs-tasks.amazonaws.com.

AI tools deployment inputs

getAiToolsInfra() returns environment variables and IAM resource ARNs needed by services that host the Rust ai_tools crate.

It reads stack outputs from:

StackReference nameTarget stack
ai-tools-cloud-storage-stackmacro-inc/document-storage/${stack}
ai-tools-cloud-storage-service-stackmacro-inc/cloud-storage-service/${stack}
ai-tools-email-service-stackmacro-inc/email-service/${stack}
ai-tools-linksharing-stackmacro-inc/link-sharing/${stack}

Returned environment variables include:

NameSource
INTERNAL_API_SECRET_KEYdocument-storage-service-auth-key-${stack} secret value
DOCUMENT_STORAGE_SERVICE_URLshared service URL map
EMAIL_SERVICE_URLshared service URL map
SYNC_SERVICE_URLshared service URL map
LEXICAL_SERVICE_URLshared service URL map
STATIC_FILE_SERVICE_URLshared service URL map
SYNC_SERVICE_AUTH_KEYsync-service-key-${stack} secret name
MCP_CREDENTIALS_KEY_SECRET_NAMEmcp-credentials-key-${stack}
DOCUMENT_STORAGE_BUCKETdocument-storage stack output
DOCX_DOCUMENT_UPLOAD_BUCKETcloud-storage-service stack output
EMAIL_SCHEDULED_QUEUEemail-service stack output
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_DISTRIBUTION_URLlink-sharing stack output
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PUBLIC_KEY_IDlink-sharing stack output
DOCUMENT_STORAGE_SERVICE_CLOUDFRONT_SIGNER_PRIVATE_KEY_SECRET_NAMElinksharing-private-key-${stack}

Returned IAM inputs include:

  • secret ARNs for sync service auth, CloudFront private key, and MCP credentials
  • queue ARN for the email scheduled queue
  • bucket ARNs for document storage and DOCX uploads

getAiToolsServiceRoleArns() returns role ARNs for services that host ai_tools, currently from mcp-server, document-cognition, and agent-schedule-service stack outputs.

MCP server stack

infra/stacks/mcp-server deploys an ECS/Fargate service using the shared document-storage ECS cluster.

Required Pulumi config consumed by the stack:

KeyMeaning
macro_db_secret_keySecrets Manager key for database URL
jwt_secret_keySecrets Manager key name for JWT secret
fusionauth_client_idSecrets Manager key for FusionAuth client ID
fusionauth_base_urlFusionAuth base URL
fusionauth_issuerFusionAuth issuer
fusionauth_client_secretSecrets Manager key for FusionAuth client secret
fusionauth_tenant_idFusionAuth tenant ID
fusionauth_api_keySecrets Manager key for FusionAuth API key
google_client_idSecrets Manager key for Google client ID
google_client_secretSecrets Manager key for Google client secret
macro_cache_secret_keySecrets Manager key for Redis endpoint
anthropic_api_keySecrets Manager key for Anthropic API key

The service also receives all getAiToolsInfra() environment variables.

Exports:

OutputMeaning
mcpServerUrlservice URL
mcpServerRoleArnECS task role ARN

FusionAuth stacks

Macro uses two separate FusionAuth-related stacks.

StackResponsibility
fusion-authRuns FusionAuth itself on ECS/Fargate with an RDS PostgreSQL database
fusionauth-instanceConfigures FusionAuth tenants, application, templates, lambdas, webhooks, signing key, and OAuth settings through pulumi-fusionauth

fusion-auth

The runtime stack creates:

  • RDS PostgreSQL database named fusionauth
  • ECS cluster named fusionauth-${stack}
  • ECS/Fargate service using image fusionauth/fusionauth-app:1.62.1
  • ALB and Route53 record
  • CloudWatch alarms for CPU, memory, and ALB 5xx
  • production-only service autoscaling

Domain names:

StackDomain
prodauth.macro.com
non-prodfusionauth-${stack}.macro.com

Database behavior:

StackBehavior
prodendpoint output is hard-coded to fusionauthdb-prod.macro.com
non-prodendpoint comes from the created RDS instance
devdatabase is publicly accessible
proddatabase is not publicly accessible

Required config:

KeyMeaning
fusion-auth:db-password-secret-keySecrets Manager key containing the database password

fusionauth-instance

The configuration stack uses the pulumi-fusionauth provider. Provider inputs are read from the fusionauth Pulumi config namespace:

KeyMeaning
fusionauth:hostFusionAuth API host
fusionauth:apiKeyFusionAuth API key

Stack config inputs include:

KeyMeaning
fusionauth-instance:fusionauth-issuertenant issuer
fusionauth-instance:fusionauth-signing-key-idsigning key ID
fusionauth-instance:fusionauth-license-key-secret-keySecrets Manager key for FusionAuth license
fusionauth-instance:smtp-user-secret-keySecrets Manager key containing { username, password }
fusionauth-instance:default-from-emailtenant default sender email
fusionauth-instance:authentication-service-domainauthentication service base URL
fusionauth-instance:authentication-service-internal-secret-keyoptional Secrets Manager key for webhook auth
fusionauth-instance:fusionauth-default-tenant-idrequired outside local
fusionauth-instance:fusionauth-client-idrequired outside local
fusionauth-instance:fusionauth-client-secret-keyrequired outside local

Configured FusionAuth resources:

  • Reactor license
  • system CORS for Apple ID POST callbacks
  • passwordless login email template
  • email verification template
  • create-user and delete-user webhooks to the authentication service
  • default tenant with SMTP, email verification, event configuration, theme, issuer, logout URL, and unverified-user deletion after 30 days
  • populate_macro_jwt JWTPopulate lambda
  • reconcile_secondary_idp_link OpenIDReconcile lambda
  • HS256 signing key
  • Macro application with passwordless login, JWT, refresh tokens, and OAuth redirects

OAuth redirect URLs include:

  • ${AUTHENTICATION_SERVICE_DOMAIN}/oauth/redirect
  • https://mcp-server.macro.com/oauth/callback in prod
  • https://mcp-server-${stack}.macro.com/oauth/callback outside prod
  • localhost redirect patterns in local and dev
The Google/Gmail secondary IdP reconcile lambda is declared in Pulumi, but the IdP wiring is documented in code as an admin-UI step until the IdP itself is managed by Pulumi.

Local FusionAuth setup

infra/stacks/fusionauth-instance/justfile provides local setup commands.

cd infra/stacks/fusionauth-instance
just setup

just setup installs workspace dependencies, downloads the FusionAuth Docker Compose .env, starts FusionAuth, waits for /api/status, initializes the local Pulumi stack, applies the FusionAuth configuration, patches the root .env, and stops the container.

The local `fusionauth-instance` stack is intended for a personal Pulumi account. Do not create it with the `macro-inc/` organization prefix.

Deployment checklist

Run `bun i` from `infra/` or use the stack-specific setup command when one exists. Run Pulumi from `infra/stacks/`, not from the repository root. Confirm `Pulumi..yaml` contains `aws:region: us-east-1` and every `config.require(...)` key consumed by that stack. For service stacks, verify upstream stacks have exported the referenced outputs. Common dependencies are `document-storage`, `cloud-storage-service`, `email-service`, and `link-sharing`. Run `pulumi up --stack dev` or `pulumi up --stack prod`.

Related pages

ECS/Fargate service component shape, load balancer behavior, and service-specific deployment patterns. Runtime FusionAuth service, tenant configuration, local setup, and OAuth integration details.

26. Build, test, and quality gates

Operations: Rust checks, clippy, SQLx preparation, TypeScript checks, Biome, Tailwind hygiene, Vitest, Playwright, and CI path filters.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/26-build-test-and-quality-gates.md
  • Generated: 2026-06-01T01:04:48.525Z

Source Files

  • justfile
  • rust/cloud-storage/justfile
  • js/package.json
  • js/app/package.json
  • js/app/justfile
  • .github/workflows/code-check-cloud-storage.yml
  • .github/workflows/web-app-check-main.yml

The repository splits operational checks between rust/cloud-storage and js/app: Rust uses just recipes around Cargo, SQLx, and Nextest, while the web app uses Bun scripts, Biome, Vitest, Playwright, and GitHub Actions path filters to keep PR checks scoped.

Command map

SurfaceWorking directoryCommandPurpose
Rust type checkrust/cloud-storagejust checkRuns cargo check with warnings denied and SQLX_OFFLINE=true.
Rust lintrust/cloud-storagejust clippyRuns workspace Clippy with all features, warnings denied, and clippy::disallowed_methods denied.
Rust formatrust/cloud-storagejust formatRuns cargo fmt; CI uses cargo fmt --check.
SQLx cacherust/cloud-storagejust prepare_dbRuns workspace cargo sqlx prepare --workspace -- --all-features.
Rust testsrust/cloud-storagecargo nextest run --all-features --lib --bins --testsMirrors the CI test runner after database setup.
Web type checkjs/appbun run type-checkRuns tsc --noEmit --skipLibCheck --project packages/app/tsconfig.json.
Web combined checkjs/appbun run checkRuns type-check and bun biome check.
Web Biome CIjs/appbunx --bun biome ci --changed --no-errors-on-unmatched --error-on-warningsMatches the main web PR Biome gate.
Tailwind hygienejs/appjust check-tailwindScans changed lines for prohibited raw Tailwind color/font utilities.
Vitestjs/appbunx vitest or bun run testRuns configured Vitest projects.
Local Playwright E2Erepository rootjust local-e2eStarts local dependencies, seeds deterministic data, then runs Playwright with LOCAL_E2E=true.
The JavaScript workspace declares `[email protected]`. Rust uses the checked-in toolchain file with Rust `1.94.0` plus `clippy`, `rustfmt`, `rust-analyzer`, and `rust-src`.

Recommended pre-PR gate

```bash cd rust/cloud-storage just check just clippy just format ``` ```bash cd rust/cloud-storage just prepare_db ``` Commit any changed `.sqlx/query-*.json` files generated by SQLx. ```bash cd js/app bun run type-check bunx --bun biome ci --changed --no-errors-on-unmatched --error-on-warnings just check-tailwind bunx vitest bun run build ``` ```bash just local-e2e ```

Rust cloud-storage gates

Local checks

rust/cloud-storage/justfile owns the local Rust quality recipes:

just check
just clippy
just format
just build

just check sets RUSTFLAGS=-Dwarnings, RUSTDOCFLAGS=-Dwarnings, CARGO_TERM_COLOR=always, and SQLX_OFFLINE=true before cargo check.

just clippy applies the same warning policy and additionally denies clippy::disallowed_methods, then runs:

cargo clippy --workspace --all-features

just format runs cargo fmt. Use CI-style format checking with:

cargo fmt --check

SQLx preparation

rust/cloud-storage/sqlx.just defines the database lifecycle wrappers. The default local database URL comes from rust/cloud-storage/database.just:

postgres://user:password@localhost:5432/macrodb

The SQLx preparation recipe is workspace-level:

cd rust/cloud-storage
just prepare_db

It expands to:

cargo sqlx prepare --workspace -- --all-features

Use this after adding or changing SQLx compile-time checked queries, migrations that affect queried schemas, or Rust test queries that require refreshed offline metadata.

Do not hand-edit `.sqlx/query-*.json` files. Generate them with `just prepare_db` from `rust/cloud-storage`.

Rust test database setup

The Rust test path uses live Postgres, not SQLx offline mode. The CI workflow starts pgvector/pgvector:pg16 and Redis, configures Postgres for high-concurrency tests, then runs:

just rust/cloud-storage/setup_test_envs
just rust/cloud-storage/initialize_dbs
cd rust/cloud-storage
cargo nextest run --all-features --lib --bins --tests --test-threads 32

setup_test_envs writes DATABASE_URL into the crate-level .env files needed by SQLx-backed tests. initialize_dbs runs the macro database create/migrate setup.

Do not run Rust tests with `SQLX_OFFLINE=true`. Offline mode is for `cargo check`, `cargo build`, and `cargo clippy`; tests validate against the live local schema.

Web app gates

TypeScript

The web app TypeScript gate runs against packages/app/tsconfig.json:

cd js/app
bun run type-check

The top-level js/package.json also exposes:

cd js
bun run check

That delegates to the app type-check command.

In CI, the TypeScript job runs when either web files changed or Rust API inputs changed. If Rust API inputs changed, the job first runs generated client validation:

cd js/app
bun run gen-api -- --check

The generator builds Rust OpenAPI binaries with SQLX_OFFLINE=true, runs them, regenerates TypeScript clients through Orval, runs Biome over generated service clients, then fails in --check mode if git diff or untracked generated files remain.

Biome

js/app/biome.jsonc configures Biome with Git VCS support, origin/main as the default branch, LF line endings, 2-space indentation, 80-column formatting, Solid rules enabled, React rules disabled, and generated-output exclusions.

CI runs:

cd js/app
biome ci --changed --no-errors-on-unmatched --error-on-warnings

Local equivalents include:

cd js/app
bunx --bun biome lint --skip=suspicious/noImportCycles
bunx --bun biome format --write
bunx --bun biome check --write

The web CI also has a separate import-cycle gate:

cd js/app
biome lint --changed --no-errors-on-unmatched --only=suspicious/noImportCycles

Tailwind hygiene

js/app/scripts/check-tailwind.ts enforces theme hygiene on changed lines only. It scans changed .tsx, .ts, .jsx, and .js files under packages/ and rejects raw utility classes matching:

  • raw color utilities such as bg-red-500, text-gray-700, border-black, fill-blue-400
  • raw white / black color utilities for the checked utility groups
  • font-berkeley and font-inter

Run it with:

cd js/app
just check-tailwind

In CI, the base branch is origin/${GITHUB_BASE_REF}. Locally, the script defaults to origin/dev.

Expected success output includes:

✅ No prohibited Tailwind classes found in changed lines!

On failure, the script prints file, line, column, matched class name, and a hint to replace raw utilities with semantic classes.

Vitest

js/app/vitest.config.ts defines a multi-project Vitest setup for package groups including websocket, core, queries, scripts, lexical-core, theme, block-channel, channel, notifications, and block-email.

Run once:

cd js/app
bunx vitest

Run via package script:

cd js/app
bun run test

Watch mode is exposed through the app justfile:

cd js/app
just test-watch

The web PR workflow installs Bun dependencies with bun install --frozen-lockfile, installs/caches Playwright Chromium when requested by the setup action, then runs bunx vitest.

Build

The app build script is:

cd js/app
bun run build

It runs from packages/app with:

MODE=development NODE_ENV=production bun run --bun build

The app justfile also provides mode-specific builds:

just build-dev
just build-staging
just build-prod
just build-tauri

Playwright and local E2E

Playwright is configured in js/app/playwright.config.ts with testDir: ./tests/e2e, fully parallel execution, trace retention on failure, and baseURL set to http://localhost:${PORT:-3000}/app.

The repository-level local E2E harness is the safest entry point:

just local-e2e

It performs the full setup:

  1. starts LocalStack with test AWS credentials,
  2. starts the local service subset through Docker Compose,
  3. seeds deterministic E2E data,
  4. runs LOCAL_E2E=true bunx playwright test in js/app.

For UI mode:

just local-e2e-ui

LOCAL_E2E=true changes Playwright to a single local-chromium project and generates a local JWT unless LOCAL_JWT is already exported. Direct Playwright runs can fail token generation if .env and seeded local data are missing; prefer the repo-level harness.

CI path filters

Cloud Storage PR check

.github/workflows/code-check-cloud-storage.yml runs on pull requests to main for opened, synchronized, reopened, and ready-for-review events. The path filter enables the workflow for changes under:

  • rust/cloud-storage/**
  • rust/rust-toolchain.toml
  • flake.nix
  • flake.lock
  • cloud-storage CI workflow and supporting GitHub actions/scripts

The workflow has three effective stages:

JobRuns whenGate
path-checkAlways on matching PR eventComputes should_run and an optional Nextest package filter.
checkshould_run == true and PR is not draftRuns Rust format check and Clippy.
testshould_run == true and PR is not draftStarts Postgres/Redis, initializes DBs, and runs Nextest.
status-checkAlwaysFails only if path-check, check, or test failed. Skipped jobs are accepted.

For package-specific Rust changes, CI computes a Nextest expression such as:

rdeps(=package_name)

Multiple package filters are joined with |. Workspace-level changes, lockfiles, toolchain changes, Cargo workspace changes, or CI/action changes force the full test suite.

Web app PR check

.github/workflows/web-app-check-main.yml runs on pull requests to main. Its path filters produce two outputs:

OutputMatching changes
should_runWeb package files, app packages/src, biome.jsonc, lexical workspaces, setup actions, and the workflow file.
api_changedRust cloud-storage Rust/Cargo files, flake files, API generation scripts, setup actions, and the workflow file.

The jobs are scoped as follows:

JobConditionCommand
typescriptshould_run == true or api_changed == trueOptionally bun run gen-api -- --check, then tsc.
biome-checkshould_run == truebiome ci --changed --no-errors-on-unmatched --error-on-warnings.
tailwindshould_run == truejust check-tailwind.
testshould_run == truebunx vitest.
cyclesshould_run == trueBiome changed-file import-cycle lint.
buildshould_run == truebun run build.
status-checkAlwaysFails only if a needed job failed. Skipped jobs are accepted.

This means a Rust API-only change can trigger the TypeScript/API generation validation without running every web-only gate.

Troubleshooting

SymptomLikely causeFix
SQLx reports missing cached query data during check/build.sqlx metadata is stale or absentRun cd rust/cloud-storage && just prepare_db.
Rust tests fail when SQLX_OFFLINE=true is setTests need a live schemaUnset SQLX_OFFLINE, initialize DBs, and rerun tests.
Web TypeScript CI fails after Rust API changesGenerated service clients are out of syncRun cd js/app && bun run gen-api, review generated files, commit updates.
Tailwind hygiene fails on new classesRaw color/font utilities were added on changed linesReplace with semantic theme classes.
Direct Playwright run cannot generate LOCAL_JWTLocal seed/env prerequisites are missingRun just local-e2e or export LOCAL_JWT after preparing local data.
Biome changed-file CI finds no files locallyWrong comparison branchFetch the expected base branch and rerun from js/app.

Next

For backend database work, pair this page with the repository’s SQLx migration and query-update conventions. For UI changes, run the web gates before opening a PR and use just local-e2e when the change depends on local services.


27. Deploy services and web app

Operations: Generic service deployment, web app deployment, production release workflow, database migrations, Pulumi stacks, and deployment concurrency.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/27-deploy-services-and-web-app.md
  • Generated: 2026-06-01T01:07:17.883Z

Source Files

  • .github/workflows/deploy-service-generic.yml
  • .github/workflows/reusable-deploy-service.yml
  • .github/workflows/deploy-web-app.yml
  • .github/workflows/release-production.yml
  • .github/workflows/migrate-macro-db.yml
  • infra/README.md
  • rust/cloud-storage/README.md

Macro deploys run through GitHub Actions workflows that build Rust service binaries or Lambda bootstrap.zip artifacts, install Pulumi TypeScript dependencies under infra/, configure AWS credentials, and run pulumi up against macro-inc/dev or macro-inc/prod.

Deployment surfaces

SurfaceEntry pointTriggerTarget
Generic service deploy.github/workflows/deploy-service-generic.ymlManual workflow_dispatchOne service from the workflow choice list
Reusable service deploy.github/workflows/reusable-deploy-service.ymlworkflow_callOne service stack
Deploy all services.github/workflows/deploy-all-services.ymlManual or production releaseEvery key in .github/services-config.json
Dev cloud-storage deploy.github/workflows/deploy-cloud-storage-on-push.ymlPush to main matching cloud-storage or infra pathsAll configured services to dev
Manual Pulumi stack deploy.github/workflows/deploy-pulumi-stack.ymlManual workflow_dispatchAny infra/stacks/<name> directory
Web app deploy.github/workflows/deploy-web-app.ymlManual or workflow_callinfra/stacks/web-app
Production release.github/workflows/release-production.ymlGitHub release released eventDB migrations, services, web app, release artifact
Database migration.github/workflows/migrate-macro-db.ymlManual workflow_dispatchmacro-db-dev or macro-db-prod

Service deployment model

Service deployments are driven by .github/services-config.json. Each service key maps source ownership and deploy artifacts to a Pulumi stack directory.

{
  "services": {
    "<service-key>": {
      "source_paths": ["rust/cloud-storage/<crate-or-area>/**"],
      "stack_path": "infra/stacks/<pulumi-project>/**",
      "deploy_binaries": ["<cargo-bin-name>"],
      "deploy_lambdas": ["<lambda-package-name>"]
    }
  }
}
FieldRequiredUsed by
stack_pathYes.github/actions/get-project-name derives the Pulumi project directory from this path.
source_pathsNoDocuments source ownership and is available for change-detection workflows.
deploy_binariesNoEnables Nix binary artifact builds through .#deploy-service-binaries-<service>.
deploy_lambdasNoEnables Lambda artifact builds through .github/scripts/build-cloud-storage-lambdas.sh.
The current `deploy-cloud-storage-on-push.yml` workflow selects all service keys from `.github/services-config.json`; it does not narrow the dev deploy to changed `source_paths`.

Generic manual service deploy

Use Deploy Service (Generic) when the target service appears in .github/workflows/deploy-service-generic.yml.

Inputs:

Service key to deploy. The key is passed to the reusable deploy workflow as `service-name`. `dev` or `prod`. Defaults to `dev`.

The workflow forwards to .github/workflows/reusable-deploy-service.yml with inherited secrets. Its concurrency group is:

deploy-${service}-${environment}

cancel-in-progress is false, so a second deploy for the same service and environment queues instead of canceling the active deploy.

Reusable service deploy lifecycle

reusable-deploy-service.yml has three jobs:

  1. build-service-binaries

    • Checks .services[$SERVICE].deploy_binaries.
    • Runs nix build ".#deploy-service-binaries-${SERVICE}" only when binaries are configured.
    • Copies result/bin/* and the Nix runtime closure from nix-store -qR result.
    • Uploads prebuilt-binaries.tar.gz as cloud-storage-service-binaries-<service>-<sha>.
  2. build-lambda-artifacts

    • Checks .services[$SERVICE].deploy_lambdas.
    • Runs .github/scripts/build-cloud-storage-lambdas.sh only when Lambdas are configured.
    • Uploads lambda-artifacts.tar.gz as cloud-storage-lambdas-<service>-<sha>.
  3. deploy

    • Resolves the Pulumi project name from stack_path.
    • Calls .github/actions/deploy-cloud-storage-pulumi.
    • Passes artifact names only when the service config enabled those artifact types.

The deploy job runs on an 8 CPU Linux runner with a per-run service identifier.

Pulumi deploy action

.github/actions/deploy-cloud-storage-pulumi performs the stack update:

The action checks out the repository, downloads optional prebuilt binary and Lambda artifacts, and extracts them under `rust/cloud-storage/prebuilt` or `rust/cloud-storage/target/lambda`. It optionally sets up Docker Buildx, installs Bun `1.2.0`, and runs `bun install` in `infra/`. It configures AWS credentials for `us-east-1` by default and logs Docker into the repository account ECR registry. It runs `pulumi/actions` with `command: up`, `stack-name: macro-inc/`, and `work-dir: ./infra/stacks/`.

Required inputs include AWS keys, PULUMI_ACCESS_TOKEN, DD_APP_KEY, DD_API_KEY, and pulumi-service-name.

Optional inputs:

InputDefaultBehavior
use-dockertrueControls Docker Buildx setup.
use-lfsfalseEnables LFS checkout.
aws-regionus-east-1AWS region for credentials.
prebuilt-binaries-artifactemptyDownloads and extracts prebuilt service binaries.
lambda-artifactsemptyDownloads and extracts Lambda zips.
cloud-storage-service-nameemptyAllows inline Lambda builds when no Lambda artifact was supplied.
dd-hosthttps://api.us5.datadoghq.com/Datadog API host for Pulumi resources.

Prebuilt binaries and Docker images

Rust service binaries are built by Nix through flake outputs named:

deploy-service-binaries-<service-key>

The Nix package copies release binaries into $out/bin and strips them when possible. CI then packages the binaries with their Nix store closure.

When USE_PREBUILT_SERVICE_BINARIES=true, infra/packages/service/src/ecr.ts switches supported Dockerfiles to prebuilt variants:

Normal DockerfilePrebuilt Dockerfile
DockerfileDockerfile.prebuilt
Dockerfile.convert_serviceDockerfile.convert_service.prebuilt
Dockerfile.search_processing_serviceDockerfile.search_processing_service.prebuilt

If a service uses a different Dockerfile and prebuilt binaries are enabled, deployment fails with:

No prebuilt Dockerfile mapping configured for <Dockerfile>

The default prebuilt image copies ./prebuilt/nix-store/ into /nix/store/, copies ./prebuilt/${SERVICE_NAME} to /app/svc, and starts it through dumb-init.

Lambda artifacts

Lambda-backed stacks read bootstrap.zip files from:

rust/cloud-storage/target/lambda/<lambda-name>/bootstrap.zip

.github/scripts/build-cloud-storage-lambdas.sh reads deploy_lambdas for the selected service, enters the Nix development shell, and runs:

cd rust/cloud-storage
just "<lambda-name>/build"
test -f "target/lambda/<lambda-name>/bootstrap.zip"

It packages the output as:

lambda-artifacts.tar.gz
└── target/
    └── lambda/
        └── <lambda-name>/
            └── bootstrap.zip

For local Pulumi deploys that touch Lambda-backed stacks, rust/cloud-storage/README.md instructs running just build_lambdas before pulumi up.

Web app deployment

.github/workflows/deploy-web-app.yml builds js/app and deploys infra/stacks/web-app.

Inputs:

`dev` or `prod`. Accepted by the workflow interface. No current workflow step reads this input.

Build command:

cd js/app
just build-${environment}

The just recipes set Vite build mode:

EnvironmentCommandBuild env
devjust build-devMODE=development NODE_ENV=production
prodjust build-prodMODE=production NODE_ENV=production

The workflow supplies these Vite variables:

VariableSource
VITE_DD_WEB_APP_IDDD_WEB_APP_ID secret
VITE_DD_WEB_APP_TOKENDD_WEB_APP_TOKEN secret
VITE_DD_HASHgithub.sha
VITE_SEGMENT_WRITE_KEYSEGMENT_WRITE_KEY secret
VITE_POSTHOG_API_KEYPOSTHOG_API_KEY secret

After building, the workflow installs infra dependencies, configures AWS credentials, and runs Pulumi:

stack-name: macro-inc/<environment>
work-dir: ./infra/stacks/web-app

It then uploads sourcemaps through Datadog CI:

cd js/app/packages/app
bun run ddupload:<environment>

For prod, the workflow also uploads the built js/app/packages/app/dist directory as the GitHub Actions artifact web-app-build.

Web app Pulumi stack behavior

infra/stacks/web-app/index.ts requires the Pulumi config key macro-web-app:path. For dev and prod, this points at:

../../../js/app/packages/app/dist

The stack fails early when the build output directory is missing or empty:

Local path of build output is empty

The stack also blocks local production deployment unless CI is set:

You are trying to deploy to prod without the CI environment variable set

The stack creates and updates:

  • an S3 website bucket for app assets;
  • a public app/app-archive.zip;
  • synced app files under ./output;
  • Cache-Control: no-store metadata on app/index.html;
  • a content-encoding Lambda@Edge function;
  • an app-route Lambda function URL;
  • a Datadog software catalog entity;
  • a production CloudFront invalidation against the website-infra stack output cdnId.

For production asset sync, JavaScript sourcemaps are excluded from the S3 sync command.

Database migrations

Database migrations run through .github/actions/migrate-cloud-storage-db.

Entry points:

WorkflowEnvironment
.github/workflows/migrate-macro-db.ymlManual dev or prod
.github/workflows/deploy-cloud-storage-on-push.ymldev before service deployment
.github/workflows/release-production.ymlprod before production service deployment

The action checks out .github/ and rust/cloud-storage/macro_db_client, then reads the database URL from AWS Secrets Manager:

EnvironmentSecret name
devmacro-db-dev
prodmacro-db-prod

If the URL does not include sslmode=, the action appends sslmode=require.

Migration commands run from rust/cloud-storage/macro_db_client:

# dev
cargo sqlx migrate run --ignore-missing

# prod
cargo sqlx migrate run

The manual migration workflow runs on the db-migrator runner. The migration action itself does not configure AWS credentials; the runner environment must be able to call AWS Secrets Manager.

Production release workflow

.github/workflows/release-production.yml runs when a GitHub release is published with the released event type.

release: released
  └─ migrate-db
      └─ deploy-cloud-storage
          └─ build web app
              └─ deploy web app
                  └─ upload release artifact

Lifecycle:

  1. migrate-db

    • Runs production SQLx migrations on db-migrator.
  2. deploy-cloud-storage

    • Calls .github/workflows/deploy-all-services.yml with environment: prod.
    • Deploys every service key in .github/services-config.json.
  3. build

    • Builds the web app with just build-prod.
    • Uploads web-app-prod-build from js/app/packages/app/dist.
  4. deploy

    • Downloads web-app-prod-build.
    • Runs Pulumi in ./infra/stacks/web-app with stack-name: macro-inc/prod.
    • Uploads production sourcemaps to Datadog.
  5. upload

    • Creates web-app-${github.ref_name}.tar.gz from the built dist directory.
    • Uploads it to the GitHub release.

The production release workflow has a single workflow-level concurrency group and does not cancel in-progress releases.

Pulumi stacks

Infrastructure is TypeScript Pulumi under infra/. Each stack lives in infra/stacks/<stack-name> and has its own Pulumi.yaml. CI runs Pulumi from the stack directory.

The repository uses AWS infrastructure in us-east-1. The workflows configure AWS with repository secrets and pass Datadog keys through Pulumi environment variables.

Common CI stack names:

Environment inputPulumi stack name
devmacro-inc/dev
prodmacro-inc/prod

Local stack deployment follows the infra README pattern:

cd infra/stacks/<stack-name>
pulumi up --stack dev
# or
pulumi up --stack prod

The web app package also exposes stack-specific scripts:

cd infra/stacks/web-app
bun run deploy:dev
bun run preview:dev
bun run deploy:prod

Deployment concurrency

WorkflowConcurrency groupCancel in progress
deploy-service-generic.ymldeploy-<service>-<environment>false
deploy-web-app.yml<workflow>-web-app-<environment>false
release-production.yml<workflow>false
deploy-all-services.yml<workflow>-all-services-<environment>false
deploy-cloud-storage-on-push.ymldeploy-cloud-storage-<git-ref>false
deploy-pulumi-stack.yml<workflow>-<pulumi-service-name>-<environment>false

Matrix limits:

WorkflowJobmax-parallel
deploy-all-services.ymlBuild service binaries8
deploy-all-services.ymlBuild Lambda artifacts8
deploy-all-services.ymlDeploy services20
deploy-cloud-storage-on-push.ymlBuild service binaries8
deploy-cloud-storage-on-push.ymlBuild individual Lambda artifacts8
deploy-cloud-storage-on-push.ymlPackage Lambda artifacts8
deploy-cloud-storage-on-push.ymlDeploy services20

A scheduled cleanup workflow, .github/workflows/cancel-stuck-cloud-storage-deploys.yml, runs hourly and cancels queued or in-progress deploy-cloud-storage-on-push.yml runs older than 90 minutes by default. It also supports manual max-age-minutes and dry-run inputs.

Adding or changing a deployable service

Add the service key to `.github/services-config.json` with `stack_path`, optional `source_paths`, optional `deploy_binaries`, and optional `deploy_lambdas`. Ensure `infra/stacks//Pulumi.yaml` exists. The `stack_path` basename must match the directory that CI should pass as `pulumi-service-name`. If `deploy_binaries` is set, ensure `flake.nix` exposes `deploy-service-binaries-` and includes every listed Cargo binary. If `deploy_lambdas` is set, ensure each `just "/build"` recipe produces `rust/cloud-storage/target/lambda//bootstrap.zip`. Services using prebuilt binary deploys must use a Dockerfile supported by `EcrImage` prebuilt mapping, or the mapping must be extended. If operators should deploy through **Deploy Service (Generic)**, add the service key to that workflow's `service` choice list.

Troubleshooting

SymptomLikely causeCheck
Service '<name>' not found in .github/services-config.jsonThe service key is missing or misspelled.Verify .github/services-config.json and the workflow input.
Error: Service '<name>' not found in services-config.jsonget-project-name could not find stack_path.Confirm the service key has a stack_path.
Lambda build exits after test -f ... bootstrap.zipThe just <lambda>/build recipe did not produce the expected zip.Run the recipe from rust/cloud-storage and inspect target/lambda/<lambda>/.
No prebuilt Dockerfile mapping configuredA prebuilt binary deploy is using an unsupported Dockerfile name.Update infra/packages/service/src/ecr.ts or use a supported Dockerfile.
Local path of build output is emptyWeb app Pulumi ran before js/app/packages/app/dist was built.Run just build-dev or just build-prod from js/app.
Production web app deploy fails outside CIThe web app Pulumi stack blocks prod when CI is not set.Use the release workflow or a CI environment.
Dev cloud-storage deploy appears stuckThe on-push workflow has queued or in-progress runs older than the threshold.Run Cancel Stuck Cloud Storage Deploys with dry-run: true, then without dry run if appropriate.

Related pages

  • Cloud-storage local build and Lambda notes in rust/cloud-storage/README.md
  • Pulumi stack authoring and local deploy notes in infra/README.md

28. Observability and debugging

Operations: Backend tracing initialization, Datadog ECS sidecars, browser RUM and logs, sourcemaps, local debug signals, and iOS WebView freeze diagnosis.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/28-observability-and-debugging.md
  • Generated: 2026-06-01T01:07:41.839Z

Source Files

  • rust/cloud-storage/macro_entrypoint/src/lib.rs
  • infra/packages/resources/src/resources/datadog.ts
  • js/app/packages/observability/src/index.ts
  • js/app/packages/app/index.tsx
  • .github/workflows/deploy-web-app.yml
  • js/app/AGENTS.md

Macro’s observability surface is split between Rust entrypoint initialization, Pulumi-managed Datadog sidecars for ECS/Fargate services, the @observability browser package, and deployment-time sourcemap upload for the web app.

Runtime map

Rust service
  MacroEntrypoint::init()
    ├─ local: pretty logs or hierarchical tracing tree
    └─ dev/prod: JSON logs + OpenTelemetry OTLP -> 127.0.0.1:4317

ECS task
  service container
    ├─ FireLens log driver -> log_router -> Datadog logs intake
    └─ OTel spans -> datadog-agent sidecar -> Datadog APM

Web app
  app/index.tsx idle import("@observability")
    ├─ Datadog RUM
    ├─ Datadog browser logs
    └─ sourcemaps uploaded by GitHub Actions after deploy

Backend tracing initialization

Rust binaries should initialize tracing through:

macro_entrypoint::MacroEntrypoint::default().init();

MacroEntrypoint::default() loads .env if present and resolves macro_env::Environment from ENVIRONMENT, falling back to production when the variable is missing or invalid. Valid values are:

ENVIRONMENTRuntime enum
prodEnvironment::Production
devEnvironment::Develop
localEnvironment::Local

Local behavior

For Environment::Local, the default subscriber uses tracing_subscriber::fmt() with:

SettingValue
ANSIenabled
filterEnvFilter::from_default_env()
file and lineenabled
formatterpretty
OpenTelemetry exporterdisabled

Set RUST_LOG to control local verbosity, for example:

RUST_LOG=document_storage_service=trace,tower_http=debug

For tree-shaped local output, build the entrypoint with tree_tracing:

macro_entrypoint::MacroEntrypoint::new(macro_env::Environment::Local)
  .local()
  .tree_tracing(Some(4))
  .build()
  .init();

Develop and production behavior

For Environment::Develop and Environment::Production, MacroEntrypoint::init():

  • installs tracing_panic::panic_hook;
  • creates an OpenTelemetry OTLP gRPC span exporter pointed at http://127.0.0.1:4317;
  • sets OpenTelemetry resource attributes from:
    • DD_SERVICE, defaulting to unknown-service;
    • DD_ENV, defaulting to unknown;
  • attaches a tracing_opentelemetry layer;
  • emits JSON logs with current span, flattened event fields, file, and line number;
  • injects Datadog correlation fields dd.trace_id and dd.span_id into each valid span-backed JSON log event.
`InitializedEntrypoint::shutdown()` exists to flush the OpenTelemetry tracer provider before process exit. Retain the returned value and call `shutdown()` for short-lived jobs where trace flushing matters.

Datadog ECS sidecars

The shared Datadog containers live in Pulumi resources and are added to ECS tasks by the service components unless a component explicitly opts out with noDatadog.

ContainerImagePurpose
log_routeramazon/aws-for-fluent-bit:latestFireLens log routing to Datadog logs intake
datadog-agentpublic.ecr.aws/datadog/agent:latestAPM/trace collection and OTLP receiver

log_router

The FireLens sidecar is essential, reserves 50 MiB, enables ECS metadata, and uses Fluent Bit’s bundled /fluent-bit/configs/parse-json.conf.

Environment:

VariableValue
ECS_FARGATEtrue
DD_API_KEYAWS Secrets Manager secret datadog-api-key
DD_ENVPulumi stack name

Service containers route logs with awsfirelens to http-intake.logs.us5.datadoghq.com and set Datadog options such as dd_service, dd_source=fargate, dd_tags, and provider=ecs.

datadog-agent

The Datadog agent sidecar listens for OTLP gRPC spans on port 4317.

VariableValue
DD_SITEus5.datadoghq.com
DD_APM_ENABLEDtrue
DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT0.0.0.0:4317
DD_APM_SAMPLE_RATE0.1 in prod, 1.0 otherwise
DD_APM_MAX_TPS100
DD_APM_RECEIVER_SOCKETempty string

The empty DD_APM_RECEIVER_SOCKET disables socket buffering in the agent configuration used by this repo.

Browser RUM and logs

The web app initializes browser observability lazily from packages/app/index.tsx. During non-HMR runs, it schedules an idle import of @observability and calls:

Observability.init(import.meta.env.__APP_VERSION__);

During vite dev, import.meta.hot is present, so Datadog browser observability is not injected.

Datadog browser configuration

@observability configures both RUM and browser logs with:

FieldValue
serviceweb-app
siteus5.datadoghq.com
envprod when import.meta.env.MODE === "production", otherwise dev
versionimport.meta.env.__APP_VERSION__ by default
RUM sessionSampleRate100
RUM sessionReplaySampleRate0
RUM trackViewsManuallytrue
RUM defaultPrivacyLevelmask
logs telemetrySampleRate0

RUM tracing uses tracecontext propagation. In production, tracing is allowed only for selected Macro service hosts: auth, cognition, document storage, email, and notification. In non-production modes, all configured SERVER_HOSTS are allowed.

Event filtering

The browser package drops known noisy events before they leave the client:

SourceFilter
RUM resource eventsnon-200 unfurl-service resources
RUM errorsResizeObserver loop completed with undelivered notifications
browser logsmessages containing unfurl-service
browser logsmessages containing the same ResizeObserver notification

Logging API

Use @observability or @observability/logger for client-side application logs:

import { logger } from '@observability';

logger.log('Uploaded file', { documentId });
logger.warn('Retrying request', { attempt });
logger.error('Failed to save draft', { cause: error, draftId });

When Datadog is unavailable, not initialized, or the app is running under HMR, the logger falls back to console.log, console.warn, or console.error.

Routing and actions

useObserveRouting() starts manual RUM views from the current route. It derives the view name from the first path segment after /app; for /app/component/:name, it uses the component name segment instead.

It also records a RUM action named split changed when the joined path segments change.

For explicit user actions:

import { startAction } from '@observability';

startAction('opened command palette', { source: 'hotkey' });

Sourcemaps and release versions

The Vite app build always enables sourcemaps. The app version is computed from packages/app/package.json plus the current short Git SHA:

<package-version>+<short-sha>

After vite build, postbuild:semver writes the same value to:

js/app/packages/app/dist/semver.txt

Deployment workflows pass Datadog browser credentials into the build, deploy the web app with Pulumi, then upload sourcemaps from js/app/packages/app:

bun run ddupload:dev
bun run ddupload:prod

The upload scripts call datadog-ci sourcemaps upload ./dist with:

Environment--service--release-version--minified-path-prefix
devweb-app$(cat ./dist/semver.txt)https://dev.macro.com/app
prodweb-app$(cat ./dist/semver.txt)https://macro.com/app

Production web-app infrastructure excludes *.js.map files from the S3 sync, so production sourcemaps are uploaded to Datadog rather than served with the app assets.

Local debug signals

Web app startup signals

On startup, the app logs the resolved version:

App Version <version>

The root document element also receives runtime attributes useful for local debugging and CSS/state inspection:

AttributeValue source
data-platformgetPlatform()
data-touch-deviceisTouchDevice()
data-modalitylast captured keydown, mousedown, or touchstart

In development mode, rendering is wrapped in a Solid ErrorBoundary that shows FatalError. In non-development builds, the root renders directly.

Dynamic import deployment mismatch

The app registers a vite:preloadError listener outside HMR. If a dynamic module load fails after a new version is deployed, the browser shows:

Please refresh page to update app to new version

Use this as the expected signal for stale chunks after deployment.

iOS WebView freeze diagnosis

On iOS WKWebView, eagerly constructing an ES module Web Worker from a tauri:// custom URL can deadlock the WebContent process. The rule in this repository is:

Do not call `new Worker(...)` at module-load time in code that runs on iOS. Construct workers lazily on first use. The `import Worker from "./worker?worker"` import is safe; instantiating it is the dangerous operation.

A lazy singleton can use a proxy:

export const svc = new Proxy({} as Service, {
  get: (_, p, r) => Reflect.get(Service.getInstance(), p, r),
});

Symptom signature

The app loads HTML, starts initial JavaScript, then JavaScript silently stops. Safari Web Inspector may attach but show no useful page state.

Diagnosis procedure

Use the simulator so logs are available through macOS unified logging.
```sh
cargo tauri ios dev "iPhone 15"
```
Use the full `/usr/bin/log` path because `zsh` has a `log` builtin.
```sh
/usr/bin/log stream --predicate 'process == "macro"' --info --debug --style compact
```
If protocol-handler request logging is enabled, the last `tauri://` request before silence is usually the file being loaded when the freeze occurred.
```sh
tail -200 <logfile> | grep -vE 'tauri:// request|tauri_protocol.rs|^\s*\\134'
```
Get the WebContent PID from a `[com.apple.WebKit:...] [...PID=N...]` log line, then inspect CPU.
```sh
ps -o pid,pcpu,comm -p <pid>
```

`0.0%` CPU with a still-alive process points to a deadlock or parked thread, not a hot loop.
```sh sample 3 -file /tmp/sample.txt ```
Look for `WebCore: Worker` stacks containing:

```text
WorkerOrWorkletScriptController::loadModuleSynchronously
  → WorkerDedicatedRunLoop::runInMode
    → Condition::waitUntilUnchecked
```

If the last request was a *-worker.js?worker_file&type=module URL and a worker thread is parked in loadModuleSynchronously, trace the corresponding new Worker(...) call and move construction behind a lazy path.

Do not treat these as primary causes unless the stack confirms them: NSKeyedArchiver main-thread warnings, IPC throttling warnings, or failed bundle-updater requests to localhost:3001.

Related pages

  • Web app deployment and sourcemap upload
  • Backend service deployment on ECS/Fargate
  • iOS/Tauri local debugging

29. Documentation site maintenance

Contribution: Maintain the Mintlify docs site, navigation manifest, generated MCP pages, local preview, broken-link checks, and writing conventions.

  • Page Markdown: https://grok-wiki.com/public/docs/macro-inc-macro-bb988e1a448e/pages/29-documentation-site-maintenance.md
  • Generated: 2026-06-01T01:06:59.884Z

Source Files

  • docs/README.md
  • docs/docs.json
  • docs/package.json
  • docs/scripts/generate-mcp-tool-pages.ts
  • docs/config/tool-pages.json
  • docs/AGENTS.md
  • docs/CONTRIBUTING.md

The documentation site lives in docs/ as a Mintlify project with docs/docs.json as the active site manifest, Bun scripts for local operations, and generated MCP tool reference pages under docs/AI/mcp/tools/.

Maintenance surface

docs/
├── docs.json                         # active Mintlify manifest
├── package.json                      # dev, lint, generate:tools, prepare
├── README.md                         # local setup and monorepo deployment notes
├── AGENTS.md                         # agent-facing writing conventions
├── CONTRIBUTING.md                   # contributor workflow and style guidance
├── development.mdx                   # local Mintlify preview guide
├── AI/mcp/tools/                     # generated MCP tool reference pages
├── config/tool-pages.json            # generated tool page list
├── config/navigation.json            # secondary navigation fragment, not the active manifest
└── scripts/
    ├── generate-mcp-tool-pages.ts
    └── generate-changelog.ts
The checked-in MCP generator writes to `docs/AI/mcp/tools/`. If a maintenance note mentions `docs/reference/tools/`, verify it against `docs/scripts/generate-mcp-tool-pages.ts` before moving files or changing navigation.

Local commands

Run documentation commands from docs/.

TaskCommandNotes
Install dependenciesbun installUses docs/package.json and docs/bun.lock.
Generate MCP tool pagesbun run generate:toolsRuns bun ./scripts/generate-mcp-tool-pages.ts.
Preview locallybun run devWraps mint dev.
Check linksbun run lintWraps mint broken-links.
Run generator through lifecycle scriptbun run prepareCalls bun run generate:tools.
cd docs
bun install
bun run generate:tools
bun run dev
bun run lint
Mintlify CLI commands may reject unsupported runtimes. The repository README recommends switching to Node 20 or Node 22 when `mint dev` or `mint broken-links` fails because of the active Node version.

Active site manifest

docs/docs.json controls the rendered site structure and global presentation.

AreaCurrent value
Schemahttps://mintlify.com/docs.json
Thememint
Site nameMacro
Default appearancedark
Primary color#ff8f05
Favicon/favicon.svg
Logos/logo/light.svg, /logo/dark.svg
Navbar CTAGet Macrohttps://macro.com
TabsDocumentation, Changelog

The Documentation tab contains Getting Started and Product groups. The AI Chat product group nests the MCP overview and generated Tool Reference pages. The Changelog tab points at changelog/introduction.

When adding a handwritten page:

  1. Create the .mdx file under the matching content folder.
  2. Add the route without the .mdx extension to docs/docs.json.
  3. Use root-relative internal links such as /product/ai-chat.
  4. Run bun run lint.
`docs/config/navigation.json` contains a `$ref` to `config/tool-pages.json`, but the active manifest in this checkout is `docs/docs.json`. Keep `docs/docs.json` synchronized unless the site is intentionally migrated to consume the navigation fragment.

Generated MCP tool pages

docs/scripts/generate-mcp-tool-pages.ts rebuilds tool docs from the Rust tool schema registry.

Inputs and outputs

PathRole
rust/cloud-storage/ai_toolsRust crate used to build and run the schema generator.
rust/cloud-storage/ai_tools/src/bin/gen_tool_schemas.rsBinary that writes schemas/tools.json.
rust/cloud-storage/ai_tools/schemas/tools.jsonIntermediate generated schema file, ignored by rust/cloud-storage/.gitignore.
docs/AI/mcp/tools/*.mdxGenerated MDX pages and generated index page.
docs/config/tool-pages.jsonGenerated route list for tool navigation.
docs/docs.jsonActive navigation manifest that must expose generated pages.

Generation lifecycle

The script runs Cargo from rust/cloud-storage with offline SQLx mode:

SQLX_OFFLINE=true cargo build --bin gen_tool_schemas

The script removes rust/cloud-storage/ai_tools/schemas, then executes the compiled gen_tool_schemas binary from rust/cloud-storage/ai_tools.

Expected binary output:

Generated ai_tools/schemas/tools.json

The script deletes and recreates docs/AI/mcp/tools/, sorts schemas by name, slugifies each tool name, writes one page per tool, and writes docs/AI/mcp/tools/index.mdx.

The script writes docs/config/tool-pages.json with routes such as:

[
  "AI/mcp/tools/index",
  "AI/mcp/tools/content-search"
]

Slug and page rules

RuleBehavior
SortingTool schemas are sorted with name.localeCompare.
Slug conversionCamelCase boundaries become hyphens; underscores and whitespace become hyphens; output is lowercase.
Page titleUses the tool schema name.
Page descriptionUses the tool schema description, or Generated from the Macro Rust tool registry.
Parameters tableBuilt from inputSchema.properties; required fields come from inputSchema.required.
Output schemaLoaded but not rendered into the generated MDX pages.

Example generated route mapping:

Tool nameRoute
ContentSearch/AI/mcp/tools/content-search
text_editor_code_execution/AI/mcp/tools/text-editor-code-execution
web_fetch/AI/mcp/tools/web-fetch

Updating MCP documentation after Rust tool changes

Use this checklist when a Rust tool name, schema, field, or description changes.

Edit the Rust tool definition, schema annotations, or tool registry entry that owns the tool behavior.

cd docs
bun run generate:tools

Check these paths first:

docs/AI/mcp/tools/
docs/config/tool-pages.json

If a tool was added, removed, or renamed, update the Tool Reference page list in docs/docs.json.

bun run lint
bun run dev

Open the local preview and verify the MCP overview, Tool Reference index, and the changed tool page.

Broken-link checks

bun run lint runs mint broken-links. Use it after:

  • adding, moving, or deleting an MDX page;
  • changing docs/docs.json navigation;
  • regenerating MCP tool pages;
  • changing root-relative links;
  • changing changelog output.

If mint broken-links fails after generated tool changes, check for one of these causes:

SymptomLikely causeFix
Tool page exists but is missing from sidebardocs/docs.json was not updatedAdd the route under the Tool Reference group.
Sidebar route points to a deleted fileTool was renamed or removedRemove or replace the stale route in docs/docs.json.
Link works in generated index but not navigationconfig/tool-pages.json and docs/docs.json divergedSync the active manifest.
CLI fails before checking linksUnsupported Node runtimeSwitch to Node 20 or Node 22 and rerun.

Writing conventions

Documentation files are MDX with YAML frontmatter. Follow the repository’s current contribution and agent guidance:

ConventionRule
VoiceUse active voice.
Reader addressUse second person in procedures.
SentencesKeep sentences concise; one idea per sentence.
HeadingsUse sentence case.
UI labelsBold UI labels, for example Settings.
Technical identifiersUse code formatting for file names, commands, paths, and code references.
TerminologyDo not alternate between synonyms for the same product concept.
ExamplesInclude concrete examples when documenting behavior or commands.

Use existing nearby pages as the shape reference. Product pages live under docs/product/, MCP setup lives under docs/AI/mcp/, and generated tool reference pages should not be manually polished in place because the generator rewrites them.

Changelog generation

docs/scripts/generate-changelog.ts is separate from the MCP generator. It fetches GitHub releases for macro-inc/macro, keeps tags matching vYYYY.M.D.N, rewrites docs/changelog/introduction.mdx, and rewrites the Changelog tab in docs/docs.json.

Operational constraints:

  • It uses GITHUB_TOKEN when present.
  • It can also rely on GitHub CLI authentication according to its file header.
  • It removes old per-release MDX files in docs/changelog/ except introduction.mdx.
  • It escapes unsupported MDX angle brackets before writing release bodies.

Run it only when intentionally refreshing changelog content from GitHub releases.

Deployment configuration

For Mintlify’s monorepo setup, configure the docs path as:

/docs

Keep deployment-facing changes inside docs/ unless the generated MCP pages require Rust schema changes. The docs project is independent from model-provider configuration: MCP reference generation reads repository-local Rust schemas and does not require an AI provider key.

Related pages

Connect AI clients to the Macro MCP endpoint. Browse generated MCP tool pages. Preview and validate the Mintlify site locally. Review release notes generated from GitHub releases.

Similar Articles