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 Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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-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. Each interface's block is on its own page, for example db.
Rules that hold for every file:
schema: 1is required. An unknown version fails withMAIN-CONFIG-0001and names the versions supported.- Secrets are references, never values:
secret:NAME, matchingsecret:[A-Z0-9_]+. A literal in a field marked secret fails withMAIN-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):
email: { key: "re_live_123" } # a literal where a secret reference belongs$ 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 2Overlays: 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.
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.yamlpayments: { 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. |
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:
$ 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:3mainsdk 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 shows the exception while it is open.
schema: 1
baseline: main27
providers: { email: [resend, ses] } # approved drivers
network: { allow: ["*.pinecart.dev"], default: deny }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-30Without 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.
schema: 1
packages: auto # from pnpm, npm or Yarn workspaces, Turborepo or Nx
policies: [policies/]
providers: { email: resend }
overlays: { order: [market, segment, env] }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.
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.
Next
Was this page helpful?