Stable APIs
View as MarkdownBy default, when a materialized view changes, mz-deploy recreates it in a
staging schema and swaps the entire schema into production. This works well
within a single project — dependencies are tracked and redeployed
automatically. But consumers in other projects break, because the schema
swap drops and recreates the materialized view, severing any downstream
dependencies that reference it.
Stable API schemas solve this problem.
Marking a schema as stable
Add SET api = stable to a schema modifier:
-- models/materialize/ontology.sql
SET api = stable;
With this in place, changed materialized views in the ontology schema are no
longer dropped and recreated. Instead, mz-deploy uses Materialize’s
replacement protocol:
ALTER MATERIALIZED VIEW ... APPLY REPLACEMENT ...
The materialized view’s computation is updated in place and its identity is preserved. Downstream consumers — whether in the same project or a different one — do not need to be redeployed and do not need to know the update happened.
How it works
You deploy the same way as always — stage, wait, promote. mz-deploy
automatically detects which schemas are marked stable and handles them
accordingly. Materialized views in stable schemas are updated in place while
preserving their identity. Everything else deploys normally.
Because the object identity is preserved, a changed stable MV does not propagate dirtiness to its dependents. Objects that depend on a stable MV are not redeployed, even if the MV’s definition changed. This prevents cascading redeployments across project boundaries.
The two-schema pattern
The recommended way to build a stable API is with two schemas: an internal schema for your transformation logic and a stable schema that exposes a clean API surface.
models/materialize/
├── ontology.sql # SET api = stable
├── ontology/
│ ├── customers.sql # Thin MV — stable API surface
│ └── orders.sql # Thin MV — stable API surface
├── internal/
│ ├── customers_cleaned.sql # View + index — transformation logic
│ └── orders_enriched.sql # View + index — transformation logic
The internal schema contains views with indexes that hold all your transformation logic. These are regular objects deployed via the normal schema-swap mechanism.
The stable schema contains thin materialized views that select from the
internal views. Each MV explicitly lists its columns (never SELECT *) and
includes a COMMENT ON describing its contract:
-- models/materialize/ontology/customers.sql
CREATE MATERIALIZED VIEW customers
IN CLUSTER ontology AS
SELECT
id,
name,
email,
status,
created_at
FROM internal.customers_cleaned;
COMMENT ON MATERIALIZED VIEW customers IS
'Canonical customer entity. Columns: id, name, email, status, created_at.';
-- models/materialize/internal/customers_cleaned.sql
CREATE VIEW customers_cleaned AS
SELECT
id,
trim(name) AS name,
lower(email) AS email,
CASE WHEN active THEN 'active' ELSE 'inactive' END AS status,
created_at
FROM orders_db.public.raw_customers;
CREATE INDEX customers_cleaned_idx IN CLUSTER ontology
ON customers_cleaned (id);
This separation gives you:
- All logic in the internal schema — easy to change, tested with unit tests, deployed via normal schema swap.
- A stable API surface — thin MVs that preserve identity across deployments. Other teams depend on these and are never disrupted.
- Explicit contracts — column lists and comments define what consumers can rely on.
Building a data mesh
Stable APIs are the foundation for a data mesh architecture in Materialize. Multiple teams can maintain independent mz-deploy projects that depend on each other’s stable schemas:
Ontology project Fulfillment project
======================== ========================
internal/ internal/
customers_cleaned shipments_joined
orders_enriched delivery_tracking
ontology/ (stable) references:
customers ──────▶ ontology.customers
orders ──────▶ ontology.orders
The Ontology project owns canonical business entities and exposes them through a stable schema. The Fulfillment project builds domain-specific views on top of the ontology. Each project:
- Has its own git repository and deployment lifecycle.
- Runs on its own cluster for independent scaling.
- Declares cross-project references as
external dependencies
in
project.toml.
When the Ontology team changes how customers_cleaned is computed, they deploy
normally. The customers MV in the stable schema is updated in place via the
replacement protocol. The Fulfillment project’s views continue working without
redeployment or coordination.
Constraints
- Only materialized views — stable schemas can only contain materialized views. Tables, views, sinks, and sources are not supported.
- No dirtiness propagation — a changed replacement MV does not mark its dependents as dirty. Dependent objects in the same project are not redeployed.
- No new objects in existing stable schemas — you cannot add a brand-new materialized view to a stable schema that already has production objects in a single deployment. Deploy the schema for the first time (or add objects when the schema is new), then update existing MVs in subsequent deployments.
- Explicit column lists — always list columns explicitly in stable MVs
rather than using
SELECT *. This makes the API contract visible and prevents accidental column additions from propagating to consumers.