---
title: "The main.yaml file"
description: "One file declares what your app is, needs and may do. Overlays hold what differs by market, segment or environment; org files set defaults."
section: "concepts"
status: "Planned for 0.1"
spec: "main-yaml"
canonical: "https://mainsdk.dev/docs/concepts/main-yaml"
updated: "2026-10-28"
---

# The main.yaml file

> One file declares what your app is, needs and may do. Overlays hold what differs by market, segment or environment; org files set defaults.

Status: Planned for 0.1 · Canonical: https://mainsdk.dev/docs/concepts/main-yaml · Updated: 2026-10-28

`main.yaml` is the file that declares your app, like `AndroidManifest.xml` or `Info.plist` on a phone. It declares what the app is, which interfaces and drivers it uses, who it may talk to and which baseline it meets. MainSDK makes the code match it, and `mainsdk check` keeps it matching.

The model is the one Terraform and Kubernetes made familiar: desired state, a plan, then apply. You edit `main.yaml` (or ask your agent to), run `mainsdk sync`, read the plan, and merge the pull request it opens.

## A complete main.yaml

```yaml title="main.yaml"
# yaml-language-server: $schema=https://mainsdk.dev/schema/main.yaml/1.json
schema: 1
project: prj_01JC8Q4W7ZK3M5N8P2R6T9V0XY   # UUIDv7, an identifier, not a secret
baseline: main27
users: { audience: business, regions: [eu, us] }
standards: [soc2, gdpr]
targets:
  web: { framework: nextjs }
  api: { runtime: node }
auth:
  driver: github
  methods: [passkeys]
ai:
  driver: bedrock
  region: us-east-1
db:
  driver: postgres
  regions: { us: secret:DATABASE_URL_US, eu: secret:DATABASE_URL_EU }
  fields:
    user.email: { label: personal }
files:
  driver: getqr
  key: secret:GETQR_API_KEY
policy:
  engine: cedar
  placement: server
network:
  allow: ["*.pinecart.dev"]
  default: deny
overlays: { order: [market, segment, env] }
exceptions: []
```

Every key and sub-key is in the [main.yaml keys](https://mainsdk.dev/docs/reference/main-yaml). Each interface's block is on its own page, for example [db](https://mainsdk.dev/docs/interfaces/db).

Rules that hold for every file:

- `schema: 1` is required. An unknown version fails with `MAIN-CONFIG-0001` and names the versions supported.
- Secrets are references, never values: `secret:NAME`, matching `secret:[A-Z0-9_]+`. A literal in a field marked secret fails with `MAIN-CONFIG-0004`.
- The project ID names the project. It is safe to commit and never works as a credential.

Your editor gets autocomplete and inline errors from the published JSON Schema, and every validation error carries a `MAIN-CONFIG-` code, the file, line and column, and a fix. A configuration error exits with code 2, not 1 (see [exit codes](https://mainsdk.dev/docs/concepts/the-check#exit-codes)):

```yaml title="main.yaml (line 9)"
email: { key: "re_live_123" }      # a literal where a secret reference belongs
```

```text
$ npx mainsdk check
mainsdk check · pinecart · env dev · Main 27
✗ config    1 finding
  MAIN-CONFIG-0004  critical  main.yaml:9:15
  fix: use secret:RESEND_API_KEY
configuration error · exit 2
```

## Overlays: only what differs

Android picks `values-hi-rIN` over `values` for a phone in India. MainSDK does the same for configuration. `main.yaml` is the base; small overlay files hold only the differences, and the most specific match wins.

```text
main.yaml                        base
main.in.yaml                     market: India
main.eu.yaml                     market: EU
main.segment.enterprise.yaml     customer segment
main.env.prod.yaml               environment
main.target.web.yaml             target
locales/en.yaml, locales/hi-IN.yaml
```

```yaml title="main.in.yaml"
payments: { driver: razorpay, methods: [upi, cards] }
auth:     { methods: [whatsapp, passkeys] }
data:     { residency: in }
i18n:     { locales: [en-IN, hi-IN, ta-IN] }
```

| Rule | How it works |
| --- | --- |
| Dimensions | Market, segment, environment, target and locale. You can add your own, such as `plan`. |
| Order | Written in `overlays.order`, never guessed. `init` writes the default `[market, segment, env]`. |
| When it resolves | Environment and target at build time. Market, segment and locale per request, from the user's and organisation's context, so one deployment serves every market. |
| Merge | Deep merge by key. Lists replace, they never append, so the result stays readable. |
| Locales | Fall back along a chain: `hi-IN`, then `hi`, then `en-IN`, then `en`. Templates follow the same chain. |
| Regions | Residency values are limited to `us`, `eu` and `in`. |

```mermaid
flowchart LR
  O[main.org.yaml] --> W[main.workspace.yaml] --> B[main.yaml] --> M[market] --> S[segment] --> E[env]
```

Overlays merge in the order you declare. With base `payments.driver: stripe`, `main.in.yaml` setting `payments: { driver: razorpay, methods: [upi, cards] }` and `main.segment.enterprise.yaml` setting `payments.methods: [cards]`, an enterprise customer in India resolves to `razorpay` with methods exactly `[cards]`.

See where every value came from:

```text
$ npx mainsdk explain config --market in --segment enterprise
payments.driver    razorpay       main.in.yaml:1
payments.methods   [cards]        main.segment.enterprise.yaml:1
auth.driver        github         main.yaml:11
data.residency     in             main.in.yaml:3
```

`mainsdk check` resolves and tests every overlay combination in use before merge.

## Inheritance and exceptions: tighten, never weaken

Platform teams set defaults once. Files are inherited in this order: `main.org.yaml` (org), `main.workspace.yaml` (repo root), `main.yaml` (the app), then overlays.

Each layer may **tighten** its parent and may not **weaken** it:

| Change | Counts as |
| --- | --- |
| Removing a host from `network.allow` | Tightening |
| Adding a host, lowering the baseline, adding a region, disabling a guarantee | Weakening |

Weakening fails unless an exception names the rule, a reason, an owner and an expiry date. An expired exception fails like no exception. The same `exceptions:` list is where you accept a check finding: `rule` is then the check code, such as `MAIN-NET-0004`, and the [Main mark](https://mainsdk.dev/docs/concepts/main27) shows the exception while it is open.

```yaml title="main.org.yaml"
schema: 1
baseline: main27
providers: { email: [resend, ses] }      # approved drivers
network: { allow: ["*.pinecart.dev"], default: deny }
```

```yaml title="main.yaml"
network:
  allow: ["*.pinecart.dev", api.partner.pinecart.dev]
exceptions:
  - rule: net.default_deny
    reason: Partner VPN gateway until the API moves to pinecart.dev
    owner: "@pinecart/platform"
    expires: 2026-11-30
```

Without the exception, resolution fails with `MAIN-CONFIG-0010 weakens main.org.yaml: network.allow`.

Still open: how a repo outside the org's monorepo finds its `main.org.yaml` (a path, a git URL or a signed delivery), and whether an exception needs an `approver` as well as an `owner`.

## Workspaces

A repo with several apps keeps one `main.yaml` per app and an optional `main.workspace.yaml` at the root with shared policies, drivers and overlays. One `main.lock` per workspace keeps every app on the same contract versions. It is configuration only and adds no runtime code.

```yaml title="main.workspace.yaml"
schema: 1
packages: auto                 # from pnpm, npm or Yarn workspaces, Turborepo or Nx
policies: [policies/]
providers: { email: resend }
overlays: { order: [market, segment, env] }
```

> **Note:** The `main.workspace.yaml` format and its inheritance ship in phase 1. Workspace discovery and `check` or `sync` per package and on affected packages arrive in phase 2. See [Monorepos](https://mainsdk.dev/docs/guides/monorepos).

## main.lock

`mainsdk sync` resolves driver versions that satisfy every interface range and writes them with SHA-256 hashes to `main.lock`. Commit it. Builds are then reproducible.

```yaml title="main.lock"
lock: 1
interfaces:
  email: { range: "^1.2", resolved: "1.4.0" }
drivers:
  resend:
    version: 2.3.1
    implements: ["email@^1.2"]
    sha256: "9f2c…"
```

## The seal

`main.yaml` and its overlays are always part of the build hash that `mainsdk seal` signs, next to the roots under `seal.include`. At boot the app recomputes that hash and reports the result in `app.info().seal` (`match`, `mismatch`, `unsealed`, `off`, or `pending` until the check has run), so a changed `main.yaml` on the server shows up as `MAIN-SEAL-0001`. See [`mainsdk seal`](https://mainsdk.dev/docs/reference/cli#seal).

## Next

- [main.yaml keys](https://mainsdk.dev/docs/reference/main-yaml)
- [The check](https://mainsdk.dev/docs/concepts/the-check)
- [Monorepos](https://mainsdk.dev/docs/guides/monorepos)
