MainSDK
Search⌘K
Docs

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.

Planned for 0.1

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.
Stability levels
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

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: 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):

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
main.in.yaml
payments: { driver: razorpay, methods: [upi, cards] }
auth:     { methods: [whatsapp, passkeys] }
data:     { residency: in }
i18n:     { locales: [en-IN, hi-IN, ta-IN] }
RuleHow it works
DimensionsMarket, segment, environment, target and locale. You can add your own, such as plan.
OrderWritten in overlays.order, never guessed. init writes the default [market, segment, env].
When it resolvesEnvironment 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.
MergeDeep merge by key. Lists replace, they never append, so the result stays readable.
LocalesFall back along a chain: hi-IN, then hi, then en-IN, then en. Templates follow the same chain.
RegionsResidency 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]
Diagram

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:

ChangeCounts as
Removing a host from network.allowTightening
Adding a host, lowering the baseline, adding a region, disabling a guaranteeWeakening

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.

main.org.yaml
schema: 1
baseline: main27
providers: { email: [resend, ses] }      # approved drivers
network: { allow: ["*.pinecart.dev"], default: deny }
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.

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] }

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.

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.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet