New Blog Post! The Citizen Developer
Read hereMassdriver Platform Update — September 2026
Resource types become versioned OCI artifacts, connections carry version ranges, and deployment approvals get separation of duty.
The contract between two bundles is the last thing in your platform without a version.
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. 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.
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.
🎯 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 form and the environments table, on the V2 API as separationOfDuty, and in the Terraform provider as separation_of_duty on massdriver_environment.
🏷️ 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. Today it's set through the API, on updateOrganizationSettings.
{{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 |
The default is {{instance.id}}-{{component.suffix}}.
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 MD_DOCS_URL, and the sidebar's documentation link follows it. Leave it unset and it falls back to https://docs.massdriver.cloud.
MD_DOCS_URL=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.
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.
