Upcoming Workshop: Citizen Developer Agentic Software Factory on AWS
Register Here!Massdriver Platform Update — September 2026
Resource types become versioned OCI artifacts, connections carry version ranges, and deployment approvals get separation of duty.
OCI-Based Resource Types + Resource Type Semantic Versioning.
Bundles got semantic versioning, release channels, and per-environment pinning last November. That fixed how a single component moves through your environments. It left one thing unversioned: the shape of the data those components pass to each other.
If a bundle publishes an aws-vpc and three other bundles consume it, the field list in that payload is an API. Adding a required field to it is a breaking change to every consumer, and until now the platform had no way to say so. You changed the schema, and the next deploy of an unrelated bundle either worked or didn't.
This release versions that contract between infrastructure and/or application components. Resource types become real published artifacts with semantic versions, connections between components declare which versions they accept, and the platform resolves the match at deploy time instead of at publish time.
Let's get into it.
📦 Resource types are versioned OCI artifacts
"Artifact definitions" are now "resource types", and they publish like bundles do.
A resource type is authored as a massdriver.yaml, published to a repository in your organization's catalog, and pinned by version. Publishing is immutable: once a version exists, it can't be overwritten, so anything pinned to it keeps getting what it got the first time.
mass resource-type create aws-vpc
mass resource-type publish ./aws-vpc
mass resource-type pull aws-vpc@2.1.0
The massdriver.yaml itself carries the version, the JSON schema, and the files the console renders alongside it:
name: postgres-authentication
version: 2.1.0
label: PostgreSQL Authentication
icon: https://example.com/postgres.svg
ui:
connectionOrientation: environmentDefault
environmentDefaultGroup: credentials
instructions:
- label: CLI Setup
path: ./instructions/cli.md
exports:
- downloadButtonText: Download .pgpass
fileFormat: yaml
templatePath: ./exports/pgpass.liquid
templateLang: liquid
schema:
$schema: http://json-schema.org/draft-07/schema
title: PostgreSQL Authentication
type: object
required:
- username
- password
properties:
username:
type: string
password:
$md.sensitive: true
type: string
instructions and exports point at files by path rather than inlining their content. A path that doesn't exist, or that resolves outside the directory, fails the publish instead of shipping an incomplete artifact.
Every resource type now has:
- A semantic version and release channels, the same model bundles use
- A repository in the OCI catalog, with the same access grants and attribute filters as bundle repos
- A published artifact containing the
massdriver.yaml, readme, changelog, icon, and the instruction and export templates it references
If you have existing resource types, they were migrated in place and keep working. mass resource-type convert <schema-file> turns a raw JSON or YAML schema into the new format, pulling inlined instruction and export content out into referenced files.
Publishing a raw JSON schema still works and now prints a deprecation warning. Raw schemas have no version of their own, so they're stored as an unversioned 0.0.0 document and can't participate in versioning or be pulled back down. That path will be removed in a future release.
🔗 Connections carry version ranges
A connection between two components now records which bundle versions it accepts on each end.
Every connection in a project blueprint carries a version range for the producing component and the consuming component. The connection is only wired up in an environment where the bundle versions actually deployed there fall inside both ranges.
That makes a staged rollout safe across environments sharing one blueprint. Staging can run the aws-vpc bundle at 2.0.0 while production still runs 1.4.0, and each environment wires up the connection that matches the versions it's actually running.
If you use GitHub Actions, you have done the manual version of this. Two actions where one's output feeds the other's input, wired up in every workflow that uses them, and then an upgrade renames the input. The wiring lives in each repo, so the rename is an edit in each repo, and you either walk them by hand or write something to walk them for you. Every one of those repos is its own little cutover, and until you finish, some of them are broken.
The wiring here lives in the project, not in the environment. You draw the new route once and every environment can read it, whenever it happens to be running versions that fall in range.
Day 2 just got a little bit easier.
An instance's Dependencies tab shows this directly. Each slot lists the resource type and the version it expects, alongside the instance currently fulfilling it, so you can see which contract a slot is asking for and which one it's getting.
Versioned links
A link carries a version range at each end, so one blueprint holds the wiring for more than one version of the same pair of components. Each environment uses the link that matches the versions it runs.
Three versions are in play, and a rename only moves one of them:
- The resource type on the line.
vpcpublishesaws-vpc@1.0.0anddbdeclares the range it accepts. - The bundle version of each component, which is what an environment runs.
- The range at each end of the link, written as
~1 → ~1or~1 → ~2.
Say db 2.0.0 renames the dependency it takes from vpc, so network becomes private_network. The contract is untouched. db 2.x still accepts aws-vpc, vpc still publishes the same aws-vpc@1.0.0, and only the slot the resource arrives on has a new name.
Upgrade dev first. Nothing feeds the new private_network slot, so it shows as an unfulfilled dependency and the deploy blocks until you draw the line from vpc.network into it. That line is stamped ~1 → ~2, and it belongs to the project rather than to dev.
That is the whole of the work. Nothing travels from one environment to the next. The moment the ~1 → ~2 line exists, every environment can read it, and an environment reads it when it runs db 2.x.
So each environment upgrades on its own schedule. Set staging's db version to 2.0.0 and its next deployment finds the ~1 → ~2 line already drawn and connects private_network. You draw nothing. Production is not involved and does not move until you change its version too, whenever that happens to be. Roll an environment back to db 1.x and it reads the ~1 → ~1 line again.
Two links on the same destination field have to cover ranges that no single environment can run at the same time. A link whose source range and destination range both overlap an existing link on that field is rejected. Links pointing at different destination fields never conflict.
🎯 Version ranges resolve at deploy time
A bundle declares the range it accepts. The platform picks the version when you deploy.
resources and dependencies replace artifacts and connections in a bundle's massdriver.yaml. Each entry names one resource type and the versions it accepts, as name@version:
dependencies:
network:
resource_type: aws-vpc@~1.2
required: true
database:
resource_type: postgres-authentication@2.1.0
required: false
resources:
api:
resource_type: aws-ecs-service@~2
required: true
dependencies are what the bundle consumes; resources are what it produces for other bundles to consume. required on a dependency means it must be connected before the bundle can deploy. On a resource it means the bundle always creates it.
The version half accepts:
| Form | Resolves to |
|---|---|
aws-vpc@1.2.3 | exactly that version |
aws-vpc@~1.2 | the newest 1.2.x |
aws-vpc@~1 | the newest 1.x |
aws-vpc@latest | the newest stable release |
aws-vpc@latest+dev | the newest release including dev builds |
The range is resolved against what's available in the target environment at deploy time rather than frozen at publish. An environment can hold one default per version, so two bundles asking for different ranges each draw the version they asked for, and a bundle picks up a compatible newer version of a dependency without republishing.
Remote references are checked against the range when you assign them, so an out-of-range or wrong-type pick is rejected up front instead of failing mid-deploy.
The old artifacts and connections blocks still work and now warn. The two forms of a slot remain mutually exclusive, so migrate a bundle one block at a time rather than mixing them.
✅ Separation of duty on deployment approvals
Turn it on for an environment and the person who proposed a change cannot be the person who approves it.
A new environment-level setting, alongside decommission protection. With it on, the account or service account that proposed a deployment cannot approve it. Proposers can still withdraw their own proposals, so the control adds a second pair of eyes without stranding a change nobody wants.
The setting is available in the environment configuration page. Click the ⚙️ sprocket icon on any environment canvas.
🏷️ Organization naming conventions
Define one Liquid template for resource names and every instance inherits it.
Set a naming convention on your organization and it becomes the name prefix for every instance deployed under it.
Naming conventions use liquid syntax and the atoms below. The naming convention controls the var.md_metadata.name_prefix and the instance's name in the API and UI. Naming conventions only apply to resources created from the time it was set to prevent accidental deletion of any resources that are using the name_prefix field in IaC to set a name or ID in AWS, etc that is not mutable. We'll be adding additional functionality to bulk change, and one off change names.
Today it's set through the API, on updateOrganizationSettings, but will be available on the organization settings page in the Sept 30th release.
Example:
{{project.id}}-{{environment.local_id}}-{{instance.local_id}}
Templates are built from a fixed vocabulary:
| Atom | Value |
|---|---|
org.id | organization identifier |
project.id, project.name | the project |
environment.id, environment.local_id, environment.name | the environment |
instance.id, instance.local_id | the instance |
component.name, component.suffix | the component in the blueprint |
attrs.<key> | any custom attribute your organization has declared |
Templates are checked when you save rather than at deploy, so a broken one never reaches your infrastructure. A template has to produce a unique name: include {{instance.id}}, or all three of {{project.id}}, {{environment.id}} (or {{environment.local_id}}), and {{instance.local_id}}. Referencing an atom outside the table, or an attribute your organization hasn't declared, is rejected with the offending name in the error. Liquid tags aren't allowed, only atoms, and the template caps at 255 characters.
A name is fixed at an instance's first deploy and doesn't change afterward, so adopting a convention affects what you create next and leaves running infrastructure alone.
⚙️ Organization settings
A settings surface on the organization, holding default bundle access and the naming convention above.
defaultBundleAccess set to ALL_PROJECTS automatically grants every new bundle repository to all projects in the organization, rather than requiring a grant per project. It defaults to off and applies to bundle repositories.
Managing it requires the new organization:manageSettings action, covered by the organization:manage umbrella. Members are now on their own tab in organization settings.
👥 Seats count active memberships
Seat usage now means active organization memberships, and SCIM is held to the limit.
A seat is an active member or a pending invitation. A member your IdP pushed into no group still holds a seat. A member it deactivated releases one.
Once you're at your limit, SCIM stops creating and reactivating users, and organization owners are notified. Re-provisioning someone who already has a seat, deactivating, and deleting all continue to work. Current usage is available as billing.seatsUsed, so you can see where you stand against your entitlement before provisioning starts getting turned away.
Massdriver doesn't decide a seat is free on its own. It follows the active flag your identity provider sends on the SCIM user. Which admin actions set that flag differs by provider: in Okta it's unassigning the user from the application, removing them from an assigned group, or deactivating them; in Entra it's unassignment, falling out of a scoping filter, being blocked from sign-in, or deletion. Deactivation and seats covers each provider, including how to stop Entra from deprovisioning users who go out of scope.
📈 Organization dashboard
A view of what's happening across the whole organization, not one project at a time.
The organization root is now a dashboard instead of a redirect to the projects list. Three tabs:
- Overview with tiles for failed, awaiting-approval, and not-yet-deployed instances, plus cards for the latest deployments, pending approvals, and failed instances
- Instances, filterable across every project, with a deployments drawer per instance
- Deployments, org-wide
The dashboard is for reading, not acting. Approving, rejecting, and deploying stay on the instance panel. Deployment details and logs open in place, so you can inspect a failure without leaving the page.
🔎 Filtering and search across lists
Filtering now works the same way everywhere instead of differing list by list:
- Repositories and resources support sorting, search, and attribute filters
- Repositories and resources filter by creation date with calendar ranges
- HTTPS property values render as live links in the instance properties table
- Dev versions display as
v<semver>-devwith the full version on hover - The instance Dependencies and Resources tabs are rebuilt as row lists, showing the resource type and version each slot is bound to
- The repo details header links to the source code for the version you have selected
🔑 Access tokens
Token expiry is now a dropdown of presets, one hour through one year, with a custom option in minutes, instead of a raw number of minutes. Personal access tokens are capped at one year.
On the API, personal access tokens have their own mutation. createPersonalAccessToken defaults its scopes to ["*"], and the deprecated createAccessToken has been removed from V2.
💰 AWS Cost and Usage Reports
CUR ingestion now supports Parquet reports in addition to the existing format.
🛠️ CLI 2.3.0
Beyond the resource-type commands above:
mass instance deploygainsplanandpropose, plus deploymentapproveandrejectcompare,clone,rollback, and remote-reference commandsmass env delete- Pagination across list commands, so large orgs get complete results
🌍 Terraform Provider 2.2.0
separation_of_dutyanddecommission_protectiononmassdriver_environment- API key and deployment token auth work side by side. Each API surface authenticates with the credential it requires, so an API key set explicitly in the provider block is no longer shadowed by a deployment token in the environment
resource_typeresolves at plan time frommassdriver.yaml, so bumping a resource type version surfaces in the planattributesis optional on projects, environments, components, and OCI repositories- Resource and OCI repository sharing grants are manageable as resources
Two upgrade notes worth reading before you apply: decommission_protection and separation_of_duty are sent on every apply and default to false, so an environment protected out of band gets unprotected unless the setting is in your config. And unprefixed environment variables like bare TOKEN or URL are no longer honored. Use the MASSDRIVER_ forms.
🏠 Self-hosted
Point the in-app documentation links at your own docs site.
Installations that serve their own documentation can set the massdriver.docsUrl Helm value, and the sidebar's documentation link follows it. Leave it unset and it falls back to https://docs.massdriver.cloud.
massdriver:
docsUrl: "https://docs.internal.example.com"
Upgrading
| Component | Version |
|---|---|
| Helm chart | 0.2.1 |
| Massdriver | 2.5.0 |
| UI | 2.1.0 |
| CLI | 2.3.0 |
| Terraform provider | 2.2.0 |
Self-hosted installs pick up the platform and UI images through the chart. The CLI and provider version independently.
Two fields left the V2 GraphQL API in this release: the createAccessToken mutation, replaced by createPersonalAccessToken and its per-subject siblings, and the unfiltered bundles query, replaced by the repo-scoped bundle query. The version-blind resourceTypes list query is deprecated and will follow. If you built against any of them directly, move before you upgrade.
Deprecations
$ref in the params schema.
A $ref in the params schema was previously able to resolve against external schemas. That worked because of a quirk in how schemas were loaded, and it was never part of the JSON Schema spec. The API changes that added versioned resource types closed that quirk, so $ref in params no longer resolves. If you use one, replace it with the schema it pointed to.
params:
properties:
region:
$ref: "https://some.url/schema.json
Becomes:
params:
properties:
region:
title: Region
type: string
enum:
- "us-east-1"
- "us-west-2"
connections and artifacts blocks in massdriver.yaml.
The connections block is now dependencies and the artifacts block is now resources. Both old blocks keep working for backwards compatibility, but they cannot express versioned resource types. Moving to dependencies and resources gets you version ranges and a flatter format.
connections:
required:
- network
- database
properties:
network:
$ref: aws-vpc
database:
$ref: postgres
artifacts:
required:
- api
properties:
api:
$ref: api-endpoint
Becomes:
dependencies:
network:
resource_type: aws-vpc@~1
required: true
database:
resource_type: postgres@~2.3
required: true
resources:
api:
resource_type: api-endpoint@1.2.3
required: true
The full schema is in the bundle YAML spec.
Closing
Versioning a bundle told you what code was running. Versioning the contract between bundles tells you whether two components can still talk to each other, and lets you answer that before a deploy rather than during one. That's the same proactive guardrail model we apply to inputs and policies, applied to the connections that hold an architecture together.
