The Monolithic dbt Project Problem
A single dbt project containing hundreds or thousands of models, owned by five or more teams, becomes a coordination bottleneck before you know it. Any team that wants to add a model needs to open a PR into the shared repo. Any team that wants to rename a column in their domain needs to check that no other team's model references it. CI takes 45 minutes because every model runs regardless of what changed. dbt Mesh, introduced with dbt Core 1.5 and expanded in 1.6 and 1.7, solves this by splitting the monolith into independently-owned domain projects that can reference each other's explicitly shared models without needing write access to each other's repositories.
The result is a federated model: the Marketing team owns and deploys their dbt project, the Finance team owns theirs, and both can safely consume stable, versioned, contractually-enforced models from the Platform team's shared project — without merge conflicts, without shared CI queues, and without organizational coupling. This mirrors the Data Mesh principle of domain-oriented decentralized data ownership at the layer of analytical transformation code.
Access Levels
Models are private (same project only), protected (same project + group), or public (referenceable cross-project). Default is protected.
Model Contracts
Public models can declare a contract — a list of columns with exact types. dbt enforces the contract at compile time and optionally at run time, preventing silent breaking changes.
Cross-Project ref()
Downstream projects reference upstream public models with {{ ref('project_name', 'model_name') }}. dbt resolves the relation at compile time without running the upstream project.
Multi-Project Repository Structure
dbt Mesh works equally well in a monorepo (all projects in one Git repository, each in a subdirectory) and in a polyrepo (each project in its own repository). The monorepo approach simplifies local development and cross-project integration testing; the polyrepo approach enforces true team independence and supports separate CI pipelines. Most organizations start with a monorepo for simplicity.
# Monorepo layout for dbt Mesh with three domain projects
# Each subfolder is an independent dbt project with its own dbt_project.yml
data-platform/
├── platform/ # Shared infrastructure project (producer)
│ ├── dbt_project.yml
│ ├── models/
│ │ ├── staging/ # private: internal raw layer
│ │ │ └── stg_orders.sql
│ │ ├── core/ # public: stable interfaces consumed downstream
│ │ │ ├── dim_customers.sql
│ │ │ └── fct_orders.sql
│ │ └── marts/ # protected: internal team use only
│ │ └── int_order_enriched.sql
│ ├── packages.yml
│ └── profiles.yml
│
├── marketing/ # Marketing domain project (consumer + producer)
│ ├── dbt_project.yml
│ ├── dependencies.yml # declares dependency on 'platform' project
│ ├── models/
│ │ ├── staging/ # private staging from marketing sources
│ │ │ └── stg_campaigns.sql
│ │ ├── core/ # public: campaign attribution models for Finance
│ │ │ └── fct_campaign_attribution.sql
│ │ └── reports/ # private: marketing dashboards
│ │ └── rpt_channel_roi.sql
│ └── profiles.yml
│
└── finance/ # Finance domain project (consumer)
├── dbt_project.yml
├── dependencies.yml # depends on 'platform' and 'marketing'
├── models/
│ ├── staging/
│ │ └── stg_invoices.sql
│ └── reports/
│ └── rpt_revenue_by_channel.sql # joins platform + marketing public models
└── profiles.ymlAccess Levels, Model Groups, and Visibility Rules
Every dbt model has an access level, set either in the model's YAML config or in dbt_project.yml. The three levels are private (only models in the same group within the same project can reference it), protected (any model in the same project, the default), and public (any model in any project). Model groups let you scope private access to a named group rather than the full project — essential for large projects where you want to enforce module boundaries within a single codebase.
# platform/models/core/schema.yml
# Declaring public models with contracts and access levels
version: 2
groups:
- name: core_platform
owner:
name: Platform Team
email: platform@company.com
models:
- name: dim_customers
description: >
Conformed customer dimension — single authoritative source for customer
attributes across all downstream domains. Public interface.
access: public
group: core_platform
config:
contract:
enforced: true
columns:
- name: customer_id
data_type: bigint
description: Surrogate key — stable across source system changes
constraints:
- type: not_null
- type: primary_key
- name: email_hash
data_type: varchar
description: SHA-256 of email, lowercased — never expose raw email downstream
constraints:
- type: not_null
- name: customer_segment
data_type: varchar
description: "Values: enterprise, smb, consumer"
- name: first_order_at
data_type: timestamp
- name: lifetime_orders
data_type: integer
- name: lifetime_revenue_usd
data_type: numeric
- name: fct_orders
description: Daily order fact — immutable append-only grain at order level
access: public
group: core_platform
config:
contract:
enforced: true
columns:
- name: order_id
data_type: bigint
constraints:
- type: not_null
- type: primary_key
- name: customer_id
data_type: bigint
constraints:
- type: not_null
- type: foreign_key
to: ref('dim_customers')
to_columns: [customer_id]
- name: ordered_at
data_type: timestamp
constraints:
- type: not_null
- name: status
data_type: varchar
- name: gross_revenue_usd
data_type: numeric
- name: net_revenue_usd
data_type: numeric
- name: int_order_enriched
description: Intermediate enrichment — internal use only, do NOT reference cross-project
access: protected
group: core_platform# platform/dbt_project.yml
# Set default access per directory — override per-model where needed
name: platform
version: "1.0.0"
config-version: 2
profile: platform
model-paths: ["models"]
analysis-paths: ["analyses"]
test-paths: ["tests"]
seed-paths: ["seeds"]
macro-paths: ["macros"]
target-path: "target"
clean-targets: ["target", "dbt_packages"]
models:
platform:
staging:
+access: private # staging models are internal implementation details
+materialized: view
core:
+access: public # core models are the domain's public API
+materialized: table
+contract:
enforced: true
marts:
+access: protected # marts are intra-project use only
+materialized: tableNote
+contract: enforced: true at the directory level in dbt_project.yml ensures every model in that directory must declare its columns explicitly. This prevents developers from accidentally publishing a public model without a contract — the compile step fails until columns are declared.Cross-Project References with dependencies.yml
A downstream project declares its upstream dependencies in dependencies.yml. dbt resolves the referenced models by looking up their manifests — either from dbt Cloud artifacts or from a local path in monorepo mode. The downstream project does not rerun the upstream models; it reads the compiled relation identifier (schema + table name) from the upstream manifest and uses it directly. This means Finance can reference Platform's fct_orderswithout ever touching Platform's source code.
# finance/dependencies.yml
# Declare upstream project dependencies for cross-project ref() resolution
projects:
- name: platform
# In monorepo: point to the sibling directory
# In polyrepo / dbt Cloud: omit this and dbt resolves via the registry
- name: marketing
# If running locally with dbt-core (not dbt Cloud):
# 1. Run 'dbt compile' in the platform project first to generate target/manifest.json
# 2. Run 'dbt compile' in the marketing project
# 3. Then run dbt in finance — it picks up the sibling manifests automatically
# when projects share a common parent directory in monorepo layout.
# For CI in polyrepo, pass manifest paths explicitly:
# dbt deps && dbt compile --target prod \
# --project-dir finance \
# --manifest platform/target/manifest.json \
# --manifest marketing/target/manifest.json-- finance/models/reports/rpt_revenue_by_channel.sql
-- Cross-project ref() syntax: ref('project_name', 'model_name')
-- dbt validates that the referenced model is public and the contract is satisfied
with orders as (
-- Cross-project reference to platform project's public fct_orders model
select
order_id,
customer_id,
ordered_at::date as order_date,
gross_revenue_usd,
net_revenue_usd
from {{ ref('platform', 'fct_orders') }}
where ordered_at >= dateadd('day', -90, current_date)
),
attributions as (
-- Cross-project reference to marketing project's public model
select
order_id,
channel,
campaign_id,
attribution_weight
from {{ ref('marketing', 'fct_campaign_attribution') }}
),
customers as (
select
customer_id,
customer_segment
from {{ ref('platform', 'dim_customers') }}
),
revenue_by_channel as (
select
o.order_date,
coalesce(a.channel, 'direct') as acquisition_channel,
c.customer_segment,
sum(o.gross_revenue_usd * coalesce(a.attribution_weight, 1.0)) as attributed_gross_revenue,
sum(o.net_revenue_usd * coalesce(a.attribution_weight, 1.0)) as attributed_net_revenue,
count(distinct o.order_id) as order_count,
count(distinct o.customer_id) as unique_customers
from orders o
left join attributions a using (order_id)
left join customers c using (customer_id)
group by 1, 2, 3
)
select * from revenue_by_channelModel Contracts — Schema Enforcement at the Public Boundary
A model contract is a list of column names and data types declared in YAML that dbt validates at compile time and, when supported by the adapter, enforces at the database level using CREATE TABLE ... CONSTRAINT DDL. When contract.enforced: true, dbt will refuse to compile a model run if the SQL's output columns do not match the declared contract — wrong column names, extra columns, or type mismatches all fail at dbt compile before any SQL runs. This prevents a developer from silently removing a column that downstream consumers depend on.
Contracts integrate naturally with dbt's incremental models and CI/CD testing patterns — you declare the contract alongside your existing schema tests, and dbt treats a contract violation as a compile error, not a test failure. This distinction matters: test failures are caught at runtime after SQL has run; contract violations are caught before any SQL executes.
# platform/models/core/fct_orders.sql
# Contract-enforced model — the SELECT must produce exactly these columns in order
{{
config(
materialized = 'incremental',
unique_key = 'order_id',
incremental_strategy = 'merge',
on_schema_change = 'fail', -- refuse to run if schema drifts vs. contract
contract = {'enforced': true}
)
}}
-- Contract requires: order_id, customer_id, ordered_at, status,
-- gross_revenue_usd, net_revenue_usd
-- Column order and types must match schema.yml declarations exactly
with source as (
select * from {{ ref('stg_orders') }}
{% if is_incremental() %}
where ordered_at > (select max(ordered_at) from {{ this }})
{% endif %}
),
enriched as (
select
order_id::bigint as order_id,
customer_id::bigint as customer_id,
ordered_at::timestamp as ordered_at,
status::varchar as status,
(line_items_total + shipping_fee)::numeric as gross_revenue_usd,
(line_items_total + shipping_fee - discount_total)::numeric as net_revenue_usd
from source
where order_id is not null
)
select * from enriched# What happens when a contract is violated
# Developer accidentally drops net_revenue_usd from the SELECT:
# $ dbt compile --select fct_orders
#
# Error:
# Compilation Error in model fct_orders (models/core/fct_orders.sql)
# Contract violation in model fct_orders:
# Column 'net_revenue_usd' is required by the contract but was not found
# in the model's columns.
# Declared in: platform/models/core/schema.yml
# Model SQL: models/core/fct_orders.sql
#
# This error happens before any SQL runs — the model is never materialized.
# Developer changes gross_revenue_usd from numeric to float:
# Error:
# Contract violation in model fct_orders:
# Column 'gross_revenue_usd' has data type 'double precision' in the model
# but 'numeric' in the contract.
#
# Fix: update the CAST in the SQL to match the declared contract type.
# Do NOT change the contract to match the wrong SQL — the contract is the
# authoritative definition of the public interface.Model Versioning — Safe Evolution of Public Interfaces
When a public model's contract needs a breaking change — removing a column, changing a type, renaming a field — dbt Mesh provides model versioning. You create a new version (e.g., v2) while keeping the old version active. Downstream consumers migrate at their own pace. When all consumers have migrated, you deprecate and eventually remove v1. This is the same semantic versioning discipline applied to APIs, now available natively in dbt. Combined with dbt macros and custom tests, versioned models can carry automated deprecation warnings into every consumer's CI run.
# platform/models/core/schema.yml — declaring model versions
models:
- name: fct_orders
latest_version: 2
deprecation_date: "2026-09-01" # v1 deprecation deadline — CI warns consumers after this date
versions:
- v: 1
defined_in: fct_orders_v1 # points to fct_orders_v1.sql
description: "Original schema — deprecated, migrate to v2 before 2026-09-01"
- v: 2
defined_in: fct_orders_v2 # points to fct_orders_v2.sql (default when unversioned ref is used)
description: "v2 adds currency_code column and breaks gross/net into separate line-item fields"
# Downstream consumers explicitly pin to a version during migration:
# {{ ref('platform', 'fct_orders', v=1) }} -- old consumers, still on v1
# {{ ref('platform', 'fct_orders', v=2) }} -- migrated consumers
# {{ ref('platform', 'fct_orders') }} -- resolves to latest_version (v2)# platform/models/core/fct_orders_v1.sql
# Legacy version — kept alive while consumers migrate to v2
# Materialized in schema as fct_orders_v1_1 (dbt appends version suffix)
{{
config(
materialized = 'table',
contract = {'enforced': true}
)
}}
select
order_id::bigint as order_id,
customer_id::bigint as customer_id,
ordered_at::timestamp as ordered_at,
status::varchar as status,
gross_revenue_usd::numeric as gross_revenue_usd,
net_revenue_usd::numeric as net_revenue_usd
from {{ ref('stg_orders') }}
---
# platform/models/core/fct_orders_v2.sql
# v2: adds currency_code and splits revenue into product/shipping components
{{
config(
materialized = 'incremental',
unique_key = 'order_id',
incremental_strategy = 'merge',
contract = {'enforced': true}
)
}}
select
order_id::bigint as order_id,
customer_id::bigint as customer_id,
ordered_at::timestamp as ordered_at,
status::varchar as status,
currency_code::varchar as currency_code, -- NEW in v2
product_revenue_usd::numeric as product_revenue_usd, -- replaces gross_revenue_usd
shipping_revenue_usd::numeric as shipping_revenue_usd, -- NEW in v2
discount_total_usd::numeric as discount_total_usd, -- NEW in v2
net_revenue_usd::numeric as net_revenue_usd
from {{ ref('stg_orders') }}
{% if is_incremental() %}
where ordered_at > (select max(ordered_at) from {{ this }})
{% endif %}Note
deprecation_date passes, dbt emits a warning for every model that still references the deprecated version. This warning appears in CI output and in dbt Cloud job logs, creating natural pressure on consuming teams to migrate without requiring the platform team to actively chase them down.CI/CD Pipelines for Multi-Project dbt Mesh
The key CI/CD challenge in a dbt Mesh monorepo is avoiding unnecessary full-project runs when only a subset of models changed. The solution is slim CI: dbt's --select state:modified+ flag runs only modified models and their downstream dependents, deferring unchanged upstream models to the production environment. In a polyrepo, each project has its own CI pipeline, and cross-project consumers trigger re-validation when the upstream project publishes a new manifest artifact.
# .github/workflows/dbt-platform-ci.yml
# CI for the platform project — slim runs on PR, full run on merge to main
name: dbt Platform CI
on:
pull_request:
paths:
- "platform/**"
push:
branches: [main]
paths:
- "platform/**"
jobs:
dbt-ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: platform
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # needed for state:modified comparison
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dbt
run: pip install dbt-snowflake==1.8.0
- name: dbt deps
run: dbt deps
- name: Download production manifest (for state comparison)
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
run: |
aws s3 cp s3://company-dbt-artifacts/platform/prod/manifest.json \
./prod-manifest/manifest.json --create-dirs
- name: dbt compile (contract validation)
env:
DBT_SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
DBT_SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
DBT_SNOWFLAKE_ROLE: ${{ secrets.SNOWFLAKE_ROLE }}
DBT_SNOWFLAKE_WAREHOUSE: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
run: |
dbt compile --target ci \
--profiles-dir .
- name: dbt run (slim CI — modified models + downstream)
if: github.event_name == 'pull_request'
run: |
dbt run \
--target ci \
--select "state:modified+" \
--state ./prod-manifest \
--defer \
--profiles-dir .
- name: dbt test (modified models only)
if: github.event_name == 'pull_request'
run: |
dbt test \
--target ci \
--select "state:modified+" \
--state ./prod-manifest \
--defer \
--profiles-dir .
- name: dbt run (full — on merge to main)
if: github.event_name == 'push'
run: dbt run --target prod --profiles-dir .
- name: dbt test (full — on merge to main)
if: github.event_name == 'push'
run: dbt test --target prod --profiles-dir .
- name: Upload manifest artifact
if: github.event_name == 'push'
run: |
aws s3 cp ./target/manifest.json \
s3://company-dbt-artifacts/platform/prod/manifest.json# Cross-project consumer CI — finance project validates against platform's manifest
# .github/workflows/dbt-finance-ci.yml
name: dbt Finance CI
on:
pull_request:
paths: ["finance/**"]
# Triggered when platform publishes a new manifest (cross-project validation)
repository_dispatch:
types: [platform-manifest-updated]
jobs:
dbt-finance-ci:
runs-on: ubuntu-latest
defaults:
run:
working-directory: finance
steps:
- uses: actions/checkout@v4
- name: Install dbt
run: pip install dbt-snowflake==1.8.0
- name: dbt deps
run: dbt deps
- name: Download upstream manifests
run: |
aws s3 cp s3://company-dbt-artifacts/platform/prod/manifest.json \
../platform/target/manifest.json --create-dirs
aws s3 cp s3://company-dbt-artifacts/marketing/prod/manifest.json \
../marketing/target/manifest.json --create-dirs
- name: dbt compile (resolves cross-project refs against upstream manifests)
run: dbt compile --target ci --profiles-dir .
- name: dbt run (slim CI)
if: github.event_name == 'pull_request'
run: |
dbt run --target ci \
--select "state:modified+" \
--state ./prod-manifest \
--defer \
--profiles-dir .
- name: dbt test
run: dbt test --target ci --select "state:modified+" --defer --profiles-dir .Governance — Ownership, Metrics Layer, and Access Policies
dbt Mesh adds a governance layer that was previously informal. Model groups with named owners mean you can trace every public model back to a team. The dbt MetricFlow semantic layer integrates with dbt Mesh: metrics defined in one project (e.g., revenue in Platform) are reusable across downstream projects without copy-pasting metric logic. Access policies prevent unauthorized cross-project references at compile time — a consuming team cannot quietly reference a protected model; dbt emits an error.
# platform/models/core/metrics.yml
# MetricFlow metric definitions on public models — reusable across projects
version: 2
metrics:
- name: gross_revenue
label: Gross Revenue (USD)
description: Total gross revenue from all completed orders
type: simple
type_params:
measure:
name: gross_revenue_usd
agg: sum
filter: |
{{ Dimension('fct_orders__status') }} = 'completed'
- name: order_count
label: Order Count
type: simple
type_params:
measure:
name: order_id
agg: count_distinct
- name: revenue_per_order
label: Revenue per Order
type: derived
type_params:
expr: "gross_revenue / order_count"
metrics:
- name: gross_revenue
- name: order_count
semantic_models:
- name: fct_orders
description: Order-grain semantic model for MetricFlow
model: ref('fct_orders')
entities:
- name: order_id
type: primary
- name: customer_id
type: foreign
entity: customer
dimensions:
- name: ordered_at
type: time
type_params:
time_granularity: day
- name: status
type: categorical
measures:
- name: gross_revenue_usd
agg: sum
expr: gross_revenue_usd
- name: order_id
agg: count_distinct
expr: order_id# Governance enforcement — what happens when access policies are violated
# finance/models/reports/rpt_internal_attempt.sql
# Attempting to reference a protected model from a different project:
#
# select * from {{ ref('platform', 'int_order_enriched') }}
#
# $ dbt compile --select rpt_internal_attempt
#
# Error:
# Compilation Error in model rpt_internal_attempt
# Node model.platform.int_order_enriched has access 'protected' and
# is not accessible to package 'finance'.
#
# To fix this: either use a public model, or ask the platform team
# to create a public model that exposes the data you need.
#
# This prevents stealth dependencies on implementation internals
# and forces the platform team to make explicit what is a public API.
# Group-level private access (intra-project enforcement):
# If int_order_enriched is in group 'core_platform' with access 'private',
# even other models in the platform project outside that group cannot ref it.Local Development and Integration Testing
Working with cross-project references locally requires that upstream project manifests are available. In a monorepo, the standard workflow is to run dbt compile in the upstream project first, then work in the downstream project. A Makefile or shell script per project standardizes this for new team members. For integration tests that span project boundaries, dbt's --defer flag lets you run only the changed downstream models, deferring unchanged upstream models to the production environment rather than re-running the full upstream project locally.
# Makefile for monorepo — standardize multi-project dev workflows
.PHONY: compile-all run-platform run-marketing run-finance test-all
# Compile upstream projects first (generates manifests for cross-project resolution)
compile-platform:
cd platform && dbt deps && dbt compile --target dev
compile-marketing: compile-platform
cd marketing && dbt deps && dbt compile --target dev
compile-all: compile-platform compile-marketing
cd finance && dbt deps && dbt compile --target dev
# Run individual projects (assumes manifests already compiled)
run-platform:
cd platform && dbt run --target dev --select "core"
run-finance-slim:
# Defer unchanged upstream to prod; only run modified finance models
cd finance && dbt run \
--target dev \
--select "state:modified+" \
--state ./prod-manifest \
--defer
test-all:
cd platform && dbt test --target dev
cd marketing && dbt test --target dev
cd finance && dbt test --target dev
# Validate all cross-project contracts are satisfied
validate-contracts:
cd platform && dbt compile --target dev
cd marketing && dbt compile --target dev
cd finance && dbt compile --target dev
@echo "All contracts validated successfully"
# Generate dbt docs across all projects
docs:
cd platform && dbt docs generate --target dev
cd marketing && dbt docs generate --target dev
cd finance && dbt docs generate --target devProduction Checklist
Start with access levels before adding cross-project refs. Audit your current monolith and mark every model as public, protected, or private before splitting into projects. The access level annotation alone adds governance value without any project restructuring.
Add contracts to all public models on day one of going multi-project. A public model without a contract can have its columns silently changed by a developer, breaking downstream consumers with no compile-time warning. Enforce contracts before the first consumer references a model cross-project.
Store compiled manifests in S3 or GCS as CI artifacts. Downstream project CI pipelines need upstream manifests to resolve cross-project refs. Use consistent S3 paths per project and environment (e.g., s3://artifacts/platform/prod/manifest.json) and update them on every successful main-branch deploy.
Use --defer with slim CI for all downstream project PR pipelines. Without deferral, Finance CI would need to run the full Platform project on every PR — defeating the purpose of splitting. With --defer, only the changed Finance models run; unchanged upstream tables are read from production.
Pin dbt-core and adapter versions across all projects in the mesh. A version mismatch between projects (e.g., Platform on dbt 1.7, Finance on dbt 1.8) can cause manifest schema incompatibilities when Finance tries to resolve Platform's cross-project refs.
Set deprecation_date on all versioned model v1 instances before releasing v2. Without a deadline, consumers never feel urgency to migrate. The deprecation date triggers CI warnings starting on that date, creating automatic pressure without manual follow-up from the platform team.
Use model groups to enforce intra-project modularity before splitting projects. If your platform project has staging, intermediate, and mart layers, assign them to groups with appropriate access levels. This prevents tight coupling within the project that would make future splits painful.
Run dbt source freshness and dbt test as separate CI steps with separate failure policies. Source freshness failures should alert on-call but not block deploys; contract violations should always block deploys. Mixing them in a single dbt test run obscures which failure type occurred.
Create a shared dbt packages repo for macros used across projects. Cross-project ref() solves model sharing; it does not solve macro sharing. Macros (generic tests, utility functions) should be extracted into a shared dbt package published to a private GitHub Packages or Artifactory registry and listed in each project's packages.yml.
Document the public model API in the model's description field, not just in comments. dbt docs generates a searchable catalog from description fields. Downstream teams use dbt docs as the primary discovery interface — a model with no description is invisible to consumers who haven't read the source code.
Running a monolithic dbt project shared by five teams where every column rename breaks downstream consumers silently, CI takes 45 minutes because unrelated models always run, or multiple domains need to share stable table interfaces without merge conflicts?
We design and implement dbt Mesh architectures — from monolith-to-mesh decomposition planning and model access level auditing to model contract definition for all public interfaces, cross-project ref() dependency wiring with dependencies.yml, model versioning with deprecation timelines, slim CI pipeline setup with state:modified+ and --defer in GitHub Actions, S3 manifest artifact storage for polyrepo cross-project compilation, MetricFlow semantic layer configuration on shared public models, shared dbt macro package extraction and private registry setup, and production governance including model group ownership, access policy enforcement, and dbt docs catalog integration. Let’s talk.
Let's Talk