You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airavata.apache.org by di...@apache.org on 2026/09/03 00:37:24 UTC

(airavata) branch refactor-single-module updated: Refactoring slurm deployment data model

This is an automated email from the ASF dual-hosted git repository.

DImuthuUpe pushed a commit to branch refactor-single-module
in repository https://gitbox.apache.org/repos/asf/airavata.git


The following commit(s) were added to refs/heads/refactor-single-module by this push:
     new 8825a52da9 Refactoring slurm deployment data model
8825a52da9 is described below

commit 8825a52da9893d9daa40684e1c6afd2c57deb2c3
Author: DImuthuUpe <di...@gmail.com>
AuthorDate: Wed Sep 2 20:37:17 2026 -0400

    Refactoring slurm deployment data model
---
 api/application/dto/application.go                 |  51 +++---
 api/application/model/deployment.go                |  20 +--
 api/application/repository/repository.go           |   8 +-
 api/application/service/service.go                 |  69 ++++----
 api/process/dto/process.go                         |   9 +-
 api/process/service/launch.go                      |   5 -
 api/process/service/process.go                     |  20 +--
 docs/api.md                                        |  96 +++++------
 internal/app/app.go                                |   2 +-
 internal/db/foreignkey_test.go                     |   3 +-
 internal/db/migrate_test.go                        |  24 +--
 .../migrations/0003_deployment_default_naming.sql  |  29 ++++
 internal/server/server_test.go                     | 187 +++++++++++----------
 13 files changed, 252 insertions(+), 271 deletions(-)

