diff --git a/pkg/service_internal/mapfixes.go b/pkg/service_internal/mapfixes.go
index cfdf31a..c8a8c79 100644
--- a/pkg/service_internal/mapfixes.go
+++ b/pkg/service_internal/mapfixes.go
@@ -2,12 +2,31 @@ package service_internal
 
 import (
 	"context"
+	"errors"
 
-	internal "git.itzana.me/strafesnet/maps-service/pkg/internal"
 	"git.itzana.me/strafesnet/maps-service/pkg/datastore"
+	internal "git.itzana.me/strafesnet/maps-service/pkg/internal"
 	"git.itzana.me/strafesnet/maps-service/pkg/model"
 )
 
+var(
+	// prevent two mapfixes with same asset id
+	ActiveMapfixStatuses = []model.MapfixStatus{
+		model.MapfixStatusUploading,
+		model.MapfixStatusValidated,
+		model.MapfixStatusValidating,
+		model.MapfixStatusAccepted,
+		model.MapfixStatusChangesRequested,
+		model.MapfixStatusSubmitted,
+		model.MapfixStatusUnderConstruction,
+	}
+)
+
+var(
+	ErrActiveMapfixSameAssetID = errors.New("There is an active mapfix with the same AssetID")
+	ErrNotAssetOwner = errors.New("You can only submit an asset you own")
+)
+
 // UpdateMapfixValidatedModel implements patchMapfixModel operation.
 //
 // Update model following role restrictions.
@@ -59,3 +78,54 @@ func (svc *Service) ActionMapfixUploaded(ctx context.Context, params internal.Ac
 	smap.Add("status_id", model.MapfixStatusUploaded)
 	return svc.DB.Mapfixes().IfStatusThenUpdate(ctx, params.MapfixID, []model.MapfixStatus{model.MapfixStatusUploading}, smap)
 }
+
+// POST /mapfixes
+func (svc *Service) CreateMapfix(ctx context.Context, request *internal.MapfixCreate) (*internal.ID, error) {
+	// Check if an active mapfix with the same asset id exists
+	{
+		filter := datastore.Optional()
+		filter.Add("asset_id", request.AssetID)
+		filter.Add("asset_version", request.AssetVersion)
+		filter.Add("status_id", ActiveMapfixStatuses)
+		active_mapfixes, err := svc.DB.Mapfixes().List(ctx, filter, model.Page{
+			Number: 1,
+			Size:   1,
+		},datastore.ListSortDisabled)
+		if err != nil {
+			return nil, err
+		}
+		if len(active_mapfixes) != 0{
+			return nil, ErrActiveMapfixSameAssetID
+		}
+	}
+
+	operation, err := svc.DB.Operations().Get(ctx, request.OperationID)
+	if err != nil {
+		return nil, err
+	}
+
+	// check if user owns asset
+	// TODO: allow bypass by admin
+	if operation.Owner != request.AssetOwner {
+		return nil, ErrNotAssetOwner
+	}
+
+	mapfix, err := svc.DB.Mapfixes().Create(ctx, model.Mapfix{
+		ID:            0,
+		DisplayName:   request.DisplayName,
+		Creator:       request.Creator,
+		GameID:        request.GameID,
+		Submitter:     request.AssetOwner,
+		AssetID:       request.AssetID,
+		AssetVersion:  request.AssetVersion,
+		Completed:     false,
+		TargetAssetID: request.TargetAssetID,
+		StatusID:      model.MapfixStatusUnderConstruction,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &internal.ID{
+		ID: mapfix.ID,
+	}, nil
+}
diff --git a/pkg/service_internal/submissions.go b/pkg/service_internal/submissions.go
index c9846d1..02a5533 100644
--- a/pkg/service_internal/submissions.go
+++ b/pkg/service_internal/submissions.go
@@ -2,12 +2,30 @@ package service_internal
 
 import (
 	"context"
+	"errors"
 
-	internal "git.itzana.me/strafesnet/maps-service/pkg/internal"
 	"git.itzana.me/strafesnet/maps-service/pkg/datastore"
+	internal "git.itzana.me/strafesnet/maps-service/pkg/internal"
 	"git.itzana.me/strafesnet/maps-service/pkg/model"
 )
 
+var(
+	// prevent two mapfixes with same asset id
+	ActiveSubmissionStatuses = []model.SubmissionStatus{
+		model.SubmissionStatusUploading,
+		model.SubmissionStatusValidated,
+		model.SubmissionStatusValidating,
+		model.SubmissionStatusAccepted,
+		model.SubmissionStatusChangesRequested,
+		model.SubmissionStatusSubmitted,
+		model.SubmissionStatusUnderConstruction,
+	}
+)
+
+var(
+	ErrActiveSubmissionSameAssetID = errors.New("There is an active submission with the same AssetID")
+)
+
 // UpdateSubmissionValidatedModel implements patchSubmissionModel operation.
 //
 // Update model following role restrictions.
@@ -60,3 +78,53 @@ func (svc *Service) ActionSubmissionUploaded(ctx context.Context, params interna
 	smap.Add("uploaded_asset_id", params.UploadedAssetID)
 	return svc.DB.Submissions().IfStatusThenUpdate(ctx, params.SubmissionID, []model.SubmissionStatus{model.SubmissionStatusUploading}, smap)
 }
+
+// POST /submissions
+func (svc *Service) CreateSubmission(ctx context.Context, request *internal.SubmissionCreate) (*internal.ID, error) {
+	// Check if an active submission with the same asset id exists
+	{
+		filter := datastore.Optional()
+		filter.Add("asset_id", request.AssetID)
+		filter.Add("asset_version", request.AssetVersion)
+		filter.Add("status_id", ActiveSubmissionStatuses)
+		active_submissions, err := svc.DB.Submissions().List(ctx, filter, model.Page{
+			Number: 1,
+			Size:   1,
+		},datastore.ListSortDisabled)
+		if err != nil {
+			return nil, err
+		}
+		if len(active_submissions) != 0{
+			return nil, ErrActiveSubmissionSameAssetID
+		}
+	}
+
+	operation, err := svc.DB.Operations().Get(ctx, request.OperationID)
+	if err != nil {
+		return nil, err
+	}
+
+	// check if user owns asset
+	// TODO: allow bypass by admin
+	if operation.Owner != request.AssetOwner {
+		return nil, ErrNotAssetOwner
+	}
+
+	submission, err := svc.DB.Submissions().Create(ctx, model.Submission{
+		ID:            0,
+		DisplayName:   request.DisplayName,
+		Creator:       request.Creator,
+		GameID:        request.GameID,
+		Submitter:     request.AssetOwner,
+		AssetID:       request.AssetID,
+		AssetVersion:  request.AssetVersion,
+		Completed:     false,
+		StatusID:      model.SubmissionStatusUnderConstruction,
+	})
+	if err != nil {
+		return nil, err
+	}
+	return &internal.ID{
+		ID: submission.ID,
+	}, nil
+}