diff --git a/api/application/dto/application.go b/api/application/dto/application.go
index 5ad054735a..4e2d478baa 100644
--- a/api/application/dto/application.go
+++ b/api/application/dto/application.go
@@ -276,17 +276,15 @@ func ToBatchJobConfigResponse(c *model.BatchJobConfig) *BatchJobConfigResponse {
 //
 // Java: org.apache.airavata.application.dto.deployment.BatchApplicationDeploymentRequestDto
 type BatchDeploymentRequest struct {
-	TemplateID      string                 `json:"templateId"`
-	SlurmClusterID  *string                `json:"slurmClusterId"`
-	SlurmRunSection string                 `json:"slurmRunSection"`
-	BatchJobConfig  *BatchJobConfigRequest `json:"batchJobConfig"`
+	TemplateID      string  `json:"templateId"`
+	SlurmClusterID  *string `json:"slurmClusterId"`
+	SlurmRunSection string  `json:"slurmRunSection"`
 
-	// DefaultSubmissionCredentialID names an SSH endpoint credential binding (the id
-	// returned by POST /api/v1/ssh-endpoint-credentials), not a bare SSH credential —
-	// a binding ties the submitting identity to both a host and an owner.
-	DefaultSubmissionCredentialID string `json:"defaultSubmissionCredentialId"`
-
-	Partition *string `json:"partition"`
+	// The resource request and partition a run of this deployment starts from. A run
+	// carries its own copy and may depart from both, so these are defaults rather than
+	// what any particular job is submitted with.
+	DefaultBatchJobConfig *BatchJobConfigRequest `json:"defaultBatchJobConfig"`
+	DefaultPartition      *string                `json:"defaultPartition"`
 }
 
 // Validate implements httpx.Validator.
@@ -294,11 +292,10 @@ func (r *BatchDeploymentRequest) Validate() []httpx.FieldError {
 	var c httpx.Constraints
 	c.NotBlank("templateId", "Template id cannot be blank", r.TemplateID)
 	c.NotBlank("slurmRunSection", "Slurm run section cannot be blank", r.SlurmRunSection)
-	c.NotBlank("defaultSubmissionCredentialId", "Default submission credential id cannot be blank", r.DefaultSubmissionCredentialID)
-	if r.BatchJobConfig == nil {
-		c.Add("batchJobConfig", "Batch job config cannot be null")
+	if r.DefaultBatchJobConfig == nil {
+		c.Add("defaultBatchJobConfig", "Default batch job config cannot be null")
 	} else {
-		c.Nested("batchJobConfig", r.BatchJobConfig)
+		c.Nested("defaultBatchJobConfig", r.DefaultBatchJobConfig)
 	}
 	return c.Fields()
 }
@@ -307,24 +304,22 @@ func (r *BatchDeploymentRequest) Validate() []httpx.FieldError {
 //
 // Java: org.apache.airavata.application.dto.deployment.BatchApplicationDeploymentResponseDto
 type BatchDeploymentResponse struct {
-	DeploymentID                  string                  `json:"deploymentId"`
-	TemplateID                    *string                 `json:"templateId"`
-	SlurmClusterID                *string                 `json:"slurmClusterId"`
-	SlurmRunSection               string                  `json:"slurmRunSection"`
-	BatchJobConfig                *BatchJobConfigResponse `json:"batchJobConfig"`
-	DefaultSubmissionCredentialID string                  `json:"defaultSubmissionCredentialId"`
-	Partition                     *string                 `json:"partition"`
+	DeploymentID          string                  `json:"deploymentId"`
+	TemplateID            *string                 `json:"templateId"`
+	SlurmClusterID        *string                 `json:"slurmClusterId"`
+	SlurmRunSection       string                  `json:"slurmRunSection"`
+	DefaultBatchJobConfig *BatchJobConfigResponse `json:"defaultBatchJobConfig"`
+	DefaultPartition      *string                 `json:"defaultPartition"`
 }
 
 func ToBatchDeploymentResponse(d *model.BatchDeployment) BatchDeploymentResponse {
 	return BatchDeploymentResponse{
-		DeploymentID:                  d.ID,
-		TemplateID:                    d.TemplateID,
-		SlurmClusterID:                d.ClusterID,
-		SlurmRunSection:               d.SlurmRunSection,
-		BatchJobConfig:                ToBatchJobConfigResponse(d.BatchJobConfig),
-		DefaultSubmissionCredentialID: d.DefaultSubmissionCredentialID,
-		Partition:                     d.Partition,
+		DeploymentID:          d.ID,
+		TemplateID:            d.TemplateID,
+		SlurmClusterID:        d.ClusterID,
+		SlurmRunSection:       d.SlurmRunSection,
+		DefaultBatchJobConfig: ToBatchJobConfigResponse(d.DefaultBatchJobConfig),
+		DefaultPartition:      d.DefaultPartition,
 	}
 }
 
diff --git a/api/application/model/deployment.go b/api/application/model/deployment.go
index 5bb3521fc6..120db6562d 100644
--- a/api/application/model/deployment.go
+++ b/api/application/model/deployment.go
@@ -5,7 +5,6 @@ import (
 	"gorm.io/gorm"
 
 	computemodel "github.com/apache/airavata/api/compute/model"
-	cred "github.com/apache/airavata/api/credentials/model"
 )
 
 // BatchJobConfig is a batch-scheduler resource request.
@@ -76,16 +75,13 @@ type BatchDeployment struct {
 	// Owned one-to-one. The foreign key lives on this side and is unique, so no two
 	// deployments can share a config. Because the key points outward, the orphaned
 	// config row is removed by AfterDelete rather than by a database cascade.
-	BatchJobConfigID string          `gorm:"column:batch_job_config_id;type:varchar(36);not null;uniqueIndex" json:"batchJobConfigId"`
-	BatchJobConfig   *BatchJobConfig `gorm:"references:ID;constraint:OnDelete:RESTRICT,OnUpdate:CASCADE" json:"batchJobConfig,omitempty"`
+	//
+	// It is the deployment's default: a run copies it into a config of its own and may
+	// depart from it, which is why the run's snapshot is what a job is submitted with.
+	DefaultBatchJobConfigID string          `gorm:"column:default_batch_job_config_id;type:varchar(36);not null;uniqueIndex" json:"defaultBatchJobConfigId"`
+	DefaultBatchJobConfig   *BatchJobConfig `gorm:"references:ID;constraint:OnDelete:RESTRICT,OnUpdate:CASCADE" json:"defaultBatchJobConfig,omitempty"`
 
-	// The default submission credential is an SSH endpoint credential binding rather
-	// than a bare SSH credential: it ties the submitting identity to a specific host
-	// and owner, the same association a job submitted under it will run as.
-	DefaultSubmissionCredentialID string                      `gorm:"column:default_submission_credential_id;type:varchar(36);not null;index" json:"defaultSubmissionCredentialId"`
-	DefaultSubmissionCredential   *cred.SSHEndpointCredential `gorm:"references:ID;constraint:OnDelete:RESTRICT,OnUpdate:CASCADE" json:"-"`
-
-	Partition *string `gorm:"column:partition;type:varchar(255)" json:"partition,omitempty"`
+	DefaultPartition *string `gorm:"column:default_partition;type:varchar(255)" json:"defaultPartition,omitempty"`
 }
 
 // TableName returns the table backing BatchDeployment.
@@ -103,9 +99,9 @@ func (d *BatchDeployment) BeforeCreate(*gorm.DB) error {
 // orphanRemoval: the foreign key points from the deployment to the config, so the
 // database cannot cascade in this direction and the cleanup has to be explicit.
 func (d *BatchDeployment) AfterDelete(tx *gorm.DB) error {
-	if d.BatchJobConfigID == "" {
+	if d.DefaultBatchJobConfigID == "" {
 		return nil
 	}
-	return tx.Where("batch_job_config_id = ?", d.BatchJobConfigID).
+	return tx.Where("batch_job_config_id = ?", d.DefaultBatchJobConfigID).
 		Delete(&BatchJobConfig{}).Error
 }
diff --git a/api/application/repository/repository.go b/api/application/repository/repository.go
index 07dcc4f632..f6af9bf7cf 100644
--- a/api/application/repository/repository.go
+++ b/api/application/repository/repository.go
@@ -110,14 +110,14 @@ func (r *BatchDeploymentRepository) WithTx(tx *gorm.DB) *BatchDeploymentReposito
 // FindAll returns every deployment with its owned batch job config.
 func (r *BatchDeploymentRepository) FindAll(ctx context.Context) ([]model.BatchDeployment, error) {
 	var out []model.BatchDeployment
-	err := r.db.WithContext(ctx).Preload("BatchJobConfig").Find(&out).Error
+	err := r.db.WithContext(ctx).Preload("DefaultBatchJobConfig").Find(&out).Error
 	return out, err
 }
 
 // FindByTemplateID returns every deployment of one template.
 func (r *BatchDeploymentRepository) FindByTemplateID(ctx context.Context, templateID string) ([]model.BatchDeployment, error) {
 	var out []model.BatchDeployment
-	err := r.db.WithContext(ctx).Preload("BatchJobConfig").
+	err := r.db.WithContext(ctx).Preload("DefaultBatchJobConfig").
 		Where("template_id = ?", templateID).Find(&out).Error
 	return out, err
 }
@@ -125,7 +125,7 @@ func (r *BatchDeploymentRepository) FindByTemplateID(ctx context.Context, templa
 // FindByID returns one deployment, or gorm.ErrRecordNotFound.
 func (r *BatchDeploymentRepository) FindByID(ctx context.Context, id string) (*model.BatchDeployment, error) {
 	var out model.BatchDeployment
-	err := r.db.WithContext(ctx).Preload("BatchJobConfig").
+	err := r.db.WithContext(ctx).Preload("DefaultBatchJobConfig").
 		First(&out, "deployment_id = ?", id).Error
 	if err != nil {
 		return nil, err
@@ -146,7 +146,7 @@ func (r *BatchDeploymentRepository) ExistsByTemplateID(ctx context.Context, temp
 // SaveConfig, so that an update mutates the existing config row rather than
 // replacing it and leaving an orphan.
 func (r *BatchDeploymentRepository) Save(ctx context.Context, d *model.BatchDeployment) error {
-	return r.db.WithContext(ctx).Omit("BatchJobConfig").Save(d).Error
+	return r.db.WithContext(ctx).Omit("DefaultBatchJobConfig").Save(d).Error
 }
 
 // SaveConfig inserts or updates an owned batch job config.
diff --git a/api/application/service/service.go b/api/application/service/service.go
index 73088e7380..83891f06f7 100644
--- a/api/application/service/service.go
+++ b/api/application/service/service.go
@@ -16,8 +16,6 @@ import (
 	model "github.com/apache/airavata/api/application/model"
 	"github.com/apache/airavata/api/application/repository"
 	computerepo "github.com/apache/airavata/api/compute/repository"
-	cred "github.com/apache/airavata/api/credentials/model"
-	credrepo "github.com/apache/airavata/api/credentials/repository"
 )
 
 func notFoundAs(err error, format string, args ...any) error {
@@ -166,11 +164,10 @@ func (s *TemplateService) Delete(ctx context.Context, id string) error {
 
 // BatchDeploymentService manages deployments: a template made runnable somewhere.
 type BatchDeploymentService struct {
-	db            *gorm.DB
-	deployments   *repository.BatchDeploymentRepository
-	templates     *repository.TemplateRepository
-	clusters      *computerepo.ClusterRepository
-	endpointCreds *credrepo.SSHEndpointCredentialRepository
+	db          *gorm.DB
+	deployments *repository.BatchDeploymentRepository
+	templates   *repository.TemplateRepository
+	clusters    *computerepo.ClusterRepository
 }
 
 // NewBatchDeploymentService returns a deployment service.
@@ -179,9 +176,8 @@ func NewBatchDeploymentService(
 	deployments *repository.BatchDeploymentRepository,
 	templates *repository.TemplateRepository,
 	clusters *computerepo.ClusterRepository,
-	endpointCreds *credrepo.SSHEndpointCredentialRepository,
 ) *BatchDeploymentService {
-	return &BatchDeploymentService{db: db, deployments: deployments, templates: templates, clusters: clusters, endpointCreds: endpointCreds}
+	return &BatchDeploymentService{db: db, deployments: deployments, templates: templates, clusters: clusters}
 }
 
 // List returns every deployment, or only those of templateID when it is non-empty.
@@ -225,25 +221,24 @@ func (s *BatchDeploymentService) Create(ctx context.Context, req *dto.BatchDeplo
 	err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
 		deployments := s.deployments.WithTx(tx)
 
-		template, cluster, endpointCred, err := s.resolveReferences(ctx, tx, req)
+		template, cluster, err := s.resolveReferences(ctx, tx, req)
 		if err != nil {
 			return err
 		}
 
 		config := &model.BatchJobConfig{}
-		dto.ApplyBatchJobConfigRequest(config, req.BatchJobConfig)
+		dto.ApplyBatchJobConfigRequest(config, req.DefaultBatchJobConfig)
 		if err := deployments.SaveConfig(ctx, config); err != nil {
 			return err
 		}
 
 		deployment := &model.BatchDeployment{
-			TemplateID:                    &template.ID,
-			ClusterID:                     cluster,
-			SlurmRunSection:               req.SlurmRunSection,
-			BatchJobConfigID:              config.ID,
-			BatchJobConfig:                config,
-			DefaultSubmissionCredentialID: endpointCred.ID,
-			Partition:                     req.Partition,
+			TemplateID:              &template.ID,
+			ClusterID:               cluster,
+			SlurmRunSection:         req.SlurmRunSection,
+			DefaultBatchJobConfigID: config.ID,
+			DefaultBatchJobConfig:   config,
+			DefaultPartition:        req.DefaultPartition,
 		}
 		if err := deployments.Save(ctx, deployment); err != nil {
 			return err
@@ -272,16 +267,16 @@ func (s *BatchDeploymentService) Update(ctx context.Context, id string, req *dto
 		if err != nil {
 			return notFoundAs(err, "Deployment not found: %s", id)
 		}
-		template, cluster, endpointCred, err := s.resolveReferences(ctx, tx, req)
+		template, cluster, err := s.resolveReferences(ctx, tx, req)
 		if err != nil {
 			return err
 		}
 
-		config := deployment.BatchJobConfig
+		config := deployment.DefaultBatchJobConfig
 		if config == nil {
-			config = &model.BatchJobConfig{ID: deployment.BatchJobConfigID}
+			config = &model.BatchJobConfig{ID: deployment.DefaultBatchJobConfigID}
 		}
-		dto.ApplyBatchJobConfigRequest(config, req.BatchJobConfig)
+		dto.ApplyBatchJobConfigRequest(config, req.DefaultBatchJobConfig)
 		if err := deployments.SaveConfig(ctx, config); err != nil {
 			return err
 		}
@@ -289,10 +284,9 @@ func (s *BatchDeploymentService) Update(ctx context.Context, id string, req *dto
 		deployment.TemplateID = &template.ID
 		deployment.ClusterID = cluster
 		deployment.SlurmRunSection = req.SlurmRunSection
-		deployment.BatchJobConfigID = config.ID
-		deployment.BatchJobConfig = config
-		deployment.DefaultSubmissionCredentialID = endpointCred.ID
-		deployment.Partition = req.Partition
+		deployment.DefaultBatchJobConfigID = config.ID
+		deployment.DefaultBatchJobConfig = config
+		deployment.DefaultPartition = req.DefaultPartition
 
 		if err := deployments.Save(ctx, deployment); err != nil {
 			return err
@@ -318,31 +312,26 @@ func (s *BatchDeploymentService) Delete(ctx context.Context, id string) error {
 	return s.deployments.Delete(ctx, deployment)
 }
 
-// resolveReferences looks up the template, optional cluster and submission credential
-// a deployment request names.
+// resolveReferences looks up the template and the optional cluster a deployment
+// request names.
 //
-// The cluster is the only optional one: an absent or blank id is legitimate and
-// leaves the deployment unbound to a cluster, while an id that is supplied but
-// unknown is an error. Conflating those would let a typo silently unbind a deployment.
-func (s *BatchDeploymentService) resolveReferences(ctx context.Context, tx *gorm.DB, req *dto.BatchDeploymentRequest) (*model.Template, *string, *cred.SSHEndpointCredential, error) {
+// The cluster is the optional one: an absent or blank id is legitimate and leaves the
+// deployment unbound to a cluster, while an id that is supplied but unknown is an
+// error. Conflating those would let a typo silently unbind a deployment.
+func (s *BatchDeploymentService) resolveReferences(ctx context.Context, tx *gorm.DB, req *dto.BatchDeploymentRequest) (*model.Template, *string, error) {
 	template, err := s.templates.WithTx(tx).FindByID(ctx, req.TemplateID)
 	if err != nil {
-		return nil, nil, nil, notFoundAs(err, "Template not found: %s", req.TemplateID)
+		return nil, nil, notFoundAs(err, "Template not found: %s", req.TemplateID)
 	}
 
 	var clusterID *string
 	if req.SlurmClusterID != nil && strings.TrimSpace(*req.SlurmClusterID) != "" {
 		cluster, err := s.clusters.WithTx(tx).FindByID(ctx, *req.SlurmClusterID)
 		if err != nil {
-			return nil, nil, nil, notFoundAs(err, "Cluster not found: %s", *req.SlurmClusterID)
+			return nil, nil, notFoundAs(err, "Cluster not found: %s", *req.SlurmClusterID)
 		}
 		clusterID = &cluster.ID
 	}
 
-	endpointCred, err := s.endpointCreds.WithTx(tx).FindByID(ctx, req.DefaultSubmissionCredentialID)
-	if err != nil {
-		return nil, nil, nil, notFoundAs(err, "SSH endpoint credential not found: %s", req.DefaultSubmissionCredentialID)
-	}
-
-	return template, clusterID, endpointCred, nil
+	return template, clusterID, nil
 }
diff --git a/api/process/dto/process.go b/api/process/dto/process.go
index 1975aac7ac..732b735746 100644
--- a/api/process/dto/process.go
+++ b/api/process/dto/process.go
@@ -36,10 +36,9 @@ type BatchProcessRequest struct {
 	// under — the id returned by POST /api/v1/ssh-endpoint-credentials, not a bare SSH
 	// credential.
 	//
-	// Optional, and the reason the deployment's is called a *default*: a run that names
-	// none submits under the deployment's, which is the ordinary case. Naming one is how
-	// a caller submits under their own identity instead, and only a binding they own or
-	// that is shared with them will be accepted.
+	// Required: a deployment carries no credential to fall back on, so every run says
+	// which identity it submits under, and only a binding the caller owns or that is
+	// shared with them will be accepted.
 	SubmissionCredentialID *string `json:"submissionCredentialId"`
 
 	// BaseWorkDir is the parent directory on the cluster this run works under; it gets
@@ -61,6 +60,7 @@ type BatchProcessRequest struct {
 func (r *BatchProcessRequest) Validate() []httpx.FieldError {
 	var c httpx.Constraints
 	c.NotBlank("deploymentId", "Deployment id cannot be blank", r.DeploymentID)
+	c.NotBlankPtr("submissionCredentialId", "Submission credential id cannot be blank", r.SubmissionCredentialID)
 	if r.BatchJobConfig == nil {
 		c.Add("batchJobConfig", "Batch job config cannot be null")
 	} else {
@@ -84,7 +84,6 @@ type BatchProcessResponse struct {
 	JobName        *string                                `json:"jobName"`
 	BatchJobConfig *applicationdto.BatchJobConfigResponse `json:"batchJobConfig"`
 
-	// Always set, whether the run named a credential or inherited the deployment's.
 	SubmissionCredentialID string `json:"submissionCredentialId"`
 
 	BaseWorkDir *string `json:"baseWorkDir"`
diff --git a/api/process/service/launch.go b/api/process/service/launch.go
index a77c6f691e..08b1449823 100644
--- a/api/process/service/launch.go
+++ b/api/process/service/launch.go
@@ -60,11 +60,6 @@ func (s *LaunchService) launchBatchProcess(ctx context.Context, process *model.P
 
 	clusterStorage := cluster.SCPDataStorage
 
-	if sshCredential == nil {
-		// TODO: Validate the access to this submittion credential before using it
-		sshCredential = batchDeployment.DefaultSubmissionCredential
-	}
-
 	if sshCredential == nil {
 		return fmt.Errorf("No SSH credential available for batch process")
 	}
diff --git a/api/process/service/process.go b/api/process/service/process.go
index 98bedae92c..930826c687 100644
--- a/api/process/service/process.go
+++ b/api/process/service/process.go
@@ -227,7 +227,7 @@ func (s *ProcessService) saveBatchProcess(
 	if err != nil {
 		return nil, notFoundAs(err, "Deployment not found: %s", req.DeploymentID)
 	}
-	submissionCredentialID, err := s.resolveSubmissionCredential(ctx, tx, deployment, req)
+	submissionCredentialID, err := s.resolveSubmissionCredential(ctx, tx, req)
 	if err != nil {
 		return nil, err
 	}
@@ -259,23 +259,21 @@ func (s *ProcessService) saveBatchProcess(
 	return batch, nil
 }
 
-// resolveSubmissionCredential decides which SSH endpoint credential binding this run
-// submits under: the one it named, or the deployment's default when it named none.
+// resolveSubmissionCredential authorises the SSH endpoint credential binding this run
+// submits under.
 //
-// Only a named binding is authorised, and it is authorised against the caller — this is
-// the one place in a self-service submission where a caller supplies an identity to act
-// under, so RequireUsable is what keeps them to their own bindings and the ones shared
-// with them. The deployment's default is exempt because it is not the caller's choice
-// at all: an admin configured it as how that deployment submits, and requiring every
-// user to hold it directly would leave ordinary submissions rejected.
+// The binding is authorised against the caller — this is the one place in a
+// self-service submission where a caller supplies an identity to act under, so
+// RequireUsable is what keeps them to their own bindings and the ones shared with
+// them. A deployment carries no default to fall back on, so validation already
+// rejects a request that names none.
 func (s *ProcessService) resolveSubmissionCredential(
 	ctx context.Context,
 	tx *gorm.DB,
-	deployment *applicationmodel.BatchDeployment,
 	req *dto.BatchProcessRequest,
 ) (string, error) {
 	if req.SubmissionCredentialID == nil || strings.TrimSpace(*req.SubmissionCredentialID) == "" {
-		return deployment.DefaultSubmissionCredentialID, nil
+		return "", httpx.BadRequest("Submission credential id cannot be blank")
 	}
 	credential, err := s.credentials.WithTx(tx).RequireUsable(ctx, *req.SubmissionCredentialID)
 	if err != nil {
diff --git a/docs/api.md b/docs/api.md
index faa1a5336c..548a278714 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -29,7 +29,7 @@ Every failure — validation, authorization, missing record — comes back in th
 }
 ```
 
-`fieldErrors` is omitted on every other kind of failure. Field names are the JSON paths of the request body, including indexes and nesting — `inputs[0].inputType`, `batchJobConfig.wallTimeMinutes`.
+`fieldErrors` is omitted on every other kind of failure. Field names are the JSON paths of the request body, including indexes and nesting — `inputs[0].inputType`, `defaultBatchJobConfig.wallTimeMinutes`.
 
 The statuses in use:
 
@@ -416,7 +416,7 @@ curl -s localhost:9095/api/v1/clusters/"$CLUSTER_ID" \
 
 Every path is scoped by its cluster, so a `partitionId` belonging to one cluster is a `404` when reached through another — an id alone is never enough to move or read a partition across clusters.
 
-`DELETE` returns `204 No Content`. Nothing holds a foreign key to a partition, so removing one never conflicts; note that a deployment names its partition by name rather than by id, and deleting the partition it names is not detected here.
+`DELETE` returns `204 No Content`. Nothing holds a foreign key to a partition, so removing one never conflicts; note that a deployment names its default partition by name rather than by id, and deleting the partition it names is not detected here.
 
 ### Create SSH Key
 
@@ -568,11 +568,11 @@ POST /api/v1/ssh-endpoint-credentials
 
 Binds an SSH credential to an [SSH endpoint](#ssh-endpoints) so the caller can act on that host. Requires only an authenticated principal — not admin — but that principal must have a matching row in `users` (see INSTALL.md's "Owning resources requires a matching `users` row"), since the binding's owner is resolved from the token rather than accepted as a request field.
 
-The resulting binding id is what [Create Batch Deployment](#create-batch-deployment) below uses as `defaultSubmissionCredentialId`: a deployment submits under a specific endpoint credential, not a bare SSH credential, since the binding is what ties the submitting identity to both a host and an owner.
+The resulting binding id is what [Create Process](#create-process) below uses as `submissionCredentialId`: a run submits under a specific endpoint credential, not a bare SSH credential, since the binding is what ties the submitting identity to both a host and an owner.
 
 **curl example**
 
-Uses `$SSH_ENDPOINT_ID` from [Create SSH Endpoint](#create-ssh-endpoint) and `$SSH_CREDENTIAL_ID` from [Create SSH Credential](#create-ssh-credential) above, and captures `sshEndpointCredentialId` into `$ENDPOINT_CREDENTIAL_ID` for the batch deployment step.
+Uses `$SSH_ENDPOINT_ID` from [Create SSH Endpoint](#create-ssh-endpoint) and `$SSH_CREDENTIAL_ID` from [Create SSH Credential](#create-ssh-credential) above, and captures `sshEndpointCredentialId` into `$ENDPOINT_CREDENTIAL_ID` for the process submission step.
 
 ```bash
 ENDPOINT_CREDENTIAL_ID=$(curl -s -X POST localhost:9095/api/v1/ssh-endpoint-credentials \
@@ -1028,7 +1028,7 @@ Returned when e.g. `templateName` is blank, an `inputType`/`outputType` is missi
 
 ## Batch Deployments
 
-A batch deployment binds an application template to a Slurm cluster: the run script section, resource request (walltime, nodes, GPUs, ...) and the credential used to submit jobs. Deployments reference a template by id (see [Create Application Template](#create-application-template)).
+A batch deployment binds an application template to a Slurm cluster: the run script section and the resource request (walltime, nodes, GPUs, ...) a run of it starts from. Which identity a job submits under is a property of the run, not of the deployment, so it is named on the process instead. Deployments reference a template by id (see [Create Application Template](#create-application-template)).
 
 ### Create Batch Deployment
 
@@ -1040,7 +1040,7 @@ Requires `ADMIN` or `SUPER_ADMIN` authority.
 
 **curl example**
 
-Uses `$TEMPLATE_ID` from [Create Application Template](#create-application-template), `$CLUSTER_ID` from [Create Cluster](#create-cluster), and `$ENDPOINT_CREDENTIAL_ID` from [Create SSH Endpoint Credential](#create-ssh-endpoint-credential) above.
+Uses `$TEMPLATE_ID` from [Create Application Template](#create-application-template) and `$CLUSTER_ID` from [Create Cluster](#create-cluster) above.
 
 ```bash
 TOKEN='<the token printed at startup>'
@@ -1052,7 +1052,7 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
     "templateId": "'"$TEMPLATE_ID"'",
     "slurmClusterId": "'"$CLUSTER_ID"'",
     "slurmRunSection": "module load alphafold\nrun_alphafold.sh --fasta_paths=$fastaFile --model_preset=$modelPreset",
-    "batchJobConfig": {
+    "defaultBatchJobConfig": {
       "wallTimeMinutes": 720,
       "allocation": "TG-BIO210001",
       "cpus": 8,
@@ -1062,8 +1062,7 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
       "gres": "gpu:1",
       "gpus": 1
     },
-    "defaultSubmissionCredentialId": "'"$ENDPOINT_CREDENTIAL_ID"'",
-    "partition": "gpu"
+    "defaultPartition": "gpu"
   }'
 ```
 
@@ -1074,31 +1073,30 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
 | `templateId` | string | required, must reference an existing application template |
 | `slurmClusterId` | string \| null | optional; if supplied, must reference an existing cluster — a typo is an error, not a silent unbind |
 | `slurmRunSection` | string | required, cannot be blank |
-| `batchJobConfig` | object | required |
-| `batchJobConfig.wallTimeMinutes` | integer | required, must be positive |
-| `batchJobConfig.allocation` | string | required, cannot be blank |
-| `batchJobConfig.cpus` | integer \| null | optional |
-| `batchJobConfig.mem` | string \| null | optional |
-| `batchJobConfig.memPerCpu` | string \| null | optional |
-| `batchJobConfig.ntasksPerNode` | integer \| null | optional |
-| `batchJobConfig.cpusPerTask` | integer \| null | optional |
-| `batchJobConfig.nodes` | integer \| null | optional |
-| `batchJobConfig.ntasks` | integer \| null | optional |
-| `batchJobConfig.gres` | string \| null | optional |
-| `batchJobConfig.gpus` | integer \| null | optional |
-| `batchJobConfig.memPerGpu` | string \| null | optional |
-| `batchJobConfig.cpusPerGpu` | string \| null | optional |
-| `batchJobConfig.gpusPerNode` | integer \| null | optional |
-| `batchJobConfig.constraints` | string \| null | optional |
-| `defaultSubmissionCredentialId` | string | required, must reference an existing SSH endpoint credential binding (see [Create SSH Endpoint Credential](#create-ssh-endpoint-credential)), not a bare SSH credential |
-| `partition` | string \| null | optional |
+| `defaultBatchJobConfig` | object | required — the resource request a run of this deployment starts from |
+| `defaultBatchJobConfig.wallTimeMinutes` | integer | required, must be positive |
+| `defaultBatchJobConfig.allocation` | string | required, cannot be blank |
+| `defaultBatchJobConfig.cpus` | integer \| null | optional |
+| `defaultBatchJobConfig.mem` | string \| null | optional |
+| `defaultBatchJobConfig.memPerCpu` | string \| null | optional |
+| `defaultBatchJobConfig.ntasksPerNode` | integer \| null | optional |
+| `defaultBatchJobConfig.cpusPerTask` | integer \| null | optional |
+| `defaultBatchJobConfig.nodes` | integer \| null | optional |
+| `defaultBatchJobConfig.ntasks` | integer \| null | optional |
+| `defaultBatchJobConfig.gres` | string \| null | optional |
+| `defaultBatchJobConfig.gpus` | integer \| null | optional |
+| `defaultBatchJobConfig.memPerGpu` | string \| null | optional |
+| `defaultBatchJobConfig.cpusPerGpu` | string \| null | optional |
+| `defaultBatchJobConfig.gpusPerNode` | integer \| null | optional |
+| `defaultBatchJobConfig.constraints` | string \| null | optional |
+| `defaultPartition` | string \| null | optional |
 
 ```json
 {
   "templateId": "b6f1c2de-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
   "slurmClusterId": "c1d2e3f4-5678-4abc-9def-0123456789ab",
   "slurmRunSection": "module load alphafold\nrun_alphafold.sh --fasta_paths=$fastaFile --model_preset=$modelPreset",
-  "batchJobConfig": {
+  "defaultBatchJobConfig": {
     "wallTimeMinutes": 720,
     "allocation": "TG-BIO210001",
     "cpus": 8,
@@ -1108,14 +1106,13 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
     "gres": "gpu:1",
     "gpus": 1
   },
-  "defaultSubmissionCredentialId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
-  "partition": "gpu"
+  "defaultPartition": "gpu"
 }
 ```
 
 **Response — `201 Created`**
 
-`deploymentId` and `batchJobConfig.batchJobConfigId` are server-generated (UUIDs).
+`deploymentId` and `defaultBatchJobConfig.batchJobConfigId` are server-generated (UUIDs).
 
 ```json
 {
@@ -1123,7 +1120,7 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
   "templateId": "b6f1c2de-3a4b-4c5d-8e6f-7a8b9c0d1e2f",
   "slurmClusterId": "c1d2e3f4-5678-4abc-9def-0123456789ab",
   "slurmRunSection": "module load alphafold\nrun_alphafold.sh --fasta_paths=$fastaFile --model_preset=$modelPreset",
-  "batchJobConfig": {
+  "defaultBatchJobConfig": {
     "batchJobConfigId": "f6a7b8c9-de01-4f23-9456-789abcdef012",
     "wallTimeMinutes": 720,
     "allocation": "TG-BIO210001",
@@ -1141,14 +1138,13 @@ curl -s -X POST localhost:9095/api/v1/slurm-deployments \
     "gpusPerNode": null,
     "constraints": null
   },
-  "defaultSubmissionCredentialId": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
-  "partition": "gpu"
+  "defaultPartition": "gpu"
 }
 ```
 
 **Validation errors — `400 Bad Request`**
 
-Returned when e.g. `templateId`, `slurmRunSection` or `defaultSubmissionCredentialId` is blank, `batchJobConfig` is missing, or `batchJobConfig.wallTimeMinutes`/`allocation` fail their checks. A `templateId`, `slurmClusterId` or `defaultSubmissionCredentialId` that does not resolve to an existing record is returned as `404 Not Found` instead.
+Returned when e.g. `templateId` or `slurmRunSection` is blank, `defaultBatchJobConfig` is missing, or `defaultBatchJobConfig.wallTimeMinutes`/`allocation` fail their checks. A `templateId` or `slurmClusterId` that does not resolve to an existing record is returned as `404 Not Found` instead.
 
 ```json
 {
@@ -1157,7 +1153,7 @@ Returned when e.g. `templateId`, `slurmRunSection` or `defaultSubmissionCredenti
   "message": "Validation failed",
   "fieldErrors": [
     { "field": "templateId", "message": "Template id cannot be blank" },
-    { "field": "batchJobConfig.wallTimeMinutes", "message": "Wall time must be positive" }
+    { "field": "defaultBatchJobConfig.wallTimeMinutes", "message": "Wall time must be positive" }
   ]
 }
 ```
@@ -1202,8 +1198,9 @@ Java service, which did the same.
 POST /api/v1/processes
 ```
 
-Uses `$DEPLOYMENT_ID` from [Create Batch Deployment](#create-batch-deployment), and the
-`inputId`/`outputId` values returned by
+Uses `$DEPLOYMENT_ID` from [Create Batch Deployment](#create-batch-deployment),
+`$ENDPOINT_CREDENTIAL_ID` from [Create SSH Endpoint Credential](#create-ssh-endpoint-credential),
+and the `inputId`/`outputId` values returned by
 [Create Application Template](#create-application-template).
 
 ```bash
@@ -1214,6 +1211,7 @@ curl -s -X POST localhost:9095/api/v1/processes \
     "processType": "BATCH_JOB",
     "batchProcess": {
       "deploymentId": "'"$DEPLOYMENT_ID"'",
+      "submissionCredentialId": "'"$ENDPOINT_CREDENTIAL_ID"'",
       "jobName": "alphafold-run-1",
       "baseWorkDir": "/scratch/$USER/alphafold",
       "batchJobConfig": {
@@ -1244,18 +1242,18 @@ There is no `userId`: ownership comes from the token.
 | Field | Type | Notes |
 |---|---|---|
 | `deploymentId` | string | required, cannot be blank |
-| `batchJobConfig` | object | required — the same shape a deployment carries |
-| `submissionCredentialId` | string \| null | optional; the SSH endpoint credential binding this run submits under. Omitted, the run inherits the deployment's `defaultSubmissionCredentialId` |
+| `batchJobConfig` | object | required — the same shape a deployment's `defaultBatchJobConfig` carries |
+| `submissionCredentialId` | string | required, cannot be blank; the SSH endpoint credential binding this run submits under |
 | `jobName` | string \| null | optional |
 | `jobId` | string \| null | optional. Writable rather than server-generated: it is the scheduler's identifier for the submitted job, learned at submission time and recorded afterwards |
 | `baseWorkDir` | string \| null | optional; the parent directory on the cluster this run works under. Launching a run that omits it fails, since there is nowhere to stage its files |
 | `inputMappings` | array | optional; replaced wholesale by a `PUT` |
 | `outputMappings` | array | optional; replaced wholesale by a `PUT` |
 
-The resource request is carried here rather than copied from the deployment's default,
-which is what lets a caller ask for different resources for a particular run. Each run
-owns its own `batchJobConfig` row, distinct from the deployment's, and deleting the
-process deletes it.
+The resource request is carried here rather than copied from the deployment's
+`defaultBatchJobConfig`, which is what lets a caller ask for different resources for a
+particular run. Each run owns its own `batchJobConfig` row, distinct from the
+deployment's, and deleting the process deletes it.
 
 `baseWorkDir` is carried here for the same reason, and used the same way: the run works
 in a subdirectory of it named for its `processId`, which is where the template's file
@@ -1264,12 +1262,10 @@ former `workDir`, so two runs of one deployment can work under different directo
 
 `submissionCredentialId` is the one field of a self-service submission that names an
 identity to act under, so it is authorized against the caller: a binding that does not
-exist is `404`, and one that is neither theirs nor shared with them is `403`. The
-deployment's default is exempt from that check — an admin configured it as how that
-deployment submits, and requiring every user to hold it directly would leave ordinary
-submissions rejected. A `PUT` re-resolves the field the same way, so omitting it falls
-back to the deployment's default rather than keeping what the run was created with. The
-response always carries the binding the run actually submits under, inherited or named.
+exist is `404`, and one that is neither theirs nor shared with them is `403`. A
+deployment carries no credential to fall back on, which is why the field is required
+rather than optional. A `PUT` re-resolves it the same way, so a body that omits it is
+rejected rather than keeping what the run was created with.
 
 **`batchProcess.inputMappings` / `batchProcess.outputMappings`**
 
diff --git a/internal/app/app.go b/internal/app/app.go
index 1e5b638aa8..d6dbc5cb33 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -130,7 +130,7 @@ func New(cfg config.Config, db *gorm.DB) *Services {
 		ClusterPartition: computesvc.NewClusterPartitionService(db, partitions, clusters),
 
 		Template:        applicationsvc.NewTemplateService(db, templates, deployments),
-		BatchDeployment: applicationsvc.NewBatchDeploymentService(db, deployments, templates, clusters, bindings),
+		BatchDeployment: applicationsvc.NewBatchDeploymentService(db, deployments, templates, clusters),
 
 		SCPDataStorage:        datasvc.NewSCPDataStorageService(db, storages, storageShares, endpoints, products, users, groupMembers),
 		SCPDataStorageSharing: datasvc.NewSCPDataStorageSharingService(db, storages, storageShares, groups, users, groupMembers),
diff --git a/internal/db/foreignkey_test.go b/internal/db/foreignkey_test.go
index ccdb565fd9..536114785b 100644
--- a/internal/db/foreignkey_test.go
+++ b/internal/db/foreignkey_test.go
@@ -50,8 +50,7 @@ func TestForeignKeyDirections(t *testing.T) {
 		{"application_template_outputs", "template_id", "application_templates", "template_id"},
 		{"batch_application_deployments", "cluster_id", "clusters", "cluster_id"},
 		{"batch_application_deployments", "template_id", "application_templates", "template_id"},
-		{"batch_application_deployments", "batch_job_config_id", "batch_job_configs", "batch_job_config_id"},
-		{"batch_application_deployments", "default_submission_credential_id", "ssh_endpoint_credentials", "ssh_endpoint_credential_id"},
+		{"batch_application_deployments", "default_batch_job_config_id", "batch_job_configs", "batch_job_config_id"},
 		{"processes", "user_id", "users", "user_id"},
 		{"batch_processes", "parent_process_id", "processes", "process_id"},
 		{"batch_processes", "deployment_id", "batch_application_deployments", "deployment_id"},
diff --git a/internal/db/migrate_test.go b/internal/db/migrate_test.go
index e4f4bd0a94..f06707e42a 100644
--- a/internal/db/migrate_test.go
+++ b/internal/db/migrate_test.go
@@ -232,23 +232,6 @@ func TestDeletingClusterCascadesToPartitions(t *testing.T) {
 func TestDeletingDeploymentRemovesOwnedBatchJobConfig(t *testing.T) {
 	gdb := newTestDB(t)
 
-	key := &credentialsmodel.SSHKey{SSHKeyName: "k", PublicKey: "ssh-ed25519 AAAA", PrivateKey: "secret"}
-	if err := gdb.Create(key).Error; err != nil {
-		t.Fatalf("create ssh key: %v", err)
-	}
-	cred := &credentialsmodel.SSHUserCredential{Username: "dimuthu", SSHKeyID: &key.ID}
-	if err := gdb.Create(cred).Error; err != nil {
-		t.Fatalf("create ssh credential: %v", err)
-	}
-	endpoint := seedSSHEndpoint(t, gdb, "anvil")
-	cluster := &computemodel.Cluster{ClusterName: "anvil", SSHEndpointID: &endpoint.ID, SlurmHome: "/usr/bin"}
-	if err := gdb.Create(cluster).Error; err != nil {
-		t.Fatalf("create cluster: %v", err)
-	}
-	binding := &credentialsmodel.SSHEndpointCredential{SSHEndpointID: &endpoint.ID, SSHCredentialID: &cred.ID}
-	if err := gdb.Create(binding).Error; err != nil {
-		t.Fatalf("create ssh endpoint credential: %v", err)
-	}
 	tmpl := &applicationmodel.Template{TemplateName: ptr.To("gromacs")}
 	if err := gdb.Create(tmpl).Error; err != nil {
 		t.Fatalf("create template: %v", err)
@@ -259,10 +242,9 @@ func TestDeletingDeploymentRemovesOwnedBatchJobConfig(t *testing.T) {
 	}
 
 	dep := &applicationmodel.BatchDeployment{
-		TemplateID:                    &tmpl.ID,
-		SlurmRunSection:               "module load gromacs\ngmx mdrun",
-		BatchJobConfigID:              cfg.ID,
-		DefaultSubmissionCredentialID: binding.ID,
+		TemplateID:              &tmpl.ID,
+		SlurmRunSection:         "module load gromacs\ngmx mdrun",
+		DefaultBatchJobConfigID: cfg.ID,
 	}
 	if err := gdb.Create(dep).Error; err != nil {
 		t.Fatalf("create deployment: %v", err)
diff --git a/internal/db/migrations/0003_deployment_default_naming.sql b/internal/db/migrations/0003_deployment_default_naming.sql
new file mode 100644
index 0000000000..285ecd4019
--- /dev/null
+++ b/internal/db/migrations/0003_deployment_default_naming.sql
@@ -0,0 +1,29 @@
+-- A deployment's resource request and partition are defaults; its credential is gone.
+--
+-- default_submission_credential_id was the binding a run submitted under when it named
+-- none of its own. Which identity a job runs as is a property of the run, not of the
+-- deployment it was launched from, so batch_processes.submission_credential_id is now
+-- the only place it is recorded and a run has to name one. Dropping the column takes
+-- its foreign key and index with it.
+--
+-- The two remaining columns are renamed to say what they are. A run copies the
+-- deployment's resource request into a batch_job_configs row of its own and may depart
+-- from it, so what the deployment holds is a starting point rather than what any
+-- particular job is submitted with — the same relationship the partition has.
+--
+-- The constraint and the index are renamed alongside the column they guard: AutoMigrate
+-- derives both names from the field, so leaving them as they were would drift a
+-- migrated database from a development one, which is what
+-- TestBaselineMigrationMatchesAutoMigrate catches.
+
+ALTER TABLE "batch_application_deployments" DROP COLUMN "default_submission_credential_id";
+
+ALTER TABLE "batch_application_deployments" RENAME COLUMN "batch_job_config_id" TO "default_batch_job_config_id";
+ALTER TABLE "batch_application_deployments" RENAME COLUMN "partition" TO "default_partition";
+
+ALTER TABLE "batch_application_deployments"
+	RENAME CONSTRAINT "fk_batch_application_deployments_batch_job_config"
+	TO "fk_batch_application_deployments_default_batch_job_config";
+
+ALTER INDEX "idx_batch_application_deployments_batch_job_config_id"
+	RENAME TO "idx_batch_application_deployments_default_batch_job_config_id";
diff --git a/internal/server/server_test.go b/internal/server/server_test.go
index d5e42e17bd..2a08347532 100644
--- a/internal/server/server_test.go
+++ b/internal/server/server_test.go
@@ -201,8 +201,7 @@ func (h *harness) seedSSHKeyAndCredential(name string) (keyID, credentialID stri
 }
 
 // seedEndpointCredential creates an endpoint, an SSH credential, and a binding between
-// them, returning the binding's id — what a deployment's defaultSubmissionCredentialId
-// points at.
+// them, returning the binding's id — what a run's submissionCredentialId points at.
 func (h *harness) seedEndpointCredential(name, token string) (endpointID, bindingID string) {
 	h.t.Helper()
 	endpointID = h.seedSSHEndpoint(name)
@@ -396,7 +395,6 @@ func TestDeletingSSHKeyInUseConflicts(t *testing.T) {
 
 func TestDeletingTemplateWithDeploymentsConflicts(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("deploy", tokenAdmin)
 
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin, map[string]any{
 		"templateName": "gromacs",
@@ -408,8 +406,7 @@ func TestDeletingTemplateWithDeploymentsConflicts(t *testing.T) {
 
 	h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": templateID, "slurmRunSection": "gmx mdrun",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "TG-1"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "TG-1"},
 	}, http.StatusCreated)
 
 	if rec := h.do(http.MethodDelete, "/api/v1/application-templates/"+templateID, tokenAdmin, nil); rec.Code != http.StatusConflict {
@@ -469,14 +466,12 @@ func TestDuplicateTemplateInputNamesAreRejected(t *testing.T) {
 // unknown is an error rather than silently ignored.
 func TestDeploymentClusterIsOptionalButValidatedWhenPresent(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("optional-cluster", tokenAdmin)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "t"}, http.StatusCreated)
 
 	base := map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 30, "allocation": "TG-2"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 30, "allocation": "TG-2"},
 	}
 
 	// Absent cluster: accepted.
@@ -1244,19 +1239,19 @@ func TestDataSharingRejectsBadRequests(t *testing.T) {
 // seedProcess submits a process owned by the caller of token and returns its id.
 func (h *harness) seedProcess(name, token string) string {
 	h.t.Helper()
-	_, bindingID := h.seedEndpointCredential(name, tokenAdmin)
+	_, bindingID := h.seedEndpointCredential(name, token)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": name}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
 	}, http.StatusCreated)
 	proc := h.mustDo(http.MethodPost, "/api/v1/processes", token, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "ALLOC"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 60, "allocation": "ALLOC"},
 		},
 	}, http.StatusCreated)
 	return proc["processId"].(string)
@@ -1431,20 +1426,20 @@ func TestProcessTaskRejectsBadRequests(t *testing.T) {
 
 func TestProcessSubmissionIsSelfServiceWithRequestedResources(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("proc", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("proc", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "proc"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
 	}, http.StatusCreated)
 
 	created := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 120, "allocation": "ALICE-ALLOC", "gpus": 2},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 120, "allocation": "ALICE-ALLOC", "gpus": 2},
 		},
 	}, http.StatusCreated)
 
@@ -1457,18 +1452,17 @@ func TestProcessSubmissionIsSelfServiceWithRequestedResources(t *testing.T) {
 		t.Errorf("config = %v, want the values from the request, not the deployment default", cfg)
 	}
 	// The process owns its own config, distinct from the deployment's.
-	if cfg["batchJobConfigId"] == deployment["batchJobConfig"].(map[string]any)["batchJobConfigId"] {
+	if cfg["batchJobConfigId"] == deployment["defaultBatchJobConfig"].(map[string]any)["batchJobConfigId"] {
 		t.Error("process and deployment share one batch job config; each must own its own")
 	}
 }
 
 // Which SSH endpoint credential a run submits under is the one place a self-service
-// submission names an identity to act under, so it is authorised against the caller —
-// while the deployment's default, which an admin chose, is not.
+// submission names an identity to act under, so it is authorised against the caller.
+// A deployment carries no default to fall back on, so naming one is required.
 func TestProcessSubmitsUnderACredentialTheCallerMayUse(t *testing.T) {
 	h := newHarness(t)
-	deployment, _, _ := h.seedDeploymentWithIO("submission")
-	deploymentDefault := deployment["defaultSubmissionCredentialId"].(string)
+	deployment, bindingID, _, _ := h.seedDeploymentWithIO("submission")
 	batch := func(credentialID any) map[string]any {
 		body := map[string]any{
 			"deploymentId":   deployment["deploymentId"],
@@ -1484,20 +1478,19 @@ func TestProcessSubmitsUnderACredentialTheCallerMayUse(t *testing.T) {
 			map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(credentialID)})
 	}
 
-	// Naming none submits under the deployment's default, which is the ordinary case:
-	// alice holds no binding of her own here, and the deployment's belongs to admin.
-	created := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice,
-		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(nil)}, http.StatusCreated)
-	if got := created["batchProcess"].(map[string]any)["submissionCredentialId"]; got != deploymentDefault {
-		t.Errorf("submissionCredentialId = %v, want the deployment default %s", got, deploymentDefault)
+	// Naming her own binding submits under it.
+	own := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice,
+		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(bindingID)}, http.StatusCreated)
+	if got := own["batchProcess"].(map[string]any)["submissionCredentialId"]; got != bindingID {
+		t.Errorf("submissionCredentialId = %v, want the named binding %s", got, bindingID)
 	}
 
-	// Naming one of her own submits under that instead.
-	_, aliceBinding := h.seedEndpointCredential("alice-submits", tokenAlice)
-	own := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice,
-		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(aliceBinding)}, http.StatusCreated)
-	if got := own["batchProcess"].(map[string]any)["submissionCredentialId"]; got != aliceBinding {
-		t.Errorf("submissionCredentialId = %v, want the named binding %s", got, aliceBinding)
+	// Naming none is a validation error rather than a run with no identity behind it.
+	rec := create(tokenAlice, nil)
+	if rec.Code != http.StatusBadRequest {
+		t.Errorf("no credential: status = %d, want 400", rec.Code)
+	} else if field := firstFieldError(t, rec); field != "batchProcess.submissionCredentialId" {
+		t.Errorf("field = %q, want it reported under the section", field)
 	}
 
 	// A binding that does not exist is a 404, and one that is neither hers nor shared
@@ -1510,13 +1503,16 @@ func TestProcessSubmitsUnderACredentialTheCallerMayUse(t *testing.T) {
 		t.Errorf("another user's credential: status = %d, want 403", rec.Code)
 	}
 
-	// An update re-resolves it the same way, so dropping the field falls back to the
-	// deployment's default rather than keeping what the run was created with.
+	// An update re-resolves it the same way, so it is required there too.
 	processID := own["processId"].(string)
+	if rec := h.do(http.MethodPut, "/api/v1/processes/"+processID, tokenAdmin,
+		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(nil)}); rec.Code != http.StatusBadRequest {
+		t.Errorf("update with no credential: status = %d, want 400", rec.Code)
+	}
 	updated := h.mustDo(http.MethodPut, "/api/v1/processes/"+processID, tokenAdmin,
-		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(nil)}, http.StatusOK)
-	if got := updated["batchProcess"].(map[string]any)["submissionCredentialId"]; got != deploymentDefault {
-		t.Errorf("submissionCredentialId after update = %v, want the deployment default", got)
+		map[string]any{"processType": "BATCH_JOB", "batchProcess": batch(bindingID)}, http.StatusOK)
+	if got := updated["batchProcess"].(map[string]any)["submissionCredentialId"]; got != bindingID {
+		t.Errorf("submissionCredentialId after update = %v, want the named binding", got)
 	}
 }
 
@@ -1524,9 +1520,9 @@ func TestProcessSubmitsUnderACredentialTheCallerMayUse(t *testing.T) {
 // output, and returns the deployment together with those two declaration ids. The
 // mapping tests need real declarations to point at: a mapping carries a foreign key to
 // the template input it supplies a value for.
-func (h *harness) seedDeploymentWithIO(name string) (deployment map[string]any, inputID, outputID string) {
+func (h *harness) seedDeploymentWithIO(name string) (deployment map[string]any, bindingID, inputID, outputID string) {
 	h.t.Helper()
-	_, bindingID := h.seedEndpointCredential(name, tokenAdmin)
+	_, bindingID = h.seedEndpointCredential(name, tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin, map[string]any{
 		"templateName": name,
 		"inputs":       []any{map[string]any{"inputName": "sequence", "inputType": "FILE"}},
@@ -1534,23 +1530,23 @@ func (h *harness) seedDeploymentWithIO(name string) (deployment map[string]any,
 	}, http.StatusCreated)
 	deployment = h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "DEFAULT"},
 	}, http.StatusCreated)
 
 	inputs := tmpl["inputs"].([]any)
 	outputs := tmpl["outputs"].([]any)
-	return deployment, inputs[0].(map[string]any)["inputId"].(string), outputs[0].(map[string]any)["outputId"].(string)
+	return deployment, bindingID, inputs[0].(map[string]any)["inputId"].(string), outputs[0].(map[string]any)["outputId"].(string)
 }
 
 // A batch job is not a resource of its own: it is configured as a section of the
 // process body, so which sections a body may carry follows from its process type.
 func TestProcessRejectsSectionsThatDoNotMatchItsType(t *testing.T) {
 	h := newHarness(t)
-	deployment, _, _ := h.seedDeploymentWithIO("sections")
+	deployment, bindingID, _, _ := h.seedDeploymentWithIO("sections")
 	batch := map[string]any{
-		"deploymentId":   deployment["deploymentId"],
-		"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
+		"deploymentId":           deployment["deploymentId"],
+		"submissionCredentialId": bindingID,
+		"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
 	}
 
 	for _, tc := range []struct {
@@ -1578,8 +1574,9 @@ func TestProcessRejectsSectionsThatDoNotMatchItsType(t *testing.T) {
 	if rec := h.do(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   "nope",
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
+			"deploymentId":           "nope",
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
 		},
 	}); rec.Code != http.StatusNotFound {
 		t.Errorf("unknown deployment: status = %d, want 404", rec.Code)
@@ -1611,11 +1608,12 @@ func TestProcessWithoutABatchSectionIsStillAProcess(t *testing.T) {
 // back nested in it, and replaced wholesale by an update.
 func TestProcessCarriesItsTemplateMappings(t *testing.T) {
 	h := newHarness(t)
-	deployment, inputID, outputID := h.seedDeploymentWithIO("mappings")
+	deployment, bindingID, inputID, outputID := h.seedDeploymentWithIO("mappings")
 	batch := func(extra map[string]any) map[string]any {
 		body := map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
 		}
 		for k, v := range extra {
 			body[k] = v
@@ -1712,13 +1710,14 @@ func TestProcessCarriesItsTemplateMappings(t *testing.T) {
 // one orphaned with nothing pointing at it.
 func TestUpdatingProcessKeepsItsOwnedConfig(t *testing.T) {
 	h := newHarness(t)
-	deployment, _, _ := h.seedDeploymentWithIO("update")
+	deployment, bindingID, _, _ := h.seedDeploymentWithIO("update")
 
 	created := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
 		},
 	}, http.StatusCreated)
 	processID := created["processId"].(string)
@@ -1727,10 +1726,11 @@ func TestUpdatingProcessKeepsItsOwnedConfig(t *testing.T) {
 	updated := h.mustDo(http.MethodPut, "/api/v1/processes/"+processID, tokenAdmin, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 90, "allocation": "B"},
-			"jobId":          "4821577",
-			"jobName":        "fold-1",
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 90, "allocation": "B"},
+			"jobId":                  "4821577",
+			"jobName":                "fold-1",
 		},
 	}, http.StatusOK)
 
@@ -1766,8 +1766,9 @@ func TestUpdatingProcessKeepsItsOwnedConfig(t *testing.T) {
 	if rec := h.do(http.MethodPut, "/api/v1/processes/"+processID, tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 90, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 90, "allocation": "B"},
 		},
 	}); rec.Code != http.StatusForbidden {
 		t.Errorf("update by the owner: status = %d, want 403", rec.Code)
@@ -1778,12 +1779,13 @@ func TestUpdatingProcessKeepsItsOwnedConfig(t *testing.T) {
 // named there rather than on the process.
 func TestListingProcessesByDeployment(t *testing.T) {
 	h := newHarness(t)
-	deployment, _, _ := h.seedDeploymentWithIO("by-deployment")
+	deployment, bindingID, _, _ := h.seedDeploymentWithIO("by-deployment")
 	body := map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "A"},
 		},
 	}
 	h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, body, http.StatusCreated)
@@ -1812,19 +1814,19 @@ func TestListingProcessesByDeployment(t *testing.T) {
 // outward and the database cannot cascade in that direction.
 func TestDeletingProcessRemovesItsOwnedConfig(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("cleanup", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("cleanup", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "cleanup"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
 	}, http.StatusCreated)
 	proc := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)
 
@@ -1844,19 +1846,19 @@ func TestDeletingProcessRemovesItsOwnedConfig(t *testing.T) {
 // reference before removing its statuses, or the RESTRICT constraints deadlock.
 func TestDeletingProcessRemovesItsStatuses(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("status-cleanup", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("status-cleanup", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "status-cleanup"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
 	}, http.StatusCreated)
 	proc := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)
 	processID := proc["processId"].(string)
@@ -1874,19 +1876,19 @@ func TestDeletingProcessRemovesItsStatuses(t *testing.T) {
 // a caller never observes a process that exists but has no status history yet.
 func TestCreatingProcessRecordsInitialCreatedStatus(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("initial-status", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("initial-status", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "initial-status"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
 	}, http.StatusCreated)
 	proc := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)
 	processID := proc["processId"].(string)
@@ -1918,19 +1920,19 @@ func TestCreatingProcessRecordsInitialCreatedStatus(t *testing.T) {
 // it — so the two write methods on the same path must fail, not silently succeed.
 func TestProcessStatusWritesAreNotExposed(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("status-readonly", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("status-readonly", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "status-readonly"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
 	}, http.StatusCreated)
 	proc := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)
 	processID := proc["processId"].(string)
@@ -1948,27 +1950,28 @@ func TestProcessStatusWritesAreNotExposed(t *testing.T) {
 // the same scoping ClusterPartition already applies to its cluster.
 func TestGetProcessStatusIsScopedToItsProcess(t *testing.T) {
 	h := newHarness(t)
-	_, bindingID := h.seedEndpointCredential("status-scope", tokenAdmin)
+	_, bindingID := h.seedEndpointCredential("status-scope", tokenAlice)
 	tmpl := h.mustDo(http.MethodPost, "/api/v1/application-templates", tokenAdmin,
 		map[string]any{"templateName": "status-scope"}, http.StatusCreated)
 	deployment := h.mustDo(http.MethodPost, "/api/v1/slurm-deployments", tokenAdmin, map[string]any{
 		"templateId": tmpl["templateId"], "slurmRunSection": "run",
-		"defaultSubmissionCredentialId": bindingID,
-		"batchJobConfig":                map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
+		"defaultBatchJobConfig": map[string]any{"wallTimeMinutes": 60, "allocation": "A"},
 	}, http.StatusCreated)
 
 	procA := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)
 	procB := h.mustDo(http.MethodPost, "/api/v1/processes", tokenAlice, map[string]any{
 		"processType": "BATCH_JOB",
 		"batchProcess": map[string]any{
-			"deploymentId":   deployment["deploymentId"],
-			"batchJobConfig": map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
+			"deploymentId":           deployment["deploymentId"],
+			"submissionCredentialId": bindingID,
+			"batchJobConfig":         map[string]any{"wallTimeMinutes": 10, "allocation": "B"},
 		},
 	}, http.StatusCreated)